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
110 lines
2.5 KiB
Go
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
|
|
}
|