diff options
| author | salmonik <salmonik@tuta.io> | 2026-10-28 23:13:24 +0100 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-10-28 23:13:24 +0100 |
| commit | fc59ef77cda14d97d854dfcaa848307cfa512b69 (patch) | |
| tree | b0fc97390b96078e0a0538e76d7c76f7c04135f1 /src/client | |
| parent | v0.4.0: add first-time TLS setup and server help (diff) | |
| download | zagan-bet-fc59ef77cda14d97d854dfcaa848307cfa512b69.tar.gz zagan-bet-fc59ef77cda14d97d854dfcaa848307cfa512b69.zip | |
v1.0.0: replace slots with 5x3 reels and five paylines
Diffstat (limited to '')
| -rw-r--r-- | src/client/games/manager.rs | 11 | ||||
| -rw-r--r-- | src/client/games/response.rs | 13 | ||||
| -rw-r--r-- | src/client/games/slots.rs | 87 |
3 files changed, 85 insertions, 26 deletions
diff --git a/src/client/games/manager.rs b/src/client/games/manager.rs index 71cd28c..8436142 100644 --- a/src/client/games/manager.rs +++ b/src/client/games/manager.rs | |||
| @@ -75,6 +75,7 @@ impl GameManager { | |||
| 75 | session.data.remove("error"); | 75 | session.data.remove("error"); |
| 76 | session.data.remove("reels"); | 76 | session.data.remove("reels"); |
| 77 | session.data.remove("win_type"); | 77 | session.data.remove("win_type"); |
| 78 | session.data.remove("win_lines"); | ||
| 78 | session.data.remove("winnings"); | 79 | session.data.remove("winnings"); |
| 79 | Ok(()) | 80 | Ok(()) |
| 80 | } | 81 | } |
| @@ -91,6 +92,15 @@ impl GameManager { | |||
| 91 | session | 92 | session |
| 92 | .data | 93 | .data |
| 93 | .insert("win_type".to_string(), result.win_type); | 94 | .insert("win_type".to_string(), result.win_type); |
| 95 | session.data.insert( | ||
| 96 | "win_lines".to_string(), | ||
| 97 | result | ||
| 98 | .win_lines | ||
| 99 | .iter() | ||
| 100 | .map(usize::to_string) | ||
| 101 | .collect::<Vec<_>>() | ||
| 102 | .join(","), | ||
| 103 | ); | ||
| 94 | session | 104 | session |
| 95 | .data | 105 | .data |
| 96 | .insert("winnings".to_string(), result.winnings.to_string()); | 106 | .insert("winnings".to_string(), result.winnings.to_string()); |
| @@ -107,6 +117,7 @@ impl GameManager { | |||
| 107 | session.phase = GamePhase::WaitingToStart; | 117 | session.phase = GamePhase::WaitingToStart; |
| 108 | session.data.remove("reels"); | 118 | session.data.remove("reels"); |
| 109 | session.data.remove("win_type"); | 119 | session.data.remove("win_type"); |
| 120 | session.data.remove("win_lines"); | ||
| 110 | session.data.remove("winnings"); | 121 | session.data.remove("winnings"); |
| 111 | session.data.insert("error".to_string(), message.into()); | 122 | session.data.insert("error".to_string(), message.into()); |
| 112 | } | 123 | } |
diff --git a/src/client/games/response.rs b/src/client/games/response.rs index 6948805..de22a75 100644 --- a/src/client/games/response.rs +++ b/src/client/games/response.rs | |||
| @@ -6,6 +6,7 @@ pub struct SpinResult { | |||
| 6 | pub balance: i64, | 6 | pub balance: i64, |
| 7 | pub reels: String, | 7 | pub reels: String, |
| 8 | pub win_type: String, | 8 | pub win_type: String, |
| 9 | pub win_lines: Vec<usize>, | ||
| 9 | } | 10 | } |
| 10 | 11 | ||
| 11 | /// Parse one server reply to `PLAY_START`. | 12 | /// Parse one server reply to `PLAY_START`. |
| @@ -45,10 +46,19 @@ pub fn parse_spin_response(response: &str) -> Result<SpinResult, String> { | |||
| 45 | .parse::<i64>() | 46 | .parse::<i64>() |
| 46 | .map_err(|_| format!("Invalid balance from server: {}", parts[4]))?; | 47 | .map_err(|_| format!("Invalid balance from server: {}", parts[4]))?; |
| 47 | 48 | ||
| 48 | if parts[5].chars().count() != 3 { | 49 | if parts[5].chars().count() != 3 && parts[5].chars().count() != 15 { |
| 49 | return Err(format!("Invalid reels from server: {}", parts[5])); | 50 | return Err(format!("Invalid reels from server: {}", parts[5])); |
| 50 | } | 51 | } |
| 51 | 52 | ||
| 53 | let win_lines = parts | ||
| 54 | .get(7) | ||
| 55 | .map(|raw| { | ||
| 56 | raw.split(',') | ||
| 57 | .filter_map(|value| value.trim().parse::<usize>().ok()) | ||
| 58 | .collect() | ||
| 59 | }) | ||
| 60 | .unwrap_or_default(); | ||
| 61 | |||
| 52 | Ok(SpinResult { | 62 | Ok(SpinResult { |
| 53 | session_id: parts[1].to_string(), | 63 | session_id: parts[1].to_string(), |
| 54 | winnings, | 64 | winnings, |
| @@ -56,5 +66,6 @@ pub fn parse_spin_response(response: &str) -> Result<SpinResult, String> { | |||
| 56 | balance, | 66 | balance, |
| 57 | reels: parts[5].to_string(), | 67 | reels: parts[5].to_string(), |
| 58 | win_type: parts[6].to_string(), | 68 | win_type: parts[6].to_string(), |
| 69 | win_lines, | ||
| 59 | }) | 70 | }) |
| 60 | } | 71 | } |
diff --git a/src/client/games/slots.rs b/src/client/games/slots.rs index 5eed34d..39c43b2 100644 --- a/src/client/games/slots.rs +++ b/src/client/games/slots.rs | |||
| @@ -1,8 +1,10 @@ | |||
| 1 | use super::traits::{Game, GamePhase, GameState}; | 1 | use super::traits::{Game, GamePhase, GameState}; |
| 2 | 2 | ||
| 3 | /// Height of the game board. It must never depend on the spin result, or an | 3 | /// Height of the game board. It must never depend on the spin result. |
| 4 | /// in-place redraw would move to the wrong line. | 4 | pub const SLOT_BOARD_LINES: usize = 9; |
| 5 | pub const SLOT_BOARD_LINES: usize = 7; | 5 | pub const LINES: usize = 5; |
| 6 | pub const REELS: usize = 5; | ||
| 7 | pub const ROWS: usize = 3; | ||
| 6 | 8 | ||
| 7 | pub struct SlotsGame; | 9 | pub struct SlotsGame; |
| 8 | 10 | ||
| @@ -11,17 +13,27 @@ impl SlotsGame { | |||
| 11 | Self | 13 | Self |
| 12 | } | 14 | } |
| 13 | 15 | ||
| 14 | fn render_reels(&self, reels: &str) -> String { | 16 | fn cell(chars: &[char], row: usize, col: usize) -> char { |
| 15 | let mut chars = reels.chars(); | 17 | if chars.len() == REELS * ROWS { |
| 16 | format!( | 18 | chars.get(col * ROWS + row).copied().unwrap_or('?') |
| 17 | "[ {} ] [ {} ] [ {} ]", | 19 | } else if chars.len() == 3 && row == 0 { |
| 18 | chars.next().unwrap_or('?'), | 20 | // Legacy 3-symbol server: degrade to the first row. |
| 19 | chars.next().unwrap_or('?'), | 21 | chars.get(col).copied().unwrap_or('?') |
| 20 | chars.next().unwrap_or('?') | 22 | } else { |
| 21 | ) | 23 | '?' |
| 24 | } | ||
| 25 | } | ||
| 26 | |||
| 27 | fn render_grid(&self, reels: &str) -> [String; ROWS] { | ||
| 28 | let chars: Vec<char> = reels.chars().collect(); | ||
| 29 | std::array::from_fn(|row| { | ||
| 30 | (0..REELS) | ||
| 31 | .map(|col| format!("[ {} ]", Self::cell(&chars, row, col))) | ||
| 32 | .collect::<Vec<_>>() | ||
| 33 | .join(" ") | ||
| 34 | }) | ||
| 22 | } | 35 | } |
| 23 | 36 | ||
| 24 | /// The fifth board line: current status, result, or error. | ||
| 25 | fn render_status(&self, state: &GameState) -> String { | 37 | fn render_status(&self, state: &GameState) -> String { |
| 26 | if let Some(error) = state.data.get("error") { | 38 | if let Some(error) = state.data.get("error") { |
| 27 | return format!("ERROR: {error}"); | 39 | return format!("ERROR: {error}"); |
| @@ -35,14 +47,29 @@ impl SlotsGame { | |||
| 35 | .get("winnings") | 47 | .get("winnings") |
| 36 | .map(String::as_str) | 48 | .map(String::as_str) |
| 37 | .unwrap_or("0"); | 49 | .unwrap_or("0"); |
| 38 | return match state.data.get("win_type").map(String::as_str) { | 50 | if winnings != "0" { |
| 39 | Some("jackpot") => format!("JACKPOT! Winnings: {winnings} PLN"), | 51 | return match state.data.get("win_type").map(String::as_str) { |
| 40 | Some("two") => format!("Two of a kind! Winnings: {winnings} PLN"), | 52 | Some("mega") => format!("MEGA WIN! {winnings} PLN"), |
| 41 | _ => "No win.".to_string(), | 53 | Some("big") => format!("BIG WIN! {winnings} PLN"), |
| 42 | }; | 54 | _ => format!("Won {winnings} PLN"), |
| 55 | }; | ||
| 56 | } | ||
| 57 | return "No win.".to_string(); | ||
| 43 | } | 58 | } |
| 44 | "Press [SPACE] to spin!".to_string() | 59 | "Press [SPACE] to spin!".to_string() |
| 45 | } | 60 | } |
| 61 | |||
| 62 | fn render_win_lines(&self, state: &GameState) -> String { | ||
| 63 | if state.phase != GamePhase::Finished { | ||
| 64 | return String::new(); | ||
| 65 | } | ||
| 66 | match state.data.get("win_lines").map(String::as_str) { | ||
| 67 | Some(lines) if !lines.is_empty() && lines != "-" => { | ||
| 68 | format!("Winning lines: {lines}") | ||
| 69 | } | ||
| 70 | _ => String::new(), | ||
| 71 | } | ||
| 72 | } | ||
| 46 | } | 73 | } |
| 47 | 74 | ||
| 48 | impl Game for SlotsGame { | 75 | impl Game for SlotsGame { |
| @@ -57,13 +84,18 @@ impl Game for SlotsGame { | |||
| 57 | .get("balance") | 84 | .get("balance") |
| 58 | .map(String::as_str) | 85 | .map(String::as_str) |
| 59 | .unwrap_or("?"); | 86 | .unwrap_or("?"); |
| 87 | let grid = self.render_grid(&reels); | ||
| 60 | 88 | ||
| 61 | format!( | 89 | format!( |
| 62 | "=== SLOTS ===\r\nBet: {} PLN Balance: {} PLN\r\n{}\r\n\r\n{}\r\n\r\n[+/-] Adjust Bet [Q] Quit", | 90 | "=== 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", |
| 63 | state.bet_amount, | 91 | state.bet_amount, |
| 92 | LINES, | ||
| 64 | balance, | 93 | balance, |
| 65 | self.render_reels(&reels), | 94 | grid[0], |
| 66 | self.render_status(state) | 95 | grid[1], |
| 96 | grid[2], | ||
| 97 | self.render_status(state), | ||
| 98 | self.render_win_lines(state) | ||
| 67 | ) | 99 | ) |
| 68 | } | 100 | } |
| 69 | 101 | ||
| @@ -74,17 +106,22 @@ impl Game for SlotsGame { | |||
| 74 | fn get_help(&self) -> String { | 106 | fn get_help(&self) -> String { |
| 75 | "SLOTS GAME\n\ | 107 | "SLOTS GAME\n\ |
| 76 | =========\n\ | 108 | =========\n\ |
| 77 | Match symbols to win!\n\ | 109 | 5 reels x 3 rows, 5 paylines.\n\ |
| 110 | Match 3, 4 or 5 symbols from the left on a payline.\n\ | ||
| 78 | \n\ | 111 | \n\ |
| 79 | Payouts:\n\ | 112 | Payouts (x total bet, 3/4/5 of a kind):\n\ |
| 80 | Three of a kind: 2x-28x bet\n\ | 113 | C: 2 / 5 / 15\n\ |
| 81 | Two of a kind: 1x-3x bet\n\ | 114 | L: 3 / 7 / 20\n\ |
| 115 | O: 3 / 8 / 25\n\ | ||
| 116 | G: 4 / 12 / 40\n\ | ||
| 117 | D: 6 / 18 / 70\n\ | ||
| 118 | S: 9 / 30 / 150\n\ | ||
| 82 | \n\ | 119 | \n\ |
| 83 | Symbols: C=Cherry, L=Lemon, O=Orange, G=Grapes, D=Diamond, S=Seven\n\ | 120 | Symbols: C=Cherry, L=Lemon, O=Orange, G=Grapes, D=Diamond, S=Seven\n\ |
| 84 | \n\ | 121 | \n\ |
| 85 | Controls:\n\ | 122 | Controls:\n\ |
| 86 | SPACE - Spin the reels\n\ | 123 | SPACE - Spin the reels\n\ |
| 87 | +/- - Adjust bet amount\n\ | 124 | +/- - Adjust total bet\n\ |
| 88 | Q - Exit the game" | 125 | Q - Exit the game" |
| 89 | .to_string() | 126 | .to_string() |
| 90 | } | 127 | } |
