feat(engine): add zone escape as Priority 1 to all built-in bots

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
This commit is contained in:
jedarden 2026-05-26 18:47:39 -04:00
parent 64aa6aef40
commit ccdec39c52
3 changed files with 141 additions and 14 deletions

View file

@ -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))],

View file

@ -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 {

View file

@ -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