From ea5e7d336719571e51ef14478ab603e71126e3d8 Mon Sep 17 00:00:00 2001 From: salmonik Date: Wed, 28 Oct 2026 22:28:05 +0100 Subject: v0.2.2: split client/server logic into the library --- src/client/games/manager.rs | 131 ++++++++++++++++++++++++++++++++++++++++++++ src/client/games/mod.rs | 3 + src/client/games/slots.rs | 61 +++++++++++++++++++++ src/client/games/traits.rs | 32 +++++++++++ 4 files changed, 227 insertions(+) create mode 100644 src/client/games/manager.rs create mode 100644 src/client/games/mod.rs create mode 100644 src/client/games/slots.rs create mode 100644 src/client/games/traits.rs (limited to 'src/client/games') diff --git a/src/client/games/manager.rs b/src/client/games/manager.rs new file mode 100644 index 0000000..be96b1a --- /dev/null +++ b/src/client/games/manager.rs @@ -0,0 +1,131 @@ +use super::slots::SlotsGame; +use super::traits::{Game, GameError, GamePhase, GameState}; +use std::collections::HashMap; +use std::io::Write; + +pub struct GameManager { + current_session: Option, + pending_game_type: Option, + games: HashMap>, +} + +impl GameManager { + pub fn new() -> Self { + let mut games: HashMap> = HashMap::new(); + games.insert("slots".to_string(), Box::new(SlotsGame::new())); + + Self { + current_session: None, + pending_game_type: None, + games, + } + } + + pub fn get_available_games(&self) -> Vec<(&str, &str)> { + self.games + .iter() + .map(|(k, v)| (k.as_str(), v.get_name())) + .collect() + } + + pub fn enter_game(&mut self, game_type: &str) -> Result<(), GameError> { + if self.current_session.is_some() { + return Err(GameError::InvalidAction); + } + + if !self.games.contains_key(game_type) { + return Err(GameError::InvalidAction); + } + + self.pending_game_type = Some(game_type.to_string()); + + self.current_session = Some(GameState { + game_type: game_type.to_string(), + bet_amount: 10, + phase: GamePhase::WaitingToStart, + data: HashMap::new(), + }); + + Ok(()) + } + + pub fn start_game(&mut self, stream: &mut W) -> Result<(), GameError> { + let game_type = self + .pending_game_type + .as_ref() + .ok_or(GameError::InvalidAction)?; + + let state = self + .current_session + .as_mut() + .ok_or(GameError::InvalidAction)?; + + if state.phase != GamePhase::WaitingToStart && state.phase != GamePhase::Finished { + return Err(GameError::InvalidAction); + } + + let bet_amount = state.bet_amount; + + let writer = stream; + writeln!(writer, "PLAY_START {} {}", game_type, bet_amount) + .map_err(|_| GameError::NetworkError)?; + + Ok(()) + } + + pub fn get_current_game(&self) -> Option<&dyn Game> { + self.current_session + .as_ref() + .and_then(|session| self.games.get(&session.game_type).map(|game| game.as_ref())) + } + + pub fn get_current_state(&self) -> Option<&GameState> { + self.current_session.as_ref() + } + + pub fn get_current_render_lines(&self) -> Option { + let session = self.current_session.as_ref()?; + let game = self.games.get(&session.game_type)?; + Some(game.render_lines(session)) + } + + pub fn is_in_game(&self) -> bool { + self.current_session.is_some() + } + + pub fn is_game_started(&self) -> bool { + self.current_session + .as_ref() + .map(|s| s.phase != GamePhase::WaitingToStart) + .unwrap_or(false) + } + + pub fn reset_phase_to_waiting(&mut self) { + if let Some(ref mut session) = self.current_session { + session.phase = GamePhase::WaitingToStart; + session.data.clear(); + } + } + + pub fn end_game(&mut self) { + self.current_session = None; + self.pending_game_type = None; + } + + pub fn adjust_bet(&mut self, delta: i64) -> Result { + if let Some(ref session) = self.current_session { + if session.phase != GamePhase::WaitingToStart { + return Err(GameError::InvalidAction); + } + } + + if let Some(ref mut session) = self.current_session { + let current_bet = session.bet_amount as i64; + let new_bet = (current_bet + delta).max(10) as u64; + session.bet_amount = new_bet; + Ok(new_bet) + } else { + Err(GameError::InvalidAction) + } + } +} diff --git a/src/client/games/mod.rs b/src/client/games/mod.rs new file mode 100644 index 0000000..aa74574 --- /dev/null +++ b/src/client/games/mod.rs @@ -0,0 +1,3 @@ +pub mod manager; +pub mod slots; +pub mod traits; diff --git a/src/client/games/slots.rs b/src/client/games/slots.rs new file mode 100644 index 0000000..6e2ebbe --- /dev/null +++ b/src/client/games/slots.rs @@ -0,0 +1,61 @@ +use super::traits::{Game, GameState}; + +pub struct SlotsGame; + +impl SlotsGame { + pub fn new() -> Self { + Self + } + + fn render_reels(&self, reels: &str) -> String { + if reels.is_empty() { + "[ ? ] [ ? ] [ ? ]".to_string() + } else { + let chars: Vec = reels.chars().collect(); + format!( + "[ {} ] [ {} ] [ {} ]\r", + chars.get(0).unwrap_or(&'?'), + chars.get(1).unwrap_or(&'?'), + chars.get(2).unwrap_or(&'?') + ) + } + } +} + +impl Game for SlotsGame { + fn get_name(&self) -> &'static str { + "Slots" + } + + fn render(&self, state: &GameState) -> String { + let reels = state.data.get("reels").cloned().unwrap_or_default(); + + format!( + "=== SLOTS ===\r\nBet: {} PLN\r\n{}\r\n\r\nPress [SPACE] to spin!\r\n\r\nControls: [+/-] Adjust Bet [Q] Quit", + state.bet_amount, + self.render_reels(&reels) + ) + } + + fn render_lines(&self, _state: &GameState) -> usize { + 6 + } + + fn get_help(&self) -> String { + "SLOTS GAME\n\ + =========\n\ + Match symbols to win!\n\ + \n\ + Payouts:\n\ + Three of a kind: 2x-28x bet\n\ + Two of a kind: 1x-3x bet\n\ + \n\ + Symbols: C=Cherry, L=Lemon, O=Orange, G=Grapes, D=Diamond, S=Seven\n\ + \n\ + Controls:\n\ + SPACE - Spin the reels\n\ + +/- - Adjust bet amount\n\ + Q - Exit the game" + .to_string() + } +} diff --git a/src/client/games/traits.rs b/src/client/games/traits.rs new file mode 100644 index 0000000..aed0c24 --- /dev/null +++ b/src/client/games/traits.rs @@ -0,0 +1,32 @@ +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub enum GameError { + InvalidAction, + NetworkError, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum GamePhase { + WaitingToStart, + Playing, + Finished, +} + +#[derive(Debug, Clone)] +pub struct GameState { + pub game_type: String, + pub bet_amount: u64, + pub phase: GamePhase, + pub data: HashMap, +} + +pub trait Game: Send + Sync { + fn get_name(&self) -> &'static str; + + fn render(&self, state: &GameState) -> String; + + fn render_lines(&self, state: &GameState) -> usize; + + fn get_help(&self) -> String; +} -- cgit v1.2.3