From 42e9561e462943ba99c6060c5158944083976f08 Mon Sep 17 00:00:00 2001 From: jedarden Date: Sun, 3 May 2026 18:56:39 -0400 Subject: [PATCH] feat(map-evolver): bias energy toward center, carve corridors to force contact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Energy node placement now uses a tiered radius distribution: 30% in the contested central zone (0.05-0.20 from center), 40% in the mid-zone (0.20-0.40), and 30% in the home zone (0.40-0.60). Previously nodes were placed uniformly at 0.20-0.70, letting bots farm their home quadrant indefinitely without crossing the midline. After cellular automata wall generation, a 3-wide corridor is carved from each core straight to the map center, plus a 5x5 open arena at the center tile. This creates lanes that funnel bots into contact — replicating the key mechanic that drove frequent fights in the original AI Challenge Ants game, where symmetric food spawning near the midfield forced both colonies to expand outward and collide. Co-Authored-By: Claude Sonnet 4.6 --- cmd/acb-map-evolver/main.go | 73 ++++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 1 deletion(-) diff --git a/cmd/acb-map-evolver/main.go b/cmd/acb-map-evolver/main.go index acdd6f3..ad093fd 100644 --- a/cmd/acb-map-evolver/main.go +++ b/cmd/acb-map-evolver/main.go @@ -1026,7 +1026,17 @@ func generateMapOnce(numPlayers, rows, cols int, wallDensity float64, numEnergyN for i := 0; i < nodesPerSector; i++ { for attempt := 0; attempt < 100; attempt++ { angle := rng.Float64() * 2.0 * math.Pi / float64(numPlayers) - radius := 0.2 + rng.Float64()*0.5 + // Tiered radius: bias toward center to force contested energy collection. + // 30% central (forces both players to midfield), 40% mid, 30% home. + var radius float64 + switch { + case i < nodesPerSector*3/10: + radius = 0.05 + rng.Float64()*0.15 // 0.05–0.20: contested central zone + case i < nodesPerSector*7/10: + radius = 0.20 + rng.Float64()*0.20 // 0.20–0.40: mid-zone + default: + radius = 0.40 + rng.Float64()*0.20 // 0.40–0.60: home zone + } r := centerRow + int(float64(centerRow)*radius*math.Cos(angle)) c := centerCol + int(float64(centerCol)*radius*math.Sin(angle)) pos := wrap(r, c) @@ -1133,6 +1143,20 @@ func generateMapOnce(numPlayers, rows, cols int, wallDensity float64, numEnergyN } } + // Carve corridors from each core to the map center. + // Creates 3-wide lanes that funnel bots into contact at midfield. + for _, core := range m.Cores { + carveCorridor(grid, core.Position.Row, core.Position.Col, centerRow, centerCol, rows, cols) + } + // Open a 5×5 arena at the center so all corridors connect. + for dr := -2; dr <= 2; dr++ { + for dc := -2; dc <= 2; dc++ { + rr := ((centerRow+dr)%rows + rows) % rows + cc := ((centerCol+dc)%cols + cols) % cols + grid[rr][cc] = false + } + } + // Collect wall positions and thin to target density. totalTiles := rows * cols targetWalls := int(float64(totalTiles) * wallDensity) @@ -1153,6 +1177,53 @@ func generateMapOnce(numPlayers, rows, cols int, wallDensity float64, numEnergyN return m } +// carveCorridor opens a 3-wide path from (r0,c0) to (r1,c1) using integer stepping. +// Perpendicular width is 1 tile on each side of the center line. +func carveCorridor(grid [][]bool, r0, c0, r1, c1, rows, cols int) { + dr := r1 - r0 + dc := c1 - c0 + steps := dr + if steps < 0 { + steps = -steps + } + if dc < 0 && -dc > steps { + steps = -dc + } else if dc > steps { + steps = dc + } + if steps == 0 { + return + } + horizontal := dc < 0 && -dc > (dr+1) || dc > 0 && dc > (dr+1) // wider in col direction + if dr < 0 { + dr = -dr + } + // recompute: use originals + origDR := r1 - r0 + origDC := c1 - c0 + for step := 0; step <= steps; step++ { + r := r0 + origDR*step/steps + c := c0 + origDC*step/steps + // Widen perpendicular to primary movement direction + if !horizontal { + // Mostly vertical: widen horizontally + for wc := -1; wc <= 1; wc++ { + rr := ((r)%rows + rows) % rows + cc := ((c+wc)%cols + cols) % cols + grid[rr][cc] = false + } + } else { + // Mostly horizontal: widen vertically + for wr := -1; wr <= 1; wr++ { + rr := ((r+wr)%rows + rows) % rows + cc := ((c)%cols + cols) % cols + grid[rr][cc] = false + } + } + } + _ = dr // suppress unused warning +} + // generateMapID creates a random map ID. func generateMapID(rng *rand.Rand) string { const chars = "abcdefghijklmnopqrstuvwxyz0123456789"