ai-code-battle/cmd/acb-evolver/internal/mapelites/grid_test.go
jedarden 80334c6e34 feat(evolver): expand MAP-Elites from 2-D to 4-D grid per §10.2
- Add Exploration and Formation axis definitions with feature extraction
  from source code pattern matching (exploration/formation indicators)
- Extend Grid key from (x,y) to (x,y,z,w) with 3⁴=81-cell behavior grid
- Update bin assignment, promotion gate, and persistence (JSON snapshot)
- Add Slice() for 2-D dashboard visualization across any axis pair
- Migration: old 2-D archives project at z=middle, w=middle
- Update cross-pollination to pad 2-element behavior vectors to 4
- Add Prometheus metrics to matchmaker (bot crashes, stale job count)
- Add rivalry detection to index builder (data/meta/rivalries.json)
- Web: batched bot list loading, leaderboard keyboard accessibility,
  improved ARIA attributes on match/playlist cards

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 15:44:39 -04:00

272 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package mapelites
import "testing"
func TestBehaviorToCell(t *testing.T) {
// 3×3×3×3 grid per §10.2
g := New(3)
cases := []struct {
agg, eco, expl, form float64
wantX, wantY, wantZ, wantW int
}{
{0.0, 0.0, 0.0, 0.0, 0, 0, 0, 0},
{1.0, 1.0, 1.0, 1.0, 2, 2, 2, 2},
{0.5, 0.5, 0.5, 0.5, 1, 1, 1, 1},
{0.15, 0.85, 0.33, 0.66, 0, 2, 0, 1},
{0.99, 0.01, 0.99, 0.01, 2, 0, 2, 0},
{0.09, 0.09, 0.09, 0.09, 0, 0, 0, 0},
{0.34, 0.67, 0.34, 0.67, 1, 2, 1, 2},
}
for _, tc := range cases {
x, y, z, w := g.BehaviorToCell(tc.agg, tc.eco, tc.expl, tc.form)
if x != tc.wantX || y != tc.wantY || z != tc.wantZ || w != tc.wantW {
t.Errorf("BehaviorToCell(%.2f, %.2f, %.2f, %.2f) = (%d,%d,%d,%d), want (%d,%d,%d,%d)",
tc.agg, tc.eco, tc.expl, tc.form, x, y, z, w, tc.wantX, tc.wantY, tc.wantZ, tc.wantW)
}
}
}
func TestTotalCells(t *testing.T) {
g := New(3)
if g.TotalCells() != 81 {
t.Errorf("3⁴ = 81 cells, got %d", g.TotalCells())
}
g10 := New(10)
if g10.TotalCells() != 10000 {
t.Errorf("10⁴ = 10000 cells, got %d", g10.TotalCells())
}
}
func TestTryPlace_EmptyCell(t *testing.T) {
g := New(3)
p, placed := g.TryPlace(1, 10.0, 0.1, 0.9, 0.5, 0.5)
if !placed {
t.Fatal("expected placement into empty cell")
}
if p.X != 0 || p.Y != 2 || p.Z != 1 || p.W != 1 {
t.Errorf("expected cell (0,2,1,1), got (%d,%d,%d,%d)", p.X, p.Y, p.Z, p.W)
}
cell := g.Get(0, 2, 1, 1)
if !cell.Occupied || cell.ProgramID != 1 || cell.Fitness != 10.0 {
t.Errorf("unexpected cell state: %+v", cell)
}
}
func TestTryPlace_LowerFitnessDoesNotReplace(t *testing.T) {
g := New(3)
g.TryPlace(1, 10.0, 0.5, 0.5, 0.5, 0.5)
_, placed := g.TryPlace(2, 5.0, 0.5, 0.5, 0.5, 0.5)
if placed {
t.Fatal("lower fitness should not replace incumbent")
}
if g.Get(1, 1, 1, 1).ProgramID != 1 {
t.Error("incumbent program 1 should still hold the cell")
}
}
func TestTryPlace_HigherFitnessReplaces(t *testing.T) {
g := New(3)
g.TryPlace(1, 10.0, 0.5, 0.5, 0.5, 0.5)
_, placed := g.TryPlace(2, 20.0, 0.5, 0.5, 0.5, 0.5)
if !placed {
t.Fatal("higher fitness should replace incumbent")
}
cell := g.Get(1, 1, 1, 1)
if cell.ProgramID != 2 || cell.Fitness != 20.0 {
t.Errorf("expected program 2 with fitness 20, got %+v", cell)
}
}
func TestTryPlace_EqualFitnessDoesNotReplace(t *testing.T) {
g := New(3)
g.TryPlace(1, 10.0, 0.5, 0.5, 0.5, 0.5)
_, placed := g.TryPlace(2, 10.0, 0.5, 0.5, 0.5, 0.5)
if placed {
t.Fatal("equal fitness should not replace incumbent")
}
}
func TestOccupiedCount(t *testing.T) {
g := New(3)
if g.OccupiedCount() != 0 {
t.Error("new grid should have 0 occupied cells")
}
g.TryPlace(1, 1.0, 0.1, 0.1, 0.1, 0.1)
g.TryPlace(2, 1.0, 0.9, 0.9, 0.9, 0.9)
g.TryPlace(3, 1.0, 0.5, 0.5, 0.5, 0.5)
if g.OccupiedCount() != 3 {
t.Errorf("expected 3 occupied cells, got %d", g.OccupiedCount())
}
// Same cell should not increase count
g.TryPlace(4, 99.0, 0.5, 0.5, 0.5, 0.5)
if g.OccupiedCount() != 3 {
t.Errorf("expected still 3 occupied cells after same-cell update, got %d", g.OccupiedCount())
}
}
func TestElite_EmptyGrid(t *testing.T) {
g := New(3)
_, found := g.Elite()
if found {
t.Fatal("empty grid should have no elite")
}
}
func TestElite(t *testing.T) {
g := New(3)
g.TryPlace(1, 5.0, 0.1, 0.1, 0.1, 0.1)
g.TryPlace(2, 15.0, 0.9, 0.9, 0.9, 0.9)
g.TryPlace(3, 10.0, 0.5, 0.5, 0.5, 0.5)
elite, found := g.Elite()
if !found {
t.Fatal("expected an elite in non-empty grid")
}
if elite.ProgramID != 2 || elite.Fitness != 15.0 {
t.Errorf("expected elite program 2 (fitness 15), got %+v", elite)
}
}
func TestAllElites(t *testing.T) {
g := New(3)
if len(g.AllElites()) != 0 {
t.Error("empty grid should return no elites")
}
g.TryPlace(1, 1.0, 0.0, 0.0, 0.0, 0.0)
g.TryPlace(2, 2.0, 0.5, 0.5, 0.5, 0.5)
g.TryPlace(3, 3.0, 1.0, 1.0, 1.0, 1.0)
elites := g.AllElites()
if len(elites) != 3 {
t.Errorf("expected 3 elites, got %d", len(elites))
}
}
func TestSeedBehaviorVectors(t *testing.T) {
// Verify that the 6 seed bots land in distinct grid cells on a 3×3×3×3 grid.
g := New(3)
bots := []struct {
id int64
name string
aggression, economy float64
exploration, formation float64
}{
{1, "gatherer", 0.1, 0.9, 0.3, 0.2},
{2, "guardian", 0.2, 0.6, 0.1, 0.8},
{3, "rusher", 0.9, 0.2, 0.5, 0.3},
{4, "swarm", 0.6, 0.5, 0.4, 0.9},
{5, "hunter", 0.7, 0.3, 0.8, 0.4},
{6, "random", 0.3, 0.4, 0.5, 0.5},
}
placed := 0
for _, b := range bots {
_, ok := g.TryPlace(b.id, 1.0, b.aggression, b.economy, b.exploration, b.formation)
if ok {
placed++
}
}
if placed != 6 {
t.Errorf("expected all 6 seed bots in distinct cells, but only %d placed", placed)
}
if g.OccupiedCount() != 6 {
t.Errorf("expected 6 occupied cells, got %d", g.OccupiedCount())
}
}
func TestSlice(t *testing.T) {
g := New(3)
// Place bots at known positions
g.TryPlace(1, 5.0, 0.1, 0.1, 0.1, 0.1) // (0,0,0,0)
g.TryPlace(2, 8.0, 0.9, 0.9, 0.1, 0.1) // (2,2,0,0)
g.TryPlace(3, 3.0, 0.5, 0.1, 0.1, 0.1) // (1,0,0,0)
// Slice: aggression×economy at z=0, w=0 (dims 2,3 fixed to 0)
slice := g.Slice(2, 0, 3, 0)
if len(slice) != 3 {
t.Fatalf("expected 3 rows in slice, got %d", len(slice))
}
if len(slice[0]) != 3 {
t.Fatalf("expected 3 cols in slice, got %d", len(slice[0]))
}
// Check (0,0) = program 1
c00 := slice[0][0]
if !c00.Occupied || c00.ProgramID != 1 {
t.Errorf("slice[0][0]: expected program 1, got %+v", c00)
}
// Check (2,2) = program 2
c22 := slice[2][2]
if !c22.Occupied || c22.ProgramID != 2 {
t.Errorf("slice[2][2]: expected program 2, got %+v", c22)
}
// Check (1,0) = program 3
c10 := slice[1][0]
if !c10.Occupied || c10.ProgramID != 3 {
t.Errorf("slice[1][0]: expected program 3, got %+v", c10)
}
}
func TestSnapshot(t *testing.T) {
g := New(3)
g.TryPlace(1, 5.0, 0.1, 0.1, 0.1, 0.1)
g.TryPlace(2, 8.0, 0.9, 0.9, 0.9, 0.9)
snap := g.Snapshot()
if snap.Size != 3 {
t.Errorf("expected size 3, got %d", snap.Size)
}
if len(snap.Cells) != 2 {
t.Errorf("expected 2 cells in snapshot, got %d", len(snap.Cells))
}
if snap.DimNames[0] != "aggression" || snap.DimNames[3] != "formation" {
t.Errorf("dim names: %v", snap.DimNames)
}
}
func TestMigration_2Dto4D(t *testing.T) {
// Simulate migrating a 2-D archive into a 4-D grid.
// Old programs had only aggression and economy.
// They should project into the 4-D grid at z=middle, w=middle.
g := New(3)
// Old 2-D program: aggression=0.5, economy=0.5
// Migrate to 4-D: exploration=0.5 (middle), formation=0.5 (middle)
middle := 0.5
g.TryPlace(1, 10.0, 0.5, 0.5, middle, middle)
cell := g.Get(1, 1, 1, 1)
if !cell.Occupied {
t.Error("migrated program should occupy (1,1,1,1)")
}
if cell.ProgramID != 1 {
t.Errorf("expected program 1, got %d", cell.ProgramID)
}
// A new 4-D program with different exploration/formation should go to a different cell
_, placed := g.TryPlace(2, 15.0, 0.5, 0.5, 0.9, 0.1)
if !placed {
t.Error("different 4-D coords should be a new cell")
}
cell2 := g.Get(1, 1, 2, 0)
if !cell2.Occupied || cell2.ProgramID != 2 {
t.Error("new program should be at (1,1,2,0)")
}
}
func TestPlacementKey(t *testing.T) {
p := Placement{X: 1, Y: 2, Z: 0, W: 2}
key := p.Key()
if key != [NumDims]int{1, 2, 0, 2} {
t.Errorf("expected [1 2 0 2], got %v", key)
}
}