feat(engine): add combat_deaths to MatchResult statistics
Add CombatDeaths []int field to MatchResult to track combat density per player. This enables monitoring of focus-fire combat across all matches and helps verify that the zone forcing function is working. Changes: - Add CombatDeaths []int to MatchResult struct - Add CombatDeaths []int to GameState for tracking during match - Increment combat death count for each killer in executeCombat - Populate combat_deaths in final match result - Update tests to include CombatDeaths in MatchResult Verified: 6-player match shows combat_deaths: [1,1,1,1,1,1] (each player killed 1 bot in mutual combat). Closes: bf-4fez Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
da5e3f1479
commit
b3982ab6d7
4 changed files with 50 additions and 41 deletions
|
|
@ -32,9 +32,10 @@ type GameState struct {
|
|||
LastTotalBots int // total living bots at last progress
|
||||
|
||||
// Zone (storm) state
|
||||
ZoneCenter Position // center of the zone (map center)
|
||||
ZoneRadius int // current radius of the safe zone
|
||||
ZoneActive bool // whether the zone is currently shrinking
|
||||
ZoneCenter Position // center of the zone (map center)
|
||||
ZoneRadius int // current radius of the safe zone
|
||||
ZoneActive bool // whether the zone is currently shrinking
|
||||
CombatDeaths []int // combat deaths per player (tracked for final stats)
|
||||
}
|
||||
|
||||
// Event represents something that happened during a turn.
|
||||
|
|
@ -61,23 +62,24 @@ func NewGameState(config Config, rng *rand.Rand) *GameState {
|
|||
initialRadius := min(config.Rows, config.Cols) / 2
|
||||
|
||||
return &GameState{
|
||||
Config: config,
|
||||
Grid: NewGrid(config.Rows, config.Cols),
|
||||
Bots: make([]*Bot, 0),
|
||||
Cores: make([]*Core, 0),
|
||||
Energy: make([]*EnergyNode, 0),
|
||||
Players: make([]*Player, 0),
|
||||
Turn: 0,
|
||||
MatchID: generateMatchID(rng),
|
||||
NextBotID: 0,
|
||||
rng: rng,
|
||||
Moves: make(map[int]Move),
|
||||
DeadBots: make([]*Bot, 0),
|
||||
Events: make([]Event, 0),
|
||||
Dominance: make(map[int]int),
|
||||
ZoneCenter: center,
|
||||
ZoneRadius: initialRadius,
|
||||
ZoneActive: false,
|
||||
Config: config,
|
||||
Grid: NewGrid(config.Rows, config.Cols),
|
||||
Bots: make([]*Bot, 0),
|
||||
Cores: make([]*Core, 0),
|
||||
Energy: make([]*EnergyNode, 0),
|
||||
Players: make([]*Player, 0),
|
||||
Turn: 0,
|
||||
MatchID: generateMatchID(rng),
|
||||
NextBotID: 0,
|
||||
rng: rng,
|
||||
Moves: make(map[int]Move),
|
||||
DeadBots: make([]*Bot, 0),
|
||||
Events: make([]Event, 0),
|
||||
Dominance: make(map[int]int),
|
||||
ZoneCenter: center,
|
||||
ZoneRadius: initialRadius,
|
||||
ZoneActive: false,
|
||||
CombatDeaths: make([]int, 0), // Will be sized when players are added
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -100,6 +102,7 @@ func (gs *GameState) AddPlayer() *Player {
|
|||
}
|
||||
gs.Players = append(gs.Players, p)
|
||||
gs.Dominance[p.ID] = 0
|
||||
gs.CombatDeaths = append(gs.CombatDeaths, 0) // Track combat deaths for this player
|
||||
return p
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ func TestMapEngagement_WinProbDependency(t *testing.T) {
|
|||
// Create a replay with alternating win probs to simulate lead changes
|
||||
replay := &Replay{
|
||||
Config: Config{Rows: 20, Cols: 20, MaxTurns: 100},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}, CombatDeaths: []int{0, 0}},
|
||||
WinProb: []WinProbEntry{
|
||||
{0.6, 0.4}, // Player 0 leading
|
||||
{0.4, 0.6}, // Player 1 leading - 1st crossing
|
||||
|
|
@ -46,7 +46,7 @@ func TestMapEngagement_WinProbDependency(t *testing.T) {
|
|||
func TestMapEngagement_CriticalMomentsDependency(t *testing.T) {
|
||||
replay := &Replay{
|
||||
Config: Config{Rows: 20, Cols: 20, MaxTurns: 100},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}, CombatDeaths: []int{0, 0}},
|
||||
CriticalMoments: []CriticalMoment{
|
||||
{Turn: 10, Delta: 0.20, Player: 0, Description: "Player 0 scores"},
|
||||
{Turn: 25, Delta: -0.25, Player: 1, Description: "Player 1 fights back"},
|
||||
|
|
@ -78,7 +78,7 @@ func TestMapEngagement_CriticalMomentsDependency(t *testing.T) {
|
|||
func TestMapEngagement_ResourceContestTurns(t *testing.T) {
|
||||
replay := &Replay{
|
||||
Config: Config{Rows: 20, Cols: 20, MaxTurns: 100},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}, CombatDeaths: []int{0, 0}},
|
||||
Turns: []ReplayTurn{
|
||||
{
|
||||
Turn: 0,
|
||||
|
|
@ -114,7 +114,7 @@ func TestMapEngagement_ResourceContestTurns(t *testing.T) {
|
|||
func TestMapEngagement_SurvivalTurns(t *testing.T) {
|
||||
replay := &Replay{
|
||||
Config: Config{Rows: 20, Cols: 20, MaxTurns: 100},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}, CombatDeaths: []int{0, 0}},
|
||||
Turns: []ReplayTurn{
|
||||
{
|
||||
Turn: 0,
|
||||
|
|
@ -168,7 +168,7 @@ func TestMapEngagement_EmptyReplay(t *testing.T) {
|
|||
func TestMapEngagement_Formula(t *testing.T) {
|
||||
replay := &Replay{
|
||||
Config: Config{Rows: 20, Cols: 20, MaxTurns: 100},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}, CombatDeaths: []int{0, 0}},
|
||||
WinProb: []WinProbEntry{
|
||||
{0.6, 0.4}, // Player 0 leading
|
||||
{0.4, 0.6}, // Player 1 leading - 1st crossing
|
||||
|
|
@ -241,7 +241,7 @@ func TestMapEngagement_Formula(t *testing.T) {
|
|||
func TestMapEngagement_NoContestedEnergy(t *testing.T) {
|
||||
replay := &Replay{
|
||||
Config: Config{Rows: 20, Cols: 20, MaxTurns: 100},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}, CombatDeaths: []int{0, 0}},
|
||||
Turns: []ReplayTurn{
|
||||
{
|
||||
Turn: 0,
|
||||
|
|
@ -275,7 +275,7 @@ func TestMapEngagement_NoContestedEnergy(t *testing.T) {
|
|||
func TestMapEngagement_PlayerElimination(t *testing.T) {
|
||||
replay := &Replay{
|
||||
Config: Config{Rows: 20, Cols: 20, MaxTurns: 100},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}, CombatDeaths: []int{0, 0}},
|
||||
Turns: []ReplayTurn{
|
||||
{
|
||||
Turn: 0,
|
||||
|
|
@ -373,7 +373,7 @@ func TestWinProb_ComputeAndSet(t *testing.T) {
|
|||
func TestMapEngagement_CombatDeaths(t *testing.T) {
|
||||
replay := &Replay{
|
||||
Config: Config{Rows: 20, Cols: 20, MaxTurns: 100},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}},
|
||||
Result: &MatchResult{Turns: 50, Scores: []int{5, 4}, CombatDeaths: []int{0, 0}},
|
||||
Turns: []ReplayTurn{
|
||||
{
|
||||
Turn: 0,
|
||||
|
|
|
|||
|
|
@ -218,6 +218,10 @@ func (gs *GameState) executeCombat() {
|
|||
"owner": e.Owner,
|
||||
"position": e.Position,
|
||||
})
|
||||
// Track combat deaths for the killer's player
|
||||
if e.Owner < len(gs.CombatDeaths) {
|
||||
gs.CombatDeaths[e.Owner]++
|
||||
}
|
||||
}
|
||||
|
||||
gs.Events = append(gs.Events, Event{
|
||||
|
|
@ -497,12 +501,13 @@ func (gs *GameState) createResult(winner int, reason string) *MatchResult {
|
|||
}
|
||||
|
||||
return &MatchResult{
|
||||
Winner: winner,
|
||||
Reason: reason,
|
||||
Turns: gs.Turn,
|
||||
Scores: scores,
|
||||
Energy: energy,
|
||||
BotsAlive: botsAlive,
|
||||
Winner: winner,
|
||||
Reason: reason,
|
||||
Turns: gs.Turn,
|
||||
Scores: scores,
|
||||
Energy: energy,
|
||||
BotsAlive: botsAlive,
|
||||
CombatDeaths: gs.CombatDeaths,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -258,13 +258,14 @@ func ConfigForPlayers(numPlayers, coresPerPlayer int) Config {
|
|||
|
||||
// MatchResult represents the outcome of a match.
|
||||
type MatchResult struct {
|
||||
Winner int `json:"winner"` // -1 for draw
|
||||
Reason string `json:"reason"` // "elimination", "dominance", "turns", "draw"
|
||||
Turns int `json:"turns"`
|
||||
Scores []int `json:"scores"`
|
||||
Energy []int `json:"energy"` // energy collected per player
|
||||
BotsAlive []int `json:"bots_alive"`
|
||||
Crashed []bool `json:"crashed"` // per-player: true if bot was marked crashed during match
|
||||
Winner int `json:"winner"` // -1 for draw
|
||||
Reason string `json:"reason"` // "elimination", "dominance", "turns", "draw"
|
||||
Turns int `json:"turns"`
|
||||
Scores []int `json:"scores"`
|
||||
Energy []int `json:"energy"` // energy collected per player
|
||||
BotsAlive []int `json:"bots_alive"`
|
||||
Crashed []bool `json:"crashed"` // per-player: true if bot was marked crashed during match
|
||||
CombatDeaths []int `json:"combat_deaths"` // bots killed in combat per player (focus-fire)
|
||||
}
|
||||
|
||||
// BotInterface defines the interface for bot decision-making.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue