diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-28 23:31:54 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-28 23:31:54 +0100 |
| commit | 7d18a268547734daf19a740b6533e9c1697baf1a (patch) | |
| tree | 789f519a19b053b8ad0ff41d2828fe22c06fb21a | |
| parent | v1.2.0: show the symbol paytable on the slots board (diff) | |
| download | zagan-bet-7d18a268547734daf19a740b6533e9c1697baf1a.tar.gz zagan-bet-7d18a268547734daf19a740b6533e9c1697baf1a.zip | |
v1.2.1: track wrapped board lines and split the paytable
| -rw-r--r-- | Cargo.toml | 2 | ||||
| -rw-r--r-- | docs/SLOTS.md | 17 | ||||
| -rw-r--r-- | docs/TESTING.md | 2 | ||||
| -rw-r--r-- | src/bin/client/repl.rs | 8 | ||||
| -rw-r--r-- | src/client/games/slots.rs | 10 | ||||
| -rw-r--r-- | src/client/ui.rs | 17 | ||||
| -rw-r--r-- | src/slots.rs | 18 | ||||
| -rw-r--r-- | tests/client_core.rs | 46 | ||||
| -rw-r--r-- | tests/client_ui.rs | 19 | ||||
| -rw-r--r-- | tests/slots.rs | 14 |
10 files changed, 106 insertions, 47 deletions
| @@ -1,6 +1,6 @@ | |||
| 1 | [package] | 1 | [package] |
| 2 | name = "zagan-bet" | 2 | name = "zagan-bet" |
| 3 | version = "1.2.0" | 3 | version = "1.2.1" |
| 4 | edition = "2024" | 4 | edition = "2024" |
| 5 | 5 | ||
| 6 | [dependencies] | 6 | [dependencies] |
diff --git a/docs/SLOTS.md b/docs/SLOTS.md index a2e778d..b8c670b 100644 --- a/docs/SLOTS.md +++ b/docs/SLOTS.md | |||
| @@ -62,7 +62,7 @@ server sends exactly which cells won. | |||
| 62 | 62 | ||
| 63 | ## Client board | 63 | ## Client board |
| 64 | 64 | ||
| 65 | The board is nine lines: | 65 | The board is ten lines: |
| 66 | 66 | ||
| 67 | ``` | 67 | ``` |
| 68 | === SLOTS === | 68 | === SLOTS === |
| @@ -70,15 +70,16 @@ Bet: 10 PLN Balance: 1000 PLN | |||
| 70 | [ C ] [ L ] [ O ] [ G ] [ D ] | 70 | [ C ] [ L ] [ O ] [ G ] [ D ] |
| 71 | [ S ] { C } { C } [ O ] [ G ] | 71 | [ S ] { C } { C } [ O ] [ G ] |
| 72 | [ D ] [ S ] { C } [ L ] [ O ] | 72 | [ D ] [ S ] { C } [ L ] [ O ] |
| 73 | Pays 3/4/5 x bet: C 2/5/15 L 3/7/20 O 3/8/25 G 4/12/40 D 6/18/70 S 9/30/150 | 73 | Pays 3/4/5 x bet: C 2/5/15 L 3/7/20 O 3/8/25 |
| 74 | G 4/12/40 D 6/18/70 S 9/30/150 | ||
| 74 | Won 20 PLN | 75 | Won 20 PLN |
| 75 | Winning lines: 2 | 76 | Winning lines: 2 |
| 76 | [+/-] Bet [SPACE] Spin [Q] Quit | 77 | [+/-] Bet [SPACE] Spin [Q] Quit |
| 77 | ``` | 78 | ``` |
| 78 | 79 | ||
| 79 | The line above the status is the compact paytable: for each symbol, the | 80 | The two lines above the status are the compact paytable: for each symbol, the |
| 80 | multipliers of the total bet for 3, 4 and 5 of a kind. Winning cells use | 81 | multipliers of the total bet for 3, 4 and 5 of a kind. It is split so it does |
| 81 | braces and are rendered yellow/bold in the terminal. The line list below the | 82 | not wrap on a narrow terminal. Winning cells use braces and are rendered |
| 82 | status names the paylines that won. Both come from the server's `win_lines` | 83 | yellow/bold in the terminal. The line list below the status names the paylines |
| 83 | and `win_cells`, which are already carried through the client state for the | 84 | that won. Both come from the server's `win_lines` and `win_cells`, which are |
| 84 | animation pass. | 85 | already carried through the client state for the animation pass. |
diff --git a/docs/TESTING.md b/docs/TESTING.md index 7f54798..84d98e7 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md | |||
| @@ -39,7 +39,7 @@ Use an 80-column terminal because several output lines are width-sensitive. | |||
| 39 | 5. `play slots 50` shows a 5x3 grid, the `Pays 3/4/5 x bet:` paytable line, and | 39 | 5. `play slots 50` shows a 5x3 grid, the `Pays 3/4/5 x bet:` paytable line, and |
| 40 | the fetched balance. | 40 | the fetched balance. |
| 41 | 6. Space shows `Spinning...`, then the 3 rows, win text, winning-line list, and | 41 | 6. Space shows `Spinning...`, then the 3 rows, win text, winning-line list, and |
| 42 | new balance inside the same 9-line board. Winning symbols appear as | 42 | new balance inside the same 10-line board. Winning symbols appear as |
| 43 | yellow/bold `{ X }` cells; the `Winning lines:` list matches. The controls | 43 | yellow/bold `{ X }` cells; the `Winning lines:` list matches. The controls |
| 44 | line is never overwritten. Slots rules and RTP: `docs/SLOTS.md`. | 44 | line is never overwritten. Slots rules and RTP: `docs/SLOTS.md`. |
| 45 | 7. `+` / `-` after a spin changes only the bet; no stale grid or result lines | 45 | 7. `+` / `-` after a spin changes only the bet; no stale grid or result lines |
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs index 58cb541..1ed3073 100644 --- a/src/bin/client/repl.rs +++ b/src/bin/client/repl.rs | |||
| @@ -17,6 +17,7 @@ use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; | |||
| 17 | use zagan_bet::client::database; | 17 | use zagan_bet::client::database; |
| 18 | use zagan_bet::client::ui::{ | 18 | use zagan_bet::client::ui::{ |
| 19 | display_width, format_pong, host_lines, input_cursor_column, lines_to_move_up, | 19 | display_width, format_pong, host_lines, input_cursor_column, lines_to_move_up, |
| 20 | visual_line_count, | ||
| 20 | }; | 21 | }; |
| 21 | use zagan_bet::protocol::io_failure_message; | 22 | use zagan_bet::protocol::io_failure_message; |
| 22 | 23 | ||
| @@ -29,7 +30,7 @@ pub struct ClientState { | |||
| 29 | pub default_port: u16, | 30 | pub default_port: u16, |
| 30 | core: ClientCore, | 31 | core: ClientCore, |
| 31 | cleanup_done: Arc<Mutex<bool>>, | 32 | cleanup_done: Arc<Mutex<bool>>, |
| 32 | /// Number of terminal lines currently occupied by the game board. | 33 | /// Number of visual terminal lines currently occupied by the game board. |
| 33 | game_lines: usize, | 34 | game_lines: usize, |
| 34 | /// Text last drawn after the `> ` prompt, used to keep the command echo | 35 | /// Text last drawn after the `> ` prompt, used to keep the command echo |
| 35 | /// in the transcript before printing command output. | 36 | /// in the transcript before printing command output. |
| @@ -255,7 +256,10 @@ impl ClientState { | |||
| 255 | .collect(); | 256 | .collect(); |
| 256 | print!("{}", rendered.join("\r\n")); | 257 | print!("{}", rendered.join("\r\n")); |
| 257 | stdout().flush().unwrap(); | 258 | stdout().flush().unwrap(); |
| 258 | self.game_lines = lines.len().max(1); | 259 | let width = crossterm::terminal::size() |
| 260 | .map(|(columns, _)| columns as usize) | ||
| 261 | .unwrap_or(80); | ||
| 262 | self.game_lines = visual_line_count(lines, width).max(1); | ||
| 259 | } | 263 | } |
| 260 | 264 | ||
| 261 | fn clear_region(&mut self) { | 265 | fn clear_region(&mut self) { |
diff --git a/src/client/games/slots.rs b/src/client/games/slots.rs index 7e59919..a417e7d 100644 --- a/src/client/games/slots.rs +++ b/src/client/games/slots.rs | |||
| @@ -1,8 +1,8 @@ | |||
| 1 | use super::traits::{Game, GamePhase, GameState}; | 1 | use super::traits::{Game, GamePhase, GameState}; |
| 2 | use crate::slots::{paytable_line, REELS, ROWS}; | 2 | use crate::slots::{paytable_lines, REELS, ROWS}; |
| 3 | 3 | ||
| 4 | /// Height of the game board. It must never depend on the spin result. | 4 | /// Height of the game board. It must never depend on the spin result. |
| 5 | pub const SLOT_BOARD_LINES: usize = 9; | 5 | pub const SLOT_BOARD_LINES: usize = 10; |
| 6 | 6 | ||
| 7 | pub struct SlotsGame; | 7 | pub struct SlotsGame; |
| 8 | 8 | ||
| @@ -102,15 +102,17 @@ impl Game for SlotsGame { | |||
| 102 | .unwrap_or("?"); | 102 | .unwrap_or("?"); |
| 103 | let winning = Self::highlighted_cells(state); | 103 | let winning = Self::highlighted_cells(state); |
| 104 | let grid = self.render_grid(&reels, &winning); | 104 | let grid = self.render_grid(&reels, &winning); |
| 105 | let paytable = paytable_lines(); | ||
| 105 | 106 | ||
| 106 | format!( | 107 | format!( |
| 107 | "=== SLOTS ===\r\nBet: {} PLN Balance: {} PLN\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n[+/-] Bet [SPACE] Spin [Q] Quit", | 108 | "=== SLOTS ===\r\nBet: {} PLN Balance: {} PLN\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n{}\r\n[+/-] Bet [SPACE] Spin [Q] Quit", |
| 108 | state.bet_amount, | 109 | state.bet_amount, |
| 109 | balance, | 110 | balance, |
| 110 | grid[0], | 111 | grid[0], |
| 111 | grid[1], | 112 | grid[1], |
| 112 | grid[2], | 113 | grid[2], |
| 113 | paytable_line(), | 114 | paytable[0], |
| 115 | paytable[1], | ||
| 114 | self.render_status(state), | 116 | self.render_status(state), |
| 115 | self.render_win_lines(state) | 117 | self.render_win_lines(state) |
| 116 | ) | 118 | ) |
diff --git a/src/client/ui.rs b/src/client/ui.rs index 493f660..11cedc7 100644 --- a/src/client/ui.rs +++ b/src/client/ui.rs | |||
| @@ -257,6 +257,23 @@ pub fn input_cursor_column(prompt_len: usize, input_len: usize) -> u16 { | |||
| 257 | .min(u16::MAX as usize) as u16 | 257 | .min(u16::MAX as usize) as u16 |
| 258 | } | 258 | } |
| 259 | 259 | ||
| 260 | /// Number of terminal rows a block of logical lines occupies after wrapping. | ||
| 261 | /// Empty lines still occupy one row. | ||
| 262 | pub fn visual_line_count(lines: &[String], width: usize) -> usize { | ||
| 263 | let width = width.max(1); | ||
| 264 | lines | ||
| 265 | .iter() | ||
| 266 | .map(|line| { | ||
| 267 | let columns = display_width(line); | ||
| 268 | if columns == 0 { | ||
| 269 | 1 | ||
| 270 | } else { | ||
| 271 | columns.div_ceil(width) | ||
| 272 | } | ||
| 273 | }) | ||
| 274 | .sum() | ||
| 275 | } | ||
| 276 | |||
| 260 | /// Format a ping round-trip time with one decimal place. | 277 | /// Format a ping round-trip time with one decimal place. |
| 261 | pub fn format_pong(elapsed: std::time::Duration) -> String { | 278 | pub fn format_pong(elapsed: std::time::Duration) -> String { |
| 262 | format!("Pong ({:.1}ms)", elapsed.as_secs_f64() * 1000.0) | 279 | format!("Pong ({:.1}ms)", elapsed.as_secs_f64() * 1000.0) |
diff --git a/src/slots.rs b/src/slots.rs index 561c449..a3b4ef4 100644 --- a/src/slots.rs +++ b/src/slots.rs | |||
| @@ -23,11 +23,23 @@ pub const PAYTABLE: [(char, u64, u64, u64); 6] = [ | |||
| 23 | ('S', 9, 30, 150), | 23 | ('S', 9, 30, 150), |
| 24 | ]; | 24 | ]; |
| 25 | 25 | ||
| 26 | /// One-line paytable rendered under the reels. | 26 | /// Paytable rendered under the reels, split so it fits a narrow terminal. |
| 27 | pub fn paytable_line() -> String { | 27 | pub fn paytable_lines() -> Vec<String> { |
| 28 | let entries: Vec<String> = PAYTABLE | 28 | let entries: Vec<String> = PAYTABLE |
| 29 | .iter() | 29 | .iter() |
| 30 | .map(|(symbol, three, four, five)| format!("{symbol} {three}/{four}/{five}")) | 30 | .map(|(symbol, three, four, five)| format!("{symbol} {three}/{four}/{five}")) |
| 31 | .collect(); | 31 | .collect(); |
| 32 | format!("Pays 3/4/5 x bet: {}", entries.join(" ")) | 32 | |
| 33 | entries | ||
| 34 | .chunks(3) | ||
| 35 | .enumerate() | ||
| 36 | .map(|(index, chunk)| { | ||
| 37 | let prefix = if index == 0 { | ||
| 38 | "Pays 3/4/5 x bet: " | ||
| 39 | } else { | ||
| 40 | "" | ||
| 41 | }; | ||
| 42 | format!("{}{}", prefix, chunk.join(" ")) | ||
| 43 | }) | ||
| 44 | .collect() | ||
| 33 | } | 45 | } |
diff --git a/tests/client_core.rs b/tests/client_core.rs index ea99d52..e75a611 100644 --- a/tests/client_core.rs +++ b/tests/client_core.rs | |||
| @@ -128,19 +128,19 @@ impl Harness { | |||
| 128 | } | 128 | } |
| 129 | 129 | ||
| 130 | #[test] | 130 | #[test] |
| 131 | fn play_shows_a_nine_line_board_and_fetches_the_balance() { | 131 | fn play_shows_a_ten_line_board_and_fetches_the_balance() { |
| 132 | let mut h = Harness::new(); | 132 | let mut h = Harness::new(); |
| 133 | h.line("play slots 50"); | 133 | h.line("play slots 50"); |
| 134 | 134 | ||
| 135 | assert_eq!(h.board().len(), 9); | 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 3/4/5 x bet:")); |
| 139 | assert!(h.board()[5].contains("S 9/30/150")); | 139 | assert!(h.board()[6].contains("G 4/12/40 D 6/18/70 S 9/30/150")); |
| 140 | assert!(h.sent.contains(&"INFO".to_string())); | 140 | assert!(h.sent.contains(&"INFO".to_string())); |
| 141 | 141 | ||
| 142 | h.reply("INFO alice 1 1000"); | 142 | h.reply("INFO alice 1 1000"); |
| 143 | assert_eq!(h.board().len(), 9); | 143 | assert_eq!(h.board().len(), 10); |
| 144 | assert!(h.board()[1].contains("Balance: 1000 PLN")); | 144 | assert!(h.board()[1].contains("Balance: 1000 PLN")); |
| 145 | } | 145 | } |
| 146 | 146 | ||
| @@ -151,22 +151,22 @@ fn spin_replaces_the_board_and_keeps_the_controls_line() { | |||
| 151 | let lines_before_spin = h.screen.lines.len(); | 151 | let lines_before_spin = h.screen.lines.len(); |
| 152 | 152 | ||
| 153 | h.key(Key::Char(' ')); | 153 | h.key(Key::Char(' ')); |
| 154 | assert_eq!(h.board().len(), 9); | 154 | assert_eq!(h.board().len(), 10); |
| 155 | assert!(h.board()[6].contains("Spinning...")); | 155 | assert!(h.board()[7].contains("Spinning...")); |
| 156 | assert!(h.board()[8].contains("[Q] Quit")); | 156 | assert!(h.board()[9].contains("[Q] Quit")); |
| 157 | assert!(h.sent.iter().any(|line| line == "PLAY_START slots 50")); | 157 | assert!(h.sent.iter().any(|line| line == "PLAY_START slots 50")); |
| 158 | assert_eq!(h.screen.lines.len(), lines_before_spin); | 158 | assert_eq!(h.screen.lines.len(), lines_before_spin); |
| 159 | 159 | ||
| 160 | h.reply("GAME_RESULT session-1 100 50 1050 CCCCLLCLODGSDGS win 1 0,3,6"); | 160 | h.reply("GAME_RESULT session-1 100 50 1050 CCCCLLCLODGSDGS win 1 0,3,6"); |
| 161 | assert_eq!(h.board().len(), 9); | 161 | assert_eq!(h.board().len(), 10); |
| 162 | assert!(h | 162 | assert!(h |
| 163 | .board() | 163 | .board() |
| 164 | .get(2) | 164 | .get(2) |
| 165 | .is_some_and(|line| line.contains("{ C } { C } { C } [ D ] [ D ]"))); | 165 | .is_some_and(|line| line.contains("{ C } { C } { C } [ D ] [ D ]"))); |
| 166 | assert!(h.board()[6].contains("Won 100 PLN")); | 166 | assert!(h.board()[7].contains("Won 100 PLN")); |
| 167 | assert!(h.board()[7].contains("Winning lines: 1")); | 167 | assert!(h.board()[8].contains("Winning lines: 1")); |
| 168 | assert!(h.board()[1].contains("Balance: 1050 PLN")); | 168 | assert!(h.board()[1].contains("Balance: 1050 PLN")); |
| 169 | assert!(h.board()[8].contains("[Q] Quit")); | 169 | assert!(h.board()[9].contains("[Q] Quit")); |
| 170 | assert_eq!(h.screen.lines.len(), lines_before_spin); | 170 | assert_eq!(h.screen.lines.len(), lines_before_spin); |
| 171 | } | 171 | } |
| 172 | 172 | ||
| @@ -180,7 +180,7 @@ fn changing_the_bet_after_a_spin_does_not_leave_stale_lines() { | |||
| 180 | 180 | ||
| 181 | h.key(Key::Char('+')); | 181 | h.key(Key::Char('+')); |
| 182 | 182 | ||
| 183 | assert_eq!(h.board().len(), 9); | 183 | assert_eq!(h.board().len(), 10); |
| 184 | assert!(h.board()[1].contains("Bet: 60 PLN")); | 184 | assert!(h.board()[1].contains("Bet: 60 PLN")); |
| 185 | assert!(h | 185 | assert!(h |
| 186 | .board() | 186 | .board() |
| @@ -205,9 +205,9 @@ fn a_losing_spin_shows_no_win_and_no_winning_lines() { | |||
| 205 | 205 | ||
| 206 | h.reply(&format!("GAME_RESULT session-1 0 10 990 {LOSS} none -")); | 206 | h.reply(&format!("GAME_RESULT session-1 0 10 990 {LOSS} none -")); |
| 207 | 207 | ||
| 208 | assert_eq!(h.board().len(), 9); | 208 | assert_eq!(h.board().len(), 10); |
| 209 | assert!(h.board()[6].contains("No win.")); | 209 | assert!(h.board()[7].contains("No win.")); |
| 210 | assert_eq!(h.board()[7], ""); | 210 | assert_eq!(h.board()[8], ""); |
| 211 | assert!(!h.board()[2].contains('{')); | 211 | assert!(!h.board()[2].contains('{')); |
| 212 | } | 212 | } |
| 213 | 213 | ||
| @@ -219,9 +219,9 @@ fn a_mega_win_shows_all_winning_lines() { | |||
| 219 | 219 | ||
| 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")); | 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 | 221 | ||
| 222 | assert_eq!(h.board().len(), 9); | 222 | assert_eq!(h.board().len(), 10); |
| 223 | assert!(h.board()[6].contains("MEGA WIN! 750 PLN")); | 223 | assert!(h.board()[7].contains("MEGA WIN! 750 PLN")); |
| 224 | assert!(h.board()[7].contains("Winning lines: 1,2,3,4,5")); | 224 | assert!(h.board()[8].contains("Winning lines: 1,2,3,4,5")); |
| 225 | assert_eq!(h.board()[2].matches("{ C }").count(), 5); | 225 | assert_eq!(h.board()[2].matches("{ C }").count(), 5); |
| 226 | } | 226 | } |
| 227 | 227 | ||
| @@ -245,8 +245,8 @@ fn a_server_error_replaces_the_status_line() { | |||
| 245 | 245 | ||
| 246 | h.reply("ERROR Insufficient balance"); | 246 | h.reply("ERROR Insufficient balance"); |
| 247 | 247 | ||
| 248 | assert_eq!(h.board().len(), 9); | 248 | assert_eq!(h.board().len(), 10); |
| 249 | assert!(h.board()[6].contains("ERROR: Insufficient balance")); | 249 | assert!(h.board()[7].contains("ERROR: Insufficient balance")); |
| 250 | assert!(h.board()[2].contains("[ ? ]")); | 250 | assert!(h.board()[2].contains("[ ? ]")); |
| 251 | assert!(!h.board().iter().any(|line| line.contains("Spinning"))); | 251 | assert!(!h.board().iter().any(|line| line.contains("Spinning"))); |
| 252 | } | 252 | } |
| @@ -259,8 +259,8 @@ fn an_empty_reply_is_an_error_not_a_silent_spin() { | |||
| 259 | 259 | ||
| 260 | h.reply(""); | 260 | h.reply(""); |
| 261 | 261 | ||
| 262 | assert_eq!(h.board().len(), 9); | 262 | assert_eq!(h.board().len(), 10); |
| 263 | assert!(h.board()[6].contains("No response from server")); | 263 | assert!(h.board()[7].contains("No response from server")); |
| 264 | } | 264 | } |
| 265 | 265 | ||
| 266 | #[test] | 266 | #[test] |
| @@ -344,7 +344,7 @@ fn play_without_a_bet_uses_the_default() { | |||
| 344 | let mut h = Harness::new(); | 344 | let mut h = Harness::new(); |
| 345 | h.line("play slots"); | 345 | h.line("play slots"); |
| 346 | 346 | ||
| 347 | assert_eq!(h.board().len(), 9); | 347 | assert_eq!(h.board().len(), 10); |
| 348 | assert!(h.board()[1].contains("Bet: 10 PLN")); | 348 | assert!(h.board()[1].contains("Bet: 10 PLN")); |
| 349 | } | 349 | } |
| 350 | 350 | ||
diff --git a/tests/client_ui.rs b/tests/client_ui.rs index a22fbe7..ea432e0 100644 --- a/tests/client_ui.rs +++ b/tests/client_ui.rs | |||
| @@ -1,6 +1,7 @@ | |||
| 1 | use zagan_bet::client::ui::{ | 1 | use zagan_bet::client::ui::{ |
| 2 | command_names, completion_lines, display_width, first_contact_lines, format_pong, | 2 | command_names, completion_lines, display_width, first_contact_lines, format_pong, |
| 3 | help_lines, host_lines, input_cursor_column, lines_to_move_up, parse_bet_arg, record_history, | 3 | help_lines, host_lines, input_cursor_column, lines_to_move_up, parse_bet_arg, record_history, |
| 4 | visual_line_count, | ||
| 4 | }; | 5 | }; |
| 5 | use std::time::Duration; | 6 | use std::time::Duration; |
| 6 | 7 | ||
| @@ -98,6 +99,24 @@ fn redraw_moves_up_by_line_count_minus_one() { | |||
| 98 | } | 99 | } |
| 99 | 100 | ||
| 100 | #[test] | 101 | #[test] |
| 102 | fn visual_line_count_accounts_for_terminal_wrapping() { | ||
| 103 | let lines = vec![ | ||
| 104 | "12345".to_string(), | ||
| 105 | "123456".to_string(), | ||
| 106 | String::new(), | ||
| 107 | ]; | ||
| 108 | |||
| 109 | assert_eq!(visual_line_count(&lines, 5), 4); // 1 + 2 + 1 | ||
| 110 | assert_eq!(visual_line_count(&lines, 6), 3); // 1 + 1 + 1 | ||
| 111 | assert_eq!(visual_line_count(&lines, 80), 3); | ||
| 112 | assert_eq!(visual_line_count(&[], 80), 0); | ||
| 113 | |||
| 114 | let wide = "x".repeat(75); | ||
| 115 | assert_eq!(visual_line_count(&[wide.clone()], 75), 1); | ||
| 116 | assert_eq!(visual_line_count(&[wide], 72), 2); | ||
| 117 | } | ||
| 118 | |||
| 119 | #[test] | ||
| 101 | fn input_cursor_is_after_the_removed_character() { | 120 | fn input_cursor_is_after_the_removed_character() { |
| 102 | assert_eq!(input_cursor_column(10, 8), 18); | 121 | assert_eq!(input_cursor_column(10, 8), 18); |
| 103 | assert_eq!(input_cursor_column(10, 0), 10); | 122 | assert_eq!(input_cursor_column(10, 0), 10); |
diff --git a/tests/slots.rs b/tests/slots.rs index b7292b8..bdf8f10 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_line, PAYTABLE}; | 3 | use zagan_bet::slots::{paytable_lines, PAYTABLE}; |
| 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 | ||
| @@ -108,16 +108,20 @@ fn theoretical_rtp_is_just_under_one() { | |||
| 108 | } | 108 | } |
| 109 | 109 | ||
| 110 | #[test] | 110 | #[test] |
| 111 | fn the_paytable_line_lists_every_symbol_and_fits_80_columns() { | 111 | fn the_paytable_lines_list_every_symbol_and_fit_80_columns() { |
| 112 | let line = paytable_line(); | 112 | let lines = paytable_lines(); |
| 113 | let joined = lines.join(" "); | ||
| 113 | 114 | ||
| 115 | assert_eq!(lines.len(), 2, "paytable should be split into two lines"); | ||
| 114 | for (symbol, three, four, five) in PAYTABLE { | 116 | for (symbol, three, four, five) in PAYTABLE { |
| 115 | assert!( | 117 | assert!( |
| 116 | line.contains(&format!("{symbol} {three}/{four}/{five}")), | 118 | joined.contains(&format!("{symbol} {three}/{four}/{five}")), |
| 117 | "paytable is missing {symbol}" | 119 | "paytable is missing {symbol}" |
| 118 | ); | 120 | ); |
| 119 | } | 121 | } |
| 120 | assert!(line.len() <= 80, "paytable is too wide: {line:?}"); | 122 | for line in lines { |
| 123 | assert!(line.len() <= 80, "paytable is too wide: {line:?}"); | ||
| 124 | } | ||
| 121 | } | 125 | } |
| 122 | 126 | ||
| 123 | #[test] | 127 | #[test] |
