- 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>
229 lines
7.4 KiB
Go
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
|
|
}
|