use crate::{
math::{erfc, igamc},
result::TestResult,
rng::Rng,
};
use std::f64::consts::SQRT_2;
const N_GAMES: usize = 200_000;
const P_WIN: f64 = 244.0 / 495.0;
pub fn craps(rng: &mut impl Rng) -> TestResult {
let mut wins = 0usize;
let mut throw_counts = [0u32; 22];
for _ in 0..N_GAMES {
let (won, throws) = play_craps(rng);
if won {
wins += 1;
}
let idx = (throws - 1).min(21);
throw_counts[idx] += 1;
}
let mu_w = N_GAMES as f64 * P_WIN;
let sigma_w = (N_GAMES as f64 * P_WIN * (1.0 - P_WIN)).sqrt();
let z_wins = (wins as f64 - mu_w) / sigma_w;
let p_wins = erfc(z_wins.abs() / SQRT_2);
let expected = expected_throw_probs();
let chi_sq: f64 = throw_counts
.iter()
.zip(expected.iter())
.filter(|(_, &e)| e * N_GAMES as f64 >= 5.0)
.map(|(&c, &e)| {
let exp = e * N_GAMES as f64;
(c as f64 - exp).powi(2) / exp
})
.sum();
let df = throw_counts
.iter()
.zip(expected.iter())
.filter(|(_, &e)| e * N_GAMES as f64 >= 5.0)
.count()
- 1;
let p_throws = igamc(df as f64 / 2.0, chi_sq / 2.0);
let fisher = -2.0 * (p_wins.ln() + p_throws.ln());
let p_value = igamc(2.0, fisher / 2.0);
TestResult::with_note(
"diehard::craps",
p_value,
format!("games={N_GAMES}, wins={wins}, p_wins={p_wins:.4}, p_throws={p_throws:.4}"),
)
}
pub fn craps_both(rng: &mut impl Rng) -> Vec<TestResult> {
let mut wins = 0usize;
let mut throw_counts = [0u32; 22];
for _ in 0..N_GAMES {
let (won, throws) = play_craps(rng);
if won {
wins += 1;
}
let idx = (throws - 1).min(21);
throw_counts[idx] += 1;
}
let mu_w = N_GAMES as f64 * P_WIN;
let sigma_w = (N_GAMES as f64 * P_WIN * (1.0 - P_WIN)).sqrt();
let z_wins = (wins as f64 - mu_w) / sigma_w;
let p_wins = erfc(z_wins.abs() / SQRT_2);
let expected = expected_throw_probs();
let chi_sq: f64 = throw_counts
.iter()
.zip(expected.iter())
.filter(|(_, &e)| e * N_GAMES as f64 >= 5.0)
.map(|(&c, &e)| {
let exp = e * N_GAMES as f64;
(c as f64 - exp).powi(2) / exp
})
.sum();
let df = throw_counts
.iter()
.zip(expected.iter())
.filter(|(_, &e)| e * N_GAMES as f64 >= 5.0)
.count()
- 1;
let p_throws = igamc(df as f64 / 2.0, chi_sq / 2.0);
vec![
TestResult::with_note(
"diehard::craps_wins",
p_wins,
format!("games={N_GAMES}, wins={wins}, z={z_wins:.4}"),
),
TestResult::with_note(
"diehard::craps_throws",
p_throws,
format!("games={N_GAMES}, df={df}, χ²={chi_sq:.4}"),
),
]
}
fn play_craps(rng: &mut impl Rng) -> (bool, usize) {
let first = roll_dice(rng);
let mut throws = 1;
match first {
7 | 11 => (true, throws),
2 | 3 | 12 => (false, throws),
point => loop {
let r = roll_dice(rng);
throws += 1;
if r == point {
return (true, throws);
}
if r == 7 {
return (false, throws);
}
},
}
}
fn roll_dice(rng: &mut impl Rng) -> u32 {
let d1 = uniform_bounded(rng, 6) + 1;
let d2 = uniform_bounded(rng, 6) + 1;
d1 + d2
}
fn uniform_bounded(rng: &mut impl Rng, bound: u32) -> u32 {
let zone = u32::MAX - (u32::MAX % bound);
loop {
let v = rng.next_u32();
if v < zone {
return v % bound;
}
}
}
fn expected_throw_probs() -> [f64; 22] {
let mut p = [0.0f64; 22];
p[0] = 12.0 / 36.0;
let points = [
(4u32, 3.0 / 36.0),
(5, 4.0 / 36.0),
(6, 5.0 / 36.0),
(8, 5.0 / 36.0),
(9, 4.0 / 36.0),
(10, 3.0 / 36.0),
];
let p7 = 6.0 / 36.0;
for k in 2usize..=200 {
let mut prob = 0.0f64;
for &(_x, px) in &points {
let resolve = px + p7;
let stay: f64 = 1.0 - resolve;
let prob_resolve = stay.powi(k as i32 - 2) * resolve;
prob += px * prob_resolve;
}
let idx = if k <= 22 { k - 1 } else { 21 };
p[idx] += prob;
}
let sum: f64 = p.iter().sum();
p.iter_mut().for_each(|v| *v /= sum);
p
}