From d42d1a533664bb4933845f2a45b032288aa08be8 Mon Sep 17 00:00:00 2001 From: jedarden Date: Wed, 17 Jun 2026 03:10:15 -0400 Subject: [PATCH] feat(evolver): update fitness function to weight kill rate alongside win rate - Updated fitness formula: fitness = 0.7*win_rate + 0.3*kill_rate (was win_rate only) - Added kill tracking to ArenaResult: TotalKills, TotalMatches, KillRate - Updated evolver system prompt to explicitly mention combat kills are valuable - Enhanced arena logging to show kill rate and total kills This change makes the LLM evolver select for combat aggression, not just win optimization. The system prompt now informs bots that kills and eliminations are part of the fitness evaluation, encouraging more aggressive strategies. Related: bf-59h --- cmd/acb-evolver/internal/arena/arena.go | 26 ++++++++++++++++++++++ cmd/acb-evolver/internal/prompt/builder.go | 7 ++++++ cmd/acb-evolver/run.go | 14 ++++++++---- 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/cmd/acb-evolver/internal/arena/arena.go b/cmd/acb-evolver/internal/arena/arena.go index 7fdbcb0..a4eccbf 100644 --- a/cmd/acb-evolver/internal/arena/arena.go +++ b/cmd/acb-evolver/internal/arena/arena.go @@ -61,6 +61,7 @@ type MatchOutcome struct { Winner int // 0=player0, 1=player1, -1=draw Scores []int Turns int + CombatDeaths []int // bots killed in combat per player (kills credited to killer) Err error } @@ -85,6 +86,11 @@ type Result struct { Draws int Errors int + // Kill statistics across all matches (errors excluded). + TotalKills int // total kills credited to candidate + TotalMatches int // non-error matches for kill rate normalization + KillRate float64 // kills per match + // OpponentWinRates maps opponent BotID → candidate win rate vs that bot. OpponentWinRates map[string]float64 @@ -195,6 +201,25 @@ func (a *Arena) Run(ctx context.Context, code, language string) (*Result, error) } } + // Compute kill statistics (combat kills credited to candidate). + totalKills := 0 + totalMatches := 0 + for _, o := range result.Outcomes { + if o.Err != nil { + continue + } + totalMatches++ + // CombatDeaths[player_idx] contains kills credited to that player + if o.CandidateSlot < len(o.CombatDeaths) { + totalKills += o.CombatDeaths[o.CandidateSlot] + } + } + result.TotalKills = totalKills + result.TotalMatches = totalMatches + if totalMatches > 0 { + result.KillRate = float64(totalKills) / float64(totalMatches) + } + // Build ordered win-rate vector for PSRO (one entry per distinct opponent). seen := make(map[string]bool) for _, o := range result.Outcomes { @@ -312,6 +337,7 @@ func (a *Arena) runMatch(ctx context.Context, candidateURL string, opp BotRecord outcome.Winner = res.Winner outcome.Scores = res.Scores outcome.Turns = res.Turns + outcome.CombatDeaths = res.CombatDeaths return outcome } diff --git a/cmd/acb-evolver/internal/prompt/builder.go b/cmd/acb-evolver/internal/prompt/builder.go index 06d64c3..d1c1869 100644 --- a/cmd/acb-evolver/internal/prompt/builder.go +++ b/cmd/acb-evolver/internal/prompt/builder.go @@ -150,6 +150,13 @@ func writeSystemContext(sb *strings.Builder, targetLang string) { 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("## Fitness Function\n") + sb.WriteString("- Your evolved bot will be evaluated using fitness = 0.7 * win_rate + 0.3 * kill_rate\n") + sb.WriteString("- Win rate: percentage of matches won\n") + sb.WriteString("- Kill rate: average kills per match (combat aggression is rewarded)\n") + sb.WriteString("- This means: combat kills and eliminations are valuable, not just resource foraging\n") + sb.WriteString("- Aggressive combat strategies that secure kills while winning are strongly favored\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") diff --git a/cmd/acb-evolver/run.go b/cmd/acb-evolver/run.go index 3b7fe8c..cd9bcad 100644 --- a/cmd/acb-evolver/run.go +++ b/cmd/acb-evolver/run.go @@ -598,9 +598,14 @@ func runCycle(ctx context.Context, db *sql.DB, store *evolverdb.Store, return false, fmt.Errorf("arena: %w", err) } - // Compute fitness (overall win rate) + // Compute fitness (weighted combination of win rate and kill rate) wr := arena.ComputeFromResult(arenaResult) - fitness := wr.Rate + winRate := wr.Rate + killRate := arenaResult.KillRate + + // Fitness = 70% win rate + 30% kill rate + // This encourages combat aggression while still rewarding winning + fitness := 0.7*winRate + 0.3*killRate // Get behavior vector var behaviorVec []float64 @@ -614,8 +619,9 @@ func runCycle(ctx context.Context, db *sql.DB, store *evolverdb.Store, store.UpdateFitness(ctx, programID, fitness, behaviorVec) if verbose { - log.Printf(" Arena result: %d W / %d L / %d D / %d err win rate=%.3f", - arenaResult.Wins, arenaResult.Losses, arenaResult.Draws, arenaResult.Errors, fitness) + log.Printf(" Arena result: %d W / %d L / %d D / %d err win_rate=%.3f kill_rate=%.3f (%d kills/%d matches) fitness=%.3f", + arenaResult.Wins, arenaResult.Losses, arenaResult.Draws, arenaResult.Errors, + winRate, killRate, arenaResult.TotalKills, arenaResult.TotalMatches, fitness) } // 7. Load MAP-Elites grid and apply promotion gate