From 6e2787d792de64c003eff03b62a2df798ef851c0 Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 29 Oct 2026 20:43:09 +0100 Subject: v2.3.0: add crash with local multiplier and pushed crash event --- tests/crash.rs | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/crash.rs (limited to 'tests/crash.rs') diff --git a/tests/crash.rs b/tests/crash.rs new file mode 100644 index 0000000..f59c52f --- /dev/null +++ b/tests/crash.rs @@ -0,0 +1,100 @@ +//! Crash math: the curve, the payout floor, and the house edge. + +use std::time::Duration; +use zagan_bet::crash::{ + crash_point_from_uniform, multiplier_at, multiplier_at_secs, payout, time_for_multiplier, + HOUSE_EDGE, MAX_MULTIPLIER, +}; + +fn close(a: f64, b: f64, tolerance: f64) { + assert!( + (a - b).abs() <= tolerance, + "expected {b} +/- {tolerance}, got {a}" + ); +} + +#[test] +fn the_multiplier_doubles_every_five_seconds() { + close(multiplier_at(Duration::ZERO), 1.0, 1e-9); + close(multiplier_at(Duration::from_secs(5)), 2.0, 1e-9); + close(multiplier_at(Duration::from_secs(10)), 4.0, 1e-6); + close(multiplier_at(Duration::from_millis(2500)), 1.414_213_562, 1e-6); + + // Never below 1x, even for nonsense input. + assert_eq!(multiplier_at_secs(-3.0), 1.0); + assert_eq!(multiplier_at_secs(f64::NAN), 1.0); + assert_eq!(multiplier_at_secs(10_000.0), MAX_MULTIPLIER); +} + +#[test] +fn the_time_for_a_multiplier_is_the_inverse_of_the_curve() { + close(time_for_multiplier(2.0).as_secs_f64(), 5.0, 1e-9); + close(time_for_multiplier(10.0).as_secs_f64(), 16.609_640, 1e-4); + close(multiplier_at(time_for_multiplier(3.5)), 3.5, 1e-9); +} + +#[test] +fn cashout_value_is_floored_to_whole_pln() { + assert_eq!(payout(10, 1.0), 10); + assert_eq!(payout(10, 1.05), 10, "half a PLN is not paid out"); + assert_eq!(payout(10, 1.10), 11); + assert_eq!(payout(10, 2.47), 24); + assert_eq!(payout(7, 3.0), 21); + + assert_eq!(payout(0, 2.0), 0); + assert_eq!(payout(10, f64::NAN), 0); + assert_eq!(payout(10, f64::INFINITY), 0); + assert_eq!( + payout(u64::MAX, MAX_MULTIPLIER), + u64::MAX, + "a huge win saturates instead of wrapping" + ); +} + +#[test] +fn crash_points_follow_the_house_edge() { + // P(crash >= m) should be (1 - edge) / m. + let samples = 100_000; + for target in [1.5_f64, 2.0, 5.0, 50.0] { + let survivors = (0..samples) + .filter(|index| { + let u = *index as f64 / samples as f64; + crash_point_from_uniform(u) >= target + }) + .count(); + let observed = survivors as f64 / samples as f64; + let expected = (1.0 - HOUSE_EDGE) / target; + close(observed, expected, 0.002); + } +} + +#[test] +fn an_instant_cashout_still_pays_the_house_edge() { + // One round in a hundred crashes at 1.00x, so cashing out instantly returns + // the stake 99% of the time and nothing 1% of the time: no free money. + let samples = 100_000; + let instant_crashes = (0..samples) + .filter(|index| { + let u = *index as f64 / samples as f64; + crash_point_from_uniform(u) <= 1.0 + }) + .count(); + close(instant_crashes as f64 / samples as f64, HOUSE_EDGE, 0.001); + + let stake = 10; + let kept = payout(stake, 1.0); + assert_eq!(kept, stake); + let ev = kept as f64 * (1.0 - HOUSE_EDGE); + close(ev, stake as f64 * (1.0 - HOUSE_EDGE), 1e-9); +} + +#[test] +fn crash_points_are_clamped_to_the_liability_cap() { + assert_eq!(crash_point_from_uniform(0.0), 1.0); + assert_eq!(crash_point_from_uniform(-1.0), 1.0); + assert_eq!(crash_point_from_uniform(f64::NAN), 1.0); + close(crash_point_from_uniform(0.5), 1.98, 1e-9); + close(crash_point_from_uniform(0.99), 99.0, 1e-9); + assert_eq!(crash_point_from_uniform(0.999_999), MAX_MULTIPLIER); + assert_eq!(crash_point_from_uniform(1.0), MAX_MULTIPLIER); +} -- cgit v1.2.3