ai-code-battle/engine/match_test.go
jedarden 9dae3bd3de fix(engine): reduce 2-player spawn radius and zone shrink step for combat density
Problem: Plan §3.7.1 claims 65-80% combat density for 2-player matches,
but actual testing showed 0% combat deaths. Zone killed all bots before
they could fight.

Root cause:
- Spawn radius 50% (10 tiles from center) put bots too far apart
- Zone shrink step 2 tiles/turn was too fast
- Bots died to zone before reaching each other

Solution:
- Reduce 2-player spawn radius from 50% to 10% (~2 tiles from center)
- Reduce zone shrink step from 2 to 1 tile/turn (slower zone)
- Bots now spawn close enough to reach safe zone and fight

Results:
- Before: 0% combat density (all zone deaths)
- After: 100% combat density (2 deaths per match across 20+ test matches)
- Tested against: swarm/gatherer, hunter/rusher, guardian/random

Updated TestSpawnRadiusOutsideZone to TestSpawnRadiusWithinReach to
reflect the new design (spawn within reach of safe zone, not outside).

Closes: bf-1jya
2026-05-25 13:50:09 -04:00

151 lines
4.4 KiB
Go

package engine
import (
"math"
"math/rand"
"testing"
)
// TestSpawnRadiusWithinReach verifies that bots spawn close enough to the center
// to reach the safe zone before it kills them. Per plan §3.7.1, the zone forces combat
// by shrinking, but bots must be able to reach the safe zone to have time to fight.
// If bots spawn too far from center, the zone kills them before combat can occur.
// Testing shows 10% spawn radius achieves 100% combat density vs 0% at 50% radius.
func TestSpawnRadiusWithinReach(t *testing.T) {
tests := []struct {
name string
numPlayers int
coresPerPlayer int
}{
{"2-player", 2, 1},
{"2-player-2-cores", 2, 2},
{"3-player", 3, 1},
{"4-player", 4, 1},
{"6-player", 6, 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := ConfigForPlayers(tt.numPlayers, tt.coresPerPlayer)
gs := NewGameState(cfg, rand.New(rand.NewSource(42)))
// Add players
for range tt.numPlayers {
gs.AddPlayer()
}
// Generate map using the match runner
mr := NewMatchRunner(cfg, WithRNG(rand.New(rand.NewSource(42))))
mr.generateMap(gs, tt.numPlayers)
// Verify all spawn positions are within reach of the safe zone
center := Position{Row: cfg.Rows / 2, Col: cfg.Cols / 2}
for _, bot := range gs.Bots {
if !bot.Alive {
continue
}
// Calculate distance from center
dist2 := gs.Grid.Distance2(bot.Position, center)
dist := math.Sqrt(float64(dist2))
// Verify spawn distance is reasonable: not too far from center
// For 2-player, spawn radius is 10% (~2 tiles from center on 40x40)
// This ensures bots can reach safe zone before zone kills them
maxSpawnDist := float64(cfg.Rows) * 0.15 // 15% of grid size as upper bound
if dist > maxSpawnDist {
t.Errorf("Player %d bot spawned at distance %.1f from center, > max spawn distance %.1f (position: %v)",
bot.Owner, dist, maxSpawnDist, bot.Position)
}
}
})
}
}
// TestSpawnRadiusForcesCombat verifies that when zone shrinks to minimum,
// bots on opposite sides are within attack radius of each other.
func TestSpawnRadiusForcesCombat(t *testing.T) {
tests := []struct {
name string
numPlayers int
coresPerPlayer int
}{
{"2-player", 2, 1},
{"3-player", 3, 1},
{"4-player", 4, 1},
{"6-player", 6, 1},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := ConfigForPlayers(tt.numPlayers, tt.coresPerPlayer)
gs := NewGameState(cfg, rand.New(rand.NewSource(42)))
// Add players
for range tt.numPlayers {
gs.AddPlayer()
}
// Generate map
mr := NewMatchRunner(cfg, WithRNG(rand.New(rand.NewSource(42))))
mr.generateMap(gs, tt.numPlayers)
// Simulate zone shrinking to minimum
// Bots would be forced toward center, ending at zone edge
centerRow := cfg.Rows / 2
centerCol := cfg.Cols / 2
// Calculate where each player's bot would end up at zone edge
// (simplified: project to zone edge along the same angle)
attackRadius := math.Sqrt(float64(cfg.AttackRadius2))
for _, bot := range gs.Bots {
if !bot.Alive || bot.Owner != 0 {
continue
}
// Vector from center to bot
dr := float64(bot.Position.Row - centerRow)
dc := float64(bot.Position.Col - centerCol)
// Normalize to zone edge
dist := math.Sqrt(dr*dr + dc*dc)
if dist == 0 {
continue
}
scale := float64(cfg.ZoneMinRadius) / dist
// Check distance to other players' projected zone edge positions
for _, other := range gs.Bots {
if !other.Alive || other.Owner <= bot.Owner {
continue
}
odr := float64(other.Position.Row - centerRow)
odc := float64(other.Position.Col - centerCol)
odist := math.Sqrt(odr*odr + odc*odc)
if odist == 0 {
continue
}
oscale := float64(cfg.ZoneMinRadius) / odist
// Distance between zone edge projections
er := centerRow + int(dr*scale)
ec := centerCol + int(dc*scale)
eor := centerRow + int(odr*oscale)
eoc := centerCol + int(odc*oscale)
// Toroidal distance
gridDist2 := gs.Grid.Distance2(Position{Row: er, Col: ec}, Position{Row: eor, Col: eoc})
gridDist := math.Sqrt(float64(gridDist2))
if gridDist > attackRadius {
t.Errorf("Players %d and %d: projected zone edge distance %.1f > attack radius %.1f (bot pos: %v, other pos: %v)",
bot.Owner, other.Owner, gridDist, attackRadius, bot.Position, other.Position)
}
}
}
})
}
}