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:
parent
2cf6437587
commit
d5515e0bca
4 changed files with 111 additions and 47 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue