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
This commit is contained in:
jedarden 2026-06-17 03:51:15 -04:00
parent 2cf6437587
commit d5515e0bca
4 changed files with 111 additions and 47 deletions

View file

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

View file

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

View file

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

View file

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