From ccdec39c52ef0a2ae8d49604acb38a6e6d456c2a Mon Sep 17 00:00:00 2001 From: jedarden Date: Tue, 26 May 2026 18:47:39 -0400 Subject: [PATCH] feat(engine): add zone escape as Priority 1 to all built-in bots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per plan ยง3.7.1, the zone forces bots into contact. This change ensures all built-in bots escape the zone FIRST when threatened (dist to zone edge < 5 tiles), before any other action like energy collection or combat. Changes: - GuardianBot, SwarmBot, HunterBot: Added zone escape as Priority 1 - Phase 13 bots (Defender, Scout, Farmer, Pacifist, Phalanx, Raider, Nomad, Opportunist, Assassin, Kamikaze): Added zone escape as Priority 1 - RandomBot: Added zone escape before random movement The getZoneEscapeDirection function was already present and correctly implements toroidal distance calculation with 5-tile safety margin. Closes: bf-4m78q --- engine/bot_local.go | 10 +++ engine/bot_strategies.go | 32 ++++++++- engine/bot_strategies_phase13.go | 113 ++++++++++++++++++++++++++++--- 3 files changed, 141 insertions(+), 14 deletions(-) diff --git a/engine/bot_local.go b/engine/bot_local.go index f30223d..b71bf95 100644 --- a/engine/bot_local.go +++ b/engine/bot_local.go @@ -76,6 +76,16 @@ func (b *RandomBot) GetMoves(state *VisibleState) ([]Move, error) { for _, bot := range state.Bots { if bot.Owner == state.You.ID { + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + moves = append(moves, Move{ + Position: bot.Position, + Direction: zoneDir, + }) + continue + } + + // Otherwise, move randomly moves = append(moves, Move{ Position: bot.Position, Direction: directions[b.rng.Intn(len(directions))], diff --git a/engine/bot_strategies.go b/engine/bot_strategies.go index 0703211..8b8b43c 100644 --- a/engine/bot_strategies.go +++ b/engine/bot_strategies.go @@ -623,7 +623,7 @@ func (b *GuardianBot) GetMoves(state *VisibleState) ([]Move, error) { usedEnergy := make(map[Position]bool) for _, bot := range myBots { - move := b.computeBotMove(bot, myCores, enemyBots, enemyPositions, energyPositions, usedEnergy, wallPositions, config) + move := b.computeBotMove(bot, myCores, enemyBots, enemyPositions, energyPositions, usedEnergy, wallPositions, config, state) if move != nil { moves = append(moves, *move) } @@ -638,7 +638,13 @@ func (b *GuardianBot) computeBotMove( enemyBots []VisibleBot, enemyPositions, energyPositions, usedEnergy, wallPositions map[Position]bool, config Config, + state *VisibleState, ) *Move { + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + return &Move{Position: bot.Position, Direction: zoneDir} + } + const perimeterRadius = 5 const safeZoneRadius = 10 @@ -806,7 +812,7 @@ func (b *SwarmBot) GetMoves(state *VisibleState) ([]Move, error) { claimed := make(map[Position]bool) // destinations already claimed by a friendly bot this turn for _, bot := range myBots { - move := b.computeBotMove(bot, myBotPositions, enemyPositions, energyPositions, swarmCenter, enemyCenter, wallPositions, claimed, config, len(myBots)) + move := b.computeBotMove(bot, myBotPositions, enemyPositions, energyPositions, swarmCenter, enemyCenter, wallPositions, claimed, config, len(myBots), state) if move != nil { dest := simulateMove(bot.Position, move.Direction, config.Rows, config.Cols) claimed[dest] = true @@ -859,10 +865,16 @@ func (b *SwarmBot) computeBotMove( wallPositions, claimed map[Position]bool, config Config, friendlyCount int, + state *VisibleState, ) *Move { + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + return &Move{Position: bot.Position, Direction: zoneDir} + } + // Solo mode: when alone or with very few units, gather energy to build the swarm if friendlyCount <= 2 { - return b.soloMove(bot, energyPositions, enemyPositions, wallPositions, config) + return b.soloMove(bot, energyPositions, enemyPositions, wallPositions, config, state) } // Target is enemy center if visible, otherwise map center @@ -942,6 +954,7 @@ func (b *SwarmBot) soloMove( bot VisibleBot, energyPositions, enemyPositions, wallPositions map[Position]bool, config Config, + state *VisibleState, ) *Move { bestDir := DirNone bestScore := -math.MaxFloat64 @@ -1099,6 +1112,13 @@ func (b *HunterBot) GetMoves(state *VisibleState) ([]Move, error) { break } + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + moves = append(moves, Move{Position: bot.Position, Direction: zoneDir}) + assignedHunters[bot.Position] = true + continue + } + // Check if this bot is close enough to be a hunter dist := distance2(bot.Position, target.Position, config.Rows, config.Cols) if dist < 400 { // Within ~20 tiles @@ -1120,6 +1140,12 @@ func (b *HunterBot) GetMoves(state *VisibleState) ([]Move, error) { continue } + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + moves = append(moves, Move{Position: bot.Position, Direction: zoneDir}) + continue + } + // Try to gather energy nearestEnergy, nearestDist := Position{}, math.MaxInt32 for pos := range energyPositions { diff --git a/engine/bot_strategies_phase13.go b/engine/bot_strategies_phase13.go index dc40545..12da89e 100644 --- a/engine/bot_strategies_phase13.go +++ b/engine/bot_strategies_phase13.go @@ -60,12 +60,17 @@ func (b *DefenderBot) GetMoves(state *VisibleState) ([]Move, error) { var dir Direction - // Priority 1: Intercept nearby enemies - if nearestEnemy != nil && enemyDist <= 50 { + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + dir = zoneDir + } + + // Priority 2: Intercept nearby enemies + if dir == DirNone && nearestEnemy != nil && enemyDist <= 50 { dir = moveToward(bot.Position, *nearestEnemy, wallSet, claimed, config) } - // Priority 2: Return to core perimeter if too far + // Priority 3: Return to core perimeter if too far if dir == DirNone && coreDist > perimeterRadius2 { nearestCore, _ := findNearestPos(bot.Position, coreSet, config) if nearestCore != nil { @@ -73,7 +78,7 @@ func (b *DefenderBot) GetMoves(state *VisibleState) ([]Move, error) { } } - // Priority 3: Gather energy within perimeter + // Priority 4: Gather energy within perimeter if dir == DirNone && len(energySet) > 0 { nearestEnergy, _ := findNearestPos(bot.Position, energySet, config) if nearestEnergy != nil { @@ -81,7 +86,7 @@ func (b *DefenderBot) GetMoves(state *VisibleState) ([]Move, error) { } } - // Priority 4: Patrol near core + // Priority 5: Patrol near core if dir == DirNone && len(coreSet) > 0 { nearestCore, _ := findNearestPos(bot.Position, coreSet, config) if nearestCore != nil { @@ -149,6 +154,16 @@ func (b *ScoutBot) GetMoves(state *VisibleState) ([]Move, error) { claimed := make(map[Position]bool) for _, bot := range myBots { + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + dest := simulateMove(bot.Position, zoneDir, config.Rows, config.Cols) + if !claimed[dest] { + claimed[dest] = true + moves = append(moves, Move{Position: bot.Position, Direction: zoneDir}) + continue + } + } + if shouldFleeFromEnemies(bot.Position, enemySet, config) { dir := fleeDirection(bot.Position, enemySet, wallSet, config) if dir != DirNone { @@ -244,7 +259,12 @@ func (b *FarmerBot) GetMoves(state *VisibleState) ([]Move, error) { for _, bot := range myBots { var dir Direction - if shouldFleeFromEnemies(bot.Position, enemySet, config) { + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + dir = zoneDir + } + + if dir == DirNone && shouldFleeFromEnemies(bot.Position, enemySet, config) { dir = fleeDirection(bot.Position, enemySet, wallSet, config) } @@ -318,6 +338,18 @@ func (b *PacifistBot) GetMoves(state *VisibleState) ([]Move, error) { sortBotsByEnemyDist(myBots, enemySet, config) for _, bot := range myBots { + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + dest := simulateMove(bot.Position, zoneDir, config.Rows, config.Cols) + if !claimed[dest] && !wallSet[dest] { + claimed[dest] = true + moves = append(moves, Move{Position: bot.Position, Direction: zoneDir}) + } else { + claimed[bot.Position] = true + } + continue + } + bestDir := DirNone bestScore := float64(math.MinInt64) @@ -394,6 +426,18 @@ func (b *PhalanxBot) GetMoves(state *VisibleState) ([]Move, error) { claimed := make(map[Position]bool) for _, bot := range myBots { + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + dest := simulateMove(bot.Position, zoneDir, config.Rows, config.Cols) + if !claimed[dest] && !wallSet[dest] { + claimed[dest] = true + moves = append(moves, Move{Position: bot.Position, Direction: zoneDir}) + } else { + claimed[bot.Position] = true + } + continue + } + bestDir := DirNone bestScore := float64(math.MinInt64) @@ -472,6 +516,18 @@ func (b *RaiderBot) GetMoves(state *VisibleState) ([]Move, error) { if assigned[bot.Position] { continue } + + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + dest := simulateMove(bot.Position, zoneDir, config.Rows, config.Cols) + if !claimed[dest] { + claimed[dest] = true + moves = append(moves, Move{Position: bot.Position, Direction: zoneDir}) + assigned[bot.Position] = true + } + continue + } + d := distance2(bot.Position, target.Position, config.Rows, config.Cols) if d < 400 { turns := b.engagementTurns[bot.Position] @@ -503,8 +559,15 @@ func (b *RaiderBot) GetMoves(state *VisibleState) ([]Move, error) { if assigned[bot.Position] { continue } - dir := DirNone - if len(energySet) > 0 { + + var dir Direction + + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + dir = zoneDir + } + + if dir == DirNone && len(energySet) > 0 { nearest, _ := findNearestPos(bot.Position, energySet, config) if nearest != nil { dir = moveToward(bot.Position, *nearest, wallSet, claimed, config) @@ -592,7 +655,12 @@ func (b *NomadBot) GetMoves(state *VisibleState) ([]Move, error) { for _, bot := range myBots { var dir Direction - if shouldFleeFromEnemies(bot.Position, enemySet, config) { + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + dir = zoneDir + } + + if dir == DirNone && shouldFleeFromEnemies(bot.Position, enemySet, config) { dir = fleeDirection(bot.Position, enemySet, wallSet, config) } @@ -698,7 +766,12 @@ func (b *OpportunistBot) GetMoves(state *VisibleState) ([]Move, error) { for _, bot := range myBots { var dir Direction - if bestTarget != nil { + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + dir = zoneDir + } + + if dir == DirNone && bestTarget != nil { dir = moveToward(bot.Position, *bestTarget, wallSet, claimed, config) } @@ -790,7 +863,13 @@ func (b *AssassinBot) GetMoves(state *VisibleState) ([]Move, error) { for _, bot := range myBots { var dir Direction - if target != nil { + + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + dir = zoneDir + } + + if dir == DirNone && target != nil { dir = moveToward(bot.Position, *target, wallSet, claimed, config) } if dir == DirNone { @@ -840,6 +919,18 @@ func (b *KamikazeBot) GetMoves(state *VisibleState) ([]Move, error) { sortBotsByEnemyDist(myBots, enemySet, config) for _, bot := range myBots { + // Priority 1: Escape zone if threatened + if zoneDir := getZoneEscapeDirection(bot.Position, state); zoneDir != DirNone { + dest := simulateMove(bot.Position, zoneDir, config.Rows, config.Cols) + if !claimed[dest] && !wallSet[dest] { + claimed[dest] = true + moves = append(moves, Move{Position: bot.Position, Direction: zoneDir}) + } else { + claimed[bot.Position] = true + } + continue + } + nearestEnemy, _ := findNearestPos(bot.Position, enemySet, config) bestDir := DirNone