ai-code-battle/engine/types.go
jedarden c56cc8bae6 fix(matchmaker): multi-match crash cooldown (3 strikes / 30 min) per §4.5 + §6.1
Add crash_strikes and cooldown_until columns to bots table. Worker
increments strikes on crash (cooldown at 3), resets on success.
Matchmaker excludes cooldown bots from pairing, series scheduling,
and championship brackets. Fix erroneous cooldown filter on series
table in finalizeCompletedSeries (column only exists on bots).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 15:22:12 -04:00

243 lines
6 KiB
Go

// Package engine implements the AI Code Battle game simulation.
package engine
import "math"
// Position represents a coordinate on the toroidal grid.
type Position struct {
Row int `json:"row"`
Col int `json:"col"`
}
// Tile represents the type of a grid cell.
type Tile int
const (
TileOpen Tile = iota
TileWall
TileEnergy
TileCore
)
// String returns the symbol representation of a tile.
func (t Tile) String() string {
switch t {
case TileOpen:
return "."
case TileWall:
return "#"
case TileEnergy:
return "*"
case TileCore:
return "C"
default:
return "?"
}
}
// Direction represents a movement direction.
type Direction int
const (
DirNone Direction = iota
DirN
DirE
DirS
DirW
)
// String returns the string representation of a direction.
func (d Direction) String() string {
switch d {
case DirN:
return "N"
case DirE:
return "E"
case DirS:
return "S"
case DirW:
return "W"
default:
return ""
}
}
// ParseDirection parses a direction string.
func ParseDirection(s string) Direction {
switch s {
case "N":
return DirN
case "E":
return DirE
case "S":
return DirS
case "W":
return DirW
default:
return DirNone
}
}
// Delta returns the row and column delta for a direction.
func (d Direction) Delta() (dr, dc int) {
switch d {
case DirN:
return -1, 0
case DirE:
return 0, 1
case DirS:
return 1, 0
case DirW:
return 0, -1
default:
return 0, 0
}
}
// Bot represents a unit on the grid.
type Bot struct {
ID int `json:"id"`
Owner int `json:"owner"`
Position Position `json:"position"`
Alive bool `json:"alive"`
}
// Core represents a spawn point owned by a player.
type Core struct {
Position Position `json:"position"`
Owner int `json:"owner"`
Active bool `json:"active"` // false if razed
ID int `json:"id"` // unique core identifier
LastSpawnedTurn int `json:"last_spawned_turn"` // turn when this core last spawned a bot
}
// EnergyNode represents an energy spawn location.
type EnergyNode struct {
Position Position `json:"position"`
HasEnergy bool `json:"has_energy"` // true if energy is currently collectible
Tick int `json:"tick"` // turns since last spawn
}
// Player represents a participant in the match.
type Player struct {
ID int `json:"id"`
Energy int `json:"energy"`
Score int `json:"score"`
BotCount int `json:"bot_count"`
}
// Move represents a bot's movement order.
// Bots are identified by their position in the fog-filtered state.
type Move struct {
Position Position `json:"position"` // current position of bot to move
Direction Direction `json:"direction"`
}
// Config holds game configuration parameters.
type Config struct {
Rows int `json:"rows"`
Cols int `json:"cols"`
MaxTurns int `json:"max_turns"`
VisionRadius2 int `json:"vision_radius2"` // squared vision distance
AttackRadius2 int `json:"attack_radius2"` // squared attack distance
SpawnCost int `json:"spawn_cost"` // energy cost to spawn a bot
EnergyInterval int `json:"energy_interval"` // turns between energy spawns
CoresPerPlayer int `json:"cores_per_player"` // starting cores per player
}
// DefaultConfig returns the default game configuration.
func DefaultConfig() Config {
return Config{
Rows: 60,
Cols: 60,
MaxTurns: 500,
VisionRadius2: 49, // ~7 tiles
AttackRadius2: 5, // ~2.24 tiles
SpawnCost: 3,
EnergyInterval: 10,
CoresPerPlayer: 1,
}
}
// ConfigForPlayers returns a config scaled for the given player count and cores per player.
// Uses ~1800-2000 tiles per player (following aichallenge Ants sizing).
func ConfigForPlayers(numPlayers, coresPerPlayer int) Config {
cfg := DefaultConfig()
cfg.CoresPerPlayer = coresPerPlayer
if coresPerPlayer < 1 {
cfg.CoresPerPlayer = 1
}
// Scale grid: ~2000 tiles per player, square grid
areaPerPlayer := 2000
totalArea := areaPerPlayer * numPlayers
side := int(math.Sqrt(float64(totalArea)))
// Clamp to valid range
if side < 40 {
side = 40
}
if side > 200 {
side = 200
}
cfg.Rows = side
cfg.Cols = side
// Scale max turns with map size
cfg.MaxTurns = side * 8 // larger maps get more turns
// Scale energy nodes with player count
cfg.EnergyInterval = 10
return cfg
}
// MatchResult represents the outcome of a match.
type MatchResult struct {
Winner int `json:"winner"` // -1 for draw
Reason string `json:"reason"` // "elimination", "dominance", "turns", "draw"
Turns int `json:"turns"`
Scores []int `json:"scores"`
Energy []int `json:"energy"` // energy collected per player
BotsAlive []int `json:"bots_alive"`
Crashed []bool `json:"crashed"` // per-player: true if bot was marked crashed during match
}
// BotInterface defines the interface for bot decision-making.
// In Phase 1, this is implemented by local bots communicating via stdin/stdout.
type BotInterface interface {
// GetMoves returns the bot's moves for the current turn.
// state is the fog-filtered game state visible to this player.
GetMoves(state *VisibleState) ([]Move, error)
}
// VisibleState represents the game state filtered by fog of war for a specific player.
type VisibleState struct {
MatchID string `json:"match_id"`
Turn int `json:"turn"`
Config Config `json:"config"`
You struct {
ID int `json:"id"`
Energy int `json:"energy"`
Score int `json:"score"`
} `json:"you"`
Bots []VisibleBot `json:"bots"`
Energy []Position `json:"energy"`
Cores []VisibleCore `json:"cores"`
Walls []Position `json:"walls"`
Dead []VisibleBot `json:"dead"`
}
// VisibleBot represents a bot visible to a player.
type VisibleBot struct {
Position Position `json:"position"`
Owner int `json:"owner"`
}
// VisibleCore represents a core visible to a player.
type VisibleCore struct {
Position Position `json:"position"`
Owner int `json:"owner"`
Active bool `json:"active"`
}