Tab/space alignment consistency from running gofmt on all packages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
311 lines
8.3 KiB
Go
311 lines
8.3 KiB
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func makeState(myID, energy int, bots []VisibleBot, energyTiles []Position, cores []VisibleCore, walls []Position) *GameState {
|
|
state := &GameState{
|
|
MatchID: "test",
|
|
Turn: 1,
|
|
Config: GameConfig{
|
|
Rows: 20,
|
|
Cols: 20,
|
|
MaxTurns: 500,
|
|
VisionRadius2: 49,
|
|
AttackRadius2: 5,
|
|
SpawnCost: 3,
|
|
EnergyInterval: 10,
|
|
},
|
|
Energy: energyTiles,
|
|
Cores: cores,
|
|
Walls: walls,
|
|
}
|
|
state.You.ID = myID
|
|
state.You.Energy = energy
|
|
state.Bots = bots
|
|
return state
|
|
}
|
|
|
|
func TestFarmerSeeksEnergy(t *testing.T) {
|
|
state := makeState(0, 0,
|
|
[]VisibleBot{{Position: Position{10, 10}, Owner: 0}},
|
|
[]Position{{10, 14}}, // energy 4 tiles east
|
|
[]VisibleCore{{Position: Position{10, 10}, Owner: 0, Active: true}},
|
|
nil,
|
|
)
|
|
|
|
s := NewFarmerStrategy()
|
|
moves := s.ComputeMoves(state)
|
|
|
|
if len(moves) != 1 {
|
|
t.Fatalf("expected 1 move, got %d", len(moves))
|
|
}
|
|
if moves[0].Direction != "E" {
|
|
t.Errorf("expected move E toward energy, got %s", moves[0].Direction)
|
|
}
|
|
}
|
|
|
|
func TestFarmerFleesEnemy(t *testing.T) {
|
|
state := makeState(0, 0,
|
|
[]VisibleBot{
|
|
{Position: Position{10, 10}, Owner: 0}, // my bot
|
|
{Position: Position{10, 12}, Owner: 1}, // enemy 2 tiles south
|
|
},
|
|
[]Position{{10, 14}}, // energy exists but should flee instead
|
|
[]VisibleCore{{Position: Position{10, 10}, Owner: 0, Active: true}},
|
|
nil,
|
|
)
|
|
|
|
s := NewFarmerStrategy()
|
|
moves := s.ComputeMoves(state)
|
|
|
|
if len(moves) != 1 {
|
|
t.Fatalf("expected 1 move, got %d", len(moves))
|
|
}
|
|
// Should flee away from enemy at (10,12), not go toward energy at (10,14)
|
|
if moves[0].Direction == "E" {
|
|
t.Errorf("bot should flee from enemy, not move toward energy; got %s", moves[0].Direction)
|
|
}
|
|
}
|
|
|
|
func TestFarmerFleesFromNearbyEnemy(t *testing.T) {
|
|
// Enemy within 3 tiles (fleeRadius2 = 9, distance2 = 4 < 9)
|
|
state := makeState(0, 0,
|
|
[]VisibleBot{
|
|
{Position: Position{10, 10}, Owner: 0},
|
|
{Position: Position{12, 10}, Owner: 1}, // enemy 2 tiles south, d2=4
|
|
},
|
|
nil,
|
|
[]VisibleCore{{Position: Position{10, 10}, Owner: 0, Active: true}},
|
|
nil,
|
|
)
|
|
|
|
s := NewFarmerStrategy()
|
|
moves := s.ComputeMoves(state)
|
|
|
|
if len(moves) != 1 {
|
|
t.Fatalf("expected 1 move, got %d", len(moves))
|
|
}
|
|
// Should flee north (away from enemy to the south)
|
|
if moves[0].Direction != "N" {
|
|
t.Errorf("expected flee north, got %s", moves[0].Direction)
|
|
}
|
|
}
|
|
|
|
func TestFarmerMultipleBotsSeekDifferentEnergy(t *testing.T) {
|
|
state := makeState(0, 6,
|
|
[]VisibleBot{
|
|
{Position: Position{5, 5}, Owner: 0},
|
|
{Position: Position{15, 15}, Owner: 0},
|
|
},
|
|
[]Position{{5, 8}, {15, 12}}, // two energy tiles
|
|
[]VisibleCore{{Position: Position{5, 5}, Owner: 0, Active: true}},
|
|
nil,
|
|
)
|
|
|
|
s := NewFarmerStrategy()
|
|
moves := s.ComputeMoves(state)
|
|
|
|
if len(moves) != 2 {
|
|
t.Fatalf("expected 2 moves, got %d", len(moves))
|
|
}
|
|
|
|
// Each bot should target its nearest energy
|
|
dirs := map[string]bool{}
|
|
for _, m := range moves {
|
|
dirs[m.Direction] = true
|
|
}
|
|
|
|
// Bot at (5,5) should move E toward (5,8)
|
|
// Bot at (15,15) should move W toward (15,12)
|
|
// We can't guarantee exact mapping, but both should have moves
|
|
if len(dirs) < 1 {
|
|
t.Errorf("expected bots to move toward different energy, got dirs: %v", dirs)
|
|
}
|
|
}
|
|
|
|
func TestFarmerHoldsOnEnergy(t *testing.T) {
|
|
// Bot already on energy tile, no enemies
|
|
state := makeState(0, 0,
|
|
[]VisibleBot{{Position: Position{10, 10}, Owner: 0}},
|
|
[]Position{{10, 10}}, // energy on same tile as bot
|
|
[]VisibleCore{{Position: Position{5, 5}, Owner: 0, Active: true}},
|
|
nil,
|
|
)
|
|
|
|
s := NewFarmerStrategy()
|
|
moves := s.ComputeMoves(state)
|
|
|
|
// Bot should hold position to collect energy (no move issued)
|
|
if len(moves) != 0 {
|
|
t.Errorf("expected bot to hold on energy (0 moves), got %d", len(moves))
|
|
}
|
|
}
|
|
|
|
func TestFarmerStaysNearCore(t *testing.T) {
|
|
// Bot far from core, no energy, no enemies
|
|
state := makeState(0, 0,
|
|
[]VisibleBot{{Position: Position{15, 15}, Owner: 0}},
|
|
nil,
|
|
[]VisibleCore{{Position: Position{5, 5}, Owner: 0, Active: true}},
|
|
nil,
|
|
)
|
|
|
|
s := NewFarmerStrategy()
|
|
moves := s.ComputeMoves(state)
|
|
|
|
if len(moves) != 1 {
|
|
t.Fatalf("expected 1 move, got %d", len(moves))
|
|
}
|
|
// Should move toward core at (5,5), i.e. N or W
|
|
dir := moves[0].Direction
|
|
if dir != "N" && dir != "W" {
|
|
t.Errorf("expected move toward core (N or W), got %s", dir)
|
|
}
|
|
}
|
|
|
|
func TestFarmerIgnoresContestedEnergy(t *testing.T) {
|
|
// Energy at (10,14) with enemy adjacent at (10,15) - contested
|
|
// Uncontested energy at (10,5)
|
|
state := makeState(0, 0,
|
|
[]VisibleBot{
|
|
{Position: Position{10, 10}, Owner: 0},
|
|
{Position: Position{10, 15}, Owner: 1}, // enemy adjacent to energy
|
|
},
|
|
[]Position{{10, 14}, {10, 5}}, // contested energy, safe energy
|
|
[]VisibleCore{{Position: Position{10, 10}, Owner: 0, Active: true}},
|
|
nil,
|
|
)
|
|
|
|
s := NewFarmerStrategy()
|
|
moves := s.ComputeMoves(state)
|
|
|
|
if len(moves) != 1 {
|
|
t.Fatalf("expected 1 move, got %d", len(moves))
|
|
}
|
|
// Should prefer safe energy at (10,5) -> move W
|
|
if moves[0].Direction != "W" {
|
|
t.Errorf("expected move W toward safe energy, got %s", moves[0].Direction)
|
|
}
|
|
}
|
|
|
|
func TestFarmerAvoidsWalls(t *testing.T) {
|
|
// Bot at (10,10), energy at (10,14), wall at (10,11) blocking direct path
|
|
state := makeState(0, 0,
|
|
[]VisibleBot{{Position: Position{10, 10}, Owner: 0}},
|
|
[]Position{{10, 14}},
|
|
[]VisibleCore{{Position: Position{10, 10}, Owner: 0, Active: true}},
|
|
[]Position{{10, 11}},
|
|
)
|
|
|
|
s := NewFarmerStrategy()
|
|
moves := s.ComputeMoves(state)
|
|
|
|
if len(moves) != 1 {
|
|
t.Fatalf("expected 1 move, got %d", len(moves))
|
|
}
|
|
// Should not move E into the wall
|
|
if moves[0].Direction == "E" {
|
|
t.Errorf("bot should avoid wall at (10,11), got direction E")
|
|
}
|
|
}
|
|
|
|
func TestDistance2(t *testing.T) {
|
|
tests := []struct {
|
|
a, b Position
|
|
rows, cols int
|
|
want int
|
|
}{
|
|
{Position{0, 0}, Position{0, 0}, 20, 20, 0},
|
|
{Position{0, 0}, Position{0, 3}, 20, 20, 9},
|
|
{Position{0, 0}, Position{3, 4}, 20, 20, 25},
|
|
// Toroidal: distance from (0,0) to (19,0) on 20-row grid = 1 row
|
|
{Position{0, 0}, Position{19, 0}, 20, 20, 1},
|
|
// Toroidal: distance from (0,0) to (0,19) on 20-col grid = 1 col
|
|
{Position{0, 0}, Position{0, 19}, 20, 20, 1},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := distance2(tt.a, tt.b, tt.rows, tt.cols)
|
|
if got != tt.want {
|
|
t.Errorf("distance2(%v, %v, %d, %d) = %d, want %d",
|
|
tt.a, tt.b, tt.rows, tt.cols, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBFS(t *testing.T) {
|
|
wallSet := map[Position]bool{{10, 11}: true}
|
|
passable := func(p Position) bool { return !wallSet[p] }
|
|
|
|
// Direct path north
|
|
dir := BFS(Position{5, 5}, Position{3, 5}, passable, 20, 20)
|
|
if dir != "N" {
|
|
t.Errorf("BFS to north: got %q, want N", dir)
|
|
}
|
|
|
|
// Path around wall: (10,10) to (10,14) with wall at (10,11)
|
|
dir = BFS(Position{10, 10}, Position{10, 14}, passable, 20, 20)
|
|
if dir == "" {
|
|
t.Error("BFS should find path around wall")
|
|
}
|
|
// Should go N or S to bypass wall at (10,11), not E
|
|
if dir == "E" {
|
|
t.Errorf("BFS should not go directly into wall, got E")
|
|
}
|
|
}
|
|
|
|
func TestSimulateMove(t *testing.T) {
|
|
tests := []struct {
|
|
pos Position
|
|
dir string
|
|
rows, cols int
|
|
want Position
|
|
}{
|
|
{Position{5, 5}, "N", 20, 20, Position{4, 5}},
|
|
{Position{5, 5}, "S", 20, 20, Position{6, 5}},
|
|
{Position{5, 5}, "E", 20, 20, Position{5, 6}},
|
|
{Position{5, 5}, "W", 20, 20, Position{5, 4}},
|
|
// Toroidal wrap
|
|
{Position{0, 0}, "N", 20, 20, Position{19, 0}},
|
|
{Position{0, 0}, "W", 20, 20, Position{0, 19}},
|
|
{Position{19, 19}, "S", 20, 20, Position{0, 19}},
|
|
{Position{19, 19}, "E", 20, 20, Position{19, 0}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := simulateMove(tt.pos, tt.dir, tt.rows, tt.cols)
|
|
if got != tt.want {
|
|
t.Errorf("simulateMove(%v, %q, %d, %d) = %v, want %v",
|
|
tt.pos, tt.dir, tt.rows, tt.cols, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFarmerNoSelfCollision(t *testing.T) {
|
|
// Two bots at (10,10) and (10,11), energy at (10,12)
|
|
// Both want to go east, but can't land on same tile
|
|
state := makeState(0, 0,
|
|
[]VisibleBot{
|
|
{Position: Position{10, 10}, Owner: 0},
|
|
{Position: Position{10, 11}, Owner: 0},
|
|
},
|
|
[]Position{{10, 12}},
|
|
[]VisibleCore{{Position: Position{10, 10}, Owner: 0, Active: true}},
|
|
nil,
|
|
)
|
|
|
|
s := NewFarmerStrategy()
|
|
moves := s.ComputeMoves(state)
|
|
|
|
// Collect destinations
|
|
dests := map[Position]bool{}
|
|
for _, m := range moves {
|
|
dest := simulateMove(m.Position, m.Direction, 20, 20)
|
|
if dests[dest] {
|
|
t.Errorf("two bots collide at %v", dest)
|
|
}
|
|
dests[dest] = true
|
|
}
|
|
}
|