ai-code-battle/engine/types.go
jedarden 30d2c22721 fix(engine): set DefaultConfig ZoneMinRadius to 1 per plan §3.7.1
DefaultConfig() is used as the base for ConfigForPlayers(), which
overrides ZoneMinRadius based on player count (2 for 2-player, 1 for
3+). The default should match the most common case (3+ players).

Per plan §3.7.1: ZoneMinRadius=1 for 3+ players, 2 for 2-player.

Closes: bf-6985
2026-05-25 16:05:43 -04:00

307 lines
8.9 KiB
Go

// Package engine implements the AI Code Battle game simulation.
package engine
import (
"encoding/json"
"fmt"
"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
}
}
// MarshalJSON serializes Direction as a string ("N", "E", "S", "W", or "").
func (d Direction) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
// UnmarshalJSON accepts both string ("N") and integer (1) representations.
func (d *Direction) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err == nil {
*d = ParseDirection(s)
return nil
}
var i int
if err := json.Unmarshal(data, &i); err != nil {
return fmt.Errorf("direction must be a string or integer: %w", err)
}
*d = Direction(i)
return nil
}
// 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
MapID string `json:"map_id,omitempty"`
SeasonID string `json:"season_id,omitempty"`
RulesVersion string `json:"rules_version,omitempty"`
// Zone (storm) configuration
ZoneEnabled bool `json:"zone_enabled"` // whether the shrinking zone is active
ZoneStartTurn int `json:"zone_start_turn"` // turn when zone starts shrinking
ZoneShrinkInterval int `json:"zone_shrink_interval"` // turns between shrink steps
ZoneShrinkStep int `json:"zone_shrink_step"` // tiles to shrink each step
ZoneMinRadius int `json:"zone_min_radius"` // minimum zone radius (stops here)
}
// DefaultConfig returns the default game configuration.
func DefaultConfig() Config {
return Config{
Rows: 40,
Cols: 40,
MaxTurns: 500,
VisionRadius2: 49, // ~7 tiles
AttackRadius2: 12, // 3.5 tiles per plan §3.4
SpawnCost: 3,
EnergyInterval: 10,
CoresPerPlayer: 2,
ZoneEnabled: true,
ZoneStartTurn: 10, // Per plan §3.7.1 (both 2-player and 3+)
ZoneShrinkInterval: 1, // Per plan §3.7.1 (both 2-player and 3+)
ZoneShrinkStep: 2, // Per plan §3.7.1 (both 2-player and 3+)
ZoneMinRadius: 1, // Per plan §3.7.1: 3+ player default (ConfigForPlayers overrides for 2-player)
}
}
// ConfigForPlayers returns a config scaled for the given player count and cores per player.
// For 2 players, uses 40x40 (800 tiles per player) to increase encounter frequency.
// For 3+ players, uses ~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: smaller maps for 2-player, ~1000 tiles/player for 3+ (high combat density)
var areaPerPlayer int
if numPlayers == 2 {
areaPerPlayer = 800 // 40x40 for 2 players
} else {
areaPerPlayer = 1000 // Reduced from 2000 to force more contact
}
totalArea := areaPerPlayer * numPlayers
side := int(math.Sqrt(float64(totalArea)))
// Clamp to valid range
if side < 30 {
side = 30
}
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
// Scale zone parameters to force combat contact
// Zone must start early to force combat before energy farming wins
// Zone diameter must be <= 2 * attack radius so bots at opposite zone edges can reach each other
// Target: 65-80% combat density per plan §3.7.1
if numPlayers == 2 {
cfg.ZoneStartTurn = 10 // Per plan §3.7.1 to force combat before bots can spread
cfg.ZoneShrinkInterval = 1 // Per plan §3.7.1
cfg.ZoneShrinkStep = 2 // Per plan §3.7.1: 2 tiles per step forces engagement
cfg.ZoneMinRadius = 2 // Per plan §3.7.1: 2-player min radius
cfg.AttackRadius2 = 25 // 5 tiles (reduced from 6 to achieve 65-80% combat density target)
} else {
cfg.ZoneStartTurn = 10 // Per plan §3.7.1
cfg.ZoneShrinkInterval = 1 // Per plan §3.7.1
cfg.ZoneShrinkStep = 2 // Per plan §3.7.1: 2 tiles per step forces engagement
cfg.ZoneMinRadius = 1 // Zone diameter (2) < attack radius (3.5), forces contact
cfg.AttackRadius2 = 12 // 3.5 tiles per plan §3.4 (3+ player)
}
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
CombatDeaths []int `json:"combat_deaths"` // bots killed in combat per player (focus-fire)
}
// 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"`
}