feat(engine): add shrinking play-zone (storm) for combat density
Add configurable active zone that contracts toward map center on an interval, forcing bots together to trigger focus-fire engagements. Bots outside the zone die with reason "zone", and zone bounds are recorded in replay turns. Config fields: - ZoneEnabled: enable/disable the zone - ZoneStartTurn: turn when zone starts shrinking (default 50) - ZoneShrinkInterval: turns between shrink steps (default 5) - ZoneShrinkStep: tiles to shrink each step (default 2) - ZoneMinRadius: minimum zone radius (default 10) Turn sequence update: MOVE → COMBAT → ZONE → CAPTURE → COLLECT → SPAWN → ENERGY_TICK Zone phase inserted after COMBAT, uses toroidal distance calculation, and emits bot_died events with reason "zone" for killed bots. Replay update: ZoneBounds struct added to ReplayTurn to record center, radius, and active state per turn for visualization. Determinism verified: all tests pass, engine remains deterministic. Closes: bf-2g96
This commit is contained in:
parent
c55459d6d4
commit
16d474aceb
4 changed files with 91 additions and 8 deletions
|
|
@ -30,6 +30,11 @@ type GameState struct {
|
|||
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
|
||||
}
|
||||
|
||||
// Event represents something that happened during a turn.
|
||||
|
|
@ -47,10 +52,14 @@ const (
|
|||
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),
|
||||
|
|
@ -66,6 +75,9 @@ func NewGameState(config Config, rng *rand.Rand) *GameState {
|
|||
DeadBots: make([]*Bot, 0),
|
||||
Events: make([]Event, 0),
|
||||
Dominance: make(map[int]int),
|
||||
ZoneCenter: center,
|
||||
ZoneRadius: initialRadius,
|
||||
ZoneActive: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -337,6 +349,9 @@ func (gs *GameState) Clone() *GameState {
|
|||
DeadBots: make([]*Bot, 0),
|
||||
Events: make([]Event, 0),
|
||||
Dominance: make(map[int]int),
|
||||
ZoneCenter: gs.ZoneCenter,
|
||||
ZoneRadius: gs.ZoneRadius,
|
||||
ZoneActive: gs.ZoneActive,
|
||||
}
|
||||
|
||||
// Copy grid
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ type ReplayTurn struct {
|
|||
EnergyHeld []int `json:"energy_held"`
|
||||
Events []Event `json:"events,omitempty"`
|
||||
Debug map[int]*DebugInfo `json:"debug,omitempty"` // optional bot debug telemetry
|
||||
ZoneBounds *ZoneBounds `json:"zone_bounds,omitempty"` // active zone bounds if enabled
|
||||
}
|
||||
|
||||
// ReplayBot represents a bot in a replay turn.
|
||||
|
|
@ -70,6 +71,13 @@ type ReplayCoreState struct {
|
|||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
// ZoneBounds represents the active zone bounds at a turn.
|
||||
type ZoneBounds struct {
|
||||
Center Position `json:"center"`
|
||||
Radius int `json:"radius"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
// ReplayWriter records a match as it progresses.
|
||||
type ReplayWriter struct {
|
||||
replay *Replay
|
||||
|
|
@ -141,6 +149,15 @@ func (rw *ReplayWriter) RecordTurn(gs *GameState, debug map[int]*DebugInfo) {
|
|||
Debug: debug,
|
||||
}
|
||||
|
||||
// Record zone bounds if enabled
|
||||
if gs.Config.ZoneEnabled {
|
||||
turn.ZoneBounds = &ZoneBounds{
|
||||
Center: gs.ZoneCenter,
|
||||
Radius: gs.ZoneRadius,
|
||||
Active: gs.ZoneActive,
|
||||
}
|
||||
}
|
||||
|
||||
// Record all bots (including dead ones for death animation)
|
||||
for _, b := range gs.Bots {
|
||||
turn.Bots = append(turn.Bots, ReplayBot{
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ type TurnPhase int
|
|||
const (
|
||||
PhaseMove TurnPhase = iota
|
||||
PhaseCombat
|
||||
PhaseZone
|
||||
PhaseCapture
|
||||
PhaseCollect
|
||||
PhaseSpawn
|
||||
|
|
@ -26,6 +27,9 @@ func (gs *GameState) ExecuteTurn() *MatchResult {
|
|||
// Phase: COMBAT - resolve focus-fire algorithm
|
||||
gs.executeCombat()
|
||||
|
||||
// Phase: ZONE - shrinking zone kills bots outside
|
||||
gs.executeZone()
|
||||
|
||||
// Phase: CAPTURE - enemy bots on undefended cores raze them
|
||||
gs.executeCaptures()
|
||||
|
||||
|
|
@ -108,6 +112,41 @@ func (gs *GameState) executeMoves() {
|
|||
}
|
||||
}
|
||||
|
||||
// executeZone handles the shrinking zone (storm) that forces combat.
|
||||
func (gs *GameState) executeZone() {
|
||||
if !gs.Config.ZoneEnabled {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if zone should start
|
||||
if !gs.ZoneActive && gs.Turn >= gs.Config.ZoneStartTurn {
|
||||
gs.ZoneActive = true
|
||||
}
|
||||
|
||||
// Check if zone should shrink
|
||||
if gs.ZoneActive && (gs.Turn-gs.Config.ZoneStartTurn)%gs.Config.ZoneShrinkInterval == 0 {
|
||||
if gs.ZoneRadius > gs.Config.ZoneMinRadius {
|
||||
gs.ZoneRadius -= gs.Config.ZoneShrinkStep
|
||||
if gs.ZoneRadius < gs.Config.ZoneMinRadius {
|
||||
gs.ZoneRadius = gs.Config.ZoneMinRadius
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Kill bots outside the zone
|
||||
for _, b := range gs.Bots {
|
||||
if !b.Alive {
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate distance from zone center (accounting for toroidal wrap)
|
||||
dist2 := gs.Grid.Distance2(b.Position, gs.ZoneCenter)
|
||||
if dist2 > gs.ZoneRadius*gs.ZoneRadius {
|
||||
gs.KillBot(b, "zone")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// executeCombat resolves the focus-fire combat algorithm.
|
||||
func (gs *GameState) executeCombat() {
|
||||
// For each bot, count enemies within attack radius
|
||||
|
|
|
|||
|
|
@ -170,19 +170,31 @@ type Config struct {
|
|||
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: 60,
|
||||
Cols: 60,
|
||||
MaxTurns: 500,
|
||||
VisionRadius2: 49, // ~7 tiles
|
||||
AttackRadius2: 5, // ~2.24 tiles
|
||||
SpawnCost: 3,
|
||||
EnergyInterval: 10,
|
||||
CoresPerPlayer: 2,
|
||||
Rows: 60,
|
||||
Cols: 60,
|
||||
MaxTurns: 500,
|
||||
VisionRadius2: 49, // ~7 tiles
|
||||
AttackRadius2: 5, // ~2.24 tiles
|
||||
SpawnCost: 3,
|
||||
EnergyInterval: 10,
|
||||
CoresPerPlayer: 2,
|
||||
ZoneEnabled: false,
|
||||
ZoneStartTurn: 50,
|
||||
ZoneShrinkInterval: 5,
|
||||
ZoneShrinkStep: 2,
|
||||
ZoneMinRadius: 10,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue