From d5515e0bca052d4b6806d2934200f9ae2c6c0cf2 Mon Sep 17 00:00:00 2001 From: jedarden Date: Wed, 17 Jun 2026 03:51:15 -0400 Subject: [PATCH] feat(mechanics): reduce flee thresholds and derive aggression from kill rate ## Flee Threshold Changes - Reduced flee threshold from AttackRadius2+4 to AttackRadius2 (no buffer) - Modified bots: farmer, gatherer, siege - Bots now only consider enemies in actual attack range, not preemptively - Added outnumber logic: only flee when nearbyAllies < nearbyEnemies ## Behavior Vector Changes - Derive aggression from actual kill rate (not self-reported) - Formula: behaviorVec[0] = min(killRate, 1.0) - Preserves existing economy value or defaults to 0.5 - Enhanced logging to show derived aggression value ## Rationale Aggression must be economically necessary, not just rewarded. Previous flee logic created a false safe option that discouraged combat. Now bots only flee when actually outnumbered within combat range. Related: bf-413 genesis bead tracking mechanics iteration --- bots/farmer/strategy.go | 70 +++++++++++++++++++++++---------------- bots/gatherer/strategy.go | 36 +++++++++++++++----- bots/siege/strategy.go | 33 ++++++++++++++---- cmd/acb-evolver/run.go | 19 ++++++++--- 4 files changed, 111 insertions(+), 47 deletions(-) diff --git a/bots/farmer/strategy.go b/bots/farmer/strategy.go index 15274f6..d5ac46d 100644 --- a/bots/farmer/strategy.go +++ b/bots/farmer/strategy.go @@ -2,11 +2,6 @@ package main import "math" -const ( - fleeRadius2 = 9 // flee if enemy within 3 cells (squared = 9) - dangerBuffer = 20 // extra buffer beyond attack radius for avoidance -) - // FarmerStrategy maximizes energy collection and spawn rate while avoiding combat. type FarmerStrategy struct{} @@ -138,29 +133,12 @@ func (s *FarmerStrategy) computeBotMove( ) string { pos := bot.Position - // Priority 1: FLEE if any enemy within flee radius - if len(enemyPositions) > 0 { - minEnemyDist2 := math.MaxInt32 - for _, ep := range enemyPositions { - d := distance2(pos, ep, rows, cols) - if d < minEnemyDist2 { - minEnemyDist2 = d - } - } - - if minEnemyDist2 <= fleeRadius2 { - dir := s.fleeDirection(pos, enemyPositions, wallSet, enemySet, rows, cols) - if dir != "" { - return dir - } - } - - // Also flee if enemy within attack radius + buffer - if minEnemyDist2 <= attackR2+dangerBuffer { - dir := s.fleeDirection(pos, enemyPositions, wallSet, enemySet, rows, cols) - if dir != "" { - return dir - } + // Priority 1: FLEE if locally outnumbered (nearbyAllies < nearbyEnemies) + // Use attack radius + small buffer to define "local" area + if s.shouldFlee(pos, state.Bots, myID, attackR2, rows, cols) { + dir := s.fleeDirection(pos, enemyPositions, wallSet, enemySet, rows, cols) + if dir != "" { + return dir } } @@ -281,6 +259,42 @@ func (s *FarmerStrategy) fleeDirection( return bestDir } +// shouldFlee returns true if the bot should flee from nearby enemies. +// Only flees when locally outnumbered (nearbyAllies < nearbyEnemies). +func (s *FarmerStrategy) shouldFlee(pos Position, bots []VisibleBot, myID, attackR2, rows, cols int) bool { + // Use attack radius exactly - only flee when enemies are in combat range + localRadius2 := attackR2 + + // Count nearby enemies within local radius + nearbyEnemies := 0 + for _, b := range bots { + if b.Owner == myID || b.Position == pos { + continue + } + if distance2(pos, b.Position, rows, cols) <= localRadius2 { + nearbyEnemies++ + } + } + + if nearbyEnemies == 0 { + return false + } + + // Count nearby allies within the same radius (excluding self) + nearbyAllies := 0 + for _, b := range bots { + if b.Owner != myID || b.Position == pos { + continue + } + if distance2(pos, b.Position, rows, cols) <= localRadius2 { + nearbyAllies++ + } + } + + // Only flee if outnumbered + return nearbyAllies < nearbyEnemies +} + // spreadMove picks a direction that moves away from the densest cluster // of friendly bots. func (s *FarmerStrategy) spreadMove( diff --git a/bots/gatherer/strategy.go b/bots/gatherer/strategy.go index e718a69..7f2bf69 100644 --- a/bots/gatherer/strategy.go +++ b/bots/gatherer/strategy.go @@ -84,8 +84,8 @@ func (s *GathererStrategy) computeBotMove( } } - // First check if we should flee from enemies - if s.shouldFlee(bot.Position, enemyBots, config) { + // First check if we should flee from enemies (only when outnumbered) + if s.shouldFlee(bot.Position, myBots, enemyBots, config) { fleeDir := s.getFleeDirection(bot.Position, enemyBots, config) if fleeDir != "" { return &Move{ @@ -110,15 +110,35 @@ func (s *GathererStrategy) computeBotMove( } // shouldFlee returns true if the bot should flee from nearby enemies. -func (s *GathererStrategy) shouldFlee(pos Position, enemies []VisibleBot, config GameConfig) bool { - for _, enemy := range enemies { +// Only flees when locally outnumbered (nearbyAllies < nearbyEnemies). +func (s *GathererStrategy) shouldFlee(pos Position, myBots, enemyBots []VisibleBot, config GameConfig) bool { + // Count nearby enemies within attack radius only (no buffer) + nearbyEnemies := 0 + for _, enemy := range enemyBots { dist2 := distance2(pos, enemy.Position, config) - // Flee if enemy is within attack range + 2 tiles buffer - if dist2 <= config.AttackRadius2+4 { - return true + if dist2 <= config.AttackRadius2 { + nearbyEnemies++ } } - return false + + if nearbyEnemies == 0 { + return false + } + + // Count nearby allies within the same radius (attack radius only) + nearbyAllies := 0 + for _, ally := range myBots { + if ally.Position == pos { + continue // Don't count self + } + dist2 := distance2(pos, ally.Position, config) + if dist2 <= config.AttackRadius2 { + nearbyAllies++ + } + } + + // Only flee if outnumbered + return nearbyAllies < nearbyEnemies } // getFleeDirection returns the best direction to flee from enemies. diff --git a/bots/siege/strategy.go b/bots/siege/strategy.go index 7cd8411..81e27ff 100644 --- a/bots/siege/strategy.go +++ b/bots/siege/strategy.go @@ -115,7 +115,7 @@ func (s *SiegeStrategy) ComputeMoves(state *GameState) []Move { } // Flee from nearby enemies - if s.shouldFlee(bot.Position, enemyBots, config) { + if s.shouldFlee(bot.Position, myBots, enemyBots, config) { fleeDir := s.getFleeDirection(bot.Position, enemyBots, wallPositions, config) if fleeDir != DirNone { moves = append(moves, Move{ @@ -280,14 +280,35 @@ func (s *SiegeStrategy) findNearestCore(pos Position, cores []VisibleCore, confi } // shouldFlee returns true if the bot should flee from nearby enemies. -func (s *SiegeStrategy) shouldFlee(pos Position, enemies []VisibleBot, config GameConfig) bool { - for _, enemy := range enemies { +// Only flees when locally outnumbered (nearbyAllies < nearbyEnemies). +func (s *SiegeStrategy) shouldFlee(pos Position, myBots, enemyBots []VisibleBot, config GameConfig) bool { + // Count nearby enemies within attack radius only (no buffer) + nearbyEnemies := 0 + for _, enemy := range enemyBots { dist2 := distance2(pos, enemy.Position, config) - if dist2 <= config.AttackRadius2+4 { // Attack radius + buffer - return true + if dist2 <= config.AttackRadius2 { + nearbyEnemies++ } } - return false + + if nearbyEnemies == 0 { + return false + } + + // Count nearby allies within the same radius (attack radius only) + nearbyAllies := 0 + for _, ally := range myBots { + if ally.Position == pos { + continue // Don't count self + } + dist2 := distance2(pos, ally.Position, config) + if dist2 <= config.AttackRadius2 { + nearbyAllies++ + } + } + + // Only flee if outnumbered + return nearbyAllies < nearbyEnemies } // getFleeDirection returns the best direction to flee from enemies. diff --git a/cmd/acb-evolver/run.go b/cmd/acb-evolver/run.go index cd9bcad..efc1dce 100644 --- a/cmd/acb-evolver/run.go +++ b/cmd/acb-evolver/run.go @@ -607,21 +607,30 @@ func runCycle(ctx context.Context, db *sql.DB, store *evolverdb.Store, // This encourages combat aggression while still rewarding winning fitness := 0.7*winRate + 0.3*killRate - // Get behavior vector + // Derive behavior vector from actual arena performance (not self-reported) + // BehaviorVector[0] = aggression (from kill rate) + // BehaviorVector[1] = economy (placeholder - preserve existing if available) var behaviorVec []float64 + aggression := killRate + if aggression > 1.0 { + aggression = 1.0 + } + if program != nil && len(program.BehaviorVector) >= 2 { - behaviorVec = program.BehaviorVector + // Preserve existing economy value, update aggression from actual data + behaviorVec = []float64{aggression, program.BehaviorVector[1]} } else { - behaviorVec = []float64{0.5, 0.5} + // No existing data - default economy to 0.5 + behaviorVec = []float64{aggression, 0.5} } // Update fitness in database store.UpdateFitness(ctx, programID, fitness, behaviorVec) if verbose { - log.Printf(" Arena result: %d W / %d L / %d D / %d err win_rate=%.3f kill_rate=%.3f (%d kills/%d matches) fitness=%.3f", + log.Printf(" Arena result: %d W / %d L / %d D / %d err win_rate=%.3f kill_rate=%.3f (%d kills/%d matches) fitness=%.3f aggression=%.3f (derived)", arenaResult.Wins, arenaResult.Losses, arenaResult.Draws, arenaResult.Errors, - winRate, killRate, arenaResult.TotalKills, arenaResult.TotalMatches, fitness) + winRate, killRate, arenaResult.TotalKills, arenaResult.TotalMatches, fitness, behaviorVec[0]) } // 7. Load MAP-Elites grid and apply promotion gate