summaryrefslogtreecommitdiffstats
path: root/src
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 /src
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--src/client/games/slots.rs33
-rw-r--r--src/server/games/slots.rs32
-rw-r--r--src/slots.rs53
3 files changed, 71 insertions, 47 deletions
diff --git a/src/client/games/slots.rs b/src/client/games/slots.rs
index a417e7d..ad9ef5c 100644
--- a/src/client/games/slots.rs
+++ b/src/client/games/slots.rs
@@ -1,5 +1,5 @@
1use super::traits::{Game, GamePhase, GameState}; 1use super::traits::{Game, GamePhase, GameState};
2use crate::slots::{paytable_lines, REELS, ROWS}; 2use crate::slots::{paytable_lines, LINE_NAMES, 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.
5pub const SLOT_BOARD_LINES: usize = 10; 5pub const SLOT_BOARD_LINES: usize = 10;
@@ -81,7 +81,21 @@ impl SlotsGame {
81 } 81 }
82 match state.data.get("win_lines").map(String::as_str) { 82 match state.data.get("win_lines").map(String::as_str) {
83 Some(lines) if !lines.is_empty() && lines != "-" => { 83 Some(lines) if !lines.is_empty() && lines != "-" => {
84 format!("Winning lines: {lines}") 84 let named: Vec<String> = lines
85 .split(',')
86 .filter_map(|raw| raw.trim().parse::<usize>().ok())
87 .map(|line| {
88 let name = line
89 .checked_sub(1)
90 .and_then(|index| LINE_NAMES.get(index))
91 .copied();
92 match name {
93 Some(name) => format!("{line} ({name})"),
94 None => line.to_string(),
95 }
96 })
97 .collect();
98 format!("Winning lines: {}", named.join(", "))
85 } 99 }
86 _ => String::new(), 100 _ => String::new(),
87 } 101 }
@@ -126,15 +140,14 @@ impl Game for SlotsGame {
126 "SLOTS GAME\n\ 140 "SLOTS GAME\n\
127 =========\n\ 141 =========\n\
128 5 reels x 3 rows, 5 paylines.\n\ 142 5 reels x 3 rows, 5 paylines.\n\
129 Match 3, 4 or 5 symbols from the left on a payline.\n\ 143 A win needs 3, 4 or 5 matching symbols from reel 1 on a payline.\n\
130 \n\ 144 \n\
131 Payouts (x total bet, 3/4/5 of a kind):\n\ 145 Payout = total bet x symbol x shape\n\
132 C: 2 / 5 / 15\n\ 146 Symbols: C=1 L=2 O=3 G=4 D=6 S=10\n\
133 L: 3 / 7 / 20\n\ 147 Shapes: 3 of a kind=1, 4 of a kind=3, 5 of a kind=14\n\
134 O: 3 / 8 / 25\n\ 148 \n\
135 G: 4 / 12 / 40\n\ 149 Lines: 1 top, 2 middle, 3 bottom, 4 V, 5 inverted V\n\
136 D: 6 / 18 / 70\n\ 150 Multiple winning lines are added together.\n\
137 S: 9 / 30 / 150\n\
138 \n\ 151 \n\
139 Symbols: C=Cherry, L=Lemon, O=Orange, G=Grapes, D=Diamond, S=Seven\n\ 152 Symbols: C=Cherry, L=Lemon, O=Orange, G=Grapes, D=Diamond, S=Seven\n\
140 \n\ 153 \n\
diff --git a/src/server/games/slots.rs b/src/server/games/slots.rs
index e9f58b1..298cd4c 100644
--- a/src/server/games/slots.rs
+++ b/src/server/games/slots.rs
@@ -1,5 +1,5 @@
1use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; 1use super::traits::{Game, GameError, GamePhase, GameResult, GameState};
2use crate::slots::PAYTABLE; 2use crate::slots::{SHAPE_MULTIPLIERS, SYMBOL_MULTIPLIERS};
3use rand::Rng; 3use rand::Rng;
4use std::collections::HashMap; 4use std::collections::HashMap;
5 5
@@ -27,10 +27,18 @@ pub struct SlotsGame {
27 27
28impl SlotsGame { 28impl SlotsGame {
29 pub fn new() -> Self { 29 pub fn new() -> Self {
30 let payouts = PAYTABLE 30 // payout = bet * symbol multiplier * shape multiplier
31 let payouts = SYMBOL_MULTIPLIERS
31 .iter() 32 .iter()
32 .map(|&(symbol, three, four, five)| { 33 .map(|&(symbol, symbol_multiplier)| {
33 (symbol, Payout { three, four, five }) 34 (
35 symbol,
36 Payout {
37 three: symbol_multiplier * SHAPE_MULTIPLIERS[0].1,
38 four: symbol_multiplier * SHAPE_MULTIPLIERS[1].1,
39 five: symbol_multiplier * SHAPE_MULTIPLIERS[2].1,
40 },
41 )
34 }) 42 })
35 .collect(); 43 .collect();
36 44
@@ -116,17 +124,15 @@ impl SlotsGame {
116 let p4 = 5.0 / 7776.0; 124 let p4 = 5.0 / 7776.0;
117 let p5 = 1.0 / 7776.0; 125 let p5 = 1.0 / 7776.0;
118 126
119 let per_line: f64 = self 127 let symbol_sum: f64 = SYMBOL_MULTIPLIERS
120 .payouts 128 .iter()
121 .values() 129 .map(|(_, multiplier)| *multiplier as f64)
122 .map(|payout| {
123 p3 * payout.three as f64
124 + p4 * payout.four as f64
125 + p5 * payout.five as f64
126 })
127 .sum(); 130 .sum();
131 let shape_ev = p3 * SHAPE_MULTIPLIERS[0].1 as f64
132 + p4 * SHAPE_MULTIPLIERS[1].1 as f64
133 + p5 * SHAPE_MULTIPLIERS[2].1 as f64;
128 134
129 per_line * PAYLINES.len() as f64 135 symbol_sum * shape_ev * PAYLINES.len() as f64
130 } 136 }
131} 137}
132 138
diff --git a/src/slots.rs b/src/slots.rs
index a3b4ef4..9e41f05 100644
--- a/src/slots.rs
+++ b/src/slots.rs
@@ -13,33 +13,38 @@ pub const PAYLINES: [[usize; REELS]; 5] = [
13 [2, 1, 0, 1, 2], // line 5: inverted V 13 [2, 1, 0, 1, 2], // line 5: inverted V
14]; 14];
15 15
16/// (symbol, 3-of-a-kind, 4-of-a-kind, 5-of-a-kind) multipliers of total bet. 16pub const LINE_NAMES: [&str; 5] = ["top", "middle", "bottom", "V", "inverted V"];
17pub const PAYTABLE: [(char, u64, u64, u64); 6] = [ 17
18 ('C', 2, 5, 15), 18/// Symbol value multipliers.
19 ('L', 3, 7, 20), 19pub const SYMBOL_MULTIPLIERS: [(char, u64); 6] = [
20 ('O', 3, 8, 25), 20 ('C', 1),
21 ('G', 4, 12, 40), 21 ('L', 2),
22 ('D', 6, 18, 70), 22 ('O', 3),
23 ('S', 9, 30, 150), 23 ('G', 4),
24 ('D', 6),
25 ('S', 10),
24]; 26];
25 27
26/// Paytable rendered under the reels, split so it fits a narrow terminal. 28/// (match length, shape multiplier): 3, 4 or 5 of a kind.
29pub const SHAPE_MULTIPLIERS: [(usize, u64); 3] = [(3, 1), (4, 3), (5, 14)];
30
31/// Paytable rendered under the reels.
32///
33/// `payout = bet * symbol_multiplier * shape_multiplier`.
27pub fn paytable_lines() -> Vec<String> { 34pub fn paytable_lines() -> Vec<String> {
28 let entries: Vec<String> = PAYTABLE 35 let symbols = SYMBOL_MULTIPLIERS
36 .iter()
37 .map(|(symbol, multiplier)| format!("{symbol}{multiplier}"))
38 .collect::<Vec<_>>()
39 .join(" ");
40 let shapes = SHAPE_MULTIPLIERS
29 .iter() 41 .iter()
30 .map(|(symbol, three, four, five)| format!("{symbol} {three}/{four}/{five}")) 42 .map(|(length, multiplier)| format!("{length}:{multiplier}"))
31 .collect(); 43 .collect::<Vec<_>>()
44 .join(" ");
32 45
33 entries 46 vec![
34 .chunks(3) 47 "Pays bet x symbol x shape".to_string(),
35 .enumerate() 48 format!("Symbol {symbols} Shape {shapes}"),
36 .map(|(index, chunk)| { 49 ]
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()
45} 50}