getZoneEscapeDirection now accepts wallSet parameter and skips directions that would move into walls. This prevents bots from getting trapped by walls when trying to escape the shrinking zone, allowing them to survive longer and actually engage in combat instead of dying to zone. Testing with RusherBot vs SwarmBot shows 85% combat density (target: 65-80%). Fixes: RandomBot getting stuck against walls and dying to zone without engaging in combat. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1304 lines
33 KiB
Go
1304 lines
33 KiB
Go
package engine
|
|
|
|
import (
|
|
"container/list"
|
|
"math"
|
|
"math/rand"
|
|
)
|
|
|
|
// getZoneEscapeDirection returns the direction toward the zone center if the bot is outside
|
|
// or near the edge of the safe zone radius. Returns DirNone if the bot is safe or zone is disabled.
|
|
// Avoids walls when choosing the escape direction.
|
|
func getZoneEscapeDirection(botPos Position, state *VisibleState, wallSet map[Position]bool) Direction {
|
|
if state.Zone == nil || !state.Zone.Active {
|
|
return DirNone
|
|
}
|
|
|
|
// Calculate distance from zone center (toroidal)
|
|
rows := state.Config.Rows
|
|
cols := state.Config.Cols
|
|
center := state.Zone.Center
|
|
|
|
dr := botPos.Row - center.Row
|
|
dc := botPos.Col - center.Col
|
|
|
|
// Account for wrapping
|
|
if dr > rows/2 {
|
|
dr -= rows
|
|
} else if dr < -rows/2 {
|
|
dr += rows
|
|
}
|
|
if dc > cols/2 {
|
|
dc -= cols
|
|
} else if dc < -cols/2 {
|
|
dc += cols
|
|
}
|
|
|
|
dist2 := dr*dr + dc*dc
|
|
radius2 := state.Zone.Radius * state.Zone.Radius
|
|
|
|
// 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 and avoids walls
|
|
bestDir := DirNone
|
|
bestReduction := 0
|
|
|
|
for _, dir := range []Direction{DirN, DirE, DirS, DirW} {
|
|
ddr, ddc := dir.Delta()
|
|
newPos := Position{
|
|
Row: ((botPos.Row+ddr)%rows + rows) % rows,
|
|
Col: ((botPos.Col+ddc)%cols + cols) % cols,
|
|
}
|
|
|
|
// Skip if blocked by wall (only check if wallSet is provided)
|
|
if wallSet != nil && wallSet[newPos] {
|
|
continue
|
|
}
|
|
// If wallSet is nil, assume all tiles are passable (RandomBot fallback)
|
|
// This is safe because the engine will ignore moves into walls
|
|
|
|
newDr := newPos.Row - center.Row
|
|
newDc := newPos.Col - center.Col
|
|
|
|
// Account for wrapping
|
|
if newDr > rows/2 {
|
|
newDr -= rows
|
|
} else if newDr < -rows/2 {
|
|
newDr += rows
|
|
}
|
|
if newDc > cols/2 {
|
|
newDc -= cols
|
|
} else if newDc < -cols/2 {
|
|
newDc += cols
|
|
}
|
|
|
|
newDist2 := newDr*newDr + newDc*newDc
|
|
reduction := dist2 - newDist2
|
|
|
|
if reduction > bestReduction {
|
|
bestReduction = reduction
|
|
bestDir = dir
|
|
}
|
|
}
|
|
|
|
return bestDir
|
|
}
|
|
|
|
return DirNone
|
|
}
|
|
|
|
// GathererBot prioritizes energy collection while avoiding combat.
|
|
type GathererBot struct {
|
|
rng *rand.Rand
|
|
}
|
|
|
|
// NewGathererBot creates a new gatherer bot.
|
|
func NewGathererBot(seed int64) *GathererBot {
|
|
return &GathererBot{
|
|
rng: rand.New(rand.NewSource(seed)),
|
|
}
|
|
}
|
|
|
|
// GetMoves returns moves focused on gathering energy.
|
|
func (b *GathererBot) GetMoves(state *VisibleState) ([]Move, error) {
|
|
if len(state.Bots) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
myID := state.You.ID
|
|
config := state.Config
|
|
|
|
// Separate my bots from enemies
|
|
myBots := make([]VisibleBot, 0)
|
|
enemyBots := make([]VisibleBot, 0)
|
|
for _, bot := range state.Bots {
|
|
if bot.Owner == myID {
|
|
myBots = append(myBots, bot)
|
|
} else {
|
|
enemyBots = append(enemyBots, bot)
|
|
}
|
|
}
|
|
|
|
// Build lookup maps
|
|
enemyPositions := make(map[Position]bool)
|
|
for _, enemy := range enemyBots {
|
|
enemyPositions[enemy.Position] = true
|
|
}
|
|
|
|
energyPositions := make(map[Position]bool)
|
|
for _, e := range state.Energy {
|
|
energyPositions[e] = true
|
|
}
|
|
|
|
wallPositions := make(map[Position]bool)
|
|
for _, w := range state.Walls {
|
|
wallPositions[w] = true
|
|
}
|
|
|
|
moves := make([]Move, 0, len(myBots))
|
|
usedEnergy := make(map[Position]bool)
|
|
|
|
for _, bot := range myBots {
|
|
move := b.computeBotMove(bot, myBots, enemyPositions, energyPositions, usedEnergy, wallPositions, config, state)
|
|
if move != nil {
|
|
moves = append(moves, *move)
|
|
}
|
|
}
|
|
|
|
return moves, nil
|
|
}
|
|
|
|
func (b *GathererBot) computeBotMove(
|
|
bot VisibleBot,
|
|
myBots []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, wallPositions); zoneDir != DirNone {
|
|
return &Move{
|
|
Position: bot.Position,
|
|
Direction: zoneDir,
|
|
}
|
|
}
|
|
|
|
// Priority 2: Check if we should flee from enemies
|
|
if b.shouldFlee(bot.Position, enemyPositions, config) {
|
|
fleeDir := b.getFleeDirection(bot.Position, enemyPositions, wallPositions, config)
|
|
if fleeDir != DirNone {
|
|
return &Move{
|
|
Position: bot.Position,
|
|
Direction: fleeDir,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find nearest untargeted energy
|
|
_, path := b.findNearestEnergy(bot.Position, energyPositions, usedEnergy, enemyPositions, wallPositions, config)
|
|
if path != nil && len(path) > 0 {
|
|
return &Move{
|
|
Position: bot.Position,
|
|
Direction: path[0],
|
|
}
|
|
}
|
|
|
|
// No energy visible - spread out to explore
|
|
return b.getExploreMove(bot.Position, myBots, enemyPositions, wallPositions, config)
|
|
}
|
|
|
|
func (b *GathererBot) shouldFlee(pos Position, enemyPositions map[Position]bool, config Config) bool {
|
|
for enemyPos := range enemyPositions {
|
|
dist2 := distance2(pos, enemyPos, config.Rows, config.Cols)
|
|
if dist2 <= config.AttackRadius2+4 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (b *GathererBot) getFleeDirection(pos Position, enemyPositions, wallPositions map[Position]bool, config Config) Direction {
|
|
// Calculate center of mass of enemies
|
|
enemyCenter := Position{Row: 0, Col: 0}
|
|
count := 0
|
|
for enemyPos := range enemyPositions {
|
|
enemyCenter.Row += enemyPos.Row
|
|
enemyCenter.Col += enemyPos.Col
|
|
count++
|
|
}
|
|
if count > 0 {
|
|
enemyCenter.Row /= count
|
|
enemyCenter.Col /= count
|
|
}
|
|
|
|
// Move away from enemy center
|
|
directions := []Direction{DirN, DirE, DirS, DirW}
|
|
bestDir := DirN
|
|
bestDist := -1
|
|
|
|
for _, dir := range directions {
|
|
newPos := simulateMove(pos, dir, config.Rows, config.Cols)
|
|
if wallPositions[newPos] {
|
|
continue
|
|
}
|
|
dist := distance2(newPos, enemyCenter, config.Rows, config.Cols)
|
|
if dist > bestDist {
|
|
bestDist = dist
|
|
bestDir = dir
|
|
}
|
|
}
|
|
|
|
return bestDir
|
|
}
|
|
|
|
func (b *GathererBot) findNearestEnergy(
|
|
start Position,
|
|
energyPositions, usedEnergy, enemyPositions, wallPositions map[Position]bool,
|
|
config Config,
|
|
) (Position, []Direction) {
|
|
type queueItem struct {
|
|
pos Position
|
|
path []Direction
|
|
}
|
|
|
|
visited := make(map[Position]bool)
|
|
queue := list.New()
|
|
queue.PushBack(queueItem{pos: start, path: []Direction{}})
|
|
|
|
var nearestEnergy Position
|
|
var bestPath []Direction
|
|
|
|
for queue.Len() > 0 {
|
|
item := queue.Remove(queue.Front()).(queueItem)
|
|
pos := item.pos
|
|
path := item.path
|
|
|
|
if visited[pos] {
|
|
continue
|
|
}
|
|
visited[pos] = true
|
|
|
|
// Check if this position has untargeted energy
|
|
if energyPositions[pos] && !usedEnergy[pos] {
|
|
nearestEnergy = pos
|
|
bestPath = path
|
|
break
|
|
}
|
|
|
|
// Don't path through enemy-adjacent tiles
|
|
if len(path) > 0 && b.isNearEnemy(pos, enemyPositions, config) {
|
|
continue
|
|
}
|
|
|
|
// Explore neighbors
|
|
directions := []Direction{DirN, DirE, DirS, DirW}
|
|
for _, dir := range directions {
|
|
nextPos := simulateMove(pos, dir, config.Rows, config.Cols)
|
|
if !visited[nextPos] && !wallPositions[nextPos] {
|
|
newPath := make([]Direction, len(path)+1)
|
|
copy(newPath, path)
|
|
newPath[len(path)] = dir
|
|
queue.PushBack(queueItem{pos: nextPos, path: newPath})
|
|
}
|
|
}
|
|
}
|
|
|
|
return nearestEnergy, bestPath
|
|
}
|
|
|
|
func (b *GathererBot) isNearEnemy(pos Position, enemyPositions map[Position]bool, config Config) bool {
|
|
directions := []Direction{DirN, DirE, DirS, DirW}
|
|
for _, dir := range directions {
|
|
adj := simulateMove(pos, dir, config.Rows, config.Cols)
|
|
if enemyPositions[adj] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (b *GathererBot) getExploreMove(
|
|
pos Position,
|
|
myBots []VisibleBot,
|
|
enemyPositions, wallPositions map[Position]bool,
|
|
config Config,
|
|
) *Move {
|
|
// Explore toward map center — that's where energy and enemies are
|
|
center := Position{Row: config.Rows / 2, Col: config.Cols / 2}
|
|
|
|
bestDir := DirNone
|
|
bestScore := -999999.0
|
|
|
|
for _, dir := range []Direction{DirN, DirE, DirS, DirW} {
|
|
newPos := simulateMove(pos, dir, config.Rows, config.Cols)
|
|
|
|
if wallPositions[newPos] {
|
|
continue
|
|
}
|
|
|
|
if b.isNearEnemy(newPos, enemyPositions, config) {
|
|
continue
|
|
}
|
|
|
|
score := 0.0
|
|
|
|
// Move toward center
|
|
distToCenter := float64(distance2(newPos, center, config.Rows, config.Cols))
|
|
currentDist := float64(distance2(pos, center, config.Rows, config.Cols))
|
|
score += (currentDist - distToCenter) * 5
|
|
|
|
// Spread out from other bots
|
|
for _, other := range myBots {
|
|
if other.Position != pos {
|
|
dist := float64(distance2(newPos, other.Position, config.Rows, config.Cols))
|
|
score += dist * 0.5
|
|
}
|
|
}
|
|
|
|
// Add slight randomness to avoid getting stuck
|
|
score += b.rng.Float64() * 2
|
|
|
|
if score > bestScore {
|
|
bestScore = score
|
|
bestDir = dir
|
|
}
|
|
}
|
|
|
|
if bestDir != DirNone {
|
|
return &Move{Position: pos, Direction: bestDir}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RusherBot aggressively rushes toward enemy cores.
|
|
type RusherBot struct {
|
|
rng *rand.Rand
|
|
knownEnemyCores map[Position]bool
|
|
}
|
|
|
|
// NewRusherBot creates a new rusher bot.
|
|
func NewRusherBot(seed int64) *RusherBot {
|
|
return &RusherBot{
|
|
rng: rand.New(rand.NewSource(seed)),
|
|
knownEnemyCores: make(map[Position]bool),
|
|
}
|
|
}
|
|
|
|
// GetMoves returns moves rushing toward enemy cores.
|
|
func (b *RusherBot) GetMoves(state *VisibleState) ([]Move, error) {
|
|
myID := state.You.ID
|
|
config := state.Config
|
|
|
|
// Update known enemy cores
|
|
for _, core := range state.Cores {
|
|
if core.Owner != myID && core.Active {
|
|
b.knownEnemyCores[core.Position] = true
|
|
}
|
|
}
|
|
|
|
// Separate my bots from enemies
|
|
myBots := make([]VisibleBot, 0)
|
|
enemyBots := make([]VisibleBot, 0)
|
|
for _, bot := range state.Bots {
|
|
if bot.Owner == myID {
|
|
myBots = append(myBots, bot)
|
|
} else {
|
|
enemyBots = append(enemyBots, bot)
|
|
}
|
|
}
|
|
|
|
if len(myBots) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Build lookup maps
|
|
enemyPositions := make(map[Position]bool)
|
|
for _, enemy := range enemyBots {
|
|
enemyPositions[enemy.Position] = true
|
|
}
|
|
|
|
wallPositions := make(map[Position]bool)
|
|
for _, w := range state.Walls {
|
|
wallPositions[w] = true
|
|
}
|
|
|
|
energyPositions := make(map[Position]bool)
|
|
for _, e := range state.Energy {
|
|
energyPositions[e] = true
|
|
}
|
|
|
|
// Find targets to rush
|
|
targets := b.getRushTargets(state, myID)
|
|
|
|
moves := make([]Move, 0, len(myBots))
|
|
|
|
for _, bot := range myBots {
|
|
// Priority 1: Escape zone if threatened
|
|
if zoneDir := getZoneEscapeDirection(bot.Position, state, wallPositions); zoneDir != DirNone {
|
|
moves = append(moves, Move{Position: bot.Position, Direction: zoneDir})
|
|
continue
|
|
}
|
|
|
|
// 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 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 !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)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Priority 3: Opportunistic: grab adjacent energy while rushing
|
|
if len(myBots) <= 2 {
|
|
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 dir := b.findBestMove(bot.Position, targets, enemyPositions, wallPositions, config); dir != DirNone {
|
|
moves = append(moves, Move{
|
|
Position: bot.Position,
|
|
Direction: dir,
|
|
})
|
|
}
|
|
nextBot:
|
|
}
|
|
|
|
return moves, nil
|
|
}
|
|
|
|
func (b *RusherBot) getRushTargets(state *VisibleState, myID int) []Position {
|
|
targets := make([]Position, 0)
|
|
|
|
// First priority: visible enemy cores
|
|
for _, core := range state.Cores {
|
|
if core.Owner != myID && core.Active {
|
|
targets = append(targets, core.Position)
|
|
}
|
|
}
|
|
|
|
// Add known enemy cores from previous turns
|
|
for pos := range b.knownEnemyCores {
|
|
found := false
|
|
for _, t := range targets {
|
|
if t == pos {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
targets = append(targets, pos)
|
|
}
|
|
}
|
|
|
|
// If no targets, explore center of map
|
|
if len(targets) == 0 {
|
|
targets = append(targets, Position{Row: state.Config.Rows / 2, Col: state.Config.Cols / 2})
|
|
}
|
|
|
|
return targets
|
|
}
|
|
|
|
func (b *RusherBot) findBestMove(
|
|
start Position,
|
|
targets []Position,
|
|
enemyPositions, wallPositions map[Position]bool,
|
|
config Config,
|
|
) Direction {
|
|
// BFS to find shortest path to any target
|
|
type queueItem struct {
|
|
pos Position
|
|
firstDir Direction
|
|
}
|
|
|
|
visited := make(map[Position]bool)
|
|
queue := list.New()
|
|
queue.PushBack(queueItem{pos: start, firstDir: DirNone})
|
|
visited[start] = true
|
|
|
|
for queue.Len() > 0 {
|
|
item := queue.Remove(queue.Front()).(queueItem)
|
|
pos := item.pos
|
|
|
|
// Check if we've reached a target
|
|
for _, target := range targets {
|
|
if pos == target {
|
|
return item.firstDir
|
|
}
|
|
}
|
|
|
|
// Explore neighbors
|
|
for _, dir := range []Direction{DirN, DirE, DirS, DirW} {
|
|
next := simulateMove(pos, dir, config.Rows, config.Cols)
|
|
|
|
if visited[next] || wallPositions[next] || enemyPositions[next] {
|
|
continue
|
|
}
|
|
|
|
visited[next] = true
|
|
firstDir := item.firstDir
|
|
if firstDir == DirNone {
|
|
firstDir = dir
|
|
}
|
|
queue.PushBack(queueItem{pos: next, firstDir: firstDir})
|
|
}
|
|
}
|
|
|
|
// No path found - pick any valid direction
|
|
for _, dir := range []Direction{DirN, DirE, DirS, DirW} {
|
|
next := simulateMove(start, dir, config.Rows, config.Cols)
|
|
if !wallPositions[next] && !enemyPositions[next] {
|
|
return dir
|
|
}
|
|
}
|
|
|
|
return DirN
|
|
}
|
|
|
|
// GuardianBot defends cores with cautious expansion.
|
|
type GuardianBot struct {
|
|
rng *rand.Rand
|
|
}
|
|
|
|
// NewGuardianBot creates a new guardian bot.
|
|
func NewGuardianBot(seed int64) *GuardianBot {
|
|
return &GuardianBot{
|
|
rng: rand.New(rand.NewSource(seed)),
|
|
}
|
|
}
|
|
|
|
// GetMoves returns moves focused on defense and cautious gathering.
|
|
func (b *GuardianBot) GetMoves(state *VisibleState) ([]Move, error) {
|
|
myID := state.You.ID
|
|
config := state.Config
|
|
|
|
// Separate bots
|
|
myBots := make([]VisibleBot, 0)
|
|
enemyBots := make([]VisibleBot, 0)
|
|
for _, bot := range state.Bots {
|
|
if bot.Owner == myID {
|
|
myBots = append(myBots, bot)
|
|
} else {
|
|
enemyBots = append(enemyBots, bot)
|
|
}
|
|
}
|
|
|
|
if len(myBots) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Find my cores
|
|
myCores := make([]VisibleCore, 0)
|
|
for _, core := range state.Cores {
|
|
if core.Owner == myID && core.Active {
|
|
myCores = append(myCores, core)
|
|
}
|
|
}
|
|
|
|
// Build lookup maps
|
|
enemyPositions := make(map[Position]bool)
|
|
for _, enemy := range enemyBots {
|
|
enemyPositions[enemy.Position] = true
|
|
}
|
|
|
|
energyPositions := make(map[Position]bool)
|
|
for _, e := range state.Energy {
|
|
energyPositions[e] = true
|
|
}
|
|
|
|
wallPositions := make(map[Position]bool)
|
|
for _, w := range state.Walls {
|
|
wallPositions[w] = true
|
|
}
|
|
|
|
moves := make([]Move, 0, len(myBots))
|
|
usedEnergy := make(map[Position]bool)
|
|
|
|
for _, bot := range myBots {
|
|
move := b.computeBotMove(bot, myCores, enemyBots, enemyPositions, energyPositions, usedEnergy, wallPositions, config, state)
|
|
if move != nil {
|
|
moves = append(moves, *move)
|
|
}
|
|
}
|
|
|
|
return moves, nil
|
|
}
|
|
|
|
func (b *GuardianBot) computeBotMove(
|
|
bot VisibleBot,
|
|
myCores []VisibleCore,
|
|
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, wallPositions); zoneDir != DirNone {
|
|
return &Move{Position: bot.Position, Direction: zoneDir}
|
|
}
|
|
|
|
const perimeterRadius = 5
|
|
const safeZoneRadius = 10
|
|
|
|
// Find nearest threatening enemy
|
|
nearestEnemy, nearestEnemyDist := b.findNearestEnemy(bot.Position, enemyBots, config)
|
|
|
|
// If enemy is close, intercept
|
|
if nearestEnemy != nil && nearestEnemyDist <= 50 {
|
|
dir := b.getDirectionToward(bot.Position, nearestEnemy.Position, wallPositions, config)
|
|
if dir != DirNone {
|
|
return &Move{Position: bot.Position, Direction: dir}
|
|
}
|
|
}
|
|
|
|
// Check if within safe zone of a core
|
|
inSafeZone := false
|
|
var nearestCore *VisibleCore
|
|
nearestCoreDist := math.MaxInt32
|
|
|
|
for i := range myCores {
|
|
core := &myCores[i]
|
|
dist := distance2(bot.Position, core.Position, config.Rows, config.Cols)
|
|
if dist < nearestCoreDist {
|
|
nearestCoreDist = dist
|
|
nearestCore = core
|
|
}
|
|
if dist <= safeZoneRadius*safeZoneRadius {
|
|
inSafeZone = true
|
|
}
|
|
}
|
|
|
|
// If outside perimeter, move toward nearest core
|
|
if nearestCore != nil && nearestCoreDist > perimeterRadius*perimeterRadius {
|
|
dir := b.getDirectionToward(bot.Position, nearestCore.Position, wallPositions, config)
|
|
if dir != DirNone {
|
|
return &Move{Position: bot.Position, Direction: dir}
|
|
}
|
|
}
|
|
|
|
// Gather energy within safe zone
|
|
if inSafeZone {
|
|
// Find nearest energy
|
|
nearestEnergy, nearestEnergyDist := Position{}, math.MaxInt32
|
|
for pos := range energyPositions {
|
|
if usedEnergy[pos] {
|
|
continue
|
|
}
|
|
dist := distance2(bot.Position, pos, config.Rows, config.Cols)
|
|
if dist < nearestEnergyDist {
|
|
nearestEnergyDist = dist
|
|
nearestEnergy = pos
|
|
}
|
|
}
|
|
|
|
if nearestEnergyDist < math.MaxInt32 {
|
|
usedEnergy[nearestEnergy] = true
|
|
dir := b.getDirectionToward(bot.Position, nearestEnergy, wallPositions, config)
|
|
if dir != DirNone {
|
|
return &Move{Position: bot.Position, Direction: dir}
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *GuardianBot) findNearestEnemy(pos Position, enemies []VisibleBot, config Config) (*VisibleBot, int) {
|
|
var nearest *VisibleBot
|
|
nearestDist := math.MaxInt32
|
|
|
|
for i := range enemies {
|
|
dist := distance2(pos, enemies[i].Position, config.Rows, config.Cols)
|
|
if dist < nearestDist {
|
|
nearestDist = dist
|
|
nearest = &enemies[i]
|
|
}
|
|
}
|
|
|
|
return nearest, nearestDist
|
|
}
|
|
|
|
func (b *GuardianBot) getDirectionToward(from, to Position, wallPositions map[Position]bool, config Config) Direction {
|
|
bestDir := DirNone
|
|
bestDist := math.MaxInt32
|
|
|
|
for _, dir := range []Direction{DirN, DirE, DirS, DirW} {
|
|
newPos := simulateMove(from, dir, config.Rows, config.Cols)
|
|
if wallPositions[newPos] {
|
|
continue
|
|
}
|
|
dist := distance2(newPos, to, config.Rows, config.Cols)
|
|
if dist < bestDist {
|
|
bestDist = dist
|
|
bestDir = dir
|
|
}
|
|
}
|
|
|
|
return bestDir
|
|
}
|
|
|
|
// SwarmBot moves as a coordinated formation.
|
|
type SwarmBot struct {
|
|
rng *rand.Rand
|
|
}
|
|
|
|
// NewSwarmBot creates a new swarm bot.
|
|
func NewSwarmBot(seed int64) *SwarmBot {
|
|
return &SwarmBot{
|
|
rng: rand.New(rand.NewSource(seed)),
|
|
}
|
|
}
|
|
|
|
// GetMoves returns formation-based moves toward enemies.
|
|
func (b *SwarmBot) GetMoves(state *VisibleState) ([]Move, error) {
|
|
myID := state.You.ID
|
|
config := state.Config
|
|
|
|
// Separate bots
|
|
myBots := make([]VisibleBot, 0)
|
|
enemyBots := make([]VisibleBot, 0)
|
|
for _, bot := range state.Bots {
|
|
if bot.Owner == myID {
|
|
myBots = append(myBots, bot)
|
|
} else {
|
|
enemyBots = append(enemyBots, bot)
|
|
}
|
|
}
|
|
|
|
if len(myBots) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Build lookup maps
|
|
enemyPositions := make(map[Position]bool)
|
|
for _, enemy := range enemyBots {
|
|
enemyPositions[enemy.Position] = true
|
|
}
|
|
|
|
wallPositions := make(map[Position]bool)
|
|
for _, w := range state.Walls {
|
|
wallPositions[w] = true
|
|
}
|
|
|
|
myBotPositions := make(map[Position]bool)
|
|
for _, bot := range myBots {
|
|
myBotPositions[bot.Position] = true
|
|
}
|
|
|
|
energyPositions := make(map[Position]bool)
|
|
for _, e := range state.Energy {
|
|
energyPositions[e] = true
|
|
}
|
|
|
|
// Calculate swarm center
|
|
swarmCenter := b.calculateCenter(myBots, config)
|
|
|
|
// Calculate enemy center if visible
|
|
var enemyCenter *Position
|
|
if len(enemyBots) > 0 {
|
|
center := b.calculateCenter(enemyBots, config)
|
|
enemyCenter = ¢er
|
|
}
|
|
|
|
moves := make([]Move, 0, len(myBots))
|
|
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), state)
|
|
if move != nil {
|
|
dest := simulateMove(bot.Position, move.Direction, config.Rows, config.Cols)
|
|
claimed[dest] = true
|
|
moves = append(moves, *move)
|
|
} else {
|
|
// Bot holds position — claim its current tile
|
|
claimed[bot.Position] = true
|
|
}
|
|
}
|
|
|
|
return moves, nil
|
|
}
|
|
|
|
const cohesionRadius2 = 9 // 3 tiles squared
|
|
|
|
func (b *SwarmBot) calculateCenter(bots []VisibleBot, config Config) Position {
|
|
if len(bots) == 0 {
|
|
return Position{Row: config.Rows / 2, Col: config.Cols / 2}
|
|
}
|
|
|
|
// Use circular mean for toroidal coordinates
|
|
sumSinRow, sumCosRow := 0.0, 0.0
|
|
sumSinCol, sumCosCol := 0.0, 0.0
|
|
|
|
rowScale := (2 * math.Pi) / float64(config.Rows)
|
|
colScale := (2 * math.Pi) / float64(config.Cols)
|
|
|
|
for _, bot := range bots {
|
|
sumSinRow += math.Sin(float64(bot.Position.Row) * rowScale)
|
|
sumCosRow += math.Cos(float64(bot.Position.Row) * rowScale)
|
|
sumSinCol += math.Sin(float64(bot.Position.Col) * colScale)
|
|
sumCosCol += math.Cos(float64(bot.Position.Col) * colScale)
|
|
}
|
|
|
|
n := float64(len(bots))
|
|
avgRow := math.Atan2(sumSinRow/n, sumCosRow/n) / rowScale
|
|
avgCol := math.Atan2(sumSinCol/n, sumCosCol/n) / colScale
|
|
|
|
row := int(math.Mod(math.Mod(avgRow, float64(config.Rows))+float64(config.Rows), float64(config.Rows)))
|
|
col := int(math.Mod(math.Mod(avgCol, float64(config.Cols))+float64(config.Cols), float64(config.Cols)))
|
|
|
|
return Position{Row: row, Col: col}
|
|
}
|
|
|
|
func (b *SwarmBot) computeBotMove(
|
|
bot VisibleBot,
|
|
myBotPositions, enemyPositions, energyPositions map[Position]bool,
|
|
swarmCenter Position,
|
|
enemyCenter *Position,
|
|
wallPositions, claimed map[Position]bool,
|
|
config Config,
|
|
friendlyCount int,
|
|
state *VisibleState,
|
|
) *Move {
|
|
// Priority 1: Escape zone if threatened
|
|
if zoneDir := getZoneEscapeDirection(bot.Position, state, wallPositions); 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, state)
|
|
}
|
|
|
|
// Target is enemy center if visible, otherwise map center
|
|
target := Position{Row: config.Rows / 2, Col: config.Cols / 2}
|
|
if enemyCenter != nil {
|
|
target = *enemyCenter
|
|
}
|
|
|
|
bestDir := DirNone
|
|
bestScore := -math.MaxFloat64
|
|
|
|
for _, dir := range []Direction{DirN, DirE, DirS, DirW} {
|
|
newPos := simulateMove(bot.Position, dir, config.Rows, config.Cols)
|
|
|
|
// Can't move into walls or enemies
|
|
if wallPositions[newPos] || enemyPositions[newPos] {
|
|
continue
|
|
}
|
|
|
|
// CRITICAL: avoid tiles claimed by another friendly bot this turn (prevents self-collision)
|
|
if claimed[newPos] {
|
|
continue
|
|
}
|
|
|
|
// Also avoid moving onto a tile occupied by a friendly bot (they might not move)
|
|
if myBotPositions[newPos] && newPos != bot.Position {
|
|
continue
|
|
}
|
|
|
|
// Check cohesion: must stay within cohesion radius of at least one friendly bot
|
|
if !b.maintainsCohesion(newPos, bot.Position, myBotPositions, config) {
|
|
continue
|
|
}
|
|
|
|
// Score this move
|
|
score := 0.0
|
|
|
|
// Prefer moving toward enemy
|
|
distToTarget := float64(distance2(newPos, target, config.Rows, config.Cols))
|
|
currentDistToTarget := float64(distance2(bot.Position, target, config.Rows, config.Cols))
|
|
score += (currentDistToTarget - distToTarget) * 10
|
|
|
|
// Prefer staying near swarm center
|
|
distToSwarmCenter := float64(distance2(newPos, swarmCenter, config.Rows, config.Cols))
|
|
score -= distToSwarmCenter * 0.5
|
|
|
|
// Bonus for being in attack range
|
|
for enemyPos := range enemyPositions {
|
|
dist := distance2(newPos, enemyPos, config.Rows, config.Cols)
|
|
if dist <= config.AttackRadius2 {
|
|
score += 50
|
|
break
|
|
}
|
|
}
|
|
|
|
// Small bonus for energy on the way
|
|
if energyPositions[newPos] {
|
|
score += 15
|
|
}
|
|
|
|
if score > bestScore {
|
|
bestScore = score
|
|
bestDir = dir
|
|
}
|
|
}
|
|
|
|
if bestDir != DirNone {
|
|
return &Move{Position: bot.Position, Direction: bestDir}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// soloMove handles movement when the swarm is too small for formation tactics.
|
|
// Gathers energy to spawn more units, advances toward enemies to build swarm via combat.
|
|
func (b *SwarmBot) soloMove(
|
|
bot VisibleBot,
|
|
energyPositions, enemyPositions, wallPositions map[Position]bool,
|
|
config Config,
|
|
state *VisibleState,
|
|
) *Move {
|
|
bestDir := DirNone
|
|
bestScore := -math.MaxFloat64
|
|
|
|
for _, dir := range []Direction{DirN, DirE, DirS, DirW} {
|
|
newPos := simulateMove(bot.Position, dir, config.Rows, config.Cols)
|
|
if wallPositions[newPos] || enemyPositions[newPos] {
|
|
continue
|
|
}
|
|
|
|
score := 0.0
|
|
|
|
// Strong bonus for energy (primary goal in solo mode: build swarm economy)
|
|
if energyPositions[newPos] {
|
|
score += 120
|
|
}
|
|
|
|
// Move toward nearest energy
|
|
for ePos := range energyPositions {
|
|
dist := float64(distance2(newPos, ePos, config.Rows, config.Cols))
|
|
currentDist := float64(distance2(bot.Position, ePos, config.Rows, config.Cols))
|
|
if dist < currentDist {
|
|
score += 25.0 / (dist + 1)
|
|
}
|
|
}
|
|
|
|
// Advance toward enemies (per plan §5.5: "advance as a group toward enemies")
|
|
// Bonus for moving closer to enemies, but secondary to energy gathering
|
|
for ePos := range enemyPositions {
|
|
dist := float64(distance2(newPos, ePos, config.Rows, config.Cols))
|
|
currentDist := float64(distance2(bot.Position, ePos, config.Rows, config.Cols))
|
|
if dist < currentDist {
|
|
// Moving toward enemy - bonus increases as we get closer
|
|
score += 40.0 / (dist + 1)
|
|
}
|
|
// Moderate bonus for being in attack range (encourages combat but doesn't override energy)
|
|
if dist <= float64(config.AttackRadius2) {
|
|
score += 35
|
|
}
|
|
}
|
|
|
|
if score > bestScore {
|
|
bestScore = score
|
|
bestDir = dir
|
|
}
|
|
}
|
|
|
|
if bestDir != DirNone {
|
|
return &Move{Position: bot.Position, Direction: bestDir}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *SwarmBot) maintainsCohesion(newPos, oldPos Position, myBotPositions map[Position]bool, config Config) bool {
|
|
for botPos := range myBotPositions {
|
|
if botPos == oldPos {
|
|
continue
|
|
}
|
|
dist := distance2(newPos, botPos, config.Rows, config.Cols)
|
|
if dist <= cohesionRadius2 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// HunterBot targets isolated enemy units.
|
|
type HunterBot struct {
|
|
rng *rand.Rand
|
|
enemyTrackers map[Position]*enemyTracker
|
|
}
|
|
|
|
type enemyTracker struct {
|
|
lastPos *Position
|
|
currentPos Position
|
|
}
|
|
|
|
// NewHunterBot creates a new hunter bot.
|
|
func NewHunterBot(seed int64) *HunterBot {
|
|
return &HunterBot{
|
|
rng: rand.New(rand.NewSource(seed)),
|
|
enemyTrackers: make(map[Position]*enemyTracker),
|
|
}
|
|
}
|
|
|
|
// GetMoves returns moves targeting isolated enemies.
|
|
func (b *HunterBot) GetMoves(state *VisibleState) ([]Move, error) {
|
|
myID := state.You.ID
|
|
config := state.Config
|
|
|
|
// Separate bots
|
|
myBots := make([]VisibleBot, 0)
|
|
enemyBots := make([]VisibleBot, 0)
|
|
for _, bot := range state.Bots {
|
|
if bot.Owner == myID {
|
|
myBots = append(myBots, bot)
|
|
} else {
|
|
enemyBots = append(enemyBots, bot)
|
|
}
|
|
}
|
|
|
|
if len(myBots) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Update enemy trackers
|
|
for _, enemy := range enemyBots {
|
|
tracker, exists := b.enemyTrackers[enemy.Position]
|
|
if !exists {
|
|
tracker = &enemyTracker{}
|
|
b.enemyTrackers[enemy.Position] = tracker
|
|
}
|
|
tracker.lastPos = &tracker.currentPos
|
|
tracker.currentPos = enemy.Position
|
|
}
|
|
|
|
// Build lookup maps
|
|
enemyPositions := make(map[Position]bool)
|
|
for _, enemy := range enemyBots {
|
|
enemyPositions[enemy.Position] = true
|
|
}
|
|
|
|
energyPositions := make(map[Position]bool)
|
|
for _, e := range state.Energy {
|
|
energyPositions[e] = true
|
|
}
|
|
|
|
wallPositions := make(map[Position]bool)
|
|
for _, w := range state.Walls {
|
|
wallPositions[w] = true
|
|
}
|
|
|
|
myBotPositions := make(map[Position]bool)
|
|
for _, bot := range myBots {
|
|
myBotPositions[bot.Position] = true
|
|
}
|
|
|
|
// Find isolated enemies
|
|
isolatedEnemies := b.findIsolatedEnemies(enemyBots, config)
|
|
|
|
// Assign hunters to targets
|
|
moves := make([]Move, 0, len(myBots))
|
|
usedEnergy := make(map[Position]bool)
|
|
assignedHunters := make(map[Position]bool)
|
|
|
|
// First, assign hunters to isolated enemies
|
|
for _, target := range isolatedEnemies {
|
|
// Assign up to 2 hunters per target
|
|
huntersAssigned := 0
|
|
for i, bot := range myBots {
|
|
if assignedHunters[bot.Position] {
|
|
continue
|
|
}
|
|
if huntersAssigned >= 2 {
|
|
break
|
|
}
|
|
|
|
// Priority 1: Escape zone if threatened
|
|
if zoneDir := getZoneEscapeDirection(bot.Position, state, wallPositions); 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
|
|
predictedPos := b.predictPosition(target)
|
|
dir := b.getDirectionToward(bot.Position, predictedPos, wallPositions, config)
|
|
if dir != DirNone {
|
|
moves = append(moves, Move{Position: bot.Position, Direction: dir})
|
|
assignedHunters[bot.Position] = true
|
|
huntersAssigned++
|
|
}
|
|
}
|
|
_ = i // silence unused variable warning
|
|
}
|
|
}
|
|
|
|
// Remaining bots gather or explore
|
|
for _, bot := range myBots {
|
|
if assignedHunters[bot.Position] {
|
|
continue
|
|
}
|
|
|
|
// Priority 1: Escape zone if threatened
|
|
if zoneDir := getZoneEscapeDirection(bot.Position, state, wallPositions); 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 {
|
|
if usedEnergy[pos] {
|
|
continue
|
|
}
|
|
dist := distance2(bot.Position, pos, config.Rows, config.Cols)
|
|
if dist < nearestDist {
|
|
nearestDist = dist
|
|
nearestEnergy = pos
|
|
}
|
|
}
|
|
|
|
if nearestDist < math.MaxInt32 {
|
|
usedEnergy[nearestEnergy] = true
|
|
dir := b.getDirectionToward(bot.Position, nearestEnergy, wallPositions, config)
|
|
if dir != DirNone {
|
|
moves = append(moves, Move{Position: bot.Position, Direction: dir})
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Explore toward center
|
|
center := Position{Row: config.Rows / 2, Col: config.Cols / 2}
|
|
dir := b.getDirectionToward(bot.Position, center, wallPositions, config)
|
|
if dir != DirNone {
|
|
moves = append(moves, Move{Position: bot.Position, Direction: dir})
|
|
}
|
|
}
|
|
|
|
return moves, nil
|
|
}
|
|
|
|
const isolationThreshold = 16 // 4 tiles squared
|
|
|
|
func (b *HunterBot) findIsolatedEnemies(enemies []VisibleBot, config Config) []VisibleBot {
|
|
isolated := make([]VisibleBot, 0)
|
|
|
|
for _, bot := range enemies {
|
|
nearestDist := math.MaxInt32
|
|
for _, other := range enemies {
|
|
if bot.Position == other.Position {
|
|
continue
|
|
}
|
|
dist := distance2(bot.Position, other.Position, config.Rows, config.Cols)
|
|
if dist < nearestDist {
|
|
nearestDist = dist
|
|
}
|
|
}
|
|
|
|
// Isolated if nearest friendly is >= 4 tiles away or only enemy
|
|
if nearestDist >= isolationThreshold || len(enemies) == 1 {
|
|
isolated = append(isolated, bot)
|
|
}
|
|
}
|
|
|
|
return isolated
|
|
}
|
|
|
|
func (b *HunterBot) predictPosition(enemy VisibleBot) Position {
|
|
tracker, exists := b.enemyTrackers[enemy.Position]
|
|
if !exists || tracker.lastPos == nil {
|
|
return enemy.Position
|
|
}
|
|
|
|
// Simple prediction: continue in same direction
|
|
dr := tracker.currentPos.Row - tracker.lastPos.Row
|
|
dc := tracker.currentPos.Col - tracker.lastPos.Col
|
|
|
|
// Handle wrap
|
|
if dr > 30 {
|
|
dr -= 60
|
|
}
|
|
if dr < -30 {
|
|
dr += 60
|
|
}
|
|
if dc > 30 {
|
|
dc -= 60
|
|
}
|
|
if dc < -30 {
|
|
dc += 60
|
|
}
|
|
|
|
return Position{
|
|
Row: (tracker.currentPos.Row + dr + 60) % 60,
|
|
Col: (tracker.currentPos.Col + dc + 60) % 60,
|
|
}
|
|
}
|
|
|
|
func (b *HunterBot) getDirectionToward(from, to Position, wallPositions map[Position]bool, config Config) Direction {
|
|
bestDir := DirNone
|
|
bestDist := math.MaxInt32
|
|
|
|
for _, dir := range []Direction{DirN, DirE, DirS, DirW} {
|
|
newPos := simulateMove(from, dir, config.Rows, config.Cols)
|
|
if wallPositions[newPos] {
|
|
continue
|
|
}
|
|
dist := distance2(newPos, to, config.Rows, config.Cols)
|
|
if dist < bestDist {
|
|
bestDist = dist
|
|
bestDir = dir
|
|
}
|
|
}
|
|
|
|
return bestDir
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
// distance2 calculates squared Euclidean distance with toroidal wrapping.
|
|
func distance2(a, b Position, rows, cols int) int {
|
|
dr := abs(a.Row - b.Row)
|
|
dc := abs(a.Col - b.Col)
|
|
|
|
// Apply toroidal wrapping
|
|
if dr > rows/2 {
|
|
dr = rows - dr
|
|
}
|
|
if dc > cols/2 {
|
|
dc = cols - dc
|
|
}
|
|
|
|
return dr*dr + dc*dc
|
|
}
|
|
|
|
// simulateMove returns the new position after moving in a direction.
|
|
func simulateMove(pos Position, dir Direction, rows, cols int) Position {
|
|
switch dir {
|
|
case DirN:
|
|
return Position{Row: (pos.Row - 1 + rows) % rows, Col: pos.Col}
|
|
case DirE:
|
|
return Position{Row: pos.Row, Col: (pos.Col + 1) % cols}
|
|
case DirS:
|
|
return Position{Row: (pos.Row + 1) % rows, Col: pos.Col}
|
|
case DirW:
|
|
return Position{Row: pos.Row, Col: (pos.Col - 1 + cols) % cols}
|
|
default:
|
|
return pos
|
|
}
|
|
}
|
|
|
|
func abs(x int) int {
|
|
if x < 0 {
|
|
return -x
|
|
}
|
|
return x
|
|
}
|