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
This commit is contained in:
jedarden 2026-06-17 03:10:15 -04:00
parent 42398eb34a
commit d42d1a5336
3 changed files with 43 additions and 4 deletions

View file

@ -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
}

View file

@ -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")

View file

@ -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