From b5a52fc0312afdf5d5fbe53275dc88d6d881c2ff Mon Sep 17 00:00:00 2001 From: salmonik Date: Thu, 29 Oct 2026 18:22:40 +0100 Subject: v2.2.0: add blackjack with reserved bets and hit/stand/double --- src/client/games/blackjack.rs | 148 ++++++++++++++++++++++++++++++++++++++++++ src/client/games/manager.rs | 49 ++++++++++++++ src/client/games/mod.rs | 1 + src/client/games/response.rs | 22 +++++++ 4 files changed, 220 insertions(+) create mode 100644 src/client/games/blackjack.rs (limited to 'src/client/games') diff --git a/src/client/games/blackjack.rs b/src/client/games/blackjack.rs new file mode 100644 index 0000000..a34eb63 --- /dev/null +++ b/src/client/games/blackjack.rs @@ -0,0 +1,148 @@ +use super::traits::{Game, GameState}; +use crate::blackjack::{card_label, BlackjackState}; + +pub const BLACKJACK_BOARD_LINES: usize = 6; + +pub struct BlackjackGame; + +impl BlackjackGame { + pub fn new() -> Self { + Self + } + + fn state(state: &GameState) -> Option { + state + .data + .get("blackjack") + .and_then(|raw| serde_json::from_str(raw).ok()) + } + + fn cards(cards: &[String]) -> String { + cards + .iter() + .map(|card| { + if card == "?" { + "[ ? ]".to_string() + } else { + format!("[{}]", card_label(card)) + } + }) + .collect::>() + .join(" ") + } + + fn result_text(result: &str, winnings: u64) -> String { + match result { + "blackjack" => format!("BLACKJACK! Won {winnings} PLN"), + "win" => format!("Win! Won {winnings} PLN"), + "push" => "Push. Bet returned.".to_string(), + "lose" => "Dealer wins.".to_string(), + "bust" => "Bust.".to_string(), + "cancelled" => "Cancelled.".to_string(), + _ => "Hand complete.".to_string(), + } + } +} + +impl Game for BlackjackGame { + fn get_name(&self) -> &'static str { + "Blackjack" + } + + fn render(&self, state: &GameState) -> String { + let status = state + .data + .get("status") + .map(String::as_str) + .unwrap_or("ready"); + let balance = state + .data + .get("balance") + .map(String::as_str) + .unwrap_or("?"); + let hand = Self::state(state); + + if let Some(error) = state.data.get("error") { + let bet = hand.as_ref().map(|hand| hand.bet).unwrap_or(state.bet_amount); + return format!( + "=== BLACKJACK ===\r\nBalance: {balance} PLN Bet: {bet} PLN\r\n\r\n\r\nERROR: {error}\r\n[Q] Quit" + ); + } + + let (dealer, dealer_total, player, player_total, bet, text, controls) = + if let Some(hand) = &hand { + let bet = if status == "player" { + hand.bet + } else { + state.bet_amount + }; + let text = match status { + "player" => { + if hand.actions.iter().any(|action| action == "double") { + "Hit, stand or double.".to_string() + } else { + "Hit or stand.".to_string() + } + } + "dealing" => "Dealing...".to_string(), + "cancelling" => "Cancelling...".to_string(), + "done" => Self::result_text(&hand.result, hand.winnings), + _ => "Press [SPACE] to deal.".to_string(), + }; + let controls = if status == "player" { + if hand.actions.iter().any(|action| action == "double") { + "[H]it [S]tand [D]ouble [Q]uit".to_string() + } else { + "[H]it [S]tand [Q]uit".to_string() + } + } else { + "[SPACE] New hand [+/-] Bet [Q] Quit".to_string() + }; + ( + Self::cards(&hand.dealer), + hand.dealer_total, + Self::cards(&hand.player), + hand.player_total, + bet, + text, + controls, + ) + } else { + ( + "—".to_string(), + 0, + "—".to_string(), + 0, + state.bet_amount, + "Press [SPACE] to deal.".to_string(), + "[SPACE] Deal [+/-] Bet [Q] Quit".to_string(), + ) + }; + + format!( + "=== BLACKJACK ===\r\nBalance: {} PLN Bet: {} PLN\r\nDealer: {} {}\r\nYou: {} {}\r\n{}\r\n{}", + balance, bet, dealer, dealer_total, player, player_total, text, controls + ) + } + + fn render_lines(&self, _state: &GameState) -> usize { + BLACKJACK_BOARD_LINES + } + + fn get_help(&self) -> String { + "BLACKJACK\n\ + =========\n\ + Six-deck shoe, fresh for every hand.\n\ + Dealer stands on all 17s.\n\ + Blackjack pays 3:2.\n\ + Double is allowed on the first two cards.\n\ + \n\ + H - hit\n\ + S - stand\n\ + D - double (when available)\n\ + SPACE - deal a new hand\n\ + +/- - change the next hand's bet\n\ + Q - leave the table" + .to_string() + } +} diff --git a/src/client/games/manager.rs b/src/client/games/manager.rs index 02e9451..4e0218b 100644 --- a/src/client/games/manager.rs +++ b/src/client/games/manager.rs @@ -1,7 +1,9 @@ +use super::blackjack::BlackjackGame; use super::response::{RouletteResult, SpinResult}; use super::roulette::RouletteGame; use super::slots::SlotsGame; use super::traits::{Game, GameError, GamePhase, GameState}; +use crate::blackjack::BlackjackState; use crate::roulette::Bets; use std::collections::HashMap; @@ -15,6 +17,7 @@ impl GameManager { let mut games: HashMap> = HashMap::new(); games.insert("slots".to_string(), Box::new(SlotsGame::new())); games.insert("roulette".to_string(), Box::new(RouletteGame::new())); + games.insert("blackjack".to_string(), Box::new(BlackjackGame::new())); Self { current_session: None, @@ -51,6 +54,9 @@ impl GameManager { data.insert("status".to_string(), "place".to_string()); data.insert("number_input".to_string(), String::new()); 0 + } else if game_type == "blackjack" { + data.insert("status".to_string(), "ready".to_string()); + 10 } else { 10 }; @@ -260,6 +266,49 @@ impl GameManager { .insert("number_input".to_string(), String::new()); } + pub fn apply_blackjack_state(&mut self, state: BlackjackState) { + let Some(session) = self.current_session.as_mut() else { + return; + }; + session.phase = if state.phase == "player" { + GamePhase::Playing + } else { + GamePhase::Finished + }; + session.bet_amount = state.base_bet; + session.data.remove("error"); + session + .data + .insert("balance".to_string(), state.balance.to_string()); + session.data.insert( + "status".to_string(), + if state.phase == "player" { + "player".to_string() + } else { + "done".to_string() + }, + ); + session.data.insert( + "blackjack".to_string(), + serde_json::to_string(&state).unwrap_or_default(), + ); + } + + pub fn blackjack_state(&self) -> Option { + self.current_session + .as_ref() + .and_then(|session| session.data.get("blackjack")) + .and_then(|raw| serde_json::from_str(raw).ok()) + } + + pub fn set_blackjack_status(&mut self, status: &str) { + if let Some(session) = self.current_session.as_mut() { + session + .data + .insert("status".to_string(), status.to_string()); + } + } + pub fn get_current_game(&self) -> Option<&dyn Game> { self.current_session .as_ref() diff --git a/src/client/games/mod.rs b/src/client/games/mod.rs index 131ffbb..40ba32a 100644 --- a/src/client/games/mod.rs +++ b/src/client/games/mod.rs @@ -1,3 +1,4 @@ +pub mod blackjack; pub mod manager; pub mod response; pub mod roulette; diff --git a/src/client/games/response.rs b/src/client/games/response.rs index 73080dd..d9883f7 100644 --- a/src/client/games/response.rs +++ b/src/client/games/response.rs @@ -141,3 +141,25 @@ pub fn parse_roulette_response(response: &str) -> Result bets, }) } + +pub fn parse_blackjack_response( + response: &str, +) -> Result { + let response = response.trim(); + if response.is_empty() { + return Err("No response from server".to_string()); + } + if let Some(message) = response.strip_prefix("ERROR") { + let message = message.trim(); + return Err(if message.is_empty() { + "Server error".to_string() + } else { + message.to_string() + }); + } + + let raw = response + .strip_prefix("BLACKJACK_STATE ") + .ok_or_else(|| format!("Unexpected server response: {response}"))?; + serde_json::from_str(raw).map_err(|error| format!("Invalid blackjack state: {error}")) +} -- cgit v1.2.3