fix(evolver): wire Nash mixture and meta weaknesses into LLM prompts, fix 4-D diversity
- Add NashMixture and MetaWeaknesses fields to meta.Description and compute them from island population proportions (§10.2 PSRO) - Update behaviorDistance to support N-D vectors for 4-D MAP-Elites grid (aggression, economy, exploration, formation) - Wire NashMixture/MetaWeaknesses through FromMetaDescription converter so they actually reach the LLM prompt (was dead code before) - Align LLM prompt with plan §15.1/§15.5: correct combat rules (focus-fire), fog of war, HTTP protocol section, Nash mixture target - Fix diversity normalization from sqrt(2) (2-D) to 2.0 (4-D max) - Rename handleUIFeedback to handleCreateFeedback (§13.6 naming) - Update tests for new fields and corrected prompt text Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
87f68044b4
commit
0813e36297
7 changed files with 183 additions and 22 deletions
|
|
@ -75,7 +75,7 @@ func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
|||
voteMW := s.voteLtr.Middleware(ipKey, func() {
|
||||
metrics.RateLimitHits.WithLabelValues("vote").Inc()
|
||||
})
|
||||
mux.HandleFunc("POST /api/feedback", fbMW(http.HandlerFunc(s.handleUIFeedback)).ServeHTTP)
|
||||
mux.HandleFunc("POST /api/feedback", fbMW(http.HandlerFunc(s.handleCreateFeedback)).ServeHTTP)
|
||||
mux.HandleFunc("GET /api/feedback/", s.handleGetFeedback)
|
||||
mux.HandleFunc("POST /api/feedback/", voteMW(http.HandlerFunc(s.handleFeedbackUpvote)).ServeHTTP)
|
||||
|
||||
|
|
@ -1492,10 +1492,10 @@ func (s *Server) handlePredictionHistory(w http.ResponseWriter, r *http.Request)
|
|||
})
|
||||
}
|
||||
|
||||
// handleUIFeedback handles POST /api/feedback
|
||||
// handleCreateFeedback handles POST /api/feedback
|
||||
// Accepts community replay feedback per plan §13.6.
|
||||
// Stores in replay_feedback table with type enum: insight, mistake, idea, highlight.
|
||||
func (s *Server) handleUIFeedback(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) handleCreateFeedback(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
writeError(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ package meta
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
|
|
@ -39,6 +40,11 @@ type Description struct {
|
|||
TopBots []BotInfo
|
||||
// DominantStrategy describes the current meta.
|
||||
DominantStrategy string
|
||||
// NashMixture describes the Nash equilibrium mixture over the population
|
||||
// (per plan §10.3 PSRO). Example: "40% swarm, 30% hunter, 30% gatherer".
|
||||
NashMixture string
|
||||
// MetaWeaknesses lists exploitable gaps in the current population.
|
||||
MetaWeaknesses []string
|
||||
// IslandStats holds population metrics per island.
|
||||
IslandStats map[string]IslandStats
|
||||
}
|
||||
|
|
@ -119,6 +125,12 @@ func (b *Builder) Build(ctx context.Context, topBotLimit int) (*Description, err
|
|||
// Infer dominant strategy from top performers
|
||||
desc.DominantStrategy = b.inferDominantStrategy(desc)
|
||||
|
||||
// Compute Nash mixture description from island population proportions
|
||||
desc.NashMixture = b.computeNashMixture(desc)
|
||||
|
||||
// Infer meta weaknesses from under-represented strategies
|
||||
desc.MetaWeaknesses = b.inferMetaWeaknesses(desc)
|
||||
|
||||
return desc, nil
|
||||
}
|
||||
|
||||
|
|
@ -146,18 +158,27 @@ func calculateDiversity(programs []*evolverdb.Program) float64 {
|
|||
}
|
||||
|
||||
avgDist := totalDist / float64(pairs)
|
||||
// Normalize: max distance in 2D unit square is sqrt(2) ≈ 1.414
|
||||
return avgDist / 1.414
|
||||
// Normalize: max distance in 4D unit hypercube is sqrt(4) = 2.0
|
||||
return avgDist / 2.0
|
||||
}
|
||||
|
||||
// behaviorDistance computes Euclidean distance between behavior vectors.
|
||||
// Supports 2-D and 4-D vectors (per plan §10.2 MAP-Elites: aggression,
|
||||
// economy, exploration, formation).
|
||||
func behaviorDistance(a, b []float64) float64 {
|
||||
if len(a) < 2 || len(b) < 2 {
|
||||
n := len(a)
|
||||
if n > len(b) {
|
||||
n = len(b)
|
||||
}
|
||||
if n == 0 {
|
||||
return 0
|
||||
}
|
||||
dx := a[0] - b[0]
|
||||
dy := a[1] - b[1]
|
||||
return math.Sqrt(dx*dx + dy*dy)
|
||||
var sum float64
|
||||
for i := 0; i < n; i++ {
|
||||
d := a[i] - b[i]
|
||||
sum += d * d
|
||||
}
|
||||
return math.Sqrt(sum)
|
||||
}
|
||||
|
||||
// inferDominantStrategy analyzes the top bots and describes the meta.
|
||||
|
|
@ -206,6 +227,106 @@ func (b *Builder) inferDominantStrategy(desc *Description) string {
|
|||
return strings.Join(strategies, " / ")
|
||||
}
|
||||
|
||||
// computeNashMixture produces a human-readable description of the Nash equilibrium
|
||||
// mixture over the island population, expressed as proportional strategy weights.
|
||||
// Per plan §10.3 PSRO: "the candidate should beat this mixture, not just one opponent."
|
||||
func (b *Builder) computeNashMixture(desc *Description) string {
|
||||
if len(desc.IslandStats) == 0 || desc.TotalBots == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Approximate Nash mixture from island population proportions.
|
||||
// Each island represents a strategy archetype (§10.2).
|
||||
strategyMap := map[string]string{
|
||||
evolverdb.IslandAlpha: "aggressive",
|
||||
evolverdb.IslandBeta: "economy",
|
||||
evolverdb.IslandGamma: "defensive",
|
||||
evolverdb.IslandDelta: "mixed",
|
||||
}
|
||||
|
||||
type mixEntry struct {
|
||||
name string
|
||||
weight float64
|
||||
}
|
||||
|
||||
var entries []mixEntry
|
||||
totalPop := 0
|
||||
for _, island := range evolverdb.AllIslands {
|
||||
stats, ok := desc.IslandStats[island]
|
||||
if !ok || stats.Count == 0 {
|
||||
continue
|
||||
}
|
||||
totalPop += stats.Count
|
||||
entries = append(entries, mixEntry{
|
||||
name: strategyMap[island],
|
||||
weight: float64(stats.Count),
|
||||
})
|
||||
}
|
||||
|
||||
if totalPop == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Normalize to percentages
|
||||
var parts []string
|
||||
for _, e := range entries {
|
||||
pct := int(math.Round(e.weight / float64(totalPop) * 100))
|
||||
if pct > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d%% %s", pct, e.name))
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
// inferMetaWeaknesses identifies exploitable gaps in the current population.
|
||||
// Per plan §10.3: "Weaknesses in the current meta are highlighted."
|
||||
func (b *Builder) inferMetaWeaknesses(desc *Description) []string {
|
||||
var weaknesses []string
|
||||
|
||||
// Check for empty islands — under-explored strategy spaces.
|
||||
strategyMap := map[string]string{
|
||||
evolverdb.IslandAlpha: "aggression",
|
||||
evolverdb.IslandBeta: "economy",
|
||||
evolverdb.IslandGamma: "defense",
|
||||
evolverdb.IslandDelta: "mixed/experimental",
|
||||
}
|
||||
|
||||
for _, island := range evolverdb.AllIslands {
|
||||
stats, ok := desc.IslandStats[island]
|
||||
if !ok || stats.Count == 0 {
|
||||
weaknesses = append(weaknesses,
|
||||
fmt.Sprintf("No bots exploring %s strategies — wide open niche",
|
||||
strategyMap[island]))
|
||||
} else if stats.Diversity < 0.2 {
|
||||
weaknesses = append(weaknesses,
|
||||
fmt.Sprintf("Low behavioral diversity in %s island (%.2f) — susceptible to counters",
|
||||
strategyMap[island], stats.Diversity))
|
||||
}
|
||||
}
|
||||
|
||||
// Check for dominant strategy monopoly.
|
||||
if desc.DominantStrategy != "" && len(desc.TopBots) >= 3 {
|
||||
islandCounts := make(map[string]int)
|
||||
for _, bot := range desc.TopBots {
|
||||
islandCounts[bot.Island]++
|
||||
}
|
||||
for island, count := range islandCounts {
|
||||
if count >= len(desc.TopBots)*2/3 {
|
||||
weaknesses = append(weaknesses,
|
||||
fmt.Sprintf("Top bots are %s-heavy (%d/%d) — counter-strategies could exploit this",
|
||||
strategyMap[island], count, len(desc.TopBots)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(weaknesses) == 0 {
|
||||
weaknesses = append(weaknesses, "Meta is relatively balanced — no obvious exploit")
|
||||
}
|
||||
|
||||
return weaknesses
|
||||
}
|
||||
|
||||
// BuildSimple creates a meta description without database access.
|
||||
// This is useful for testing or when database is not available.
|
||||
func BuildSimple(totalBots int, topBots []BotInfo, islandStats map[string]IslandStats) *Description {
|
||||
|
|
|
|||
|
|
@ -141,15 +141,15 @@ func TestCalculateDiversity_IdenticalPrograms(t *testing.T) {
|
|||
|
||||
func TestCalculateDiversity_DiversePrograms(t *testing.T) {
|
||||
programs := []*evolverdb.Program{
|
||||
{ID: 1, BehaviorVector: []float64{0.0, 0.0}},
|
||||
{ID: 2, BehaviorVector: []float64{1.0, 1.0}},
|
||||
{ID: 1, BehaviorVector: []float64{0.0, 0.0, 0.0, 0.0}},
|
||||
{ID: 2, BehaviorVector: []float64{1.0, 1.0, 1.0, 1.0}},
|
||||
}
|
||||
|
||||
got := calculateDiversity(programs)
|
||||
|
||||
// Distance between (0,0) and (1,1) is sqrt(2), squared is 2
|
||||
// Normalized by 2 (max squared distance is 2)
|
||||
// Expected: sqrt(2) / sqrt(2) = 1.0
|
||||
// Distance between (0,0,0,0) and (1,1,1,1) is sqrt(4) = 2.0
|
||||
// Normalized by 2.0 (max distance in 4D unit hypercube is sqrt(4) = 2.0)
|
||||
// Expected: 2.0 / 2.0 = 1.0
|
||||
if got < 0.9 || got > 1.1 {
|
||||
t.Errorf("expected diversity close to 1.0 for maximally diverse programs, got %f", got)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,13 @@ type MetaDescription struct {
|
|||
TopBots []BotSummary
|
||||
// DominantStrategy is a narrative description of the current meta.
|
||||
DominantStrategy string
|
||||
// NashMixture describes the current Nash equilibrium mixture over the
|
||||
// population (per plan §10.3 PSRO). When non-empty, the candidate should
|
||||
// beat this mixture, not just one opponent. Example:
|
||||
// "40% swarm, 30% hunter, 30% gatherer"
|
||||
NashMixture string
|
||||
// MetaWeaknesses lists known exploitable gaps in the current population.
|
||||
MetaWeaknesses []string
|
||||
// IslandStats summarises each island's population and fitness.
|
||||
IslandStats map[string]IslandStat
|
||||
}
|
||||
|
|
@ -135,12 +142,20 @@ func writeSystemContext(sb *strings.Builder, targetLang string) {
|
|||
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("- Grid: toroidal (wraps horizontally and vertically) 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")
|
||||
sb.WriteString("- Combat: focus-fire algorithm. A bot dies if ANY enemy within attack radius²=5 has ≤ its own enemy count.\n")
|
||||
sb.WriteString(" - 2v1: the lone bot dies, pair survives. 1v1: both die. Tight formations are defensive.\n")
|
||||
sb.WriteString("- Collect energy tiles (uncontested adjacent bots only) to gain energy.\n")
|
||||
sb.WriteString("- Win by: sole survivor, dominance (≥80% bots for 100 turns), or highest score at turn 500.\n")
|
||||
sb.WriteString("- Vision radius²=49 (~7 tiles). Fog of war: you only see tiles within vision of your bots.\n\n")
|
||||
sb.WriteString("## HTTP Protocol\n")
|
||||
sb.WriteString("- Your bot is an HTTP server listening on port 8080.\n")
|
||||
sb.WriteString("- Engine POSTs game state (JSON) to /turn each turn. You have 3 seconds to respond.\n")
|
||||
sb.WriteString("- Response: {\"moves\": [{\"row\":10,\"col\":15,\"direction\":\"N\"}], \"debug\": {...}}\n")
|
||||
sb.WriteString("- Headers include HMAC-SHA256 signature: X-ACB-Signature, X-ACB-Match-Id, X-ACB-Turn.\n")
|
||||
sb.WriteString("- 10 consecutive failures → bot marked crashed (units hold position for rest of match).\n\n")
|
||||
}
|
||||
|
||||
func writeIslandContext(sb *strings.Builder, island string, generation int) {
|
||||
|
|
@ -170,6 +185,11 @@ func writeMetaSection(sb *strings.Builder, meta MetaDescription) {
|
|||
if meta.DominantStrategy != "" {
|
||||
fmt.Fprintf(sb, "Dominant strategy: %s\n", meta.DominantStrategy)
|
||||
}
|
||||
// Nash equilibrium mixture per plan §10.3 ("Beat this mix").
|
||||
if meta.NashMixture != "" {
|
||||
fmt.Fprintf(sb, "Nash equilibrium mixture: %s\n", meta.NashMixture)
|
||||
sb.WriteString("Your candidate must beat this mixture, not just one opponent.\n")
|
||||
}
|
||||
if len(meta.TopBots) > 0 {
|
||||
sb.WriteString("\nTop-rated bots:\n")
|
||||
for i, bot := range meta.TopBots {
|
||||
|
|
@ -184,6 +204,12 @@ func writeMetaSection(sb *strings.Builder, meta MetaDescription) {
|
|||
sb.WriteString(line)
|
||||
}
|
||||
}
|
||||
if len(meta.MetaWeaknesses) > 0 {
|
||||
sb.WriteString("\nKnown weaknesses in current population:\n")
|
||||
for _, w := range meta.MetaWeaknesses {
|
||||
fmt.Fprintf(sb, " - %s\n", w)
|
||||
}
|
||||
}
|
||||
if len(meta.IslandStats) > 0 {
|
||||
sb.WriteString("\nIsland population stats:\n")
|
||||
for _, island := range evolverdb.AllIslands {
|
||||
|
|
@ -237,7 +263,10 @@ func writeParentSection(sb *strings.Builder, parents []*evolverdb.Program) {
|
|||
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 {
|
||||
if len(p.BehaviorVector) >= 4 {
|
||||
fmt.Fprintf(sb, "Behavior: aggression=%.2f economy=%.2f exploration=%.2f formation=%.2f\n",
|
||||
p.BehaviorVector[0], p.BehaviorVector[1], p.BehaviorVector[2], p.BehaviorVector[3])
|
||||
} else if len(p.BehaviorVector) >= 2 {
|
||||
fmt.Fprintf(sb, "Behavior: aggression=%.2f economy=%.2f\n",
|
||||
p.BehaviorVector[0], p.BehaviorVector[1])
|
||||
}
|
||||
|
|
@ -255,8 +284,9 @@ func writeTaskSection(sb *strings.Builder, targetLang string) {
|
|||
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("3. Can beat the Nash mixture described above (not just one opponent).\n")
|
||||
sb.WriteString("4. Is complete and self-contained (define all required game types inline).\n")
|
||||
sb.WriteString("5. Fits in a single file under 10 KB.\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")
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ func TestAssemble_containsGameRules(t *testing.T) {
|
|||
Generation: 1,
|
||||
}
|
||||
got := Assemble(r)
|
||||
for _, want := range []string{"60×60", "energy", "spawn", "toroidal"} {
|
||||
for _, want := range []string{"toroidal", "energy", "spawn", "focus-fire"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("expected prompt to contain %q", want)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ func FromMetaDescription(d *meta.Description) MetaDescription {
|
|||
return MetaDescription{
|
||||
TotalBots: d.TotalBots,
|
||||
DominantStrategy: d.DominantStrategy,
|
||||
NashMixture: d.NashMixture,
|
||||
MetaWeaknesses: append([]string(nil), d.MetaWeaknesses...),
|
||||
TopBots: FromBotInfos(d.TopBots),
|
||||
IslandStats: FromIslandStatsMap(d.IslandStats),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,8 @@ func TestFromMetaDescription_Full(t *testing.T) {
|
|||
desc := &meta.Description{
|
||||
TotalBots: 42,
|
||||
DominantStrategy: "aggressive",
|
||||
NashMixture: "60% aggressive, 40% economy",
|
||||
MetaWeaknesses: []string{"No bots exploring defense", "Low diversity in alpha"},
|
||||
TopBots: []meta.BotInfo{
|
||||
{Name: "bot1", Rating: 1600, Island: "alpha", Evolved: true},
|
||||
{Name: "bot2", Rating: 1500, Island: "beta", Evolved: false},
|
||||
|
|
@ -137,6 +139,12 @@ func TestFromMetaDescription_Full(t *testing.T) {
|
|||
if got.DominantStrategy != "aggressive" {
|
||||
t.Errorf("expected DominantStrategy 'aggressive', got %q", got.DominantStrategy)
|
||||
}
|
||||
if got.NashMixture != "60% aggressive, 40% economy" {
|
||||
t.Errorf("expected NashMixture, got %q", got.NashMixture)
|
||||
}
|
||||
if len(got.MetaWeaknesses) != 2 || got.MetaWeaknesses[0] != "No bots exploring defense" {
|
||||
t.Errorf("expected 2 MetaWeaknesses, got %v", got.MetaWeaknesses)
|
||||
}
|
||||
if len(got.TopBots) != 2 {
|
||||
t.Errorf("expected 2 TopBots, got %d", len(got.TopBots))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue