From 33452f55afcad603f72e054d56ad7848a3d09503 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 28 Oct 2026 23:24:08 +0100 Subject: v1.2.0: show the symbol paytable on the slots board --- Cargo.toml | 2 +- docs/SLOTS.md | 14 ++++++++------ docs/TESTING.md | 3 ++- src/client/games/slots.rs | 8 +++----- src/lib.rs | 1 + src/server/games/slots.rs | 29 ++++++++--------------------- src/slots.rs | 33 +++++++++++++++++++++++++++++++++ tests/client_core.rs | 4 +++- tests/slots.rs | 14 ++++++++++++++ 9 files changed, 73 insertions(+), 35 deletions(-) create mode 100644 src/slots.rs diff --git a/Cargo.toml b/Cargo.toml index 7de7c4f..85c7e16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "zagan-bet" -version = "1.1.0" +version = "1.2.0" edition = "2024" [dependencies] diff --git a/docs/SLOTS.md b/docs/SLOTS.md index a64bdbf..a2e778d 100644 --- a/docs/SLOTS.md +++ b/docs/SLOTS.md @@ -66,17 +66,19 @@ The board is nine lines: ``` === SLOTS === -Bet: 10 PLN Lines: 5 Balance: 1000 PLN +Bet: 10 PLN Balance: 1000 PLN [ C ] [ L ] [ O ] [ G ] [ D ] [ S ] { C } { C } [ O ] [ G ] [ D ] [ S ] { C } [ L ] [ O ] - +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 Won 20 PLN Winning lines: 2 [+/-] Bet [SPACE] Spin [Q] Quit ``` -Winning cells use braces and are rendered yellow/bold in the terminal. The -line list below the status names the paylines that won. Both come from the -server's `win_lines` and `win_cells`, which are already carried through the -client state for the animation pass. +The line above the status is the compact paytable: for each symbol, the +multipliers of the total bet for 3, 4 and 5 of a kind. Winning cells use +braces and are rendered yellow/bold in the terminal. The line list below the +status names the paylines that won. Both come from the server's `win_lines` +and `win_cells`, which are already carried through the client state for the +animation pass. diff --git a/docs/TESTING.md b/docs/TESTING.md index 8e35c5a..7f54798 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -36,7 +36,8 @@ Use an 80-column terminal because several output lines are width-sensitive. prompt, Ctrl+C exits and restores the terminal. 4. `help` fits 80 columns. Tab completion wraps within 76 columns and the cursor stays visible. -5. `play slots 50` shows a 5x3 grid, `Lines: 5`, and the fetched balance. +5. `play slots 50` shows a 5x3 grid, the `Pays 3/4/5 x bet:` paytable line, and + the fetched balance. 6. Space shows `Spinning...`, then the 3 rows, win text, winning-line list, and new balance inside the same 9-line board. Winning symbols appear as yellow/bold `{ X }` cells; the `Winning lines:` list matches. The controls diff --git a/src/client/games/slots.rs b/src/client/games/slots.rs index c367fcb..7e59919 100644 --- a/src/client/games/slots.rs +++ b/src/client/games/slots.rs @@ -1,10 +1,8 @@ use super::traits::{Game, GamePhase, GameState}; +use crate::slots::{paytable_line, REELS, ROWS}; /// Height of the game board. It must never depend on the spin result. pub const SLOT_BOARD_LINES: usize = 9; -pub const LINES: usize = 5; -pub const REELS: usize = 5; -pub const ROWS: usize = 3; pub struct SlotsGame; @@ -106,13 +104,13 @@ impl Game for SlotsGame { let grid = self.render_grid(&reels, &winning); format!( - "=== SLOTS ===\r\nBet: {} PLN Lines: {} Balance: {} PLN\r\n{}\r\n{}\r\n{}\r\n\r\n{}\r\n{}\r\n[+/-] Bet [SPACE] Spin [Q] Quit", + "=== 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", state.bet_amount, - LINES, balance, grid[0], grid[1], grid[2], + paytable_line(), self.render_status(state), self.render_win_lines(state) ) diff --git a/src/lib.rs b/src/lib.rs index e700173..a261ab0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,4 +8,5 @@ pub mod client; pub mod protocol; pub mod server; +pub mod slots; pub mod tofu; diff --git a/src/server/games/slots.rs b/src/server/games/slots.rs index 1d71c9d..e9f58b1 100644 --- a/src/server/games/slots.rs +++ b/src/server/games/slots.rs @@ -1,19 +1,9 @@ use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; +use crate::slots::PAYTABLE; use rand::Rng; use std::collections::HashMap; -pub const REELS: usize = 5; -pub const ROWS: usize = 3; -pub const SYMBOLS: [char; 6] = ['C', 'L', 'O', 'G', 'D', 'S']; - -/// Five paylines, each entry is the row used on reel 0..4. -pub const PAYLINES: [[usize; REELS]; 5] = [ - [0, 0, 0, 0, 0], // line 1: top - [1, 1, 1, 1, 1], // line 2: middle - [2, 2, 2, 2, 2], // line 3: bottom - [0, 1, 2, 1, 0], // line 4: V - [2, 1, 0, 1, 2], // line 5: inverted V -]; +pub use crate::slots::{PAYLINES, REELS, ROWS, SYMBOLS}; #[derive(Clone, Copy)] struct Payout { @@ -37,15 +27,12 @@ pub struct SlotsGame { impl SlotsGame { pub fn new() -> Self { - let mut payouts = HashMap::new(); - - // RTP = 425/432 = 0.9838 (1.62% house edge) with five paylines. - payouts.insert('C', Payout { three: 2, four: 5, five: 15 }); - payouts.insert('L', Payout { three: 3, four: 7, five: 20 }); - payouts.insert('O', Payout { three: 3, four: 8, five: 25 }); - payouts.insert('G', Payout { three: 4, four: 12, five: 40 }); - payouts.insert('D', Payout { three: 6, four: 18, five: 70 }); - payouts.insert('S', Payout { three: 9, four: 30, five: 150 }); + let payouts = PAYTABLE + .iter() + .map(|&(symbol, three, four, five)| { + (symbol, Payout { three, four, five }) + }) + .collect(); Self { payouts, diff --git a/src/slots.rs b/src/slots.rs new file mode 100644 index 0000000..561c449 --- /dev/null +++ b/src/slots.rs @@ -0,0 +1,33 @@ +//! Shared slots rules used by both the server engine and the client board. + +pub const REELS: usize = 5; +pub const ROWS: usize = 3; +pub const SYMBOLS: [char; 6] = ['C', 'L', 'O', 'G', 'D', 'S']; + +/// Five paylines, each entry is the row used on reel 0..4. +pub const PAYLINES: [[usize; REELS]; 5] = [ + [0, 0, 0, 0, 0], // line 1: top + [1, 1, 1, 1, 1], // line 2: middle + [2, 2, 2, 2, 2], // line 3: bottom + [0, 1, 2, 1, 0], // line 4: V + [2, 1, 0, 1, 2], // line 5: inverted V +]; + +/// (symbol, 3-of-a-kind, 4-of-a-kind, 5-of-a-kind) multipliers of total bet. +pub const PAYTABLE: [(char, u64, u64, u64); 6] = [ + ('C', 2, 5, 15), + ('L', 3, 7, 20), + ('O', 3, 8, 25), + ('G', 4, 12, 40), + ('D', 6, 18, 70), + ('S', 9, 30, 150), +]; + +/// One-line paytable rendered under the reels. +pub fn paytable_line() -> String { + let entries: Vec = PAYTABLE + .iter() + .map(|(symbol, three, four, five)| format!("{symbol} {three}/{four}/{five}")) + .collect(); + format!("Pays 3/4/5 x bet: {}", entries.join(" ")) +} diff --git a/tests/client_core.rs b/tests/client_core.rs index 91ddcab..ea99d52 100644 --- a/tests/client_core.rs +++ b/tests/client_core.rs @@ -134,7 +134,9 @@ fn play_shows_a_nine_line_board_and_fetches_the_balance() { assert_eq!(h.board().len(), 9); assert!(h.board()[1].contains("Bet: 50 PLN")); - assert!(h.board()[1].contains("Lines: 5")); + assert!(!h.board()[1].contains("Lines:")); + assert!(h.board()[5].contains("Pays 3/4/5 x bet:")); + assert!(h.board()[5].contains("S 9/30/150")); assert!(h.sent.contains(&"INFO".to_string())); h.reply("INFO alice 1 1000"); diff --git a/tests/slots.rs b/tests/slots.rs index 857e607..b7292b8 100644 --- a/tests/slots.rs +++ b/tests/slots.rs @@ -1,5 +1,6 @@ //! Regression tests for the server-side 5x3 slots game. +use zagan_bet::slots::{paytable_line, PAYTABLE}; use zagan_bet::server::games::slots::{LineWin, SlotsGame, PAYLINES, REELS, ROWS}; use zagan_bet::server::games::traits::{Game, GamePhase}; @@ -106,6 +107,19 @@ fn theoretical_rtp_is_just_under_one() { assert!(rtp > 0.98, "house edge should be tiny"); } +#[test] +fn the_paytable_line_lists_every_symbol_and_fits_80_columns() { + let line = paytable_line(); + + for (symbol, three, four, five) in PAYTABLE { + assert!( + line.contains(&format!("{symbol} {three}/{four}/{five}")), + "paytable is missing {symbol}" + ); + } + assert!(line.len() <= 80, "paytable is too wide: {line:?}"); +} + #[test] fn an_enormous_bet_saturates_instead_of_overflowing() { let (winnings, _) = payout(["SSSSS", "SSSSS", "SSSSS"], u64::MAX); -- cgit v1.2.3