diff --git a/engine/bot_strategies.go b/engine/bot_strategies.go index a3c60c9..0703211 100644 --- a/engine/bot_strategies.go +++ b/engine/bot_strategies.go @@ -36,9 +36,9 @@ func getZoneEscapeDirection(botPos Position, state *VisibleState) Direction { dist2 := dr*dr + dc*dc radius2 := state.Zone.Radius * state.Zone.Radius - // Safety margin: move toward center if within 2 tiles of zone edge - // This anticipates the shrinking zone and prevents getting caught outside - safetyMargin2 := 4 // (2 tiles)^2 + // Safety margin: move toward center if within 5 tiles of zone edge + // This accounts for zone shrinking (1 tile/turn) and gives time to reach safety + safetyMargin2 := 25 // (5 tiles)^2 - anticipates ~5 turns of zone shrink if dist2 >= radius2-safetyMargin2 { // Move toward center: choose direction that reduces distance bestDir := DirNone @@ -416,16 +416,38 @@ func (b *RusherBot) GetMoves(state *VisibleState) ([]Move, error) { // Priority 2: Before zone starts, collect energy instead of rushing // This prevents early mutual destruction; let the zone force combat if state.Zone == nil || !state.Zone.Active { - // Zone not active yet: collect adjacent energy or hold + // Zone not active yet: collect adjacent energy only if toward center + center := Position{Row: state.Config.Rows / 2, Col: state.Config.Cols / 2} + bestDir := DirNone + bestDist2 := -1 for _, dir := range []Direction{DirN, DirE, DirS, DirW} { adj := simulateMove(bot.Position, dir, config.Rows, config.Cols) - if energyPositions[adj] && !wallPositions[adj] { - moves = append(moves, Move{Position: bot.Position, Direction: dir}) - delete(energyPositions, adj) - goto nextBot + if !wallPositions[adj] && !enemyPositions[adj] { + dr := adj.Row - center.Row + dc := adj.Col - center.Col + dist2 := dr*dr + dc*dc + // Prefer energy if it's toward center + if energyPositions[adj] { + if bestDir == DirNone || dist2 < bestDist2 { + bestDir = dir + bestDist2 = dist2 + } + } else if bestDir == DirNone { + // No energy at this position: consider it as fallback + if dist2 < bestDist2 || bestDist2 == -1 { + bestDir = dir + bestDist2 = dist2 + } + } + } + } + if bestDir != DirNone { + moves = append(moves, Move{Position: bot.Position, Direction: bestDir}) + adjPos := simulateMove(bot.Position, bestDir, config.Rows, config.Cols) + if energyPositions[adjPos] { + delete(energyPositions, adjPos) } } - // No adjacent energy: hold position (don't rush yet) continue } diff --git a/engine/match.go b/engine/match.go index 9d881a7..ca1dee4 100644 --- a/engine/match.go +++ b/engine/match.go @@ -137,6 +137,16 @@ func (mr *MatchRunner) Run() (*MatchResult, *Replay, error) { // Run the match var result *MatchResult for gs.Turn < mr.config.MaxTurns { + // Activate zone BEFORE getting moves on the turn when it starts + // This gives bots a chance to see the zone is active and react + if !gs.ZoneActive && (gs.Turn+1) >= gs.Config.ZoneStartTurn { + if mr.verbose { + mr.logger.Printf("Activating zone at turn %d (next turn will be %d)", gs.Turn, gs.Turn+1) + } + gs.ZoneActive = true + gs.setInitialZoneRadius() + } + // Get moves from all bots concurrently moves := mr.getMovesFromBots(gs) @@ -364,7 +374,7 @@ func (mr *MatchRunner) generateMap(gs *GameState, numPlayers int) { var primaryRadius, secondaryRadius float64 if numPlayers == 2 { - primaryRadius = 0.32 // ~6.4 tiles from center on 40x40 grid (~13 tiles apart) + primaryRadius = 0.28 // ~5.6 tiles from center on 40x40 grid (~11.2 tiles apart) secondaryRadius = 0.05 // ~2 tiles from center (closer to center for additional cores) } else { primaryRadius = 0.10 // ~5 tiles from center on 50x50 grid diff --git a/engine/turn.go b/engine/turn.go index 99cfd35..a8e3ce7 100644 --- a/engine/turn.go +++ b/engine/turn.go @@ -118,21 +118,15 @@ func (gs *GameState) executeZone() { return } - // Check if zone should start - zoneJustStarted := false - if !gs.ZoneActive && gs.Turn >= gs.Config.ZoneStartTurn { - gs.ZoneActive = true - zoneJustStarted = true - // When zone starts, set radius to a fixed initial size based on map dimensions - // This forces bots toward the center regardless of how far they've spread - gs.setInitialZoneRadius() - } + // Zone is now activated BEFORE getting moves (in RunMatch) + // This allows bots to see the zone is active and react accordingly // Zone center is fixed at map center (set in NewGameState) // This forces bots toward the center as the zone shrinks, ensuring contact. // Check if zone should shrink (skip the turn zone starts) - if gs.ZoneActive && !zoneJustStarted && (gs.Turn-gs.Config.ZoneStartTurn)%gs.Config.ZoneShrinkInterval == 0 { + // The zone starts at turn ZoneStartTurn, so we skip shrinking on that turn + if gs.ZoneActive && gs.Turn > gs.Config.ZoneStartTurn && (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 { @@ -193,9 +187,10 @@ func (gs *GameState) setInitialZoneRadius() { distToEdge = halfCols } - // Set initial zone radius to ~55% of the distance from center to edge - // This maximizes combat engagement time while still forcing bots inward - gs.ZoneRadius = (distToEdge * 55) / 100 + // Set initial zone radius to 90% of the distance from center to edge + // This ensures all spawn positions (32% from center) are inside the zone + // Bots have time to react before the zone shrinks to force combat + gs.ZoneRadius = (distToEdge * 90) / 100 // Ensure minimum initial radius of 7 for very small maps if gs.ZoneRadius < 7 {