ai-code-battle/cmd/acb-evolver/internal/mapelites/grid_test.go
jedarden 5669688984 Add validation pipeline, sandbox, and evolution DB layer (Phase 7)
Three-stage fail-fast validator for LLM-generated bot candidates:
- syntax.go: language-aware parse (go/parser for Go; py_compile, rustfmt,
  tsc, javac, php -l for others; brace-balance fallback)
- schema.go: regex detection of /health + /turn endpoints and "moves" field
- sandbox.go: nsjail-isolated smoke test — builds bot, polls /health, sends
  5 signed /turn requests, verifies JSON moves responses
- validator.go: orchestrates stages with fail-fast short-circuit

DB layer:
- programs table + CRUD (create, get, list, updateFitness, setPromoted)
- validation_log table with RecordValidation, IslandPassRates,
  IslandValidationStats for per-island pass-rate tracking
- seed.go: 6 generation-0 bots across alpha/beta/gamma/delta islands

MAP-Elites grid (mapelites/grid.go): 2-D behavior grid on aggression×economy
axes; TryPlace keeps the fittest occupant per niche.

acb-evolver CLI gains two new subcommands:
  validate <file> -lang <lang> [-island <island>] [-nsjail] [-nolog]
  validation-stats (tabular per-island pass-rate breakdown)

cmd/acb-api/db.go: add programs table to API schema so the API can query
promoted evolved bots.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 22:45:13 -04:00

170 lines
3.8 KiB
Go

package mapelites
import "testing"
func TestBehaviorToCell(t *testing.T) {
g := New(10)
cases := []struct {
agg, eco float64
wantX, wantY int
}{
{0.0, 0.0, 0, 0},
{1.0, 1.0, 9, 9},
{0.5, 0.5, 5, 5},
{0.15, 0.85, 1, 8},
{0.99, 0.01, 9, 0},
{0.09, 0.09, 0, 0},
{0.1, 0.9, 1, 9},
}
for _, tc := range cases {
x, y := g.BehaviorToCell(tc.agg, tc.eco)
if x != tc.wantX || y != tc.wantY {
t.Errorf("BehaviorToCell(%.2f, %.2f) = (%d, %d), want (%d, %d)",
tc.agg, tc.eco, x, y, tc.wantX, tc.wantY)
}
}
}
func TestTryPlace_EmptyCell(t *testing.T) {
g := New(10)
p, placed := g.TryPlace(1, 10.0, 0.1, 0.9)
if !placed {
t.Fatal("expected placement into empty cell")
}
if p.X != 1 || p.Y != 9 {
t.Errorf("expected cell (1,9), got (%d,%d)", p.X, p.Y)
}
cell := g.Get(1, 9)
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(10)
g.TryPlace(1, 10.0, 0.5, 0.5)
_, placed := g.TryPlace(2, 5.0, 0.5, 0.5)
if placed {
t.Fatal("lower fitness should not replace incumbent")
}
if g.Get(5, 5).ProgramID != 1 {
t.Error("incumbent program 1 should still hold the cell")
}
}
func TestTryPlace_HigherFitnessReplaces(t *testing.T) {
g := New(10)
g.TryPlace(1, 10.0, 0.5, 0.5)
_, placed := g.TryPlace(2, 20.0, 0.5, 0.5)
if !placed {
t.Fatal("higher fitness should replace incumbent")
}
cell := g.Get(5, 5)
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(10)
g.TryPlace(1, 10.0, 0.5, 0.5)
_, placed := g.TryPlace(2, 10.0, 0.5, 0.5)
if placed {
t.Fatal("equal fitness should not replace incumbent")
}
}
func TestOccupiedCount(t *testing.T) {
g := New(10)
if g.OccupiedCount() != 0 {
t.Error("new grid should have 0 occupied cells")
}
g.TryPlace(1, 1.0, 0.1, 0.1)
g.TryPlace(2, 1.0, 0.9, 0.9)
g.TryPlace(3, 1.0, 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)
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(10)
_, found := g.Elite()
if found {
t.Fatal("empty grid should have no elite")
}
}
func TestElite(t *testing.T) {
g := New(10)
g.TryPlace(1, 5.0, 0.1, 0.1)
g.TryPlace(2, 15.0, 0.9, 0.9)
g.TryPlace(3, 10.0, 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(10)
if len(g.AllElites()) != 0 {
t.Error("empty grid should return no elites")
}
g.TryPlace(1, 1.0, 0.0, 0.0)
g.TryPlace(2, 2.0, 0.5, 0.5)
g.TryPlace(3, 3.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 10x10 grid.
g := New(10)
bots := []struct {
id int64
name string
aggression float64
economy float64
}{
{1, "gatherer", 0.1, 0.9},
{2, "guardian", 0.2, 0.6},
{3, "rusher", 0.9, 0.2},
{4, "swarm", 0.6, 0.5},
{5, "hunter", 0.7, 0.3},
{6, "random", 0.3, 0.4},
}
placed := 0
for _, b := range bots {
_, ok := g.TryPlace(b.id, 1.0, b.aggression, b.economy)
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())
}
}