diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-28 23:45:43 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-28 23:45:43 +0100 |
| commit | 9951e2d5afbcabb4bd8016b6d160653c1ec5ed8e (patch) | |
| tree | e1179b0523356dfb5d43f3d6d4614203f6fa8dec /tests | |
| parent | v1.2.1: track wrapped board lines and split the paytable (diff) | |
| download | zagan-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.rs | 23 | ||||
| -rw-r--r-- | tests/server_core.rs | 4 | ||||
| -rw-r--r-- | tests/slots.rs | 40 |
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] |
| 523 | fn parses_a_five_by_three_result_with_winning_lines() { | 524 | fn 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 | ||
| 3 | use zagan_bet::slots::{paytable_lines, PAYTABLE}; | 3 | use zagan_bet::slots::{paytable_lines, LINE_NAMES, SHAPE_MULTIPLIERS, SYMBOL_MULTIPLIERS}; |
| 4 | use zagan_bet::server::games::slots::{LineWin, SlotsGame, PAYLINES, REELS, ROWS}; | 4 | use zagan_bet::server::games::slots::{LineWin, SlotsGame, PAYLINES, REELS, ROWS}; |
| 5 | use zagan_bet::server::games::traits::{Game, GamePhase}; | 5 | use zagan_bet::server::games::traits::{Game, GamePhase}; |
| 6 | 6 | ||
| @@ -44,7 +44,7 @@ fn the_wire_grid_is_five_reels_by_three_rows() { | |||
| 44 | fn three_of_a_kind_on_the_top_line_pays() { | 44 | fn 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() { | |||
| 53 | fn four_of_a_kind_pays_more_than_three() { | 53 | fn 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() { | |||
| 62 | fn five_of_a_kind_pays_all_lines() { | 62 | fn 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() { | |||
| 73 | fn the_v_payline_is_evaluated() { | 73 | fn 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() { | |||
| 90 | fn winning_lines_are_added_together() { | 90 | fn 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() { | |||
| 102 | fn theoretical_rtp_is_just_under_one() { | 102 | fn 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] |
| 111 | fn the_paytable_lines_list_every_symbol_and_fit_80_columns() { | 111 | fn 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] | ||
| 118 | fn 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"); |
