summaryrefslogtreecommitdiffstats
path: root/tests/slots.rs
blob: 5fb751acb6882ae35804d9c8bc35deb99f2a4baa (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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//! Regression tests for the server-side 5x3 slots game.

use zagan_bet::server::games::slots::{SlotsGame, PAYLINES, REELS, ROWS};
use zagan_bet::server::games::traits::{Game, GamePhase};

/// Convert row-major test rows into the column-major wire format.
fn grid(rows: [&str; 3]) -> String {
    let cells: Vec<Vec<char>> = rows.iter().map(|row| row.chars().collect()).collect();
    let mut result = String::new();
    for col in 0..REELS {
        for row in 0..ROWS {
            result.push(cells[row][col]);
        }
    }
    result
}

fn payout(rows: [&str; 3], bet: u64) -> (u64, Vec<usize>) {
    let reels = grid(rows);
    let game = SlotsGame::with_grid(&reels);
    let cells: Vec<char> = reels.chars().collect();
    game.calculate_payout(&cells, bet)
}

#[test]
fn the_wire_grid_is_five_reels_by_three_rows() {
    assert_eq!(REELS, 5);
    assert_eq!(ROWS, 3);
    assert_eq!(PAYLINES.len(), 5);

    let game = SlotsGame::new();
    let state = game.prepare_state("alice", 10);
    let state = game.process_action(&state, "spin", &[]).unwrap();
    let reels = state.data.get("reels").unwrap();
    assert_eq!(reels.chars().count(), REELS * ROWS);
}

#[test]
fn three_of_a_kind_on_the_top_line_pays() {
    let (winnings, lines) = payout(["CCCDD", "CLLGG", "CLOSS"], 10);

    assert_eq!(winnings, 20, "CCC on line 1 pays 2x total bet");
    assert_eq!(lines, vec![1]);
}

#[test]
fn four_of_a_kind_pays_more_than_three() {
    let (winnings, lines) = payout(["CCCCD", "LOGDS", "OGDSL"], 10);

    assert_eq!(winnings, 50, "CCCC pays 5x total bet");
    assert_eq!(lines, vec![1]);
}

#[test]
fn five_of_a_kind_pays_all_lines() {
    let (winnings, lines) = payout(["SSSSS", "SSSSS", "SSSSS"], 10);

    assert_eq!(winnings, 7500, "five SSSSS lines at 150x each");
    assert_eq!(lines, vec![1, 2, 3, 4, 5]);
}

#[test]
fn the_v_payline_is_evaluated() {
    let (winnings, lines) = payout(["COGDC", "LCLCG", "OGCSD"], 10);

    assert_eq!(winnings, 150, "V line with five C symbols pays 15x");
    assert_eq!(lines, vec![4]);
}

#[test]
fn no_matching_line_pays_nothing() {
    let (winnings, lines) = payout(["CLOGD", "LOSGC", "DGDSC"], 100);

    assert_eq!(winnings, 0);
    assert!(lines.is_empty());
}

#[test]
fn winning_lines_are_added_together() {
    let (winnings, lines) = payout(["SSSSS", "LLLLL", "OOOOO"], 10);

    assert_eq!(winnings, (150 + 20 + 25) * 10);
    assert_eq!(lines, vec![1, 2, 3]);
}

#[test]
fn theoretical_rtp_is_just_under_one() {
    let rtp = SlotsGame::new().theoretical_rtp();

    assert!((rtp - 425.0 / 432.0).abs() < 1e-12);
    assert!(rtp < 1.0, "house edge must be positive");
    assert!(rtp > 0.98, "house edge should be tiny");
}

#[test]
fn an_enormous_bet_saturates_instead_of_overflowing() {
    let (winnings, _) = payout(["SSSSS", "SSSSS", "SSSSS"], u64::MAX);

    assert_eq!(winnings, u64::MAX);
}

#[test]
fn a_spin_produces_a_finished_state_and_a_result_with_win_lines() {
    let game = SlotsGame::with_grid(&grid(["CCCDD", "CLLGG", "CLOSS"]));
    let state = game.prepare_state("alice", 10);

    let state = game.process_action(&state, "spin", &[]).unwrap();
    assert_eq!(state.phase, GamePhase::Finished);

    let result = game.calculate_result(&state).unwrap();
    assert_eq!(result.winnings, 20);
    assert_eq!(result.result_data.get("win_type").unwrap(), "win");
    assert_eq!(result.result_data.get("win_lines").unwrap(), "1");
}

#[test]
fn calculate_result_rejects_an_unfinished_state() {
    let game = SlotsGame::new();
    let state = game.prepare_state("alice", 10);

    assert!(game.calculate_result(&state).is_err());
}