summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-28 23:24:08 +0100
committersalmonik <salmonik@tuta.io>2026-10-28 23:24:08 +0100
commit33452f55afcad603f72e054d56ad7848a3d09503 (patch)
treefcef1b0e4a88284ff3fc652fbaecdb087413a17f
parentv1.1.0: highlight winning symbols on the slots board (diff)
downloadzagan-bet-33452f55afcad603f72e054d56ad7848a3d09503.tar.gz
zagan-bet-33452f55afcad603f72e054d56ad7848a3d09503.zip
v1.2.0: show the symbol paytable on the slots board
-rw-r--r--Cargo.toml2
-rw-r--r--docs/SLOTS.md14
-rw-r--r--docs/TESTING.md3
-rw-r--r--src/client/games/slots.rs8
-rw-r--r--src/lib.rs1
-rw-r--r--src/server/games/slots.rs29
-rw-r--r--src/slots.rs33
-rw-r--r--tests/client_core.rs4
-rw-r--r--tests/slots.rs14
9 files changed, 73 insertions, 35 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 7de7c4f..85c7e16 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
1[package] 1[package]
2name = "zagan-bet" 2name = "zagan-bet"
3version = "1.1.0" 3version = "1.2.0"
4edition = "2024" 4edition = "2024"
5 5
6[dependencies] 6[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:
66 66
67``` 67```
68=== SLOTS === 68=== SLOTS ===
69Bet: 10 PLN Lines: 5 Balance: 1000 PLN 69Bet: 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 73Pays 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
74Won 20 PLN 74Won 20 PLN
75Winning lines: 2 75Winning lines: 2
76[+/-] Bet [SPACE] Spin [Q] Quit 76[+/-] Bet [SPACE] Spin [Q] Quit
77``` 77```
78 78
79Winning cells use braces and are rendered yellow/bold in the terminal. The 79The line above the status is the compact paytable: for each symbol, the
80line list below the status names the paylines that won. Both come from the 80multipliers of the total bet for 3, 4 and 5 of a kind. Winning cells use
81server's `win_lines` and `win_cells`, which are already carried through the 81braces and are rendered yellow/bold in the terminal. The line list below the
82client state for the animation pass. 82status names the paylines that won. Both come from the server's `win_lines`
83and `win_cells`, which are already carried through the client state for the
84animation 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.
36 prompt, Ctrl+C exits and restores the terminal. 36 prompt, Ctrl+C exits and restores the terminal.
374. `help` fits 80 columns. Tab completion wraps within 76 columns and the 374. `help` fits 80 columns. Tab completion wraps within 76 columns and the
38 cursor stays visible. 38 cursor stays visible.
395. `play slots 50` shows a 5x3 grid, `Lines: 5`, and the fetched balance. 395. `play slots 50` shows a 5x3 grid, the `Pays 3/4/5 x bet:` paytable line, and
40 the fetched balance.
406. Space shows `Spinning...`, then the 3 rows, win text, winning-line list, and 416. Space shows `Spinning...`, then the 3 rows, win text, winning-line list, and
41 new balance inside the same 9-line board. Winning symbols appear as 42 new balance inside the same 9-line board. Winning symbols appear as
42 yellow/bold `{ X }` cells; the `Winning lines:` list matches. The controls 43 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 @@
1use super::traits::{Game, GamePhase, GameState}; 1use super::traits::{Game, GamePhase, GameState};
2use crate::slots::{paytable_line, REELS, ROWS};
2 3
3/// 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.
4pub const SLOT_BOARD_LINES: usize = 9; 5pub const SLOT_BOARD_LINES: usize = 9;
5pub const LINES: usize = 5;
6pub const REELS: usize = 5;
7pub const ROWS: usize = 3;
8 6
9pub struct SlotsGame; 7pub struct SlotsGame;
10 8
@@ -106,13 +104,13 @@ impl Game for SlotsGame {
106 let grid = self.render_grid(&reels, &winning); 104 let grid = self.render_grid(&reels, &winning);
107 105
108 format!( 106 format!(
109 "=== 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", 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",
110 state.bet_amount, 108 state.bet_amount,
111 LINES,
112 balance, 109 balance,
113 grid[0], 110 grid[0],
114 grid[1], 111 grid[1],
115 grid[2], 112 grid[2],
113 paytable_line(),
116 self.render_status(state), 114 self.render_status(state),
117 self.render_win_lines(state) 115 self.render_win_lines(state)
118 ) 116 )
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 @@
8pub mod client; 8pub mod client;
9pub mod protocol; 9pub mod protocol;
10pub mod server; 10pub mod server;
11pub mod slots;
11pub mod tofu; 12pub 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 @@
1use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; 1use super::traits::{Game, GameError, GamePhase, GameResult, GameState};
2use crate::slots::PAYTABLE;
2use rand::Rng; 3use rand::Rng;
3use std::collections::HashMap; 4use std::collections::HashMap;
4 5
5pub const REELS: usize = 5; 6pub use crate::slots::{PAYLINES, REELS, ROWS, SYMBOLS};
6pub const ROWS: usize = 3;
7pub const SYMBOLS: [char; 6] = ['C', 'L', 'O', 'G', 'D', 'S'];
8
9/// Five paylines, each entry is the row used on reel 0..4.
10pub const PAYLINES: [[usize; REELS]; 5] = [
11 [0, 0, 0, 0, 0], // line 1: top
12 [1, 1, 1, 1, 1], // line 2: middle
13 [2, 2, 2, 2, 2], // line 3: bottom
14 [0, 1, 2, 1, 0], // line 4: V
15 [2, 1, 0, 1, 2], // line 5: inverted V
16];
17 7
18#[derive(Clone, Copy)] 8#[derive(Clone, Copy)]
19struct Payout { 9struct Payout {
@@ -37,15 +27,12 @@ pub struct SlotsGame {
37 27
38impl SlotsGame { 28impl SlotsGame {
39 pub fn new() -> Self { 29 pub fn new() -> Self {
40 let mut payouts = HashMap::new(); 30 let payouts = PAYTABLE
41 31 .iter()
42 // RTP = 425/432 = 0.9838 (1.62% house edge) with five paylines. 32 .map(|&(symbol, three, four, five)| {
43 payouts.insert('C', Payout { three: 2, four: 5, five: 15 }); 33 (symbol, Payout { three, four, five })
44 payouts.insert('L', Payout { three: 3, four: 7, five: 20 }); 34 })
45 payouts.insert('O', Payout { three: 3, four: 8, five: 25 }); 35 .collect();
46 payouts.insert('G', Payout { three: 4, four: 12, five: 40 });
47 payouts.insert('D', Payout { three: 6, four: 18, five: 70 });
48 payouts.insert('S', Payout { three: 9, four: 30, five: 150 });
49 36
50 Self { 37 Self {
51 payouts, 38 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 @@
1//! Shared slots rules used by both the server engine and the client board.
2
3pub const REELS: usize = 5;
4pub const ROWS: usize = 3;
5pub const SYMBOLS: [char; 6] = ['C', 'L', 'O', 'G', 'D', 'S'];
6
7/// Five paylines, each entry is the row used on reel 0..4.
8pub const PAYLINES: [[usize; REELS]; 5] = [
9 [0, 0, 0, 0, 0], // line 1: top
10 [1, 1, 1, 1, 1], // line 2: middle
11 [2, 2, 2, 2, 2], // line 3: bottom
12 [0, 1, 2, 1, 0], // line 4: V
13 [2, 1, 0, 1, 2], // line 5: inverted V
14];
15
16/// (symbol, 3-of-a-kind, 4-of-a-kind, 5-of-a-kind) multipliers of total bet.
17pub const PAYTABLE: [(char, u64, u64, u64); 6] = [
18 ('C', 2, 5, 15),
19 ('L', 3, 7, 20),
20 ('O', 3, 8, 25),
21 ('G', 4, 12, 40),
22 ('D', 6, 18, 70),
23 ('S', 9, 30, 150),
24];
25
26/// One-line paytable rendered under the reels.
27pub fn paytable_line() -> String {
28 let entries: Vec<String> = PAYTABLE
29 .iter()
30 .map(|(symbol, three, four, five)| format!("{symbol} {three}/{four}/{five}"))
31 .collect();
32 format!("Pays 3/4/5 x bet: {}", entries.join(" "))
33}
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() {
134 134
135 assert_eq!(h.board().len(), 9); 135 assert_eq!(h.board().len(), 9);
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: 5")); 137 assert!(!h.board()[1].contains("Lines:"));
138 assert!(h.board()[5].contains("Pays 3/4/5 x bet:"));
139 assert!(h.board()[5].contains("S 9/30/150"));
138 assert!(h.sent.contains(&"INFO".to_string())); 140 assert!(h.sent.contains(&"INFO".to_string()));
139 141
140 h.reply("INFO alice 1 1000"); 142 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 @@
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_line, PAYTABLE};
3use zagan_bet::server::games::slots::{LineWin, SlotsGame, PAYLINES, REELS, ROWS}; 4use zagan_bet::server::games::slots::{LineWin, SlotsGame, PAYLINES, REELS, ROWS};
4use zagan_bet::server::games::traits::{Game, GamePhase}; 5use zagan_bet::server::games::traits::{Game, GamePhase};
5 6
@@ -107,6 +108,19 @@ fn theoretical_rtp_is_just_under_one() {
107} 108}
108 109
109#[test] 110#[test]
111fn the_paytable_line_lists_every_symbol_and_fits_80_columns() {
112 let line = paytable_line();
113
114 for (symbol, three, four, five) in PAYTABLE {
115 assert!(
116 line.contains(&format!("{symbol} {three}/{four}/{five}")),
117 "paytable is missing {symbol}"
118 );
119 }
120 assert!(line.len() <= 80, "paytable is too wide: {line:?}");
121}
122
123#[test]
110fn an_enormous_bet_saturates_instead_of_overflowing() { 124fn an_enormous_bet_saturates_instead_of_overflowing() {
111 let (winnings, _) = payout(["SSSSS", "SSSSS", "SSSSS"], u64::MAX); 125 let (winnings, _) = payout(["SSSSS", "SSSSS", "SSSSS"], u64::MAX);
112 126