ai-code-battle/engine/bot_local.go
jedarden 6d3f3506b3 Implement Phase 1 core engine: grid, combat, fog of war, turn execution
- Add engine package with toroidal grid, game state, turn execution
- Implement focus-fire combat resolution with simultaneous deaths
- Add fog of war visibility filtering for bot state
- Implement energy collection (contested resources denied)
- Add bot spawning at active cores
- Implement win conditions: elimination, draw, dominance, turns
- Add replay JSON writer for match recording
- Add match runner with concurrent bot communication
- Add CLI tools: acb-local (match runner), acb-mapgen (map generator)
- Add comprehensive unit tests (26 tests passing)

Exit criteria met: can run complete 500-turn matches and produce valid replays

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 01:48:27 -04:00

100 lines
2.3 KiB
Go

package engine
import (
"bufio"
"encoding/json"
"fmt"
"io"
"math/rand"
"os"
)
// LocalBot is a bot that communicates via stdin/stdout (Phase 1).
// This is used for local development and testing.
type LocalBot struct {
stdin io.Reader
stdout io.Writer
}
// NewLocalBot creates a new local bot using stdin/stdout.
func NewLocalBot() *LocalBot {
return &LocalBot{
stdin: os.Stdin,
stdout: os.Stdout,
}
}
// NewLocalBotWithIO creates a local bot with custom IO (for testing).
func NewLocalBotWithIO(stdin io.Reader, stdout io.Writer) *LocalBot {
return &LocalBot{
stdin: stdin,
stdout: stdout,
}
}
// GetMoves reads game state from stdin and writes moves to stdout.
func (b *LocalBot) GetMoves(state *VisibleState) ([]Move, error) {
// Write state to stdout as JSON
encoder := json.NewEncoder(b.stdout)
if err := encoder.Encode(state); err != nil {
return nil, fmt.Errorf("failed to encode state: %w", err)
}
// Read moves from stdin
scanner := bufio.NewScanner(b.stdin)
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to read moves: %w", err)
}
return nil, fmt.Errorf("EOF reading moves")
}
var moves []Move
if err := json.Unmarshal(scanner.Bytes(), &moves); err != nil {
return nil, fmt.Errorf("failed to decode moves: %w", err)
}
return moves, nil
}
// RandomBot is a simple bot that makes random moves.
type RandomBot struct {
rng *rand.Rand
}
// NewRandomBot creates a new random bot.
func NewRandomBot(seed int64) *RandomBot {
return &RandomBot{
rng: rand.New(rand.NewSource(seed)),
}
}
// GetMoves returns random moves for all visible bots.
func (b *RandomBot) GetMoves(state *VisibleState) ([]Move, error) {
moves := make([]Move, 0)
directions := []Direction{DirN, DirE, DirS, DirW}
for _, bot := range state.Bots {
if bot.Owner == state.You.ID {
moves = append(moves, Move{
Position: bot.Position,
Direction: directions[b.rng.Intn(len(directions))],
})
}
}
return moves, nil
}
// IdleBot is a bot that never moves.
type IdleBot struct{}
// NewIdleBot creates a new idle bot.
func NewIdleBot() *IdleBot {
return &IdleBot{}
}
// GetMoves returns no moves (bot stays in place).
func (b *IdleBot) GetMoves(state *VisibleState) ([]Move, error) {
return []Move{}, nil
}