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