use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::arena::historian::StatsStorage;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PositionStats {
pub seat_index: usize,
pub games_played: usize,
pub profit: f32,
pub profit_per_game: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentStats {
pub agent_name: String,
pub total_profit: f32,
pub total_games: usize,
pub wins: usize,
pub losses: usize,
pub breakeven: usize,
pub profit_per_game: f32,
pub profit_per_100_hands: f32,
pub roi_percent: f32,
pub position_stats: Vec<PositionStats>,
pub vpip_percent: f32,
pub pfr_percent: f32,
pub three_bet_percent: f32,
pub aggression_factor: f32,
pub cbet_percent: f32,
pub wtsd_percent: f32,
pub wsd_percent: f32,
pub steal_percent: f32,
pub aggression_frequency: f32,
pub flop_aggression_factor: f32,
pub turn_aggression_factor: f32,
pub river_aggression_factor: f32,
pub preflop_win_rate: f32,
pub flop_win_rate: f32,
pub turn_win_rate: f32,
pub river_win_rate: f32,
}
pub struct AgentStatsBuilder {
agent_accumulated: Vec<StatsStorage>,
position_tracking: Vec<HashMap<usize, (usize, f32)>>,
agent_names: Vec<String>,
}
impl AgentStatsBuilder {
pub fn new(agent_names: Vec<String>) -> Self {
let num_agents = agent_names.len();
let agent_accumulated = (0..num_agents)
.map(|_| StatsStorage::new_with_num_players(1))
.collect();
let position_tracking = (0..num_agents).map(|_| HashMap::new()).collect();
Self {
agent_accumulated,
position_tracking,
agent_names,
}
}
pub fn merge_permutation_stats(&mut self, permutation: &[usize], stats: &StatsStorage) {
for (seat_idx, &agent_idx) in permutation.iter().enumerate() {
let agent_stats = &mut self.agent_accumulated[agent_idx];
let player_idx = 0;
agent_stats.actions_count[player_idx] += stats.actions_count[seat_idx];
agent_stats.vpip_count[player_idx] += stats.vpip_count[seat_idx];
agent_stats.vpip_total[player_idx] += stats.vpip_total[seat_idx];
agent_stats.raise_count[player_idx] += stats.raise_count[seat_idx];
agent_stats.hands_played[player_idx] += stats.hands_played[seat_idx];
agent_stats.hands_vpip[player_idx] += stats.hands_vpip[seat_idx];
agent_stats.hands_pfr[player_idx] += stats.hands_pfr[seat_idx];
agent_stats.preflop_raise_count[player_idx] += stats.preflop_raise_count[seat_idx];
agent_stats.preflop_actions[player_idx] += stats.preflop_actions[seat_idx];
agent_stats.three_bet_count[player_idx] += stats.three_bet_count[seat_idx];
agent_stats.three_bet_opportunities[player_idx] +=
stats.three_bet_opportunities[seat_idx];
agent_stats.call_count[player_idx] += stats.call_count[seat_idx];
agent_stats.bet_count[player_idx] += stats.bet_count[seat_idx];
agent_stats.total_profit[player_idx] += stats.total_profit[seat_idx];
agent_stats.total_invested[player_idx] += stats.total_invested[seat_idx];
agent_stats.games_won[player_idx] += stats.games_won[seat_idx];
agent_stats.games_lost[player_idx] += stats.games_lost[seat_idx];
agent_stats.games_breakeven[player_idx] += stats.games_breakeven[seat_idx];
agent_stats.preflop_wins[player_idx] += stats.preflop_wins[seat_idx];
agent_stats.flop_wins[player_idx] += stats.flop_wins[seat_idx];
agent_stats.turn_wins[player_idx] += stats.turn_wins[seat_idx];
agent_stats.river_wins[player_idx] += stats.river_wins[seat_idx];
agent_stats.preflop_completes[player_idx] += stats.preflop_completes[seat_idx];
agent_stats.flop_completes[player_idx] += stats.flop_completes[seat_idx];
agent_stats.turn_completes[player_idx] += stats.turn_completes[seat_idx];
agent_stats.river_completes[player_idx] += stats.river_completes[seat_idx];
agent_stats.cbet_opportunities[player_idx] += stats.cbet_opportunities[seat_idx];
agent_stats.cbet_count[player_idx] += stats.cbet_count[seat_idx];
agent_stats.wtsd_opportunities[player_idx] += stats.wtsd_opportunities[seat_idx];
agent_stats.wtsd_count[player_idx] += stats.wtsd_count[seat_idx];
agent_stats.showdown_count[player_idx] += stats.showdown_count[seat_idx];
agent_stats.showdown_wins[player_idx] += stats.showdown_wins[seat_idx];
agent_stats.fold_count[player_idx] += stats.fold_count[seat_idx];
agent_stats.flop_bets[player_idx] += stats.flop_bets[seat_idx];
agent_stats.flop_raises[player_idx] += stats.flop_raises[seat_idx];
agent_stats.flop_calls[player_idx] += stats.flop_calls[seat_idx];
agent_stats.turn_bets[player_idx] += stats.turn_bets[seat_idx];
agent_stats.turn_raises[player_idx] += stats.turn_raises[seat_idx];
agent_stats.turn_calls[player_idx] += stats.turn_calls[seat_idx];
agent_stats.river_bets[player_idx] += stats.river_bets[seat_idx];
agent_stats.river_raises[player_idx] += stats.river_raises[seat_idx];
agent_stats.river_calls[player_idx] += stats.river_calls[seat_idx];
agent_stats.steal_opportunities[player_idx] += stats.steal_opportunities[seat_idx];
agent_stats.steal_count[player_idx] += stats.steal_count[seat_idx];
let pos_map = &mut self.position_tracking[agent_idx];
let (games, profit) = pos_map.entry(seat_idx).or_insert((0, 0.0));
*games += 1;
*profit += stats.total_profit[seat_idx];
}
}
pub fn build(self) -> HashMap<String, AgentStats> {
let mut agent_stats = HashMap::new();
for (agent_idx, agent_name) in self.agent_names.iter().enumerate() {
let stats = &self.agent_accumulated[agent_idx];
let player_idx = 0;
let mut position_stats = Vec::new();
if let Some(pos_map) = self.position_tracking.get(agent_idx) {
for (seat_idx, (games_played, total_profit)) in pos_map {
let profit_per_game = if *games_played > 0 {
total_profit / *games_played as f32
} else {
0.0
};
position_stats.push(PositionStats {
seat_index: *seat_idx,
games_played: *games_played,
profit: *total_profit,
profit_per_game,
});
}
}
position_stats.sort_by_key(|ps| ps.seat_index);
let total_games = stats.games_won[player_idx]
+ stats.games_lost[player_idx]
+ stats.games_breakeven[player_idx];
let agent_stat = AgentStats {
agent_name: agent_name.clone(),
total_profit: stats.total_profit[player_idx],
total_games,
wins: stats.games_won[player_idx],
losses: stats.games_lost[player_idx],
breakeven: stats.games_breakeven[player_idx],
profit_per_game: stats.profit_per_game(player_idx),
profit_per_100_hands: stats.profit_per_game(player_idx) * 100.0,
roi_percent: stats.roi_percent(player_idx),
position_stats,
vpip_percent: stats.vpip_percent(player_idx),
pfr_percent: stats.pfr_percent(player_idx),
three_bet_percent: stats.three_bet_percent(player_idx),
aggression_factor: stats.aggression_factor(player_idx),
cbet_percent: stats.cbet_percent(player_idx),
wtsd_percent: stats.wtsd_percent(player_idx),
wsd_percent: stats.wsd_percent(player_idx),
steal_percent: stats.steal_percent(player_idx),
aggression_frequency: stats.aggression_frequency(player_idx),
flop_aggression_factor: stats.flop_aggression_factor(player_idx),
turn_aggression_factor: stats.turn_aggression_factor(player_idx),
river_aggression_factor: stats.river_aggression_factor(player_idx),
preflop_win_rate: stats.preflop_win_rate(player_idx),
flop_win_rate: stats.flop_win_rate(player_idx),
turn_win_rate: stats.turn_win_rate(player_idx),
river_win_rate: stats.river_win_rate(player_idx),
};
agent_stats.insert(agent_name.clone(), agent_stat);
}
agent_stats
}
pub fn agent_names(&self) -> &[String] {
&self.agent_names
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_agent_stats_builder_new() {
let names = vec!["Agent1".to_string(), "Agent2".to_string()];
let builder = AgentStatsBuilder::new(names.clone());
assert_eq!(builder.agent_names(), &names);
assert_eq!(builder.agent_accumulated.len(), 2);
assert_eq!(builder.position_tracking.len(), 2);
}
#[test]
fn test_agent_stats_builder_build_empty() {
let names = vec!["Agent1".to_string()];
let builder = AgentStatsBuilder::new(names);
let stats = builder.build();
assert_eq!(stats.len(), 1);
let agent_stats = stats.get("Agent1").unwrap();
assert_eq!(agent_stats.agent_name, "Agent1");
assert_eq!(agent_stats.total_games, 0);
assert_eq!(agent_stats.total_profit, 0.0);
}
#[test]
fn test_merge_permutation_stats() {
let names = vec!["Agent0".to_string(), "Agent1".to_string()];
let mut builder = AgentStatsBuilder::new(names);
let mut stats = StatsStorage::new_with_num_players(2);
stats.total_profit[0] = 50.0; stats.total_profit[1] = -50.0; stats.total_invested[0] = 100.0;
stats.total_invested[1] = 100.0;
stats.games_won[0] = 1;
stats.games_lost[1] = 1;
stats.hands_played[0] = 1;
stats.hands_played[1] = 1;
builder.merge_permutation_stats(&[0, 1], &stats);
let result = builder.build();
let agent0 = result.get("Agent0").unwrap();
assert_eq!(agent0.total_profit, 50.0);
assert_eq!(agent0.wins, 1);
assert_eq!(agent0.losses, 0);
let agent1 = result.get("Agent1").unwrap();
assert_eq!(agent1.total_profit, -50.0);
assert_eq!(agent1.wins, 0);
assert_eq!(agent1.losses, 1);
}
#[test]
fn test_merge_permutation_stats_multiple_games() {
let names = vec!["Agent0".to_string(), "Agent1".to_string()];
let mut builder = AgentStatsBuilder::new(names);
let mut stats1 = StatsStorage::new_with_num_players(2);
stats1.total_profit[0] = 50.0;
stats1.total_profit[1] = -50.0;
stats1.total_invested[0] = 100.0;
stats1.total_invested[1] = 100.0;
stats1.games_won[0] = 1;
stats1.games_lost[1] = 1;
builder.merge_permutation_stats(&[0, 1], &stats1);
let mut stats2 = StatsStorage::new_with_num_players(2);
stats2.total_profit[0] = 30.0; stats2.total_profit[1] = -30.0; stats2.total_invested[0] = 80.0;
stats2.total_invested[1] = 80.0;
stats2.games_won[0] = 1;
stats2.games_lost[1] = 1;
builder.merge_permutation_stats(&[1, 0], &stats2);
let result = builder.build();
let agent0 = result.get("Agent0").unwrap();
assert_eq!(agent0.total_profit, 20.0);
assert_eq!(agent0.wins, 1);
assert_eq!(agent0.losses, 1);
let agent1 = result.get("Agent1").unwrap();
assert_eq!(agent1.total_profit, -20.0);
assert_eq!(agent1.wins, 1);
assert_eq!(agent1.losses, 1);
}
#[test]
fn test_position_stats_tracking() {
let names = vec!["Agent0".to_string(), "Agent1".to_string()];
let mut builder = AgentStatsBuilder::new(names);
let mut stats1 = StatsStorage::new_with_num_players(2);
stats1.total_profit[0] = 100.0;
stats1.games_won[0] = 1;
builder.merge_permutation_stats(&[0, 1], &stats1);
let mut stats2 = StatsStorage::new_with_num_players(2);
stats2.total_profit[1] = -20.0;
stats2.games_lost[1] = 1;
builder.merge_permutation_stats(&[1, 0], &stats2);
let result = builder.build();
let agent0 = result.get("Agent0").unwrap();
assert_eq!(agent0.position_stats.len(), 2);
let seat0_stats = agent0
.position_stats
.iter()
.find(|p| p.seat_index == 0)
.unwrap();
assert_eq!(seat0_stats.games_played, 1);
assert_eq!(seat0_stats.profit, 100.0);
let seat1_stats = agent0
.position_stats
.iter()
.find(|p| p.seat_index == 1)
.unwrap();
assert_eq!(seat1_stats.games_played, 1);
assert_eq!(seat1_stats.profit, -20.0);
}
#[test]
fn test_roi_calculation_in_agent_stats() {
let names = vec!["Agent0".to_string(), "Agent1".to_string()];
let mut builder = AgentStatsBuilder::new(names);
let mut stats = StatsStorage::new_with_num_players(2);
stats.total_profit[0] = 50.0;
stats.total_invested[0] = 200.0;
stats.games_won[0] = 1;
builder.merge_permutation_stats(&[0, 1], &stats);
let result = builder.build();
let agent0 = result.get("Agent0").unwrap();
assert!((agent0.roi_percent - 25.0).abs() < 0.01);
}
#[test]
fn test_agent_stats_serialization() {
let stats = AgentStats {
agent_name: "Test".to_string(),
total_profit: 100.0,
total_games: 10,
wins: 6,
losses: 3,
breakeven: 1,
profit_per_game: 10.0,
profit_per_100_hands: 1000.0,
roi_percent: 25.0,
position_stats: vec![PositionStats {
seat_index: 0,
games_played: 10,
profit: 100.0,
profit_per_game: 10.0,
}],
vpip_percent: 30.0,
pfr_percent: 20.0,
three_bet_percent: 5.0,
aggression_factor: 2.0,
cbet_percent: 60.0,
wtsd_percent: 25.0,
wsd_percent: 50.0,
steal_percent: 30.0,
aggression_frequency: 40.0,
flop_aggression_factor: 2.5,
turn_aggression_factor: 2.0,
river_aggression_factor: 1.5,
preflop_win_rate: 20.0,
flop_win_rate: 30.0,
turn_win_rate: 35.0,
river_win_rate: 40.0,
};
let json = serde_json::to_string(&stats).unwrap();
assert!(json.contains("\"agent_name\":\"Test\""));
assert!(json.contains("\"total_profit\":100.0"));
assert!(json.contains("\"roi_percent\":25.0"));
let deserialized: AgentStats = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.agent_name, "Test");
assert_eq!(deserialized.total_profit, 100.0);
assert_eq!(deserialized.roi_percent, 25.0);
}
#[test]
fn test_merge_permutation_stats_arithmetic_raise_count() {
let names = vec!["Agent0".to_string()];
let mut builder = AgentStatsBuilder::new(names);
builder.agent_accumulated[0].raise_count[0] = 10;
let mut stats = StatsStorage::new_with_num_players(1);
stats.raise_count[0] = 5;
builder.merge_permutation_stats(&[0], &stats);
assert_eq!(
builder.agent_accumulated[0].raise_count[0], 15,
"raise_count should be 10 + 5 = 15"
);
}
#[test]
fn test_merge_permutation_stats_arithmetic_preflop_actions() {
let names = vec!["Agent0".to_string()];
let mut builder = AgentStatsBuilder::new(names);
builder.agent_accumulated[0].preflop_actions[0] = 8;
let mut stats = StatsStorage::new_with_num_players(1);
stats.preflop_actions[0] = 4;
builder.merge_permutation_stats(&[0], &stats);
assert_eq!(
builder.agent_accumulated[0].preflop_actions[0], 12,
"preflop_actions should be 8 + 4 = 12"
);
}
#[test]
fn test_merge_permutation_stats_arithmetic_three_bet() {
let names = vec!["Agent0".to_string()];
let mut builder = AgentStatsBuilder::new(names);
builder.agent_accumulated[0].three_bet_count[0] = 3;
builder.agent_accumulated[0].three_bet_opportunities[0] = 10;
let mut stats = StatsStorage::new_with_num_players(1);
stats.three_bet_count[0] = 2;
stats.three_bet_opportunities[0] = 5;
builder.merge_permutation_stats(&[0], &stats);
assert_eq!(
builder.agent_accumulated[0].three_bet_count[0], 5,
"three_bet_count should be 3 + 2 = 5"
);
assert_eq!(
builder.agent_accumulated[0].three_bet_opportunities[0], 15,
"three_bet_opportunities should be 10 + 5 = 15"
);
}
#[test]
fn test_merge_permutation_stats_arithmetic_call_bet_count() {
let names = vec!["Agent0".to_string()];
let mut builder = AgentStatsBuilder::new(names);
builder.agent_accumulated[0].call_count[0] = 7;
builder.agent_accumulated[0].bet_count[0] = 6;
let mut stats = StatsStorage::new_with_num_players(1);
stats.call_count[0] = 3;
stats.bet_count[0] = 4;
builder.merge_permutation_stats(&[0], &stats);
assert_eq!(
builder.agent_accumulated[0].call_count[0], 10,
"call_count should be 7 + 3 = 10"
);
assert_eq!(
builder.agent_accumulated[0].bet_count[0], 10,
"bet_count should be 6 + 4 = 10"
);
}
#[test]
fn test_merge_permutation_stats_arithmetic_round_wins() {
let names = vec!["Agent0".to_string()];
let mut builder = AgentStatsBuilder::new(names);
builder.agent_accumulated[0].preflop_wins[0] = 2;
builder.agent_accumulated[0].flop_wins[0] = 3;
builder.agent_accumulated[0].turn_wins[0] = 4;
builder.agent_accumulated[0].river_wins[0] = 5;
let mut stats = StatsStorage::new_with_num_players(1);
stats.preflop_wins[0] = 1;
stats.flop_wins[0] = 2;
stats.turn_wins[0] = 3;
stats.river_wins[0] = 4;
builder.merge_permutation_stats(&[0], &stats);
assert_eq!(
builder.agent_accumulated[0].preflop_wins[0], 3,
"preflop_wins should be 2 + 1 = 3"
);
assert_eq!(
builder.agent_accumulated[0].flop_wins[0], 5,
"flop_wins should be 3 + 2 = 5"
);
assert_eq!(
builder.agent_accumulated[0].turn_wins[0], 7,
"turn_wins should be 4 + 3 = 7"
);
assert_eq!(
builder.agent_accumulated[0].river_wins[0], 9,
"river_wins should be 5 + 4 = 9"
);
}
#[test]
fn test_merge_permutation_stats_arithmetic_round_completes() {
let names = vec!["Agent0".to_string()];
let mut builder = AgentStatsBuilder::new(names);
builder.agent_accumulated[0].preflop_completes[0] = 10;
builder.agent_accumulated[0].flop_completes[0] = 8;
builder.agent_accumulated[0].turn_completes[0] = 6;
builder.agent_accumulated[0].river_completes[0] = 4;
let mut stats = StatsStorage::new_with_num_players(1);
stats.preflop_completes[0] = 5;
stats.flop_completes[0] = 4;
stats.turn_completes[0] = 3;
stats.river_completes[0] = 2;
builder.merge_permutation_stats(&[0], &stats);
assert_eq!(
builder.agent_accumulated[0].preflop_completes[0], 15,
"preflop_completes should be 10 + 5 = 15"
);
assert_eq!(
builder.agent_accumulated[0].flop_completes[0], 12,
"flop_completes should be 8 + 4 = 12"
);
assert_eq!(
builder.agent_accumulated[0].turn_completes[0], 9,
"turn_completes should be 6 + 3 = 9"
);
assert_eq!(
builder.agent_accumulated[0].river_completes[0], 6,
"river_completes should be 4 + 2 = 6"
);
}
#[test]
fn test_merge_permutation_stats_arithmetic_advanced_stats() {
let names = vec!["Agent0".to_string()];
let mut builder = AgentStatsBuilder::new(names);
builder.agent_accumulated[0].cbet_opportunities[0] = 5;
builder.agent_accumulated[0].cbet_count[0] = 3;
builder.agent_accumulated[0].wtsd_opportunities[0] = 10;
builder.agent_accumulated[0].wtsd_count[0] = 4;
builder.agent_accumulated[0].showdown_count[0] = 8;
builder.agent_accumulated[0].showdown_wins[0] = 5;
builder.agent_accumulated[0].fold_count[0] = 12;
let mut stats = StatsStorage::new_with_num_players(1);
stats.cbet_opportunities[0] = 3;
stats.cbet_count[0] = 2;
stats.wtsd_opportunities[0] = 5;
stats.wtsd_count[0] = 2;
stats.showdown_count[0] = 3;
stats.showdown_wins[0] = 2;
stats.fold_count[0] = 4;
builder.merge_permutation_stats(&[0], &stats);
assert_eq!(
builder.agent_accumulated[0].cbet_opportunities[0], 8,
"cbet_opportunities should be 5 + 3 = 8"
);
assert_eq!(
builder.agent_accumulated[0].cbet_count[0], 5,
"cbet_count should be 3 + 2 = 5"
);
assert_eq!(
builder.agent_accumulated[0].wtsd_opportunities[0], 15,
"wtsd_opportunities should be 10 + 5 = 15"
);
assert_eq!(
builder.agent_accumulated[0].wtsd_count[0], 6,
"wtsd_count should be 4 + 2 = 6"
);
assert_eq!(
builder.agent_accumulated[0].showdown_count[0], 11,
"showdown_count should be 8 + 3 = 11"
);
assert_eq!(
builder.agent_accumulated[0].showdown_wins[0], 7,
"showdown_wins should be 5 + 2 = 7"
);
assert_eq!(
builder.agent_accumulated[0].fold_count[0], 16,
"fold_count should be 12 + 4 = 16"
);
}
#[test]
fn test_merge_permutation_stats_arithmetic_per_street_actions() {
let names = vec!["Agent0".to_string()];
let mut builder = AgentStatsBuilder::new(names);
builder.agent_accumulated[0].flop_bets[0] = 3;
builder.agent_accumulated[0].flop_raises[0] = 2;
builder.agent_accumulated[0].flop_calls[0] = 5;
builder.agent_accumulated[0].turn_bets[0] = 4;
builder.agent_accumulated[0].turn_raises[0] = 3;
builder.agent_accumulated[0].turn_calls[0] = 6;
builder.agent_accumulated[0].river_bets[0] = 2;
builder.agent_accumulated[0].river_raises[0] = 1;
builder.agent_accumulated[0].river_calls[0] = 4;
let mut stats = StatsStorage::new_with_num_players(1);
stats.flop_bets[0] = 2;
stats.flop_raises[0] = 1;
stats.flop_calls[0] = 3;
stats.turn_bets[0] = 2;
stats.turn_raises[0] = 2;
stats.turn_calls[0] = 4;
stats.river_bets[0] = 1;
stats.river_raises[0] = 1;
stats.river_calls[0] = 2;
builder.merge_permutation_stats(&[0], &stats);
assert_eq!(
builder.agent_accumulated[0].flop_bets[0], 5,
"flop_bets should be 3 + 2 = 5"
);
assert_eq!(
builder.agent_accumulated[0].flop_raises[0], 3,
"flop_raises should be 2 + 1 = 3"
);
assert_eq!(
builder.agent_accumulated[0].flop_calls[0], 8,
"flop_calls should be 5 + 3 = 8"
);
assert_eq!(
builder.agent_accumulated[0].turn_bets[0], 6,
"turn_bets should be 4 + 2 = 6"
);
assert_eq!(
builder.agent_accumulated[0].turn_raises[0], 5,
"turn_raises should be 3 + 2 = 5"
);
assert_eq!(
builder.agent_accumulated[0].turn_calls[0], 10,
"turn_calls should be 6 + 4 = 10"
);
assert_eq!(
builder.agent_accumulated[0].river_bets[0], 3,
"river_bets should be 2 + 1 = 3"
);
assert_eq!(
builder.agent_accumulated[0].river_raises[0], 2,
"river_raises should be 1 + 1 = 2"
);
assert_eq!(
builder.agent_accumulated[0].river_calls[0], 6,
"river_calls should be 4 + 2 = 6"
);
}
#[test]
fn test_merge_permutation_stats_arithmetic_steal_stats() {
let names = vec!["Agent0".to_string()];
let mut builder = AgentStatsBuilder::new(names);
builder.agent_accumulated[0].steal_opportunities[0] = 10;
builder.agent_accumulated[0].steal_count[0] = 6;
let mut stats = StatsStorage::new_with_num_players(1);
stats.steal_opportunities[0] = 5;
stats.steal_count[0] = 3;
builder.merge_permutation_stats(&[0], &stats);
assert_eq!(
builder.agent_accumulated[0].steal_opportunities[0], 15,
"steal_opportunities should be 10 + 5 = 15"
);
assert_eq!(
builder.agent_accumulated[0].steal_count[0], 9,
"steal_count should be 6 + 3 = 9"
);
}
#[test]
fn test_build_total_games_addition() {
let names = vec!["Agent0".to_string()];
let mut builder = AgentStatsBuilder::new(names);
let mut stats = StatsStorage::new_with_num_players(1);
stats.games_won[0] = 5;
stats.games_lost[0] = 3;
stats.games_breakeven[0] = 2;
builder.merge_permutation_stats(&[0], &stats);
let result = builder.build();
let agent0 = result.get("Agent0").unwrap();
assert_eq!(
agent0.total_games, 10,
"total_games should be 5 + 3 + 2 = 10"
);
}
#[test]
fn test_build_profit_per_100_hands_multiplication() {
let names = vec!["Agent0".to_string()];
let mut builder = AgentStatsBuilder::new(names);
let mut stats = StatsStorage::new_with_num_players(1);
stats.total_profit[0] = 50.0;
stats.games_won[0] = 3;
stats.games_lost[0] = 2;
stats.games_breakeven[0] = 0;
builder.merge_permutation_stats(&[0], &stats);
let result = builder.build();
let agent0 = result.get("Agent0").unwrap();
assert!(
(agent0.profit_per_100_hands - 1000.0).abs() < 0.01,
"profit_per_100_hands should be 10.0 * 100 = 1000.0, got {}",
agent0.profit_per_100_hands
);
}
#[test]
fn test_position_stats_profit_per_game_division() {
let names = vec!["Agent0".to_string()];
let mut builder = AgentStatsBuilder::new(names);
let mut stats1 = StatsStorage::new_with_num_players(1);
stats1.total_profit[0] = 30.0;
stats1.games_won[0] = 1;
builder.merge_permutation_stats(&[0], &stats1);
let mut stats2 = StatsStorage::new_with_num_players(1);
stats2.total_profit[0] = 20.0;
stats2.games_won[0] = 1;
builder.merge_permutation_stats(&[0], &stats2);
let result = builder.build();
let agent0 = result.get("Agent0").unwrap();
let seat0 = agent0
.position_stats
.iter()
.find(|p| p.seat_index == 0)
.unwrap();
assert_eq!(seat0.games_played, 2);
assert!((seat0.profit - 50.0).abs() < 0.01);
assert!(
(seat0.profit_per_game - 25.0).abs() < 0.01,
"profit_per_game should be 50.0 / 2 = 25.0, got {}",
seat0.profit_per_game
);
}
#[test]
fn test_position_stats_games_greater_than_zero_check() {
let names = vec!["Agent0".to_string()];
let builder = AgentStatsBuilder::new(names);
let result = builder.build();
let agent0 = result.get("Agent0").unwrap();
assert!(agent0.position_stats.is_empty());
}
}