summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-28 23:45:43 +0100
committersalmonik <salmonik@tuta.io>2026-10-28 23:45:43 +0100
commit9951e2d5afbcabb4bd8016b6d160653c1ec5ed8e (patch)
treee1179b0523356dfb5d43f3d6d4614203f6fa8dec /tests
parentv1.2.1: track wrapped board lines and split the paytable (diff)
downloadzagan-bet-9951e2d5afbcabb4bd8016b6d160653c1ec5ed8e.tar.gz
zagan-bet-9951e2d5afbcabb4bd8016b6d160653c1ec5ed8e.zip
v1.3.0: simplify payouts to bet x symbol x shape
Diffstat (limited to '')
-rw-r--r--tests/client_core.rs23
-rw-r--r--tests/server_core.rs4
-rw-r--r--tests/slots.rs40
3 files changed, 41 insertions, 26 deletions
diff --git a/tests/client_core.rs b/tests/client_core.rs
index e75a611..fd88566 100644
--- a/tests/client_core.rs
+++ b/tests/client_core.rs
@@ -135,8 +135,9 @@ fn play_shows_a_ten_line_board_and_fetches_the_balance() {
135 assert_eq!(h.board().len(), 10); 135 assert_eq!(h.board().len(), 10);
136 assert!(h.board()[1].contains("Bet: 50 PLN")); 136 assert!(h.board()[1].contains("Bet: 50 PLN"));
137 assert!(!h.board()[1].contains("Lines:")); 137 assert!(!h.board()[1].contains("Lines:"));
138 assert!(h.board()[5].contains("Pays 3/4/5 x bet:")); 138 assert!(h.board()[5].contains("Pays bet x symbol x shape"));
139 assert!(h.board()[6].contains("G 4/12/40 D 6/18/70 S 9/30/150")); 139 assert!(h.board()[6].contains("Symbol C1 L2 O3 G4 D6 S10"));
140 assert!(h.board()[6].contains("Shape 3:1 4:3 5:14"));
140 assert!(h.sent.contains(&"INFO".to_string())); 141 assert!(h.sent.contains(&"INFO".to_string()));
141 142
142 h.reply("INFO alice 1 1000"); 143 h.reply("INFO alice 1 1000");
@@ -157,15 +158,15 @@ fn spin_replaces_the_board_and_keeps_the_controls_line() {
157 assert!(h.sent.iter().any(|line| line == "PLAY_START slots 50")); 158 assert!(h.sent.iter().any(|line| line == "PLAY_START slots 50"));
158 assert_eq!(h.screen.lines.len(), lines_before_spin); 159 assert_eq!(h.screen.lines.len(), lines_before_spin);
159 160
160 h.reply("GAME_RESULT session-1 100 50 1050 CCCCLLCLODGSDGS win 1 0,3,6"); 161 h.reply("GAME_RESULT session-1 50 50 1000 CCCCLLCLODGSDGS win 1 0,3,6");
161 assert_eq!(h.board().len(), 10); 162 assert_eq!(h.board().len(), 10);
162 assert!(h 163 assert!(h
163 .board() 164 .board()
164 .get(2) 165 .get(2)
165 .is_some_and(|line| line.contains("{ C } { C } { C } [ D ] [ D ]"))); 166 .is_some_and(|line| line.contains("{ C } { C } { C } [ D ] [ D ]")));
166 assert!(h.board()[7].contains("Won 100 PLN")); 167 assert!(h.board()[7].contains("Won 50 PLN"));
167 assert!(h.board()[8].contains("Winning lines: 1")); 168 assert!(h.board()[8].contains("Winning lines: 1 (top)"));
168 assert!(h.board()[1].contains("Balance: 1050 PLN")); 169 assert!(h.board()[1].contains("Balance: 1000 PLN"));
169 assert!(h.board()[9].contains("[Q] Quit")); 170 assert!(h.board()[9].contains("[Q] Quit"));
170 assert_eq!(h.screen.lines.len(), lines_before_spin); 171 assert_eq!(h.screen.lines.len(), lines_before_spin);
171} 172}
@@ -175,7 +176,7 @@ fn changing_the_bet_after_a_spin_does_not_leave_stale_lines() {
175 let mut h = Harness::new(); 176 let mut h = Harness::new();
176 h.enter_slots("50"); 177 h.enter_slots("50");
177 h.key(Key::Char(' ')); 178 h.key(Key::Char(' '));
178 h.reply("GAME_RESULT session-1 100 50 1050 CCCCLLCLODGSDGS win 1 0,3,6"); 179 h.reply("GAME_RESULT session-1 50 50 1000 CCCCLLCLODGSDGS win 1 0,3,6");
179 let lines_before_bet = h.screen.lines.len(); 180 let lines_before_bet = h.screen.lines.len();
180 181
181 h.key(Key::Char('+')); 182 h.key(Key::Char('+'));
@@ -217,11 +218,11 @@ fn a_mega_win_shows_all_winning_lines() {
217 h.enter_slots("10"); 218 h.enter_slots("10");
218 h.key(Key::Char(' ')); 219 h.key(Key::Char(' '));
219 220
220 h.reply(&format!("GAME_RESULT session-1 750 10 1740 {ALL_C} mega 1,2,3,4,5 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14")); 221 h.reply(&format!("GAME_RESULT session-1 700 10 1690 {ALL_C} mega 1,2,3,4,5 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14"));
221 222
222 assert_eq!(h.board().len(), 10); 223 assert_eq!(h.board().len(), 10);
223 assert!(h.board()[7].contains("MEGA WIN! 750 PLN")); 224 assert!(h.board()[7].contains("MEGA WIN! 700 PLN"));
224 assert!(h.board()[8].contains("Winning lines: 1,2,3,4,5")); 225 assert!(h.board()[8].contains("Winning lines: 1 (top), 2 (middle), 3 (bottom), 4 (V), 5 (inverted V)"));
225 assert_eq!(h.board()[2].matches("{ C }").count(), 5); 226 assert_eq!(h.board()[2].matches("{ C }").count(), 5);
226} 227}
227 228
@@ -521,7 +522,7 @@ fn disconnect_clears_connection_state() {
521 522
522#[test] 523#[test]
523fn parses_a_five_by_three_result_with_winning_lines() { 524fn parses_a_five_by_three_result_with_winning_lines() {
524 let response = format!("GAME_RESULT session-1 100 50 1050 {WIN3} win 1 0,3,6"); 525 let response = format!("GAME_RESULT session-1 50 50 1000 {WIN3} win 1 0,3,6");
525 let result = parse_spin_response(&response).expect("valid 5x3 result"); 526 let result = parse_spin_response(&response).expect("valid 5x3 result");
526 527
527 assert_eq!(result.reels.chars().count(), 15); 528 assert_eq!(result.reels.chars().count(), 15);
diff --git a/tests/server_core.rs b/tests/server_core.rs
index 5c3c316..3f5dc9a 100644
--- a/tests/server_core.rs
+++ b/tests/server_core.rs
@@ -188,10 +188,10 @@ fn a_winning_spin_updates_the_balance_and_the_audit_row() {
188 188
189 let response = core.handle_message(&mut state, "PLAY_START slots 10"); 189 let response = core.handle_message(&mut state, "PLAY_START slots 10");
190 assert!(response.starts_with("GAME_RESULT"), "spin failed: {response}"); 190 assert!(response.starts_with("GAME_RESULT"), "spin failed: {response}");
191 assert!(response.contains(" 20 10 1010 "), "response: {response}"); 191 assert!(response.contains(" 10 10 1000 "), "response: {response}");
192 assert!(response.ends_with(" win 1 0,3,6"), "response: {response}"); 192 assert!(response.ends_with(" win 1 0,3,6"), "response: {response}");
193 193
194 assert_eq!(core.balance("alice"), Some(1010)); 194 assert_eq!(core.balance("alice"), Some(1000));
195 assert_eq!(core.play_count("alice"), 1); 195 assert_eq!(core.play_count("alice"), 1);
196 assert_eq!(core.session_count(), 0); 196 assert_eq!(core.session_count(), 0);
197} 197}
diff --git a/tests/slots.rs b/tests/slots.rs
index bdf8f10..8bdd9af 100644
--- a/tests/slots.rs
+++ b/tests/slots.rs
@@ -1,6 +1,6 @@
1//! Regression tests for the server-side 5x3 slots game. 1//! Regression tests for the server-side 5x3 slots game.
2 2
3use zagan_bet::slots::{paytable_lines, PAYTABLE}; 3use zagan_bet::slots::{paytable_lines, LINE_NAMES, SHAPE_MULTIPLIERS, SYMBOL_MULTIPLIERS};
4use zagan_bet::server::games::slots::{LineWin, SlotsGame, PAYLINES, REELS, ROWS}; 4use zagan_bet::server::games::slots::{LineWin, SlotsGame, PAYLINES, REELS, ROWS};
5use zagan_bet::server::games::traits::{Game, GamePhase}; 5use zagan_bet::server::games::traits::{Game, GamePhase};
6 6
@@ -44,7 +44,7 @@ fn the_wire_grid_is_five_reels_by_three_rows() {
44fn three_of_a_kind_on_the_top_line_pays() { 44fn three_of_a_kind_on_the_top_line_pays() {
45 let (winnings, wins) = payout(["CCCDD", "CLLGG", "CLOSS"], 10); 45 let (winnings, wins) = payout(["CCCDD", "CLLGG", "CLOSS"], 10);
46 46
47 assert_eq!(winnings, 20, "CCC on line 1 pays 2x total bet"); 47 assert_eq!(winnings, 10, "CCC: 10 * C(1) * shape3(1)");
48 assert_eq!(lines(&wins), vec![1]); 48 assert_eq!(lines(&wins), vec![1]);
49 assert_eq!(wins[0].cells, vec![0, 3, 6]); 49 assert_eq!(wins[0].cells, vec![0, 3, 6]);
50} 50}
@@ -53,7 +53,7 @@ fn three_of_a_kind_on_the_top_line_pays() {
53fn four_of_a_kind_pays_more_than_three() { 53fn four_of_a_kind_pays_more_than_three() {
54 let (winnings, wins) = payout(["CCCCD", "LOGDS", "OGDSL"], 10); 54 let (winnings, wins) = payout(["CCCCD", "LOGDS", "OGDSL"], 10);
55 55
56 assert_eq!(winnings, 50, "CCCC pays 5x total bet"); 56 assert_eq!(winnings, 30, "CCCC: 10 * C(1) * shape4(3)");
57 assert_eq!(lines(&wins), vec![1]); 57 assert_eq!(lines(&wins), vec![1]);
58 assert_eq!(wins[0].cells, vec![0, 3, 6, 9]); 58 assert_eq!(wins[0].cells, vec![0, 3, 6, 9]);
59} 59}
@@ -62,7 +62,7 @@ fn four_of_a_kind_pays_more_than_three() {
62fn five_of_a_kind_pays_all_lines() { 62fn five_of_a_kind_pays_all_lines() {
63 let (winnings, wins) = payout(["SSSSS", "SSSSS", "SSSSS"], 10); 63 let (winnings, wins) = payout(["SSSSS", "SSSSS", "SSSSS"], 10);
64 64
65 assert_eq!(winnings, 7500, "five SSSSS lines at 150x each"); 65 assert_eq!(winnings, 7000, "five SSSSS lines: 5 * 10 * S(10) * shape5(14)");
66 assert_eq!(lines(&wins), vec![1, 2, 3, 4, 5]); 66 assert_eq!(lines(&wins), vec![1, 2, 3, 4, 5]);
67 assert_eq!(wins[0].cells, vec![0, 3, 6, 9, 12]); 67 assert_eq!(wins[0].cells, vec![0, 3, 6, 9, 12]);
68 assert_eq!(wins[3].cells, vec![0, 4, 8, 10, 12]); 68 assert_eq!(wins[3].cells, vec![0, 4, 8, 10, 12]);
@@ -73,7 +73,7 @@ fn five_of_a_kind_pays_all_lines() {
73fn the_v_payline_is_evaluated() { 73fn the_v_payline_is_evaluated() {
74 let (winnings, wins) = payout(["COGDC", "LCLCG", "OGCSD"], 10); 74 let (winnings, wins) = payout(["COGDC", "LCLCG", "OGCSD"], 10);
75 75
76 assert_eq!(winnings, 150, "V line with five C symbols pays 15x"); 76 assert_eq!(winnings, 140, "V line: 10 * C(1) * shape5(14)");
77 assert_eq!(lines(&wins), vec![4]); 77 assert_eq!(lines(&wins), vec![4]);
78 assert_eq!(wins[0].cells, vec![0, 4, 8, 10, 12]); 78 assert_eq!(wins[0].cells, vec![0, 4, 8, 10, 12]);
79} 79}
@@ -90,7 +90,7 @@ fn no_matching_line_pays_nothing() {
90fn winning_lines_are_added_together() { 90fn winning_lines_are_added_together() {
91 let (winnings, wins) = payout(["SSSSS", "LLLLL", "OOOOO"], 10); 91 let (winnings, wins) = payout(["SSSSS", "LLLLL", "OOOOO"], 10);
92 92
93 assert_eq!(winnings, (150 + 20 + 25) * 10); 93 assert_eq!(winnings, (10 + 2 + 3) * 14 * 10);
94 assert_eq!(lines(&wins), vec![1, 2, 3]); 94 assert_eq!(lines(&wins), vec![1, 2, 3]);
95 95
96 let mut cells: Vec<usize> = wins.iter().flat_map(|win| win.cells.clone()).collect(); 96 let mut cells: Vec<usize> = wins.iter().flat_map(|win| win.cells.clone()).collect();
@@ -102,21 +102,35 @@ fn winning_lines_are_added_together() {
102fn theoretical_rtp_is_just_under_one() { 102fn theoretical_rtp_is_just_under_one() {
103 let rtp = SlotsGame::new().theoretical_rtp(); 103 let rtp = SlotsGame::new().theoretical_rtp();
104 104
105 assert!((rtp - 425.0 / 432.0).abs() < 1e-12); 105 assert!((rtp - 3835.0 / 3888.0).abs() < 1e-12);
106 assert!(rtp < 1.0, "house edge must be positive"); 106 assert!(rtp < 1.0, "house edge must be positive");
107 assert!(rtp > 0.98, "house edge should be tiny"); 107 assert!(rtp > 0.98, "house edge should be tiny");
108} 108}
109 109
110#[test] 110#[test]
111fn the_paytable_lines_list_every_symbol_and_fit_80_columns() { 111fn payline_names_match_the_paylines() {
112 assert_eq!(LINE_NAMES.len(), PAYLINES.len());
113 assert_eq!(LINE_NAMES[3], "V");
114 assert_eq!(LINE_NAMES[4], "inverted V");
115}
116
117#[test]
118fn the_paytable_lines_show_the_product_formula_and_fit_80_columns() {
112 let lines = paytable_lines(); 119 let lines = paytable_lines();
113 let joined = lines.join(" "); 120 let joined = lines.join(" ");
114 121
115 assert_eq!(lines.len(), 2, "paytable should be split into two lines"); 122 assert_eq!(lines.len(), 2, "paytable should be two lines");
116 for (symbol, three, four, five) in PAYTABLE { 123 assert!(joined.contains("Pays bet x symbol x shape"));
124 for (symbol, multiplier) in SYMBOL_MULTIPLIERS {
125 assert!(
126 joined.contains(&format!("{symbol}{multiplier}")),
127 "paytable is missing symbol {symbol}"
128 );
129 }
130 for (length, multiplier) in SHAPE_MULTIPLIERS {
117 assert!( 131 assert!(
118 joined.contains(&format!("{symbol} {three}/{four}/{five}")), 132 joined.contains(&format!("{length}:{multiplier}")),
119 "paytable is missing {symbol}" 133 "paytable is missing shape {length}"
120 ); 134 );
121 } 135 }
122 for line in lines { 136 for line in lines {
@@ -140,7 +154,7 @@ fn a_spin_produces_a_finished_state_and_a_result_with_win_lines() {
140 assert_eq!(state.phase, GamePhase::Finished); 154 assert_eq!(state.phase, GamePhase::Finished);
141 155
142 let result = game.calculate_result(&state).unwrap(); 156 let result = game.calculate_result(&state).unwrap();
143 assert_eq!(result.winnings, 20); 157 assert_eq!(result.winnings, 10);
144 assert_eq!(result.result_data.get("win_type").unwrap(), "win"); 158 assert_eq!(result.result_data.get("win_type").unwrap(), "win");
145 assert_eq!(result.result_data.get("win_lines").unwrap(), "1"); 159 assert_eq!(result.result_data.get("win_lines").unwrap(), "1");
146 assert_eq!(result.result_data.get("win_cells").unwrap(), "0,3,6"); 160 assert_eq!(result.result_data.get("win_cells").unwrap(), "0,3,6");