fix(mapgen): align core placement radius with spawn radius fixes

Plan: §3.7.1 Spawn mechanics, §3.8 Map Generation

The map generator was using outdated core placement radius (0.35) that
placed cores too far apart on 40x40 maps (~28 tiles between cores on
opposite sides). This exceeded the attack radius (6 tiles for 2-player,
3.5 tiles for 3+ player), meaning generated maps didn't force combat.

The match runner was already fixed in commit e8fda06 to use:
- 2-player: primaryRadius = 0.15 (6 tiles apart = attack radius)
- 3+ player: primaryRadius = 0.063 (~3.4 tiles, within attack radius)

This change aligns cmd/acb-mapgen with the same logic, ensuring all
generated maps place cores within attack range.

Also adds validation test TestGenerateMap_CoresWithinAttackRadius to
verify cores are placed within attack radius on standard 40x40 maps.

Closes: bf-2wn4

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-05-25 08:35:13 -04:00
parent 736b0f1bd1
commit 166d3ee277
2 changed files with 80 additions and 2 deletions

View file

@ -162,10 +162,18 @@ func generateMap(numPlayers, rows, cols int, wallDensity float64, numEnergyNodes
return Position{Row: r, Col: c}
}
// Generate cores with rotational symmetry
// Generate cores with rotational symmetry.
// Per plan §3.7.1: zone forces combat, but spawn must put bots within attack range.
// For 2 players: within attack radius (6 tiles) so idle bots fight immediately
// For 3+ players: within attack radius (3.5 tiles) for same reason
var radius float64
if numPlayers == 2 {
radius = 0.15 // 6 tiles apart = exactly attack radius (6)
} else {
radius = 0.063 // ~3.4 tiles apart on toroidal grid (within attack radius of 3.46)
}
for p := 0; p < numPlayers; p++ {
angle := float64(p) * 2.0 * math.Pi / float64(numPlayers)
radius := 0.35 // 35% from center
r := centerRow + int(float64(centerRow)*radius*math.Cos(angle))
c := centerCol + int(float64(centerCol)*radius*math.Sin(angle))
m.Cores = append(m.Cores, Core{

View file

@ -1,6 +1,7 @@
package main
import (
"fmt"
"math"
"math/rand"
"testing"
@ -223,3 +224,72 @@ func TestGenerateMap_CenterWeightedEnergy(t *testing.T) {
t.Errorf("expected at least %d energy nodes in central zone, got %d", minCentral, centralCount)
}
}
func TestGenerateMap_CoresWithinAttackRadius(t *testing.T) {
// Per plan §3.7.1: spawn must put bots within attack range.
// For 2 players: within attack radius (6 tiles)
// For 3+ players: within attack radius (3.5 tiles)
// Uses standard 40x40 map size where spawn radii are calibrated.
testCases := []struct {
numPlayers int
attackRadius float64
expectedRadius float64
}{
{2, 6.0, 0.15}, // 2-player: 6 tile attack radius, 0.15 spawn radius
{3, 3.5, 0.063}, // 3+ player: 3.5 tile attack radius, 0.063 spawn radius
{4, 3.5, 0.063},
{6, 3.5, 0.063},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%dplayers", tc.numPlayers), func(t *testing.T) {
rng := rand.New(rand.NewSource(42))
m := EnsureConnectivity(tc.numPlayers, 40, 40, 0.15, 20, rng, 100)
if m == nil {
t.Fatalf("failed to generate map for %d players", tc.numPlayers)
}
if len(m.Cores) != tc.numPlayers {
t.Fatalf("expected %d cores, got %d", tc.numPlayers, len(m.Cores))
}
centerRow, centerCol := m.Rows/2, m.Cols/2
// Verify each core is at the expected radius from center
for _, c := range m.Cores {
dr := float64(c.Position.Row) - float64(centerRow)
dc := float64(c.Position.Col) - float64(centerCol)
dist := math.Sqrt(dr*dr + dc*dc)
expectedDist := float64(centerRow) * tc.expectedRadius
tolerance := 2.0 // Allow 2 tiles of tolerance for rounding
if math.Abs(dist-expectedDist) > tolerance {
t.Errorf("core at %v: distance %.2f from center, expected %.2f (tolerance %.2f)",
c.Position, dist, expectedDist, tolerance)
}
}
// Verify cores are within attack radius of each other (toroidal distance)
for i := 0; i < len(m.Cores); i++ {
for j := i + 1; j < len(m.Cores); j++ {
c1 := m.Cores[i].Position
c2 := m.Cores[j].Position
// Toroidal distance
dr := float64(c2.Row - c1.Row)
dc := float64(c2.Col - c1.Col)
// Find shortest distance on torus
height, width := float64(m.Rows), float64(m.Cols)
dr = math.Min(math.Abs(dr), height-math.Abs(dr))
dc = math.Min(math.Abs(dc), width-math.Abs(dc))
dist := math.Sqrt(dr*dr + dc*dc)
if dist > tc.attackRadius+1.0 { // +1 tolerance for rounding
t.Errorf("cores %d and %d are %.2f apart, exceeding attack radius %.2f",
i, j, dist, tc.attackRadius)
}
}
}
})
}
}