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>
111 lines
2.6 KiB
Go
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
|
|
}
|