summaryrefslogtreecommitdiffstats
path: root/tests/crash.rs
blob: f59c52f9112c265cbb77f3769e24dce408c932ed (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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);
}