summaryrefslogtreecommitdiffstats
path: root/src/server/games
diff options
context:
space:
mode:
Diffstat (limited to 'src/server/games')
-rw-r--r--src/server/games/mod.rs4
-rw-r--r--src/server/games/registry.rs55
-rw-r--r--src/server/games/session.rs77
-rw-r--r--src/server/games/slots.rs179
-rw-r--r--src/server/games/traits.rs67
5 files changed, 382 insertions, 0 deletions
diff --git a/src/server/games/mod.rs b/src/server/games/mod.rs
new file mode 100644
index 0000000..4c62760
--- /dev/null
+++ b/src/server/games/mod.rs
@@ -0,0 +1,4 @@
1pub mod registry;
2pub mod session;
3pub mod slots;
4pub mod traits;
diff --git a/src/server/games/registry.rs b/src/server/games/registry.rs
new file mode 100644
index 0000000..3b8c391
--- /dev/null
+++ b/src/server/games/registry.rs
@@ -0,0 +1,55 @@
1#![allow(dead_code)]
2
3use super::traits::Game;
4use std::collections::HashMap;
5use std::sync::{Arc, Mutex};
6
7pub type GameRef = Arc<dyn Game + Send + Sync>;
8
9pub struct GameRegistry {
10 games: Arc<Mutex<HashMap<String, GameRef>>>,
11}
12
13impl GameRegistry {
14 pub fn new() -> Self {
15 Self {
16 games: Arc::new(Mutex::new(HashMap::new())),
17 }
18 }
19
20 pub fn register_game<G: Game + Send + Sync + 'static>(&self, game: G) {
21 let game_type = game.game_type().to_string();
22 let mut games = self.games.lock().unwrap();
23 games.insert(game_type, Arc::new(game));
24 }
25
26 pub fn get_game(&self, game_type: &str) -> Option<GameRef> {
27 let games = self.games.lock().unwrap();
28 games.get(game_type).cloned()
29 }
30
31 pub fn list_games(&self) -> Vec<String> {
32 let games = self.games.lock().unwrap();
33 games.keys().cloned().collect()
34 }
35
36 pub fn game_exists(&self, game_type: &str) -> bool {
37 let games = self.games.lock().unwrap();
38 games.contains_key(game_type)
39 }
40}
41
42// Global registry instance
43use super::slots::SlotsGame;
44use once_cell::sync::Lazy;
45
46static GAME_REGISTRY: Lazy<GameRegistry> = Lazy::new(|| {
47 let registry = GameRegistry::new();
48 // Register games here
49 registry.register_game(SlotsGame::new());
50 registry
51});
52
53pub fn get_game_registry() -> &'static GameRegistry {
54 &GAME_REGISTRY
55}
diff --git a/src/server/games/session.rs b/src/server/games/session.rs
new file mode 100644
index 0000000..97ceaf3
--- /dev/null
+++ b/src/server/games/session.rs
@@ -0,0 +1,77 @@
1#![allow(dead_code)]
2
3use super::traits::{GameError, GameState};
4use std::collections::HashMap;
5use std::sync::{Arc, Mutex};
6use uuid::Uuid;
7
8#[derive(Debug, Clone)]
9pub struct GameSession {
10 pub session_id: String,
11 pub username: String,
12 pub game_type: String,
13 pub bet_amount: u64,
14 pub state: GameState,
15}
16
17pub struct SessionManager {
18 sessions: Arc<Mutex<HashMap<String, GameSession>>>,
19}
20
21impl SessionManager {
22 pub fn new() -> Self {
23 Self {
24 sessions: Arc::new(Mutex::new(HashMap::new())),
25 }
26 }
27
28 pub fn create_session(&self, username: &str, game_type: String, state: GameState) -> String {
29 let session_id = Uuid::new_v4().to_string();
30 let session = GameSession {
31 session_id: session_id.clone(),
32 username: username.to_string(),
33 game_type,
34 bet_amount: state.bet_amount,
35 state,
36 };
37
38 let mut sessions = self.sessions.lock().unwrap();
39 sessions.insert(session_id.clone(), session);
40 session_id
41 }
42
43 pub fn get_session(&self, session_id: &str) -> Result<GameSession, GameError> {
44 let sessions = self.sessions.lock().unwrap();
45 sessions
46 .get(session_id)
47 .cloned()
48 .ok_or(GameError::SessionNotFound)
49 }
50
51 pub fn update_session(&self, session_id: &str, new_state: GameState) -> Result<(), GameError> {
52 let mut sessions = self.sessions.lock().unwrap();
53 if let Some(session) = sessions.get_mut(session_id) {
54 session.state = new_state;
55 Ok(())
56 } else {
57 Err(GameError::SessionNotFound)
58 }
59 }
60
61 pub fn remove_session(&self, session_id: &str) -> Result<(), GameError> {
62 let mut sessions = self.sessions.lock().unwrap();
63 sessions
64 .remove(session_id)
65 .ok_or(GameError::SessionNotFound)
66 .map(|_| ())
67 }
68
69 pub fn get_user_sessions(&self, username: &str) -> Vec<GameSession> {
70 let sessions = self.sessions.lock().unwrap();
71 sessions
72 .values()
73 .filter(|s| s.username == username)
74 .cloned()
75 .collect()
76 }
77}
diff --git a/src/server/games/slots.rs b/src/server/games/slots.rs
new file mode 100644
index 0000000..0a9797d
--- /dev/null
+++ b/src/server/games/slots.rs
@@ -0,0 +1,179 @@
1use super::traits::{Game, GameError, GamePhase, GameResult, GameState};
2use rand::Rng;
3use std::collections::HashMap;
4
5pub struct SlotsGame {
6 symbols: Vec<char>,
7 payouts: HashMap<String, u64>,
8}
9
10impl SlotsGame {
11 pub fn new() -> Self {
12 let symbols = vec!['C', 'L', 'O', 'G', 'D', 'S'];
13 let mut payouts = HashMap::new();
14
15 // Balanced payouts: EV = 210/216 ≈ 0.972 (2.8% house edge)
16 payouts.insert("CCC".to_string(), 2);
17 payouts.insert("LLL".to_string(), 3);
18 payouts.insert("OOO".to_string(), 5);
19 payouts.insert("GGG".to_string(), 8);
20 payouts.insert("DDD".to_string(), 14);
21 payouts.insert("SSS".to_string(), 28);
22
23 payouts.insert("CC".to_string(), 1);
24 payouts.insert("LL".to_string(), 1);
25 payouts.insert("OO".to_string(), 1);
26 payouts.insert("GG".to_string(), 2);
27 payouts.insert("DD".to_string(), 2);
28 payouts.insert("SS".to_string(), 3);
29
30 Self { symbols, payouts }
31 }
32
33 fn spin_reels(&self) -> Vec<char> {
34 let mut rng = rand::thread_rng();
35 (0..3)
36 .map(|_| self.symbols[rng.gen_range(0..self.symbols.len())])
37 .collect()
38 }
39
40 pub fn calculate_payout(&self, reels: &[char], bet: u64) -> u64 {
41 let reel_string: String = reels.iter().collect();
42
43 if let Some(&multiplier) = self.payouts.get(&reel_string) {
44 return bet * multiplier;
45 }
46
47 // Check all three pairs including non-adjacent (e.g. [C,D,C])
48 let pairs: [(usize, usize); 3] = [(0, 1), (1, 2), (0, 2)];
49 let mut best: u64 = 0;
50 for &(a, b) in &pairs {
51 if reels[a] == reels[b] {
52 let two_of_kind = format!("{}{}", reels[a], reels[a]);
53 if let Some(&multiplier) = self.payouts.get(&two_of_kind) {
54 best = best.max(bet * multiplier);
55 }
56 }
57 }
58 best
59 }
60}
61
62impl Game for SlotsGame {
63 fn game_type(&self) -> &'static str {
64 "slots"
65 }
66
67 fn get_name(&self) -> &'static str {
68 "Slots"
69 }
70
71 fn get_description(&self) -> &'static str {
72 "Spin the reels and match symbols to win!"
73 }
74
75 fn prepare_state(&self, username: &str, bet_amount: u64) -> GameState {
76 let mut data = HashMap::new();
77 data.insert("username".to_string(), username.to_string());
78 data.insert("status".to_string(), "ready".to_string());
79
80 GameState {
81 session_id: String::new(),
82 game_type: "slots".to_string(),
83 bet_amount,
84 phase: GamePhase::WaitingToStart,
85 data,
86 }
87 }
88
89 fn process_action(
90 &self,
91 state: &GameState,
92 action: &str,
93 _params: &[&str],
94 ) -> Result<GameState, GameError> {
95 if state.phase == GamePhase::Finished {
96 return Err(GameError::GameOver);
97 }
98
99 match action {
100 "spin" => {
101 if state.phase != GamePhase::WaitingToStart && state.phase != GamePhase::Playing {
102 return Err(GameError::InvalidAction(
103 "Cannot spin in current phase".to_string(),
104 ));
105 }
106
107 let reels = self.spin_reels();
108 let reel_string: String = reels.iter().collect();
109
110 let mut new_data = state.data.clone();
111 new_data.insert("reels".to_string(), reel_string.clone());
112 new_data.insert("status".to_string(), "spun".to_string());
113
114 let is_jackpot = reels[0] == reels[1] && reels[1] == reels[2];
115 let is_two = reels[0] == reels[1] || reels[1] == reels[2] || reels[0] == reels[2];
116 new_data.insert(
117 "win_type".to_string(),
118 if is_jackpot {
119 "jackpot".to_string()
120 } else if is_two {
121 "two".to_string()
122 } else {
123 "none".to_string()
124 },
125 );
126
127 Ok(GameState {
128 session_id: state.session_id.clone(),
129 game_type: state.game_type.clone(),
130 bet_amount: state.bet_amount,
131 phase: GamePhase::Finished,
132 data: new_data,
133 })
134 }
135 _ => Err(GameError::InvalidAction(format!(
136 "Unknown action: {}",
137 action
138 ))),
139 }
140 }
141
142 fn calculate_result(&self, state: &GameState) -> Result<GameResult, GameError> {
143 if state.phase != GamePhase::Finished {
144 return Err(GameError::InternalError("Game not finished".to_string()));
145 }
146
147 let reel_string = state
148 .data
149 .get("reels")
150 .ok_or(GameError::InternalError("No reels found".to_string()))?;
151
152 let reels: Vec<char> = reel_string.chars().collect();
153 let winnings = self.calculate_payout(&reels, state.bet_amount);
154
155 let mut result_data = HashMap::new();
156 result_data.insert("reels".to_string(), reel_string.clone());
157 result_data.insert(
158 "payout_multiplier".to_string(),
159 if winnings > 0 && state.bet_amount > 0 {
160 (winnings / state.bet_amount).to_string()
161 } else {
162 "0".to_string()
163 },
164 );
165 result_data.insert(
166 "win_type".to_string(),
167 state.data.get("win_type").cloned().unwrap_or_default(),
168 );
169
170 Ok(GameResult {
171 session_id: state.session_id.clone(),
172 game_type: state.game_type.clone(),
173 bet_amount: state.bet_amount,
174 winnings,
175 final_balance: 0,
176 result_data,
177 })
178 }
179}
diff --git a/src/server/games/traits.rs b/src/server/games/traits.rs
new file mode 100644
index 0000000..d56b334
--- /dev/null
+++ b/src/server/games/traits.rs
@@ -0,0 +1,67 @@
1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub enum GameError {
6 InvalidAction(String),
7 InsufficientBalance,
8 InvalidBet,
9 GameOver,
10 SessionNotFound,
11 InternalError(String),
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub enum GamePhase {
16 WaitingToStart,
17 Playing,
18 Finished,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct GameState {
23 pub session_id: String,
24 pub game_type: String,
25 pub bet_amount: u64,
26 pub phase: GamePhase,
27 pub data: HashMap<String, String>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct GameResult {
32 pub session_id: String,
33 pub game_type: String,
34 pub bet_amount: u64,
35 pub winnings: u64,
36 pub final_balance: u64,
37 pub result_data: HashMap<String, String>,
38}
39
40pub trait Game: Send + Sync {
41 fn game_type(&self) -> &'static str;
42
43 fn get_name(&self) -> &'static str;
44
45 fn get_description(&self) -> &'static str;
46
47 fn validate_bet(&self, bet_amount: u64, user_balance: u64) -> Result<(), GameError> {
48 if bet_amount == 0 {
49 return Err(GameError::InvalidBet);
50 }
51 if bet_amount > user_balance {
52 return Err(GameError::InsufficientBalance);
53 }
54 Ok(())
55 }
56
57 fn prepare_state(&self, username: &str, bet_amount: u64) -> GameState;
58
59 fn process_action(
60 &self,
61 state: &GameState,
62 action: &str,
63 params: &[&str],
64 ) -> Result<GameState, GameError>;
65
66 fn calculate_result(&self, state: &GameState) -> Result<GameResult, GameError>;
67}