use rand::{RngExt, rngs::SmallRng, seq::SliceRandom};
pub fn simulate_icm_tournament(chip_stacks: &[i32], payments: &[i32]) -> Vec<i32> {
let mut remaining_stacks: Vec<i32> = chip_stacks.into();
let mut rng: SmallRng = rand::make_rng();
let mut next_place = remaining_stacks.len() - 1;
let mut winnings = vec![0; remaining_stacks.len()];
let mut remaining_players: Vec<usize> = (0..chip_stacks.len()).collect();
while !remaining_players.is_empty() {
remaining_players.shuffle(&mut rng);
let hero = remaining_players.pop().expect("There should always be one");
if let Some(villan) = remaining_players.pop() {
let hero_won: bool = rng.random_bool(0.5);
let effective_stacks = remaining_stacks[hero].min(remaining_stacks[villan]);
let hero_change: i32 = if hero_won {
effective_stacks
} else {
-effective_stacks
};
remaining_stacks[hero] += hero_change;
remaining_stacks[villan] -= hero_change;
if remaining_stacks[hero] == 0 {
if next_place < payments.len() {
winnings[hero] = payments[next_place];
}
next_place -= 1;
} else {
remaining_players.push(hero);
}
if remaining_stacks[villan] == 0 {
if next_place < payments.len() {
winnings[villan] = payments[next_place];
}
next_place -= 1;
} else {
remaining_players.push(villan);
}
} else {
winnings[hero] = payments[next_place];
};
}
winnings
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_num_players_works() {
let payments = vec![10_000, 6_000, 4_000, 1_000, 800];
let mut rng: SmallRng = rand::make_rng();
for num_players in &[2, 3, 4, 5, 15, 16, 32] {
let chips: Vec<i32> = (0..*num_players)
.map(|_pn| rng.random_range(1..500))
.collect();
let _res = simulate_icm_tournament(&chips, &payments);
}
}
#[test]
fn test_huge_lead_wins() {
let stacks = vec![1000, 2, 1];
let payments = vec![100, 30, 10];
let mut total_winnings = vec![0; 3];
let num_trials = 1000;
for _i in 0..num_trials {
let single_wins = simulate_icm_tournament(&stacks, &payments);
total_winnings = total_winnings
.iter()
.zip(single_wins.iter())
.map(|(a, b)| a + b)
.collect()
}
let final_share: Vec<f64> = total_winnings
.iter()
.map(|v| f64::from(*v) / f64::from(num_trials))
.collect();
assert!(
final_share[0] > final_share[1],
"The total winnings of a player with most of the chips should be above the rest."
);
}
#[test]
fn about_same() {
let stacks = vec![1000, 1000, 999];
let payments = vec![100, 30, 10];
let mut total_winnings = vec![0; 3];
let num_trials = 1000;
for _i in 0..num_trials {
let single_wins = simulate_icm_tournament(&stacks, &payments);
total_winnings = total_winnings
.iter()
.zip(single_wins.iter())
.map(|(a, b)| a + b)
.collect();
}
let final_share: Vec<f64> = total_winnings
.iter()
.map(|v| f64::from(*v) / f64::from(num_trials))
.collect();
let sum: f64 = final_share.iter().sum();
let avg = sum / (final_share.len() as f64);
for &share in final_share.iter() {
assert!(share < 1.1 * avg);
assert!(1.1 * share > avg);
}
}
#[test]
fn test_total_winnings_consistent() {
let payments = vec![100, 30, 10];
let stacks = vec![100, 100, 100]; let total_pool: i32 = payments.iter().sum();
for _ in 0..100 {
let winnings = simulate_icm_tournament(&stacks, &payments);
let total_winnings: i32 = winnings.iter().sum();
assert_eq!(
total_winnings, total_pool,
"Total winnings should equal total payment pool when players == payments"
);
}
let stacks_2 = vec![100, 100]; let expected_pool_2: i32 = payments[0] + payments[1];
for _ in 0..100 {
let winnings = simulate_icm_tournament(&stacks_2, &payments);
let total_winnings: i32 = winnings.iter().sum();
assert_eq!(
total_winnings, expected_pool_2,
"With 2 players, should get 1st and 2nd place payouts"
);
}
}
#[test]
fn test_all_players_get_payout_when_enough_payments() {
let stacks = vec![100, 100, 100];
let payments = vec![100, 50, 25];
for _ in 0..100 {
let winnings = simulate_icm_tournament(&stacks, &payments);
let mut received_payouts: Vec<i32> = winnings.to_vec();
received_payouts.sort();
let mut expected_payouts = payments.clone();
expected_payouts.sort();
assert_eq!(
received_payouts, expected_payouts,
"Each payment should go to exactly one player"
);
}
}
#[test]
fn test_more_players_than_payments() {
let stacks = vec![100, 100, 100, 100, 100];
let payments = vec![100, 50];
for _ in 0..100 {
let winnings = simulate_icm_tournament(&stacks, &payments);
let paid_count = winnings.iter().filter(|&&w| w > 0).count();
assert_eq!(
paid_count, 2,
"Only 2 players should be paid when payments has 2 entries"
);
let total: i32 = winnings.iter().sum();
assert_eq!(total, 150, "Total should match payment pool");
}
}
#[test]
fn test_two_player_winner_takes_all() {
let stacks = vec![100, 50];
let payments = vec![100, 0];
for _ in 0..100 {
let winnings = simulate_icm_tournament(&stacks, &payments);
assert!(
(winnings[0] == 100 && winnings[1] == 0)
|| (winnings[0] == 0 && winnings[1] == 100),
"One player should win all, got: {:?}",
winnings
);
}
}
#[test]
fn test_dominant_chip_leader_wins_first() {
let stacks = vec![10000, 1, 1];
let payments = vec![100, 0, 0];
let mut player0_first_count = 0;
let trials = 1000;
for _ in 0..trials {
let winnings = simulate_icm_tournament(&stacks, &payments);
if winnings[0] == 100 {
player0_first_count += 1;
}
}
assert!(
player0_first_count > 950,
"Chip leader should win almost always, but only won {}/{}",
player0_first_count,
trials
);
}
}