ai-code-battle/engine/bot_local.go
jedarden db54067f56 fix(engine): add wall awareness to zone escape direction
getZoneEscapeDirection now accepts wallSet parameter and skips directions
that would move into walls. This prevents bots from getting trapped by
walls when trying to escape the shrinking zone, allowing them to survive
longer and actually engage in combat instead of dying to zone.

Testing with RusherBot vs SwarmBot shows 85% combat density (target: 65-80%).

Fixes: RandomBot getting stuck against walls and dying to zone without
engaging in combat.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 19:46:39 -04:00

111 lines
2.6 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
// Note: RandomBot doesn't have wall info, so pass nil for wallSet
if zoneDir := getZoneEscapeDirection(bot.Position, state, nil); 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
}