Tab/space alignment consistency from running gofmt on all packages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
365 lines
9.4 KiB
Go
365 lines
9.4 KiB
Go
package engine
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"testing"
|
|
)
|
|
|
|
func TestGridWrap(t *testing.T) {
|
|
g := NewGrid(60, 60)
|
|
|
|
tests := []struct {
|
|
row, col int
|
|
want Position
|
|
}{
|
|
{0, 0, Position{0, 0}},
|
|
{59, 59, Position{59, 59}},
|
|
{60, 0, Position{0, 0}}, // wrap row
|
|
{0, 60, Position{0, 0}}, // wrap col
|
|
{-1, 0, Position{59, 0}}, // negative wrap row
|
|
{0, -1, Position{0, 59}}, // negative wrap col
|
|
{65, 65, Position{5, 5}}, // both wrap
|
|
{-5, -5, Position{55, 55}}, // both negative wrap
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := g.Wrap(tt.row, tt.col)
|
|
if got != tt.want {
|
|
t.Errorf("Wrap(%d, %d) = %v, want %v", tt.row, tt.col, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGridDistance2(t *testing.T) {
|
|
g := NewGrid(60, 60)
|
|
|
|
tests := []struct {
|
|
a, b Position
|
|
want int
|
|
}{
|
|
// Direct distances
|
|
{Position{0, 0}, Position{0, 0}, 0},
|
|
{Position{0, 0}, Position{0, 3}, 9},
|
|
{Position{0, 0}, Position{3, 4}, 25}, // 3-4-5 triangle
|
|
{Position{10, 10}, Position{13, 14}, 25},
|
|
|
|
// Toroidal wrapping - shorter path across boundary
|
|
{Position{0, 0}, Position{59, 0}, 1}, // distance 1 via wrap
|
|
{Position{0, 0}, Position{58, 0}, 4}, // distance 2 via wrap
|
|
{Position{0, 0}, Position{0, 59}, 1}, // distance 1 via wrap col
|
|
{Position{0, 0}, Position{59, 59}, 2}, // distance sqrt(2) via corner wrap
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := g.Distance2(tt.a, tt.b)
|
|
if got != tt.want {
|
|
t.Errorf("Distance2(%v, %v) = %d, want %d", tt.a, tt.b, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGridInRadius(t *testing.T) {
|
|
g := NewGrid(60, 60)
|
|
|
|
// Test vision radius 49 (default ~7 tiles)
|
|
center := Position{30, 30}
|
|
|
|
// Should be visible
|
|
visible := []Position{
|
|
{30, 30}, // self
|
|
{30, 31}, // adjacent
|
|
{30, 37}, // 7 tiles away (dist^2 = 49)
|
|
{37, 30}, // 7 tiles away (dist^2 = 49)
|
|
{33, 33}, // diagonal (dist^2 = 18)
|
|
{34, 34}, // diagonal (dist^2 = 32)
|
|
{35, 34}, // dist^2 = 25 + 16 = 41
|
|
}
|
|
for _, p := range visible {
|
|
if !g.InRadius(center, p, 49) {
|
|
t.Errorf("Position %v should be within radius 49 of %v", p, center)
|
|
}
|
|
}
|
|
|
|
// Should not be visible
|
|
notVisible := []Position{
|
|
{30, 38}, // 8 tiles away, dist^2 = 64 > 49
|
|
{38, 38}, // diagonal 8*2 = 128 > 49
|
|
}
|
|
for _, p := range notVisible {
|
|
if g.InRadius(center, p, 49) {
|
|
t.Errorf("Position %v should NOT be within radius 49 of %v", p, center)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGridAttackRadius(t *testing.T) {
|
|
g := NewGrid(60, 60)
|
|
|
|
// Attack radius 5 (default ~2.24 tiles)
|
|
center := Position{30, 30}
|
|
|
|
// Attack radius includes cardinal, diagonal neighbors, and one more ring
|
|
// dist^2 <= 5 means: 0, 1, 2, 4, 5
|
|
// (1,0) = 1, (1,1) = 2, (2,0) = 4, (2,1) = 5
|
|
inAttack := []Position{
|
|
{30, 30}, // self (dist 0)
|
|
{30, 31}, // cardinal (dist 1)
|
|
{31, 31}, // diagonal (dist 2)
|
|
{32, 30}, // 2 tiles (dist 4)
|
|
{32, 31}, // (dist 5)
|
|
}
|
|
for _, p := range inAttack {
|
|
if !g.InRadius(center, p, 5) {
|
|
t.Errorf("Position %v should be in attack radius of %v", p, center)
|
|
}
|
|
}
|
|
|
|
// Outside attack radius
|
|
outAttack := []Position{
|
|
{33, 30}, // 3 tiles (dist 9 > 5)
|
|
{32, 32}, // (dist 8 > 5)
|
|
}
|
|
for _, p := range outAttack {
|
|
if g.InRadius(center, p, 5) {
|
|
t.Errorf("Position %v should NOT be in attack radius of %v", p, center)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGridWalls(t *testing.T) {
|
|
g := NewGrid(10, 10)
|
|
|
|
// Initially no walls
|
|
if g.IsWall(Position{5, 5}) {
|
|
t.Error("Position should not be a wall initially")
|
|
}
|
|
|
|
// Set a wall
|
|
g.Set(5, 5, TileWall)
|
|
if !g.IsWall(Position{5, 5}) {
|
|
t.Error("Position should be a wall after setting")
|
|
}
|
|
if g.IsPassable(Position{5, 5}) {
|
|
t.Error("Wall should not be passable")
|
|
}
|
|
|
|
// Remove wall
|
|
g.Set(5, 5, TileOpen)
|
|
if g.IsWall(Position{5, 5}) {
|
|
t.Error("Position should not be a wall after clearing")
|
|
}
|
|
}
|
|
|
|
func TestGridMove(t *testing.T) {
|
|
g := NewGrid(60, 60)
|
|
|
|
tests := []struct {
|
|
start Position
|
|
dir Direction
|
|
want Position
|
|
}{
|
|
{Position{30, 30}, DirN, Position{29, 30}},
|
|
{Position{30, 30}, DirS, Position{31, 30}},
|
|
{Position{30, 30}, DirE, Position{30, 31}},
|
|
{Position{30, 30}, DirW, Position{30, 29}},
|
|
// Wrap at edges
|
|
{Position{0, 0}, DirN, Position{59, 0}},
|
|
{Position{0, 0}, DirW, Position{0, 59}},
|
|
{Position{59, 59}, DirS, Position{0, 59}},
|
|
{Position{59, 59}, DirE, Position{59, 0}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := g.Move(tt.start, tt.dir)
|
|
if got != tt.want {
|
|
t.Errorf("Move(%v, %v) = %v, want %v", tt.start, tt.dir, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGridVisibleFrom(t *testing.T) {
|
|
g := NewGrid(60, 60)
|
|
|
|
// Single bot at center
|
|
positions := []Position{{30, 30}}
|
|
visible := g.VisibleFrom(positions, 49)
|
|
|
|
// Should see positions within radius
|
|
if !visible[Position{30, 30}] {
|
|
t.Error("Should see own position")
|
|
}
|
|
if !visible[Position{30, 37}] {
|
|
t.Error("Should see position 7 tiles away (dist^2 = 49)")
|
|
}
|
|
if visible[Position{30, 38}] {
|
|
t.Error("Should NOT see position 8 tiles away (dist^2 = 64 > 49)")
|
|
}
|
|
|
|
// Multiple bots - union of visibility
|
|
positions = []Position{{10, 10}, {50, 50}}
|
|
visible = g.VisibleFrom(positions, 49)
|
|
|
|
if !visible[Position{10, 10}] {
|
|
t.Error("Should see first bot position")
|
|
}
|
|
if !visible[Position{50, 50}] {
|
|
t.Error("Should see second bot position")
|
|
}
|
|
if !visible[Position{17, 10}] {
|
|
t.Error("Should see 7 tiles from first bot")
|
|
}
|
|
if !visible[Position{43, 50}] {
|
|
t.Error("Should see 7 tiles from second bot (via wrap)")
|
|
}
|
|
}
|
|
|
|
func TestGridRandomPassable(t *testing.T) {
|
|
g := NewGrid(10, 10)
|
|
|
|
// Add some walls
|
|
g.Set(5, 5, TileWall)
|
|
g.Set(5, 6, TileWall)
|
|
|
|
rng := rand.New(rand.NewSource(42))
|
|
|
|
// Get many random positions and verify they're all passable
|
|
for i := 0; i < 100; i++ {
|
|
p := g.RandomPassable(rng)
|
|
if !g.IsPassable(p) {
|
|
t.Errorf("RandomPassable returned impassable position %v", p)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSqrtApprox(t *testing.T) {
|
|
tests := []struct {
|
|
n int
|
|
want int
|
|
}{
|
|
{0, 0},
|
|
{1, 1},
|
|
{4, 2},
|
|
{9, 3},
|
|
{16, 4},
|
|
{25, 5},
|
|
{49, 7},
|
|
{50, 7}, // sqrt(50) ≈ 7.07
|
|
{100, 10},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := sqrtApprox(tt.n)
|
|
if got != tt.want {
|
|
t.Errorf("sqrtApprox(%d) = %d, want %d", tt.n, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestINV6_ToroidalBounds is a property-based fuzz test that verifies
|
|
// INV-6: No bot, energy, core, or wall position ever has row < 0,
|
|
// row >= rows, col < 0, or col >= cols after any movement operation.
|
|
// All movement must use (pos + delta + size) % size.
|
|
//
|
|
// This test runs thousands of random scenarios with various grid sizes
|
|
// and operations to enforce the bound invariant.
|
|
func TestINV6_ToroidalBounds(t *testing.T) {
|
|
// Test various grid dimensions
|
|
testCases := []struct {
|
|
rows int
|
|
cols int
|
|
}{
|
|
{30, 30}, // Minimum
|
|
{40, 60}, // Rectangular
|
|
{60, 60}, // Standard square
|
|
{100, 100}, // Large
|
|
{120, 80}, // Large rectangular
|
|
{200, 200}, // Maximum
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(fmt.Sprintf("%dx%d", tc.rows, tc.cols), func(t *testing.T) {
|
|
// Use multiple random seeds for thoroughness
|
|
seeds := []int64{42, 123, 456, 789, 999, 1337, 2024, 0, -1, 1}
|
|
|
|
for _, seed := range seeds {
|
|
t.Run(fmt.Sprintf("seed_%d", seed), func(t *testing.T) {
|
|
rng := rand.New(rand.NewSource(seed))
|
|
testToroidalBoundsScenario(t, rng, tc.rows, tc.cols)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// testToroidalBoundsScenario runs a random scenario and verifies all positions stay in bounds.
|
|
func testToroidalBoundsScenario(t *testing.T, rng *rand.Rand, rows, cols int) {
|
|
g := NewGrid(rows, cols)
|
|
|
|
// Add random walls
|
|
numWalls := rng.Intn(rows * cols / 20) // Up to 5% wall density
|
|
for i := 0; i < numWalls; i++ {
|
|
row := rng.Intn(rows*3) - rows // Can be negative or >= rows
|
|
col := rng.Intn(cols*3) - cols
|
|
g.Set(row, col, TileWall)
|
|
}
|
|
|
|
// Test that all wall positions are in bounds
|
|
for pos := range g.Walls {
|
|
if !posInBounds(pos, rows, cols) {
|
|
t.Errorf("Wall position %v out of bounds for %dx%d grid", pos, rows, cols)
|
|
}
|
|
}
|
|
|
|
// Test Wrap with random positions (including out of bounds)
|
|
for i := 0; i < 1000; i++ {
|
|
row := rng.Intn(rows*10) - rows*5 // Wide range including negative
|
|
col := rng.Intn(cols*10) - cols*5
|
|
wrapped := g.Wrap(row, col)
|
|
|
|
if !posInBounds(wrapped, rows, cols) {
|
|
t.Errorf("Wrap(%d, %d) = %v out of bounds for %dx%d grid", row, col, wrapped, rows, cols)
|
|
}
|
|
}
|
|
|
|
// Test Move from random positions in all directions
|
|
testPositions := []Position{
|
|
{0, 0}, {0, cols - 1}, {rows - 1, 0}, {rows - 1, cols - 1}, // Corners
|
|
{rows / 2, cols / 2}, // Center
|
|
{0, cols / 2}, {rows - 1, cols / 2}, // Middle of edges
|
|
}
|
|
|
|
directions := []Direction{DirN, DirE, DirS, DirW}
|
|
|
|
for _, pos := range testPositions {
|
|
for _, dir := range directions {
|
|
moved := g.Move(pos, dir)
|
|
if !posInBounds(moved, rows, cols) {
|
|
t.Errorf("Move(%v, %v) = %v out of bounds for %dx%d grid", pos, dir, moved, rows, cols)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test Neighbors returns positions within bounds
|
|
center := Position{rows / 2, cols / 2}
|
|
neighbors := g.Neighbors(center, 49) // vision radius
|
|
for _, n := range neighbors {
|
|
if !posInBounds(n, rows, cols) {
|
|
t.Errorf("Neighbors(%v, 49) returned out-of-bounds position %v for %dx%d grid", center, n, rows, cols)
|
|
}
|
|
}
|
|
|
|
// Test VisibleFrom returns positions within bounds
|
|
positions := []Position{{0, 0}, {rows - 1, cols - 1}}
|
|
visible := g.VisibleFrom(positions, 49)
|
|
for pos := range visible {
|
|
if !posInBounds(pos, rows, cols) {
|
|
t.Errorf("VisibleFrom returned out-of-bounds position %v for %dx%d grid", pos, rows, cols)
|
|
}
|
|
}
|
|
}
|
|
|
|
// posInBounds checks if a position is within the grid bounds.
|
|
func posInBounds(pos Position, rows, cols int) bool {
|
|
return pos.Row >= 0 && pos.Row < rows && pos.Col >= 0 && pos.Col < cols
|
|
}
|