ai-code-battle/engine/game.go
jedarden c36d98f4ac style(engine): align struct field names in GameState
Align ZoneCenter and ZoneRadius field spacing for consistency.

No functional change.
2026-05-24 19:19:45 -04:00

422 lines
9.9 KiB
Go

package engine
import (
"encoding/json"
"fmt"
"math/rand"
)
// GameState represents the complete state of a match.
type GameState struct {
Config Config
Grid *Grid
Bots []*Bot
Cores []*Core
Energy []*EnergyNode
Players []*Player
Turn int
MatchID string
NextBotID int
NextCoreID int
rng *rand.Rand
// Turn state
Moves map[int]Move // bot ID -> move
DeadBots []*Bot // bots that died this turn (for fog display)
Events []Event // events that occurred this turn
Dominance map[int]int // player -> consecutive turns with 80%+ bots
// Stalemate detection
StalemateTurns int // consecutive turns with no progress
LastTotalEnergy int // total energy held by all players at last progress
LastTotalBots int // total living bots at last progress
// Zone (storm) state
ZoneCenter Position // center of the zone (map center)
ZoneRadius int // current radius of the safe zone
ZoneActive bool // whether the zone is currently shrinking
CombatDeaths []int // combat deaths per player (tracked for final stats)
}
// Event represents something that happened during a turn.
type Event struct {
Type string `json:"type"`
Turn int `json:"turn"`
Details interface{} `json:"details"`
}
// Event types
const (
EventBotSpawned = "bot_spawned"
EventBotDied = "bot_died"
EventEnergyCollected = "energy_collected"
EventCoreCaptured = "core_captured"
EventCombatDeath = "combat_death"
EventCollisionDeath = "collision_death"
EventZoneDeath = "zone_death"
)
// NewGameState creates a new game state with the given configuration.
func NewGameState(config Config, rng *rand.Rand) *GameState {
center := Position{Row: config.Rows / 2, Col: config.Cols / 2}
initialRadius := min(config.Rows, config.Cols) / 2
return &GameState{
Config: config,
Grid: NewGrid(config.Rows, config.Cols),
Bots: make([]*Bot, 0),
Cores: make([]*Core, 0),
Energy: make([]*EnergyNode, 0),
Players: make([]*Player, 0),
Turn: 0,
MatchID: generateMatchID(rng),
NextBotID: 0,
rng: rng,
Moves: make(map[int]Move),
DeadBots: make([]*Bot, 0),
Events: make([]Event, 0),
Dominance: make(map[int]int),
ZoneCenter: center,
ZoneRadius: initialRadius,
ZoneActive: false,
CombatDeaths: make([]int, 0), // Will be sized when players are added
}
}
// generateMatchID creates a random match identifier.
func generateMatchID(rng *rand.Rand) string {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 8)
for i := range b {
b[i] = chars[rng.Intn(len(chars))]
}
return "m_" + string(b)
}
// AddPlayer adds a new player to the game.
func (gs *GameState) AddPlayer() *Player {
p := &Player{
ID: len(gs.Players),
Energy: 0,
Score: 0,
}
gs.Players = append(gs.Players, p)
gs.Dominance[p.ID] = 0
gs.CombatDeaths = append(gs.CombatDeaths, 0) // Track combat deaths for this player
return p
}
// AddCore adds a core for a player at the given position.
func (gs *GameState) AddCore(owner int, pos Position) *Core {
c := &Core{
Position: gs.Grid.WrapPos(pos),
Owner: owner,
Active: true,
ID: gs.NextCoreID,
}
gs.NextCoreID++
gs.Cores = append(gs.Cores, c)
gs.Grid.SetPos(c.Position, TileCore)
// Player starts with 1 point per core
if owner < len(gs.Players) {
gs.Players[owner].Score++
}
return c
}
// AddEnergyNode adds an energy node at the given position.
func (gs *GameState) AddEnergyNode(pos Position) *EnergyNode {
en := &EnergyNode{
Position: gs.Grid.WrapPos(pos),
HasEnergy: false,
Tick: 0,
}
gs.Energy = append(gs.Energy, en)
return en
}
// SpawnBot spawns a new bot for a player at the given position.
func (gs *GameState) SpawnBot(owner int, pos Position) *Bot {
b := &Bot{
ID: gs.NextBotID,
Owner: owner,
Position: gs.Grid.WrapPos(pos),
Alive: true,
}
gs.NextBotID++
gs.Bots = append(gs.Bots, b)
if owner < len(gs.Players) {
gs.Players[owner].BotCount++
}
gs.Events = append(gs.Events, Event{
Type: EventBotSpawned,
Turn: gs.Turn,
Details: map[string]interface{}{
"bot_id": b.ID,
"owner": owner,
"pos": b.Position,
},
})
return b
}
// GetPlayerBots returns all living bots for a player.
func (gs *GameState) GetPlayerBots(playerID int) []*Bot {
var bots []*Bot
for _, b := range gs.Bots {
if b.Alive && b.Owner == playerID {
bots = append(bots, b)
}
}
return bots
}
// GetLivingBotCount returns the count of living bots.
func (gs *GameState) GetLivingBotCount() int {
count := 0
for _, b := range gs.Bots {
if b.Alive {
count++
}
}
return count
}
// GetPlayerLivingBotCount returns the count of living bots for a player.
func (gs *GameState) GetPlayerLivingBotCount(playerID int) int {
count := 0
for _, b := range gs.Bots {
if b.Alive && b.Owner == playerID {
count++
}
}
return count
}
// GetLivingPlayers returns IDs of players with at least one living bot.
func (gs *GameState) GetLivingPlayers() []int {
alive := make(map[int]bool)
for _, b := range gs.Bots {
if b.Alive {
alive[b.Owner] = true
}
}
var result []int
for pid := range alive {
result = append(result, pid)
}
return result
}
// KillBot marks a bot as dead and records the event.
func (gs *GameState) KillBot(bot *Bot, reason string) {
if !bot.Alive {
return
}
bot.Alive = false
gs.DeadBots = append(gs.DeadBots, bot)
if bot.Owner < len(gs.Players) {
gs.Players[bot.Owner].BotCount--
}
gs.Events = append(gs.Events, Event{
Type: EventBotDied,
Turn: gs.Turn,
Details: map[string]interface{}{
"bot_id": bot.ID,
"owner": bot.Owner,
"pos": bot.Position,
"reason": reason,
},
})
}
// SubmitMove records a move for a bot at a given position.
func (gs *GameState) SubmitMove(pos Position, dir Direction) {
// Find the bot at this position
for _, b := range gs.Bots {
if b.Alive && b.Position == pos {
gs.Moves[b.ID] = Move{Position: pos, Direction: dir}
return
}
}
}
// ClearTurnState clears the per-turn state.
func (gs *GameState) ClearTurnState() {
gs.Moves = make(map[int]Move)
gs.DeadBots = make([]*Bot, 0)
gs.Events = make([]Event, 0)
}
// GetVisibleState returns the game state filtered by fog of war for a player.
func (gs *GameState) GetVisibleState(playerID int) *VisibleState {
vs := &VisibleState{
MatchID: gs.MatchID,
Turn: gs.Turn,
Config: gs.Config,
}
vs.You.ID = playerID
vs.You.Energy = gs.Players[playerID].Energy
vs.You.Score = gs.Players[playerID].Score
// Get positions of player's bots
playerPositions := make([]Position, 0)
for _, b := range gs.Bots {
if b.Alive && b.Owner == playerID {
playerPositions = append(playerPositions, b.Position)
}
}
// Calculate visible tiles
visible := gs.Grid.VisibleFrom(playerPositions, gs.Config.VisionRadius2)
// Filter bots
vs.Bots = make([]VisibleBot, 0)
for _, b := range gs.Bots {
if b.Alive && visible[b.Position] {
vs.Bots = append(vs.Bots, VisibleBot{
Position: b.Position,
Owner: b.Owner,
})
}
}
// Filter dead bots (visible for one turn)
for _, b := range gs.DeadBots {
if visible[b.Position] {
vs.Dead = append(vs.Dead, VisibleBot{
Position: b.Position,
Owner: b.Owner,
})
}
}
// Filter energy nodes with energy
vs.Energy = make([]Position, 0)
for _, en := range gs.Energy {
if en.HasEnergy && visible[en.Position] {
vs.Energy = append(vs.Energy, en.Position)
}
}
// Filter cores
vs.Cores = make([]VisibleCore, 0)
for _, c := range gs.Cores {
if visible[c.Position] {
vs.Cores = append(vs.Cores, VisibleCore{
Position: c.Position,
Owner: c.Owner,
Active: c.Active,
})
}
}
// Filter walls
vs.Walls = make([]Position, 0)
for p := range gs.Grid.Walls {
if visible[p] {
vs.Walls = append(vs.Walls, p)
}
}
return vs
}
// ToJSON returns a JSON representation of the game state.
func (gs *GameState) ToJSON() ([]byte, error) {
return json.MarshalIndent(gs, "", " ")
}
// Clone creates a deep copy of the game state.
func (gs *GameState) Clone() *GameState {
newGS := &GameState{
Config: gs.Config,
Grid: NewGrid(gs.Config.Rows, gs.Config.Cols),
Bots: make([]*Bot, len(gs.Bots)),
Cores: make([]*Core, len(gs.Cores)),
Energy: make([]*EnergyNode, len(gs.Energy)),
Players: make([]*Player, len(gs.Players)),
Turn: gs.Turn,
MatchID: gs.MatchID,
NextBotID: gs.NextBotID,
NextCoreID: gs.NextCoreID,
rng: gs.rng,
Moves: make(map[int]Move),
DeadBots: make([]*Bot, 0),
Events: make([]Event, 0),
Dominance: make(map[int]int),
ZoneCenter: gs.ZoneCenter,
ZoneRadius: gs.ZoneRadius,
ZoneActive: gs.ZoneActive,
}
// Copy grid
for p := range gs.Grid.Walls {
newGS.Grid.Walls[p] = true
}
for row := 0; row < gs.Config.Rows; row++ {
for col := 0; col < gs.Config.Cols; col++ {
newGS.Grid.Tiles[row][col] = gs.Grid.Tiles[row][col]
}
}
// Copy bots
for i, b := range gs.Bots {
newGS.Bots[i] = &Bot{
ID: b.ID,
Owner: b.Owner,
Position: b.Position,
Alive: b.Alive,
}
}
// Copy cores
for i, c := range gs.Cores {
newGS.Cores[i] = &Core{
Position: c.Position,
Owner: c.Owner,
Active: c.Active,
ID: c.ID,
LastSpawnedTurn: c.LastSpawnedTurn,
}
}
// Copy energy nodes
for i, en := range gs.Energy {
newGS.Energy[i] = &EnergyNode{
Position: en.Position,
HasEnergy: en.HasEnergy,
Tick: en.Tick,
}
}
// Copy players
for i, p := range gs.Players {
newGS.Players[i] = &Player{
ID: p.ID,
Energy: p.Energy,
Score: p.Score,
BotCount: p.BotCount,
}
}
// Copy dominance
for k, v := range gs.Dominance {
newGS.Dominance[k] = v
}
return newGS
}
// String returns a string representation of the game state.
func (gs *GameState) String() string {
return fmt.Sprintf("GameState{Turn: %d, Players: %d, Bots: %d, Living: %d}",
gs.Turn, len(gs.Players), len(gs.Bots), gs.GetLivingBotCount())
}