diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/client/games/slots.rs | 33 | ||||
| -rw-r--r-- | src/server/games/slots.rs | 32 | ||||
| -rw-r--r-- | src/slots.rs | 53 |
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 @@ | |||
| 1 | use super::traits::{Game, GamePhase, GameState}; | 1 | use super::traits::{Game, GamePhase, GameState}; |
| 2 | use crate::slots::{paytable_lines, REELS, ROWS}; | 2 | use 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. |
| 5 | pub const SLOT_BOARD_LINES: usize = 10; | 5 | pub 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 @@ | |||
| 1 | use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; | 1 | use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; |
| 2 | use crate::slots::PAYTABLE; | 2 | use crate::slots::{SHAPE_MULTIPLIERS, SYMBOL_MULTIPLIERS}; |
| 3 | use rand::Rng; | 3 | use rand::Rng; |
| 4 | use std::collections::HashMap; | 4 | use std::collections::HashMap; |
| 5 | 5 | ||
| @@ -27,10 +27,18 @@ pub struct SlotsGame { | |||
| 27 | 27 | ||
| 28 | impl SlotsGame { | 28 | impl 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. | 16 | pub const LINE_NAMES: [&str; 5] = ["top", "middle", "bottom", "V", "inverted V"]; |
| 17 | pub const PAYTABLE: [(char, u64, u64, u64); 6] = [ | 17 | |
| 18 | ('C', 2, 5, 15), | 18 | /// Symbol value multipliers. |
| 19 | ('L', 3, 7, 20), | 19 | pub 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. |
| 29 | pub 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`. | ||
| 27 | pub fn paytable_lines() -> Vec<String> { | 34 | pub 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 | } |
