From 166d3ee27759ec8870b86cacff1c2f01869f74a5 Mon Sep 17 00:00:00 2001 From: jedarden Date: Mon, 25 May 2026 08:35:13 -0400 Subject: [PATCH] fix(mapgen): align core placement radius with spawn radius fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cmd/acb-mapgen/main.go | 12 +++++- cmd/acb-mapgen/mapgen_test.go | 70 +++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/cmd/acb-mapgen/main.go b/cmd/acb-mapgen/main.go index 53183da..35176df 100644 --- a/cmd/acb-mapgen/main.go +++ b/cmd/acb-mapgen/main.go @@ -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{ diff --git a/cmd/acb-mapgen/mapgen_test.go b/cmd/acb-mapgen/mapgen_test.go index 8133085..25f4533 100644 --- a/cmd/acb-mapgen/mapgen_test.go +++ b/cmd/acb-mapgen/mapgen_test.go @@ -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) + } + } + } + }) + } +}