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:
parent
736b0f1bd1
commit
166d3ee277
2 changed files with 80 additions and 2 deletions
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue