- Complete autonomous evolution pipeline with island model (4 islands) - MAP-Elites behavior grid integration for diversity - LLM ensemble integration (fast + strong model tiers) - 3-stage validation pipeline (syntax → schema → sandbox smoke test) - Evaluation arena (10-match mini-tournament per candidate) - Promotion gate (Nash equilibrium PSRO + MAP-Elites niche fill) - Retirement policy (auto-retire low-rated bots, population cap) - Live export to R2 for evolution dashboard - Enhanced replay viewer with commentary and win probability - Added series, seasons, and predictions pages All tests passing. Phase 7 exit criteria met. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
260 lines
8.9 KiB
Go
260 lines
8.9 KiB
Go
// Package prompt assembles evolution prompts for the LLM ensemble.
|
||
//
|
||
// A prompt is built from three sources:
|
||
// - Parent programs: high-fitness individuals sampled from the island
|
||
// population (typically via tournament selection).
|
||
// - Replay analysis: key moments, strategies, and weaknesses extracted
|
||
// from recent match replays.
|
||
// - Meta description: a snapshot of the current leaderboard and dominant
|
||
// strategies, giving the LLM competitive context.
|
||
package prompt
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
|
||
evolverdb "github.com/aicodebattle/acb/cmd/acb-evolver/internal/db"
|
||
)
|
||
|
||
// MatchSummary captures the salient facts from a completed match replay.
|
||
type MatchSummary struct {
|
||
// MatchID is the unique identifier of the match in the database.
|
||
MatchID string
|
||
// WinnerName is the name of the winning bot (empty for draws).
|
||
WinnerName string
|
||
// LoserName is the name of the losing bot (empty for draws).
|
||
LoserName string
|
||
// Condition is one of "elimination", "dominance", "turns", or "draw".
|
||
Condition string
|
||
// TurnCount is the number of turns played.
|
||
TurnCount int
|
||
// Scores holds the final score for each player slot.
|
||
Scores []int
|
||
// KeyMoments are natural-language sentences describing notable events.
|
||
KeyMoments []string
|
||
// Strategies lists the key tactics observed in the winning side.
|
||
Strategies []string
|
||
// Weaknesses lists exploitable patterns observed in the losing side.
|
||
Weaknesses []string
|
||
}
|
||
|
||
// BotSummary is a brief leaderboard entry.
|
||
type BotSummary struct {
|
||
Name string
|
||
Rating float64
|
||
Island string
|
||
Evolved bool
|
||
}
|
||
|
||
// IslandStat summarises a single island's population.
|
||
type IslandStat struct {
|
||
Count int
|
||
AvgFitness float64
|
||
TopFitness float64
|
||
}
|
||
|
||
// MetaDescription captures the current state of the competitive meta.
|
||
type MetaDescription struct {
|
||
// TotalBots is the number of registered bots.
|
||
TotalBots int
|
||
// TopBots lists the highest-rated bots in descending order.
|
||
TopBots []BotSummary
|
||
// DominantStrategy is a narrative description of the current meta.
|
||
DominantStrategy string
|
||
// IslandStats summarises each island's population and fitness.
|
||
IslandStats map[string]IslandStat
|
||
}
|
||
|
||
// Request bundles everything the prompt builder needs to produce a prompt.
|
||
type Request struct {
|
||
// Parents are the programs selected as evolutionary parents.
|
||
Parents []*evolverdb.Program
|
||
// Replays is the recent match history used for strategy analysis.
|
||
Replays []MatchSummary
|
||
// Meta describes the current competitive landscape.
|
||
Meta MetaDescription
|
||
// Island is the island this candidate will compete on.
|
||
Island string
|
||
// TargetLang is the programming language for the evolved bot
|
||
// (e.g. "go", "python", "rust", "typescript", "java", "php").
|
||
TargetLang string
|
||
// Generation is the current evolution generation number.
|
||
Generation int
|
||
// TaskOverride replaces the default task section when set (used for retry prompts).
|
||
TaskOverride string
|
||
}
|
||
|
||
// Assemble builds the full LLM prompt from a Request.
|
||
// The returned string is ready to be sent as the user message to the LLM.
|
||
func Assemble(r Request) string {
|
||
var sb strings.Builder
|
||
|
||
writeSystemContext(&sb, r.TargetLang)
|
||
writeIslandContext(&sb, r.Island, r.Generation)
|
||
writeMetaSection(&sb, r.Meta)
|
||
writeReplaySection(&sb, r.Replays)
|
||
writeParentSection(&sb, r.Parents)
|
||
if r.TaskOverride != "" {
|
||
sb.WriteString("## Task\n")
|
||
sb.WriteString(r.TaskOverride)
|
||
} else {
|
||
writeTaskSection(&sb, r.TargetLang)
|
||
}
|
||
|
||
return sb.String()
|
||
}
|
||
|
||
func writeSystemContext(sb *strings.Builder, targetLang string) {
|
||
sb.WriteString("You are an AI bot evolution engine for a competitive grid strategy game.\n")
|
||
sb.WriteString("Your task is to write an improved bot strategy in ")
|
||
sb.WriteString(langDisplayName(targetLang))
|
||
sb.WriteString(" based on the parents and match analysis provided.\n\n")
|
||
|
||
sb.WriteString("## Game Rules\n")
|
||
sb.WriteString("- Grid: 60×60 toroidal grid with walls, energy pickups, and player cores\n")
|
||
sb.WriteString("- Bots spawn from your core (costs 3 energy). Spawn whenever energy ≥ 3.\n")
|
||
sb.WriteString("- Each turn: move each bot one step (N/E/S/W) or stay. Submit all moves as JSON.\n")
|
||
sb.WriteString("- Collect energy tiles to gain energy. Attack enemy bots by moving onto them.\n")
|
||
sb.WriteString("- Win by: eliminating all enemy bots, controlling >50% score, or having the most score when turns run out (max 500).\n")
|
||
sb.WriteString("- Vision radius²=49 (~7 tiles). Attack by moving into an enemy.\n\n")
|
||
}
|
||
|
||
func writeIslandContext(sb *strings.Builder, island string, generation int) {
|
||
sb.WriteString("## Island Context\n")
|
||
fmt.Fprintf(sb, "Evolving on island **%s** (generation %d).\n", island, generation)
|
||
switch island {
|
||
case evolverdb.IslandAlpha:
|
||
sb.WriteString("Island Alpha favors aggressive, core-rushing strategies.\n")
|
||
case evolverdb.IslandBeta:
|
||
sb.WriteString("Island Beta favors energy-focused, economic strategies.\n")
|
||
case evolverdb.IslandGamma:
|
||
sb.WriteString("Island Gamma favors defensive, adaptive strategies.\n")
|
||
case evolverdb.IslandDelta:
|
||
sb.WriteString("Island Delta is experimental — any novel strategy is welcome.\n")
|
||
}
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
func writeMetaSection(sb *strings.Builder, meta MetaDescription) {
|
||
if meta.TotalBots == 0 && len(meta.TopBots) == 0 {
|
||
return
|
||
}
|
||
sb.WriteString("## Current Meta\n")
|
||
if meta.TotalBots > 0 {
|
||
fmt.Fprintf(sb, "Total active bots: %d\n", meta.TotalBots)
|
||
}
|
||
if meta.DominantStrategy != "" {
|
||
fmt.Fprintf(sb, "Dominant strategy: %s\n", meta.DominantStrategy)
|
||
}
|
||
if len(meta.TopBots) > 0 {
|
||
sb.WriteString("\nTop-rated bots:\n")
|
||
for i, bot := range meta.TopBots {
|
||
line := fmt.Sprintf(" %d. %s (rating %.0f", i+1, bot.Name, bot.Rating)
|
||
if bot.Island != "" {
|
||
line += fmt.Sprintf(", island: %s", bot.Island)
|
||
}
|
||
if bot.Evolved {
|
||
line += ", evolved"
|
||
}
|
||
line += ")\n"
|
||
sb.WriteString(line)
|
||
}
|
||
}
|
||
if len(meta.IslandStats) > 0 {
|
||
sb.WriteString("\nIsland population stats:\n")
|
||
for _, island := range evolverdb.AllIslands {
|
||
if stat, ok := meta.IslandStats[island]; ok {
|
||
fmt.Fprintf(sb, " %s: %d programs, avg fitness %.3f, top fitness %.3f\n",
|
||
island, stat.Count, stat.AvgFitness, stat.TopFitness)
|
||
}
|
||
}
|
||
}
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
func writeReplaySection(sb *strings.Builder, replays []MatchSummary) {
|
||
if len(replays) == 0 {
|
||
return
|
||
}
|
||
sb.WriteString("## Recent Match Analysis\n")
|
||
for i, m := range replays {
|
||
fmt.Fprintf(sb, "\n### Match %d (ID: %s)\n", i+1, m.MatchID)
|
||
if m.Condition == "draw" || m.WinnerName == "" {
|
||
fmt.Fprintf(sb, "Result: Draw (%d turns)\n", m.TurnCount)
|
||
} else {
|
||
fmt.Fprintf(sb, "Result: %s defeated %s (%s, %d turns)\n",
|
||
m.WinnerName, m.LoserName, m.Condition, m.TurnCount)
|
||
}
|
||
if len(m.Scores) > 0 {
|
||
fmt.Fprintf(sb, "Scores: %v\n", m.Scores)
|
||
}
|
||
if len(m.Strategies) > 0 {
|
||
fmt.Fprintf(sb, "Winning strategies: %s\n", strings.Join(m.Strategies, ", "))
|
||
}
|
||
if len(m.Weaknesses) > 0 {
|
||
fmt.Fprintf(sb, "Exploited weaknesses: %s\n", strings.Join(m.Weaknesses, ", "))
|
||
}
|
||
if len(m.KeyMoments) > 0 {
|
||
sb.WriteString("Key moments:\n")
|
||
for _, moment := range m.KeyMoments {
|
||
sb.WriteString(" - " + moment + "\n")
|
||
}
|
||
}
|
||
}
|
||
sb.WriteString("\n")
|
||
}
|
||
|
||
func writeParentSection(sb *strings.Builder, parents []*evolverdb.Program) {
|
||
if len(parents) == 0 {
|
||
return
|
||
}
|
||
sb.WriteString("## Parent Programs\n")
|
||
sb.WriteString("Study these parents and improve upon them:\n\n")
|
||
for i, p := range parents {
|
||
fmt.Fprintf(sb, "### Parent %d (ID: %d, fitness: %.3f, language: %s)\n",
|
||
i+1, p.ID, p.Fitness, p.Language)
|
||
if len(p.BehaviorVector) >= 2 {
|
||
fmt.Fprintf(sb, "Behavior: aggression=%.2f economy=%.2f\n",
|
||
p.BehaviorVector[0], p.BehaviorVector[1])
|
||
}
|
||
sb.WriteString("\n```" + p.Language + "\n")
|
||
sb.WriteString(p.Code)
|
||
if !strings.HasSuffix(p.Code, "\n") {
|
||
sb.WriteByte('\n')
|
||
}
|
||
sb.WriteString("```\n\n")
|
||
}
|
||
}
|
||
|
||
func writeTaskSection(sb *strings.Builder, targetLang string) {
|
||
sb.WriteString("## Task\n")
|
||
fmt.Fprintf(sb, "Write an **improved** bot strategy in **%s** that:\n", langDisplayName(targetLang))
|
||
sb.WriteString("1. Addresses the weaknesses and counter-strategies identified in the match analysis.\n")
|
||
sb.WriteString("2. Builds on the best tactical patterns from the parent programs.\n")
|
||
sb.WriteString("3. Introduces at least one novel tactical improvement not present in the parents.\n")
|
||
sb.WriteString("4. Is complete and self-contained (define all required game types inline).\n\n")
|
||
sb.WriteString("Return **only** the complete bot code in a single fenced code block with no additional explanation:\n")
|
||
sb.WriteString("```" + targetLang + "\n")
|
||
sb.WriteString("// your complete bot code here\n")
|
||
sb.WriteString("```\n")
|
||
}
|
||
|
||
// langDisplayName returns a human-readable name for a language identifier.
|
||
func langDisplayName(lang string) string {
|
||
switch lang {
|
||
case "go":
|
||
return "Go"
|
||
case "python":
|
||
return "Python"
|
||
case "rust":
|
||
return "Rust"
|
||
case "typescript":
|
||
return "TypeScript"
|
||
case "java":
|
||
return "Java"
|
||
case "php":
|
||
return "PHP"
|
||
default:
|
||
return lang
|
||
}
|
||
}
|