ai-code-battle/cmd/acb-evolver/internal/crosspoll/crosspoll.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

229 lines
7.4 KiB
Go

// Package crosspoll implements island cross-pollination per §10.2 of the plan.
//
// Every 50 generations, the top program from each island is copied to a random
// other island. If the target island uses a different language, the LLM
// translates the code.
package crosspoll
import (
"context"
"fmt"
"log"
"math/rand"
evolverdb "github.com/aicodebattle/acb/cmd/acb-evolver/internal/db"
"github.com/aicodebattle/acb/cmd/acb-evolver/internal/llm"
)
const generationInterval = 50
// PollinationResult records a single cross-pollination event.
type PollinationResult struct {
SourceIsland string
TargetIsland string
ProgramID int64
Translated bool
SourceLang string
TargetLang string
}
// programStore abstracts the database operations needed by cross-pollination.
type programStore interface {
MaxGenerationByIsland(ctx context.Context) (map[string]int, error)
ListTopByIsland(ctx context.Context, island string, limit int) ([]*evolverdb.Program, error)
Create(ctx context.Context, p *evolverdb.Program) (int64, error)
}
// llmGenerator abstracts the LLM client for code translation.
type llmGenerator interface {
Generate(ctx context.Context, req llm.GenerateRequest) (*llm.GenerateResponse, error)
}
// Checker determines which islands need cross-pollination and executes it.
type Checker struct {
store programStore
client llmGenerator
rng *rand.Rand
}
// NewChecker creates a Checker backed by the given store and LLM client.
func NewChecker(store *evolverdb.Store, client *llm.Client, rng *rand.Rand) *Checker {
return &Checker{store: store, client: client, rng: rng}
}
// CheckAndPollinate checks all islands and performs cross-pollination for any
// island whose generation is a multiple of 50. Returns the list of pollination
// events that occurred.
//
// prevGens tracks the last-known max generation per island (from the previous
// check). An island triggers pollination only when it crosses a 50-generation
// boundary since the last check, preventing duplicate events.
func (c *Checker) CheckAndPollinate(ctx context.Context, prevGens map[string]int, verbose bool) ([]PollinationResult, error) {
curGens, err := c.store.MaxGenerationByIsland(ctx)
if err != nil {
return nil, fmt.Errorf("query max generations: %w", err)
}
var results []PollinationResult
for _, island := range evolverdb.AllIslands {
cur := curGens[island]
prev := prevGens[island]
// Find the next 50-boundary after prev that cur has reached or passed.
nextBoundary := ((prev / generationInterval) + 1) * generationInterval
for nextBoundary <= cur && nextBoundary > 0 {
if verbose {
log.Printf(" Cross-pollination: island %s hit generation %d", island, nextBoundary)
}
result, err := c.pollinateIsland(ctx, island, verbose)
if err != nil {
log.Printf(" Cross-pollination error for island %s at gen %d: %v", island, nextBoundary, err)
} else {
results = append(results, result)
}
nextBoundary += generationInterval
}
prevGens[island] = cur
}
return results, nil
}
// pollinateIsland copies the top program from sourceIsland to a random other
// island, translating if the languages differ.
func (c *Checker) pollinateIsland(ctx context.Context, sourceIsland string, verbose bool) (PollinationResult, error) {
// Get top program by fitness on the source island.
topProgs, err := c.store.ListTopByIsland(ctx, sourceIsland, 1)
if err != nil {
return PollinationResult{}, fmt.Errorf("list top on %s: %w", sourceIsland, err)
}
if len(topProgs) == 0 {
return PollinationResult{}, fmt.Errorf("no programs on island %s", sourceIsland)
}
top := topProgs[0]
// Pick a random target island (different from source).
targetIsland := c.pickTargetIsland(sourceIsland)
// Determine target language from the most-recent program on the target island.
targetLang := top.Language // default: same language
targetProgs, err := c.store.ListTopByIsland(ctx, targetIsland, 1)
if err != nil {
return PollinationResult{}, fmt.Errorf("list top on target %s: %w", targetIsland, err)
}
if len(targetProgs) > 0 {
targetLang = targetProgs[0].Language
}
translated := false
code := top.Code
if top.Language != targetLang {
// Translate via LLM
translatedCode, err := c.translate(ctx, top.Code, top.Language, targetLang)
if err != nil {
log.Printf(" Translation %s→%s failed: %v; copying original code", top.Language, targetLang, err)
} else {
code = translatedCode
translated = true
}
}
// Copy the program to the target island (same generation, new entry).
behaviorVec := padBehaviorVec(top.BehaviorVector)
newID, err := c.store.Create(ctx, &evolverdb.Program{
Code: code,
Language: targetLang,
Island: targetIsland,
Generation: top.Generation,
ParentIDs: []int64{top.ID},
BehaviorVector: behaviorVec,
Fitness: top.Fitness * 0.9, // slight fitness penalty for migration
Promoted: false,
})
if err != nil {
return PollinationResult{}, fmt.Errorf("insert migrated program: %w", err)
}
result := PollinationResult{
SourceIsland: sourceIsland,
TargetIsland: targetIsland,
ProgramID: newID,
Translated: translated,
SourceLang: top.Language,
TargetLang: targetLang,
}
log.Printf(" Cross-pollinated: island %s → %s, program %d → %d, lang %s→%s (translated=%v)",
sourceIsland, targetIsland, top.ID, newID, top.Language, targetLang, translated)
return result, nil
}
// pickTargetIsland returns a random island different from source.
func (c *Checker) pickTargetIsland(source string) string {
others := make([]string, 0, len(evolverdb.AllIslands)-1)
for _, island := range evolverdb.AllIslands {
if island != source {
others = append(others, island)
}
}
return others[c.rng.Intn(len(others))]
}
// translate invokes the LLM to translate bot code from one language to another.
func (c *Checker) translate(ctx context.Context, code, fromLang, toLang string) (string, error) {
prompt := buildTranslationPrompt(code, fromLang, toLang)
req := llm.GenerateRequest{
Prompt: prompt,
Tier: llm.TierStrong, // use strong model for accurate translation
TargetLang: toLang,
}
resp, err := c.client.Generate(ctx, req)
if err != nil {
return "", fmt.Errorf("llm translation: %w", err)
}
if resp.Candidate == nil {
return "", fmt.Errorf("llm returned no candidate")
}
return resp.Candidate.Code, nil
}
func buildTranslationPrompt(code, fromLang, toLang string) string {
return fmt.Sprintf(`You are translating a competitive bot for AI Code Battle from %s to %s.
The bot is an HTTP server that:
- Listens on port 8080
- Handles GET /health (returns 200)
- Handles POST /turn with HMAC-SHA256 request verification
- Returns JSON: {"moves": [{"row": N, "col": N, "direction": "N"|"E"|"S"|"W"}]}
- May include optional "debug" field in response
Translate the following bot preserving the EXACT same strategy and behavior.
Use idiomatic %s patterns and standard library only.
The translated bot must be a complete, self-contained HTTP server.
Source code in %s:
`+"```"+`
%s
`+"```"+`
Return ONLY the translated %s code in a single fenced code block:`, fromLang, toLang, toLang, fromLang, code, toLang)
}
// padBehaviorVec ensures a behavior vector has at least 4 elements,
// padding with 0.5 for missing dimensions.
func padBehaviorVec(v []float64) []float64 {
out := make([]float64, 4)
for i := range out {
out[i] = 0.5
}
for i := 0; i < len(v) && i < 4; i++ {
out[i] = v[i]
}
return out
}