ai-code-battle/engine/bot_local.go
jedarden ccdec39c52 feat(engine): add zone escape as Priority 1 to all built-in bots
Per plan §3.7.1, the zone forces bots into contact. This change ensures
all built-in bots escape the zone FIRST when threatened (dist to zone
edge < 5 tiles), before any other action like energy collection or combat.

Changes:
- GuardianBot, SwarmBot, HunterBot: Added zone escape as Priority 1
- Phase 13 bots (Defender, Scout, Farmer, Pacifist, Phalanx, Raider,
  Nomad, Opportunist, Assassin, Kamikaze): Added zone escape as Priority 1
- RandomBot: Added zone escape before random movement

The getZoneEscapeDirection function was already present and correctly
implements toroidal distance calculation with 5-tile safety margin.

Closes: bf-4m78q
2026-05-26 18:47:39 -04:00

110 lines
2.5 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 {
// Priority 1: Escape zone if threatened
if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone {
moves = append(moves, Move{
Position: bot.Position,
Direction: zoneDir,
})
continue
}
// Otherwise, move randomly
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
}