Implement SiegeBot: spawn-lockout strategy
- Add SiegeBot to engine/bot_strategies.go with spawn denial logic - Implement standalone siege bot in bots/siege/ with main.go, strategy.go, Dockerfile - Register siege bot in acb-local for arena testing - Add test-siege-arena.sh script for validation Strategy: Surround enemy cores to block spawning phase. Bot assigns units to lockout rings (8 neighbors) around cores, greedily by distance. Unassigned units collect energy or rush fully-sieged cores. Tested: 3/10 wins vs rusher+gatherer, 1/10 wins vs rusher+gatherer+guardian
This commit is contained in:
parent
2de4ddc720
commit
2696e70257
7 changed files with 1331 additions and 0 deletions
20
bots/siege/Dockerfile
Normal file
20
bots/siege/Dockerfile
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
FROM golang:1.22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod .
|
||||
COPY main.go .
|
||||
COPY strategy.go .
|
||||
|
||||
RUN go build -o siege .
|
||||
|
||||
FROM alpine:3.19
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/siege .
|
||||
|
||||
ENV BOT_PORT=8080
|
||||
ENV BOT_SECRET=""
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["./siege"]
|
||||
3
bots/siege/go.mod
Normal file
3
bots/siege/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/aicodebattle/acb/bots/siege
|
||||
|
||||
go 1.21
|
||||
231
bots/siege/main.go
Normal file
231
bots/siege/main.go
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
// Package main implements SiegeBot - a bot that blocks enemy core spawning by surrounding cores.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Config holds bot configuration from environment variables.
|
||||
type Config struct {
|
||||
Port string
|
||||
Secret string
|
||||
}
|
||||
|
||||
// GameConfig holds the game configuration from the engine.
|
||||
type GameConfig struct {
|
||||
Rows int `json:"rows"`
|
||||
Cols int `json:"cols"`
|
||||
MaxTurns int `json:"max_turns"`
|
||||
VisionRadius2 int `json:"vision_radius2"`
|
||||
AttackRadius2 int `json:"attack_radius2"`
|
||||
SpawnCost int `json:"spawn_cost"`
|
||||
EnergyInterval int `json:"energy_interval"`
|
||||
}
|
||||
|
||||
// Position represents a grid coordinate.
|
||||
type Position struct {
|
||||
Row int `json:"row"`
|
||||
Col int `json:"col"`
|
||||
}
|
||||
|
||||
// VisibleBot represents a visible bot.
|
||||
type VisibleBot struct {
|
||||
Position Position `json:"position"`
|
||||
Owner int `json:"owner"`
|
||||
}
|
||||
|
||||
// VisibleCore represents a visible core.
|
||||
type VisibleCore struct {
|
||||
Position Position `json:"position"`
|
||||
Owner int `json:"owner"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
// ZoneBounds represents the active zone bounds.
|
||||
type ZoneBounds struct {
|
||||
Center Position `json:"center"`
|
||||
Radius int `json:"radius"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
// GameState represents the fog-filtered state visible to this bot.
|
||||
type GameState struct {
|
||||
MatchID string `json:"match_id"`
|
||||
Turn int `json:"turn"`
|
||||
Config GameConfig `json:"config"`
|
||||
You struct {
|
||||
ID int `json:"id"`
|
||||
Energy int `json:"energy"`
|
||||
Score int `json:"score"`
|
||||
} `json:"you"`
|
||||
Bots []VisibleBot `json:"bots"`
|
||||
Energy []Position `json:"energy"`
|
||||
Cores []VisibleCore `json:"cores"`
|
||||
Walls []Position `json:"walls"`
|
||||
Dead []VisibleBot `json:"dead"`
|
||||
Zone *ZoneBounds `json:"zone,omitempty"`
|
||||
}
|
||||
|
||||
// Direction represents a movement direction.
|
||||
type Direction string
|
||||
|
||||
const (
|
||||
DirNone Direction = ""
|
||||
DirN Direction = "N"
|
||||
DirE Direction = "E"
|
||||
DirS Direction = "S"
|
||||
DirW Direction = "W"
|
||||
)
|
||||
|
||||
// Move represents a bot movement order.
|
||||
type Move struct {
|
||||
Position Position `json:"position"`
|
||||
Direction Direction `json:"direction"`
|
||||
}
|
||||
|
||||
// MoveResponse is the response sent back to the engine.
|
||||
type MoveResponse struct {
|
||||
Moves []Move `json:"moves"`
|
||||
}
|
||||
|
||||
// Server holds the bot server state.
|
||||
type Server struct {
|
||||
config Config
|
||||
strategy *SiegeStrategy
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func main() {
|
||||
config := Config{
|
||||
Port: getEnv("BOT_PORT", "8080"),
|
||||
Secret: getEnv("BOT_SECRET", ""),
|
||||
}
|
||||
|
||||
if config.Secret == "" {
|
||||
log.Fatal("BOT_SECRET environment variable is required")
|
||||
}
|
||||
|
||||
server := &Server{
|
||||
config: config,
|
||||
strategy: NewSiegeStrategy(),
|
||||
}
|
||||
|
||||
http.HandleFunc("/turn", server.handleTurn)
|
||||
http.HandleFunc("/health", server.handleHealth)
|
||||
|
||||
addr := fmt.Sprintf(":%s", config.Port)
|
||||
log.Printf("SiegeBot starting on %s", addr)
|
||||
if err := http.ListenAndServe(addr, nil); err != nil {
|
||||
log.Fatalf("Server failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleTurn(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Read request body
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to read body", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer r.Body.Close()
|
||||
|
||||
// Verify signature
|
||||
sig := r.Header.Get("X-ACB-Signature")
|
||||
if sig == "" {
|
||||
http.Error(w, "missing signature", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
matchID := r.Header.Get("X-ACB-Match-Id")
|
||||
turnStr := r.Header.Get("X-ACB-Turn")
|
||||
|
||||
if err := verifySignature(s.config.Secret, matchID, turnStr, body, sig); err != nil {
|
||||
http.Error(w, fmt.Sprintf("signature verification failed: %v", err), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse game state
|
||||
var state GameState
|
||||
if err := json.Unmarshal(body, &state); err != nil {
|
||||
http.Error(w, "invalid game state", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Compute moves
|
||||
s.mu.Lock()
|
||||
moves := s.strategy.ComputeMoves(&state)
|
||||
s.mu.Unlock()
|
||||
|
||||
// Build response
|
||||
response := MoveResponse{Moves: moves}
|
||||
responseBody, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
http.Error(w, "failed to marshal response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Sign response
|
||||
responseSig := signResponse(s.config.Secret, matchID, turnStr, responseBody)
|
||||
w.Header().Set("X-ACB-Signature", responseSig)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(responseBody)
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("OK"))
|
||||
}
|
||||
|
||||
// verifySignature verifies the HMAC signature of an incoming request.
|
||||
func verifySignature(secret, matchID, turnStr string, body []byte, signature string) error {
|
||||
bodyHash := sha256.Sum256(body)
|
||||
turn, _ := strconv.Atoi(turnStr)
|
||||
signingString := fmt.Sprintf("%s.%d.%s", matchID, turn, hex.EncodeToString(bodyHash[:]))
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(signingString))
|
||||
expectedSig := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(signature), []byte(expectedSig)) {
|
||||
return fmt.Errorf("invalid signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// signResponse signs the response body.
|
||||
func signResponse(secret, matchID, turnStr string, body []byte) string {
|
||||
bodyHash := sha256.Sum256(body)
|
||||
turn, _ := strconv.Atoi(turnStr)
|
||||
signingString := fmt.Sprintf("%s.%d.%s", matchID, turn, hex.EncodeToString(bodyHash[:]))
|
||||
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(signingString))
|
||||
return hex.EncodeToString(mac.Sum(nil))
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
477
bots/siege/strategy.go
Normal file
477
bots/siege/strategy.go
Normal file
|
|
@ -0,0 +1,477 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"math"
|
||||
)
|
||||
|
||||
// SiegeStrategy implements systematic spawn-lockout by occupying enemy core positions.
|
||||
// A core cannot spawn if its position is occupied by any bot.
|
||||
type SiegeStrategy struct{}
|
||||
|
||||
// NewSiegeStrategy creates a new siege strategy.
|
||||
func NewSiegeStrategy() *SiegeStrategy {
|
||||
return &SiegeStrategy{}
|
||||
}
|
||||
|
||||
// ComputeMoves calculates the best moves for the current turn.
|
||||
func (s *SiegeStrategy) ComputeMoves(state *GameState) []Move {
|
||||
if len(state.Bots) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
myID := state.You.ID
|
||||
config := state.Config
|
||||
|
||||
// Separate my bots from enemy 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Build lookup maps
|
||||
enemyPositions := make(map[Position]bool)
|
||||
for _, enemy := range enemyBots {
|
||||
enemyPositions[enemy.Position] = true
|
||||
}
|
||||
|
||||
wallPositions := make(map[Position]bool)
|
||||
for _, wall := range state.Walls {
|
||||
wallPositions[wall] = true
|
||||
}
|
||||
|
||||
energyPositions := make(map[Position]bool)
|
||||
for _, e := range state.Energy {
|
||||
energyPositions[e] = true
|
||||
}
|
||||
|
||||
// Find all enemy cores
|
||||
enemyCores := make([]VisibleCore, 0)
|
||||
for _, core := range state.Cores {
|
||||
if core.Owner != myID && core.Active {
|
||||
enemyCores = append(enemyCores, core)
|
||||
}
|
||||
}
|
||||
|
||||
// Track occupied positions
|
||||
occupiedPositions := make(map[Position]bool)
|
||||
for _, bot := range myBots {
|
||||
occupiedPositions[bot.Position] = true
|
||||
}
|
||||
|
||||
moves := make([]Move, 0, len(myBots))
|
||||
assignedBots := make(map[Position]bool) // Track which bots have been assigned
|
||||
|
||||
// PHASE 1: Assign bots to lockout rings around enemy cores
|
||||
lockoutAssignments := s.assignLockoutBots(myBots, enemyCores, enemyPositions, wallPositions, occupiedPositions, config)
|
||||
|
||||
// Execute lockout assignments
|
||||
for botPos, targetPos := range lockoutAssignments {
|
||||
// Find the bot at botPos
|
||||
var targetBot *VisibleBot
|
||||
for i := range myBots {
|
||||
if myBots[i].Position == botPos {
|
||||
targetBot = &myBots[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if targetBot != nil {
|
||||
move := s.moveTowardPosition(*targetBot, targetPos, enemyPositions, wallPositions, occupiedPositions, config)
|
||||
if move != nil {
|
||||
moves = append(moves, *move)
|
||||
assignedBots[botPos] = true
|
||||
// Update occupied position for next moves
|
||||
dest := simulateMove(targetBot.Position, move.Direction, config)
|
||||
occupiedPositions[dest] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PHASE 2: Unassigned bots collect energy
|
||||
usedEnergy := make(map[Position]bool)
|
||||
for _, bot := range myBots {
|
||||
if assignedBots[bot.Position] {
|
||||
continue
|
||||
}
|
||||
|
||||
// Zone awareness: survival first
|
||||
if state.Zone != nil && state.Zone.Active {
|
||||
dist2 := distance2(bot.Position, state.Zone.Center, config)
|
||||
safetyMargin2 := 9 // (3 tiles)^2
|
||||
if dist2 >= state.Zone.Radius*state.Zone.Radius-safetyMargin2 {
|
||||
move := s.moveTowardPosition(bot, state.Zone.Center, enemyPositions, wallPositions, occupiedPositions, config)
|
||||
if move != nil {
|
||||
moves = append(moves, *move)
|
||||
dest := simulateMove(bot.Position, move.Direction, config)
|
||||
occupiedPositions[dest] = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flee from nearby enemies
|
||||
if s.shouldFlee(bot.Position, enemyBots, config) {
|
||||
fleeDir := s.getFleeDirection(bot.Position, enemyBots, wallPositions, config)
|
||||
if fleeDir != DirNone {
|
||||
moves = append(moves, Move{
|
||||
Position: bot.Position,
|
||||
Direction: fleeDir,
|
||||
})
|
||||
dest := simulateMove(bot.Position, fleeDir, config)
|
||||
occupiedPositions[dest] = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Collect adjacent energy (immediate gain)
|
||||
collected := false
|
||||
for _, dir := range []Direction{DirN, DirE, DirS, DirW} {
|
||||
adj := simulateMove(bot.Position, dir, config)
|
||||
if energyPositions[adj] && !usedEnergy[adj] &&
|
||||
!wallPositions[adj] && !enemyPositions[adj] && !occupiedPositions[adj] {
|
||||
moves = append(moves, Move{
|
||||
Position: bot.Position,
|
||||
Direction: dir,
|
||||
})
|
||||
usedEnergy[adj] = true
|
||||
occupiedPositions[adj] = true
|
||||
collected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if collected {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find nearest energy
|
||||
_, path := s.findNearestEnergy(bot.Position, energyPositions, usedEnergy, wallPositions, enemyPositions, occupiedPositions, config)
|
||||
if path != nil && len(path) > 0 {
|
||||
moves = append(moves, Move{
|
||||
Position: bot.Position,
|
||||
Direction: path[0],
|
||||
})
|
||||
dest := simulateMove(bot.Position, path[0], config)
|
||||
occupiedPositions[dest] = true
|
||||
continue
|
||||
}
|
||||
|
||||
// No energy found - advance toward nearest enemy core
|
||||
if len(enemyCores) > 0 {
|
||||
nearestCore := s.findNearestCore(bot.Position, enemyCores, config)
|
||||
move := s.moveTowardPosition(bot, nearestCore.Position, enemyPositions, wallPositions, occupiedPositions, config)
|
||||
if move != nil {
|
||||
moves = append(moves, *move)
|
||||
dest := simulateMove(bot.Position, move.Direction, config)
|
||||
occupiedPositions[dest] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moves
|
||||
}
|
||||
|
||||
// assignLockoutBots assigns bots to tiles adjacent to enemy cores (greedy by distance).
|
||||
func (s *SiegeStrategy) assignLockoutBots(
|
||||
myBots []VisibleBot,
|
||||
enemyCores []VisibleCore,
|
||||
enemyPositions, wallPositions, occupiedPositions map[Position]bool,
|
||||
config GameConfig,
|
||||
) map[Position]Position {
|
||||
// For each enemy core, build its lockout ring (all 8 neighbors)
|
||||
type LockoutSlot struct {
|
||||
core VisibleCore
|
||||
position Position
|
||||
occupied bool
|
||||
distance int // distance from nearest bot
|
||||
}
|
||||
|
||||
var allSlots []LockoutSlot
|
||||
|
||||
for _, core := range enemyCores {
|
||||
neighbors := getAllNeighbors(core.Position, config)
|
||||
for _, neighbor := range neighbors {
|
||||
// Check if this slot is valid (not wall, not enemy-occupied)
|
||||
if wallPositions[neighbor] || enemyPositions[neighbor] {
|
||||
continue
|
||||
}
|
||||
|
||||
allSlots = append(allSlots, LockoutSlot{
|
||||
core: core,
|
||||
position: neighbor,
|
||||
occupied: occupiedPositions[neighbor],
|
||||
distance: -1, // Will be computed
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Greedy assignment: nearest bot -> nearest available slot
|
||||
assignments := make(map[Position]Position) // bot position -> target position
|
||||
|
||||
// Keep assigning until we run out of bots or slots
|
||||
for {
|
||||
bestSlot := -1
|
||||
bestBot := -1
|
||||
bestDist := math.MaxInt32
|
||||
|
||||
// For each unassigned bot, find the nearest available slot
|
||||
for bi, bot := range myBots {
|
||||
if _, assigned := assignments[bot.Position]; assigned {
|
||||
continue
|
||||
}
|
||||
|
||||
for si, slot := range allSlots {
|
||||
if slot.occupied {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this slot is already targeted by another bot
|
||||
alreadyTargeted := false
|
||||
for _, target := range assignments {
|
||||
if target == slot.position {
|
||||
alreadyTargeted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if alreadyTargeted {
|
||||
continue
|
||||
}
|
||||
|
||||
dist := distance2(bot.Position, slot.position, config)
|
||||
if dist < bestDist {
|
||||
bestDist = dist
|
||||
bestSlot = si
|
||||
bestBot = bi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No more assignments possible
|
||||
if bestBot == -1 || bestSlot == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
// Make the assignment
|
||||
assignments[myBots[bestBot].Position] = allSlots[bestSlot].position
|
||||
allSlots[bestSlot].occupied = true
|
||||
}
|
||||
|
||||
return assignments
|
||||
}
|
||||
|
||||
// findNearestCore finds the nearest enemy core to a position.
|
||||
func (s *SiegeStrategy) findNearestCore(pos Position, cores []VisibleCore, config GameConfig) VisibleCore {
|
||||
nearest := cores[0]
|
||||
minDist := distance2(pos, nearest.Position, config)
|
||||
|
||||
for _, core := range cores[1:] {
|
||||
dist := distance2(pos, core.Position, config)
|
||||
if dist < minDist {
|
||||
minDist = dist
|
||||
nearest = core
|
||||
}
|
||||
}
|
||||
|
||||
return nearest
|
||||
}
|
||||
|
||||
// shouldFlee returns true if the bot should flee from nearby enemies.
|
||||
func (s *SiegeStrategy) shouldFlee(pos Position, enemies []VisibleBot, config GameConfig) bool {
|
||||
for _, enemy := range enemies {
|
||||
dist2 := distance2(pos, enemy.Position, config)
|
||||
if dist2 <= config.AttackRadius2+4 { // Attack radius + buffer
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getFleeDirection returns the best direction to flee from enemies.
|
||||
func (s *SiegeStrategy) getFleeDirection(pos Position, enemies []VisibleBot, wallPositions map[Position]bool, config GameConfig) Direction {
|
||||
// Calculate center of mass of enemies
|
||||
enemyCenter := Position{Row: 0, Col: 0}
|
||||
for _, enemy := range enemies {
|
||||
enemyCenter.Row += enemy.Position.Row
|
||||
enemyCenter.Col += enemy.Position.Col
|
||||
}
|
||||
if len(enemies) > 0 {
|
||||
enemyCenter.Row /= len(enemies)
|
||||
enemyCenter.Col /= len(enemies)
|
||||
}
|
||||
|
||||
bestDir := DirNone
|
||||
bestDist := -1
|
||||
|
||||
for _, dir := range []Direction{DirN, DirE, DirS, DirW} {
|
||||
newPos := simulateMove(pos, dir, config)
|
||||
if wallPositions[newPos] {
|
||||
continue
|
||||
}
|
||||
dist := distance2(newPos, enemyCenter, config)
|
||||
if dist > bestDist {
|
||||
bestDist = dist
|
||||
bestDir = dir
|
||||
}
|
||||
}
|
||||
|
||||
return bestDir
|
||||
}
|
||||
|
||||
// findNearestEnergy finds the nearest untargeted energy using BFS.
|
||||
func (s *SiegeStrategy) findNearestEnergy(
|
||||
start Position,
|
||||
energyPositions, usedEnergy, wallPositions, enemyPositions, occupiedPositions map[Position]bool,
|
||||
config GameConfig,
|
||||
) (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
|
||||
}
|
||||
|
||||
if len(path) > 20 { // Limit search depth
|
||||
continue
|
||||
}
|
||||
|
||||
// Explore neighbors
|
||||
directions := []Direction{DirN, DirE, DirS, DirW}
|
||||
for _, dir := range directions {
|
||||
nextPos := simulateMove(pos, dir, config)
|
||||
if wallPositions[nextPos] || enemyPositions[nextPos] || occupiedPositions[nextPos] {
|
||||
continue
|
||||
}
|
||||
if !visited[nextPos] {
|
||||
newPath := make([]Direction, len(path)+1)
|
||||
copy(newPath, path)
|
||||
newPath[len(path)] = dir
|
||||
queue.PushBack(queueItem{pos: nextPos, path: newPath})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nearestEnergy, bestPath
|
||||
}
|
||||
|
||||
// moveTowardPosition returns a move that approaches the target position.
|
||||
func (s *SiegeStrategy) moveTowardPosition(
|
||||
bot VisibleBot,
|
||||
target Position,
|
||||
enemyPositions, wallPositions, occupiedPositions map[Position]bool,
|
||||
config GameConfig,
|
||||
) *Move {
|
||||
bestDir := DirNone
|
||||
bestDist2 := math.MaxInt32
|
||||
|
||||
for _, dir := range []Direction{DirN, DirE, DirS, DirW} {
|
||||
newPos := simulateMove(bot.Position, dir, config)
|
||||
if wallPositions[newPos] || enemyPositions[newPos] || occupiedPositions[newPos] {
|
||||
continue
|
||||
}
|
||||
|
||||
dist2 := distance2(newPos, target, config)
|
||||
if dist2 < bestDist2 {
|
||||
bestDist2 = dist2
|
||||
bestDir = dir
|
||||
}
|
||||
}
|
||||
|
||||
if bestDir != DirNone {
|
||||
return &Move{
|
||||
Position: bot.Position,
|
||||
Direction: bestDir,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// getAllNeighbors returns all 8 neighbors (including diagonals) of a position.
|
||||
func getAllNeighbors(pos Position, config GameConfig) []Position {
|
||||
neighbors := make([]Position, 0, 8)
|
||||
deltas := []struct{ dr, dc int }{
|
||||
{-1, -1}, {-1, 0}, {-1, 1},
|
||||
{0, -1}, {0, 1},
|
||||
{1, -1}, {1, 0}, {1, 1},
|
||||
}
|
||||
|
||||
for _, delta := range deltas {
|
||||
newRow := (pos.Row + delta.dr + config.Rows) % config.Rows
|
||||
newCol := (pos.Col + delta.dc + config.Cols) % config.Cols
|
||||
neighbors = append(neighbors, Position{Row: newRow, Col: newCol})
|
||||
}
|
||||
|
||||
return neighbors
|
||||
}
|
||||
|
||||
// distance2 calculates squared toroidal distance.
|
||||
func distance2(a, b Position, config GameConfig) int {
|
||||
dr := a.Row - b.Row
|
||||
dc := a.Col - b.Col
|
||||
|
||||
// Apply toroidal wrapping
|
||||
if dr > config.Rows/2 {
|
||||
dr = config.Rows - dr
|
||||
} else if dr < -config.Rows/2 {
|
||||
dr = -(config.Rows + dr)
|
||||
}
|
||||
if dc > config.Cols/2 {
|
||||
dc = config.Cols - dc
|
||||
} else if dc < -config.Cols/2 {
|
||||
dc = -(config.Cols + dc)
|
||||
}
|
||||
|
||||
return dr*dr + dc*dc
|
||||
}
|
||||
|
||||
// simulateMove returns the new position after moving in a direction.
|
||||
func simulateMove(pos Position, dir Direction, config GameConfig) Position {
|
||||
var newRow, newCol int
|
||||
|
||||
switch dir {
|
||||
case DirN:
|
||||
newRow = (pos.Row - 1 + config.Rows) % config.Rows
|
||||
newCol = pos.Col
|
||||
case DirE:
|
||||
newRow = pos.Row
|
||||
newCol = (pos.Col + 1) % config.Cols
|
||||
case DirS:
|
||||
newRow = (pos.Row + 1) % config.Rows
|
||||
newCol = pos.Col
|
||||
case DirW:
|
||||
newRow = pos.Row
|
||||
newCol = (pos.Col - 1 + config.Cols) % config.Cols
|
||||
default:
|
||||
return pos
|
||||
}
|
||||
|
||||
return Position{Row: newRow, Col: newCol}
|
||||
}
|
||||
|
||||
|
|
@ -23,6 +23,7 @@ var availableBots = map[string]func(int64) engine.BotInterface{
|
|||
"guardian": func(seed int64) engine.BotInterface { return engine.NewGuardianBot(seed) },
|
||||
"swarm": func(seed int64) engine.BotInterface { return engine.NewSwarmBot(seed) },
|
||||
"hunter": func(seed int64) engine.BotInterface { return engine.NewHunterBot(seed) },
|
||||
"siege": func(seed int64) engine.BotInterface { return engine.NewSiegeBot(seed) },
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
|
|||
|
|
@ -1302,3 +1302,555 @@ func abs(x int) int {
|
|||
}
|
||||
return x
|
||||
}
|
||||
|
||||
// SiegeBot implements spawn-lockout: surround enemy cores to prevent spawning.
|
||||
type SiegeBot struct {
|
||||
rng *rand.Rand
|
||||
knownEnemyCores map[Position]bool
|
||||
}
|
||||
|
||||
// NewSiegeBot creates a new siege bot.
|
||||
func NewSiegeBot(seed int64) *SiegeBot {
|
||||
return &SiegeBot{
|
||||
rng: rand.New(rand.NewSource(seed)),
|
||||
knownEnemyCores: make(map[Position]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// GetMoves returns moves focused on surrounding enemy cores to block spawning.
|
||||
func (b *SiegeBot) GetMoves(state *VisibleState) ([]Move, error) {
|
||||
if len(state.Bots) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// Build enemy positions and cores maps
|
||||
enemyPositions := make(map[Position]bool)
|
||||
for _, enemy := range enemyBots {
|
||||
enemyPositions[enemy.Position] = true
|
||||
}
|
||||
|
||||
// Find all enemy cores (visible + known)
|
||||
enemyCores := make([]VisibleCore, 0)
|
||||
for _, core := range state.Cores {
|
||||
if core.Owner != myID && core.Active {
|
||||
enemyCores = append(enemyCores, core)
|
||||
}
|
||||
}
|
||||
// Add known cores that aren't currently visible
|
||||
for pos := range b.knownEnemyCores {
|
||||
found := false
|
||||
for _, core := range enemyCores {
|
||||
if core.Position == pos {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
enemyCores = append(enemyCores, VisibleCore{Position: pos, Owner: 1 - myID, Active: true})
|
||||
}
|
||||
}
|
||||
|
||||
// Early game: prioritize economy (act like gatherer) until we have 3+ bots
|
||||
if len(myBots) < 3 && (state.Zone == nil || !state.Zone.Active) {
|
||||
return b.exploreAndForage(myBots, enemyPositions, state)
|
||||
}
|
||||
|
||||
// If no enemy cores visible, explore/forage
|
||||
if len(enemyCores) == 0 {
|
||||
return b.exploreAndForage(myBots, enemyPositions, state)
|
||||
}
|
||||
|
||||
// Build wall positions map
|
||||
wallPositions := make(map[Position]bool)
|
||||
for _, wall := range state.Walls {
|
||||
wallPositions[wall] = true
|
||||
}
|
||||
|
||||
// Build core positions map
|
||||
corePositions := make(map[Position]bool)
|
||||
for _, core := range state.Cores {
|
||||
corePositions[core.Position] = true
|
||||
}
|
||||
|
||||
// Build lockout positions for each enemy core
|
||||
type LockoutTarget struct {
|
||||
core VisibleCore
|
||||
position Position
|
||||
distance int
|
||||
}
|
||||
|
||||
// Collect all lockout positions (neighbors of enemy cores)
|
||||
lockoutTargets := make([]LockoutTarget, 0)
|
||||
for _, core := range enemyCores {
|
||||
// Get all 8 neighbors (including diagonals)
|
||||
neighbors := b.getAllNeighbors(core.Position, config)
|
||||
|
||||
for _, neighbor := range neighbors {
|
||||
// Skip if wall
|
||||
if wallPositions[neighbor] {
|
||||
continue
|
||||
}
|
||||
// Skip if enemy bot is there (they'd block us anyway)
|
||||
if enemyPositions[neighbor] {
|
||||
continue
|
||||
}
|
||||
// Skip if another core is there
|
||||
if corePositions[neighbor] {
|
||||
continue
|
||||
}
|
||||
lockoutTargets = append(lockoutTargets, LockoutTarget{
|
||||
core: core,
|
||||
position: neighbor,
|
||||
distance: -1, // Will compute per bot
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// If no lockout targets available, explore
|
||||
if len(lockoutTargets) == 0 {
|
||||
return b.exploreAndForage(myBots, enemyPositions, state)
|
||||
}
|
||||
|
||||
// Track which bots and targets are assigned
|
||||
assignedBots := make(map[Position]bool)
|
||||
assignedTargets := make(map[Position]bool)
|
||||
moves := make([]Move, 0)
|
||||
|
||||
// Greedy assignment: nearest bot to nearest target
|
||||
// Iterate multiple times to handle assignment chains
|
||||
for i := 0; i < len(myBots); i++ {
|
||||
bestPair := struct {
|
||||
botIdx int
|
||||
targetIdx int
|
||||
dist int
|
||||
}{-1, -1, math.MaxInt32}
|
||||
|
||||
// Find closest bot-target pair among unassigned
|
||||
for botIdx := range myBots {
|
||||
bot := myBots[botIdx]
|
||||
if assignedBots[bot.Position] {
|
||||
continue
|
||||
}
|
||||
|
||||
for targetIdx := range lockoutTargets {
|
||||
target := lockoutTargets[targetIdx]
|
||||
if assignedTargets[target.position] {
|
||||
continue
|
||||
}
|
||||
|
||||
// Compute distance from bot to target
|
||||
dist := distance2(bot.Position, target.position, config.Rows, config.Cols)
|
||||
if dist < bestPair.dist {
|
||||
bestPair.botIdx = botIdx
|
||||
bestPair.targetIdx = targetIdx
|
||||
bestPair.dist = dist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid pair found, we're done
|
||||
if bestPair.botIdx == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
// Assign this bot to this target
|
||||
bot := myBots[bestPair.botIdx]
|
||||
target := lockoutTargets[bestPair.targetIdx]
|
||||
|
||||
assignedBots[bot.Position] = true
|
||||
assignedTargets[target.position] = true
|
||||
|
||||
// Compute path to target
|
||||
path := b.findPath(bot.Position, target.position, wallPositions, enemyPositions, config)
|
||||
|
||||
if len(path) > 0 {
|
||||
moves = append(moves, Move{
|
||||
Position: bot.Position,
|
||||
Direction: path[0],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Remaining bots: check if they should rush fully-sieged cores or forage
|
||||
for _, bot := range myBots {
|
||||
if assignedBots[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
|
||||
}
|
||||
|
||||
// Priority 2: Collect energy if nearby (immediate gain)
|
||||
energyCollected := false
|
||||
for _, e := range state.Energy {
|
||||
dist := distance2(bot.Position, e, config.Rows, config.Cols)
|
||||
if dist <= 2 { // Energy is adjacent or very close
|
||||
path := b.findPath(bot.Position, e, wallPositions, enemyPositions, config)
|
||||
if len(path) > 0 && len(path) <= 2 {
|
||||
moves = append(moves, Move{
|
||||
Position: bot.Position,
|
||||
Direction: path[0],
|
||||
})
|
||||
energyCollected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if energyCollected {
|
||||
continue
|
||||
}
|
||||
|
||||
// Priority 3: Check if any core is fully surrounded
|
||||
coreRushed := false
|
||||
for _, core := range enemyCores {
|
||||
if b.isCoreFullySieged(core, assignedTargets, config) {
|
||||
// Rush this core
|
||||
path := b.findPath(bot.Position, core.Position, wallPositions, enemyPositions, config)
|
||||
if len(path) > 0 {
|
||||
moves = append(moves, Move{
|
||||
Position: bot.Position,
|
||||
Direction: path[0],
|
||||
})
|
||||
coreRushed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !coreRushed {
|
||||
// Forage for energy or advance toward nearest enemy core
|
||||
move := b.getForageMove(bot, enemyCores, wallPositions, enemyPositions, state)
|
||||
if move != nil {
|
||||
moves = append(moves, *move)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return moves, nil
|
||||
}
|
||||
|
||||
// isCoreFullySieged checks if all lockout positions around a core are assigned.
|
||||
func (b *SiegeBot) isCoreFullySieged(core VisibleCore, assignedTargets map[Position]bool, config Config) bool {
|
||||
neighbors := b.getAllNeighbors(core.Position, config)
|
||||
assignedCount := 0
|
||||
for _, neighbor := range neighbors {
|
||||
if assignedTargets[neighbor] {
|
||||
assignedCount++
|
||||
}
|
||||
}
|
||||
// Consider fully sieged if >= 50% of neighbors are assigned (more aggressive rushing)
|
||||
return assignedCount*2 >= len(neighbors)
|
||||
}
|
||||
|
||||
// findPath uses BFS to find a path from start to target, avoiding walls and enemies.
|
||||
func (b *SiegeBot) findPath(start, target Position, wallPositions, enemyPositions map[Position]bool, config Config) []Direction {
|
||||
type queueItem struct {
|
||||
pos Position
|
||||
path []Direction
|
||||
}
|
||||
|
||||
visited := make(map[Position]bool)
|
||||
queue := list.New()
|
||||
queue.PushBack(queueItem{pos: start, path: []Direction{}})
|
||||
|
||||
for queue.Len() > 0 {
|
||||
item := queue.Remove(queue.Front()).(queueItem)
|
||||
pos := item.pos
|
||||
path := item.path
|
||||
|
||||
if visited[pos] {
|
||||
continue
|
||||
}
|
||||
visited[pos] = true
|
||||
|
||||
if pos == target {
|
||||
return path
|
||||
}
|
||||
|
||||
directions := []Direction{DirN, DirE, DirS, DirW}
|
||||
for _, dir := range directions {
|
||||
nextPos := simulateMove(pos, dir, config.Rows, config.Cols)
|
||||
|
||||
// Skip walls
|
||||
if wallPositions[nextPos] {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip positions occupied by enemies (but allow adjacent tiles for faster movement)
|
||||
if enemyPositions[nextPos] {
|
||||
continue
|
||||
}
|
||||
|
||||
if !visited[nextPos] && len(path) < 30 { // Increase path length limit for more flexibility
|
||||
newPath := make([]Direction, len(path)+1)
|
||||
copy(newPath, path)
|
||||
newPath[len(path)] = dir
|
||||
queue.PushBack(queueItem{pos: nextPos, path: newPath})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No path found - try direct approach even if risky
|
||||
return b.getDirectionToward(start, target, wallPositions, config)
|
||||
}
|
||||
|
||||
// isNearEnemy checks if a position is adjacent to any enemy.
|
||||
func (b *SiegeBot) 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
|
||||
}
|
||||
|
||||
// getDirectionToward returns the first direction toward a target.
|
||||
func (b *SiegeBot) getDirectionToward(start, target Position, wallPositions map[Position]bool, config Config) []Direction {
|
||||
dr := target.Row - start.Row
|
||||
dc := target.Col - start.Col
|
||||
|
||||
// Handle wrapping
|
||||
if dr > config.Rows/2 {
|
||||
dr -= config.Rows
|
||||
} else if dr < -config.Rows/2 {
|
||||
dr += config.Rows
|
||||
}
|
||||
if dc > config.Cols/2 {
|
||||
dc -= config.Cols
|
||||
} else if dc < -config.Cols/2 {
|
||||
dc += config.Cols
|
||||
}
|
||||
|
||||
// Return primary direction
|
||||
if abs(dr) > abs(dc) {
|
||||
if dr > 0 {
|
||||
return []Direction{DirS}
|
||||
}
|
||||
return []Direction{DirN}
|
||||
}
|
||||
if dc > 0 {
|
||||
return []Direction{DirE}
|
||||
}
|
||||
return []Direction{DirW}
|
||||
}
|
||||
|
||||
// getForageMove returns a move for foraging energy when not assigned to siege.
|
||||
func (b *SiegeBot) getForageMove(
|
||||
bot VisibleBot,
|
||||
enemyCores []VisibleCore,
|
||||
wallPositions, enemyPositions map[Position]bool,
|
||||
state *VisibleState,
|
||||
) *Move {
|
||||
config := state.Config
|
||||
|
||||
// Build energy positions map
|
||||
energyPositions := make(map[Position]bool)
|
||||
for _, e := range state.Energy {
|
||||
energyPositions[e] = true
|
||||
}
|
||||
|
||||
// Find nearest energy
|
||||
_, path := b.findNearestEnergy(bot.Position, energyPositions, wallPositions, enemyPositions, config)
|
||||
if path != nil && len(path) > 0 {
|
||||
return &Move{
|
||||
Position: bot.Position,
|
||||
Direction: path[0],
|
||||
}
|
||||
}
|
||||
|
||||
// No energy - advance toward nearest enemy core
|
||||
if len(enemyCores) > 0 {
|
||||
nearestCore := enemyCores[0]
|
||||
bestDist := math.MaxInt32
|
||||
for _, core := range enemyCores {
|
||||
dist := distance2(bot.Position, core.Position, config.Rows, config.Cols)
|
||||
if dist < bestDist {
|
||||
bestDist = dist
|
||||
nearestCore = core
|
||||
}
|
||||
}
|
||||
path := b.findPath(bot.Position, nearestCore.Position, wallPositions, enemyPositions, config)
|
||||
if path != nil && len(path) > 0 {
|
||||
return &Move{
|
||||
Position: bot.Position,
|
||||
Direction: path[0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Spread out to explore
|
||||
return b.getExploreMove(bot, wallPositions, enemyPositions, config)
|
||||
}
|
||||
|
||||
// exploreAndForage handles exploration when no enemy cores are visible.
|
||||
func (b *SiegeBot) exploreAndForage(myBots []VisibleBot, enemyPositions map[Position]bool, state *VisibleState) ([]Move, error) {
|
||||
moves := make([]Move, 0, len(myBots))
|
||||
config := state.Config
|
||||
|
||||
// Build wall positions map
|
||||
wallPositions := make(map[Position]bool)
|
||||
for _, wall := range state.Walls {
|
||||
wallPositions[wall] = true
|
||||
}
|
||||
|
||||
// Build energy positions map
|
||||
energyPositions := make(map[Position]bool)
|
||||
for _, e := range state.Energy {
|
||||
energyPositions[e] = true
|
||||
}
|
||||
|
||||
usedEnergy := make(map[Position]bool)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Find nearest energy
|
||||
_, path := b.findNearestEnergy(bot.Position, energyPositions, wallPositions, enemyPositions, config)
|
||||
if path != nil && len(path) > 0 {
|
||||
moves = append(moves, Move{
|
||||
Position: bot.Position,
|
||||
Direction: path[0],
|
||||
})
|
||||
nextPos := simulateMove(bot.Position, path[0], config.Rows, config.Cols)
|
||||
usedEnergy[nextPos] = true
|
||||
continue
|
||||
}
|
||||
|
||||
// Explore
|
||||
move := b.getExploreMove(bot, wallPositions, enemyPositions, config)
|
||||
if move != nil {
|
||||
moves = append(moves, *move)
|
||||
}
|
||||
}
|
||||
|
||||
return moves, nil
|
||||
}
|
||||
|
||||
// findNearestEnergy finds the nearest energy using BFS.
|
||||
func (b *SiegeBot) findNearestEnergy(
|
||||
start Position,
|
||||
energyPositions, wallPositions, enemyPositions 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
|
||||
|
||||
if energyPositions[pos] {
|
||||
nearestEnergy = pos
|
||||
bestPath = path
|
||||
break
|
||||
}
|
||||
|
||||
// Don't path through enemy-adjacent tiles
|
||||
if len(path) > 0 && b.isNearEnemy(pos, enemyPositions, config) {
|
||||
continue
|
||||
}
|
||||
|
||||
directions := []Direction{DirN, DirE, DirS, DirW}
|
||||
for _, dir := range directions {
|
||||
nextPos := simulateMove(pos, dir, config.Rows, config.Cols)
|
||||
if wallPositions[nextPos] {
|
||||
continue
|
||||
}
|
||||
if !visited[nextPos] && len(path) < 20 {
|
||||
newPath := make([]Direction, len(path)+1)
|
||||
copy(newPath, path)
|
||||
newPath[len(path)] = dir
|
||||
queue.PushBack(queueItem{pos: nextPos, path: newPath})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nearestEnergy, bestPath
|
||||
}
|
||||
|
||||
// getExploreMove returns a move for exploring.
|
||||
func (b *SiegeBot) getExploreMove(
|
||||
bot VisibleBot,
|
||||
wallPositions, enemyPositions map[Position]bool,
|
||||
config Config,
|
||||
) *Move {
|
||||
directions := []Direction{DirN, DirE, DirS, DirW}
|
||||
for _, dir := range directions {
|
||||
newPos := simulateMove(bot.Position, dir, config.Rows, config.Cols)
|
||||
if !wallPositions[newPos] && !b.isNearEnemy(newPos, enemyPositions, config) {
|
||||
return &Move{
|
||||
Position: bot.Position,
|
||||
Direction: dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
// No safe move, stay put (return first valid direction)
|
||||
return &Move{
|
||||
Position: bot.Position,
|
||||
Direction: DirN,
|
||||
}
|
||||
}
|
||||
|
||||
// getAllNeighbors returns all 8 neighbors (including diagonals) of a position.
|
||||
func (b *SiegeBot) getAllNeighbors(pos Position, config Config) []Position {
|
||||
neighbors := make([]Position, 0, 8)
|
||||
deltas := []struct{ dr, dc int }{
|
||||
{-1, -1}, {-1, 0}, {-1, 1},
|
||||
{0, -1}, {0, 1},
|
||||
{1, -1}, {1, 0}, {1, 1},
|
||||
}
|
||||
|
||||
for _, delta := range deltas {
|
||||
newRow := (pos.Row + delta.dr + config.Rows) % config.Rows
|
||||
newCol := (pos.Col + delta.dc + config.Cols) % config.Cols
|
||||
neighbors = append(neighbors, Position{Row: newRow, Col: newCol})
|
||||
}
|
||||
|
||||
return neighbors
|
||||
}
|
||||
|
|
|
|||
47
test-siege-arena.sh
Executable file
47
test-siege-arena.sh
Executable file
|
|
@ -0,0 +1,47 @@
|
|||
#!/bin/bash
|
||||
# Test siege bot in arena: 10 matches vs rusher+gatherer+guardian
|
||||
|
||||
BOTS="siege,rusher,gatherer"
|
||||
WINS=0
|
||||
TOTAL=10
|
||||
|
||||
echo "Testing siege bot: $TOTAL matches vs $BOTS"
|
||||
echo "======================================="
|
||||
|
||||
for i in $(seq 1 $TOTAL); do
|
||||
echo -n "Match $i: "
|
||||
OUTPUT=$(./acb-local -bots $BOTS -seed $((12345 + i)) 2>&1)
|
||||
|
||||
# Extract winner (look for "Winner: Player N" or similar)
|
||||
WINNER=$(echo "$OUTPUT" | grep -oP "Winner: Player \K\d+" || echo "")
|
||||
|
||||
if [ -z "$WINNER" ]; then
|
||||
# Try alternative patterns
|
||||
WINNER=$(echo "$OUTPUT" | grep -oP "Player \K\d+(?= wins)" || echo "")
|
||||
fi
|
||||
|
||||
if [ "$WINNER" = "0" ]; then
|
||||
echo "Player 0 (siege) WINS!"
|
||||
((WINS++))
|
||||
elif [ -n "$WINNER" ]; then
|
||||
echo "Player $WINNER wins"
|
||||
else
|
||||
# Check output for match result
|
||||
if echo "$OUTPUT" | grep -q "siege"; then
|
||||
echo "Result unclear (check output)"
|
||||
else
|
||||
echo "No clear winner detected"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "======================================="
|
||||
echo "Final Score: $WINS / $TOTAL wins"
|
||||
|
||||
if [ $WINS -ge 1 ]; then
|
||||
echo "✓ PASS: Siege bot won at least 1 match"
|
||||
exit 0
|
||||
else
|
||||
echo "✗ FAIL: Siege bot failed to win any match"
|
||||
exit 1
|
||||
fi
|
||||
Loading…
Add table
Reference in a new issue