summaryrefslogtreecommitdiffstats
path: root/src/bin
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/client/connection.rs2
-rw-r--r--src/bin/client/database.rs118
-rw-r--r--src/bin/client/games/manager.rs134
-rw-r--r--src/bin/client/games/mod.rs3
-rw-r--r--src/bin/client/games/slots.rs61
-rw-r--r--src/bin/client/games/traits.rs32
-rw-r--r--src/bin/client/main.rs2
-rw-r--r--src/bin/client/mod.rs6
-rw-r--r--src/bin/client/repl.rs6
-rw-r--r--src/bin/server/database.rs74
-rw-r--r--src/bin/server/games/mod.rs4
-rw-r--r--src/bin/server/games/registry.rs55
-rw-r--r--src/bin/server/games/session.rs77
-rw-r--r--src/bin/server/games/slots.rs179
-rw-r--r--src/bin/server/games/traits.rs67
-rw-r--r--src/bin/server/handler.rs8
-rw-r--r--src/bin/server/main.rs2
-rw-r--r--src/bin/server/mod.rs3
18 files changed, 8 insertions, 825 deletions
diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs
index 6aff321..e54b646 100644
--- a/src/bin/client/connection.rs
+++ b/src/bin/client/connection.rs
@@ -1,4 +1,4 @@
1use crate::database; 1use zagan_bet::client::database;
2use crate::tls::TlsConnector; 2use crate::tls::TlsConnector;
3use rusqlite::Connection; 3use rusqlite::Connection;
4use std::io::{Read, Write}; 4use std::io::{Read, Write};
diff --git a/src/bin/client/database.rs b/src/bin/client/database.rs
deleted file mode 100644
index e1fa42d..0000000
--- a/src/bin/client/database.rs
+++ /dev/null
@@ -1,118 +0,0 @@
1use rusqlite::Connection;
2
3pub fn init_db(conn: &Connection) {
4 conn.execute(
5 "CREATE TABLE IF NOT EXISTS hosts (
6 host TEXT PRIMARY KEY,
7 last_used DATETIME DEFAULT CURRENT_TIMESTAMP,
8 fingerprint TEXT
9 )",
10 [],
11 )
12 .unwrap();
13 // Migration for databases created before TOFU trust.
14 let _ = conn.execute("ALTER TABLE hosts ADD COLUMN fingerprint TEXT", []);
15 conn.execute(
16 "CREATE TABLE IF NOT EXISTS credentials (
17 host TEXT PRIMARY KEY,
18 username TEXT NOT NULL,
19 password TEXT NOT NULL
20 )",
21 [],
22 )
23 .unwrap();
24}
25
26pub fn get_last_host(conn: &Connection) -> Option<String> {
27 let mut stmt = conn
28 .prepare("SELECT host FROM hosts ORDER BY last_used DESC LIMIT 1")
29 .unwrap();
30 let mut rows = stmt.query([]).unwrap();
31 if let Ok(Some(row)) = rows.next() {
32 row.get(0).ok()
33 } else {
34 None
35 }
36}
37
38pub fn save_host(conn: &Connection, host: &str) {
39 conn.execute(
40 "INSERT INTO hosts (host) VALUES (?1)
41 ON CONFLICT(host) DO UPDATE SET last_used = CURRENT_TIMESTAMP",
42 [host],
43 )
44 .unwrap();
45}
46
47pub fn save_credentials(
48 conn: &Connection,
49 host: &str,
50 username: &str,
51 password: &str,
52) -> Result<(), Box<dyn std::error::Error>> {
53 conn.execute(
54 "INSERT OR REPLACE INTO credentials (host, username, password) VALUES (?1, ?2, ?3)",
55 (host, username, password),
56 )?;
57 Ok(())
58}
59
60pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)> {
61 let mut stmt = conn
62 .prepare("SELECT username, password FROM credentials WHERE host = ?1")
63 .unwrap();
64 let mut rows = stmt.query([host]).unwrap();
65 if let Ok(Some(row)) = rows.next() {
66 let username: String = row.get(0).unwrap();
67 let password: String = row.get(1).unwrap();
68 Some((username, password))
69 } else {
70 None
71 }
72}
73
74/// SHA-256 SPKI fingerprint of a host's certificate, if trusted.
75pub fn get_fingerprint(conn: &Connection, host: &str) -> Option<String> {
76 let mut stmt = conn
77 .prepare("SELECT fingerprint FROM hosts WHERE host = ?1")
78 .ok()?;
79 let mut rows = stmt.query([host]).ok()?;
80 match rows.next() {
81 Ok(Some(row)) => row.get(0).ok(),
82 _ => None,
83 }
84}
85
86pub fn save_fingerprint(
87 conn: &Connection,
88 host: &str,
89 fingerprint: &str,
90) -> Result<(), rusqlite::Error> {
91 // Hosts may not exist yet (fingerprint can be saved before first
92 // successful connect completes save_host).
93 conn.execute("INSERT OR IGNORE INTO hosts (host) VALUES (?1)", [host])?;
94 conn.execute(
95 "UPDATE hosts SET fingerprint = ?2 WHERE host = ?1",
96 (host, fingerprint),
97 )?;
98 Ok(())
99}
100
101/// Remove all trust and credentials for a host ("forget this host").
102pub fn forget_host(conn: &Connection, host: &str) -> Result<(), rusqlite::Error> {
103 conn.execute("DELETE FROM hosts WHERE host = ?1", [host])?;
104 conn.execute("DELETE FROM credentials WHERE host = ?1", [host])?;
105 Ok(())
106}
107
108/// List all remembered hosts with their fingerprints.
109pub fn get_all_hosts(conn: &Connection) -> Vec<(String, Option<String>)> {
110 let mut stmt = conn
111 .prepare("SELECT host, fingerprint FROM hosts ORDER BY last_used DESC")
112 .unwrap();
113 let rows = stmt.query_map([], |row| {
114 Ok((row.get(0)?, row.get(1)?))
115 })
116 .unwrap();
117 rows.filter_map(|r| r.ok()).collect()
118}
diff --git a/src/bin/client/games/manager.rs b/src/bin/client/games/manager.rs
deleted file mode 100644
index 0b5b4cb..0000000
--- a/src/bin/client/games/manager.rs
+++ /dev/null
@@ -1,134 +0,0 @@
1use crate::games::slots::SlotsGame;
2use crate::games::traits::{Game, GameError, GamePhase, GameState};
3use std::collections::HashMap;
4use std::io::Write;
5
6pub struct GameManager {
7 current_session: Option<GameState>,
8 pending_game_type: Option<String>,
9 games: HashMap<String, Box<dyn Game>>,
10}
11
12impl GameManager {
13 pub fn new() -> Self {
14 let mut games: HashMap<String, Box<dyn Game>> = HashMap::new();
15 games.insert("slots".to_string(), Box::new(SlotsGame::new()));
16
17 Self {
18 current_session: None,
19 pending_game_type: None,
20 games,
21 }
22 }
23
24 pub fn get_available_games(&self) -> Vec<(&str, &str)> {
25 self.games
26 .iter()
27 .map(|(k, v)| (k.as_str(), v.get_name()))
28 .collect()
29 }
30
31 pub fn enter_game(&mut self, game_type: &str) -> Result<(), GameError> {
32 if self.current_session.is_some() {
33 return Err(GameError::InvalidAction);
34 }
35
36 if !self.games.contains_key(game_type) {
37 return Err(GameError::InvalidAction);
38 }
39
40 self.pending_game_type = Some(game_type.to_string());
41
42 self.current_session = Some(GameState {
43 game_type: game_type.to_string(),
44 bet_amount: 10,
45 phase: GamePhase::WaitingToStart,
46 data: HashMap::new(),
47 });
48
49 Ok(())
50 }
51
52 pub fn start_game(
53 &mut self,
54 stream: &mut crate::connection::TlsStream,
55 ) -> Result<(), GameError> {
56 let game_type = self
57 .pending_game_type
58 .as_ref()
59 .ok_or(GameError::InvalidAction)?;
60
61 let state = self
62 .current_session
63 .as_mut()
64 .ok_or(GameError::InvalidAction)?;
65
66 if state.phase != GamePhase::WaitingToStart && state.phase != GamePhase::Finished {
67 return Err(GameError::InvalidAction);
68 }
69
70 let bet_amount = state.bet_amount;
71
72 let writer = stream;
73 writeln!(writer, "PLAY_START {} {}", game_type, bet_amount)
74 .map_err(|_| GameError::NetworkError)?;
75
76 Ok(())
77 }
78
79 pub fn get_current_game(&self) -> Option<&dyn Game> {
80 self.current_session
81 .as_ref()
82 .and_then(|session| self.games.get(&session.game_type).map(|game| game.as_ref()))
83 }
84
85 pub fn get_current_state(&self) -> Option<&GameState> {
86 self.current_session.as_ref()
87 }
88
89 pub fn get_current_render_lines(&self) -> Option<usize> {
90 let session = self.current_session.as_ref()?;
91 let game = self.games.get(&session.game_type)?;
92 Some(game.render_lines(session))
93 }
94
95 pub fn is_in_game(&self) -> bool {
96 self.current_session.is_some()
97 }
98
99 pub fn is_game_started(&self) -> bool {
100 self.current_session
101 .as_ref()
102 .map(|s| s.phase != GamePhase::WaitingToStart)
103 .unwrap_or(false)
104 }
105
106 pub fn reset_phase_to_waiting(&mut self) {
107 if let Some(ref mut session) = self.current_session {
108 session.phase = GamePhase::WaitingToStart;
109 session.data.clear();
110 }
111 }
112
113 pub fn end_game(&mut self) {
114 self.current_session = None;
115 self.pending_game_type = None;
116 }
117
118 pub fn adjust_bet(&mut self, delta: i64) -> Result<u64, GameError> {
119 if let Some(ref session) = self.current_session {
120 if session.phase != GamePhase::WaitingToStart {
121 return Err(GameError::InvalidAction);
122 }
123 }
124
125 if let Some(ref mut session) = self.current_session {
126 let current_bet = session.bet_amount as i64;
127 let new_bet = (current_bet + delta).max(10) as u64;
128 session.bet_amount = new_bet;
129 Ok(new_bet)
130 } else {
131 Err(GameError::InvalidAction)
132 }
133 }
134}
diff --git a/src/bin/client/games/mod.rs b/src/bin/client/games/mod.rs
deleted file mode 100644
index aa74574..0000000
--- a/src/bin/client/games/mod.rs
+++ /dev/null
@@ -1,3 +0,0 @@
1pub mod manager;
2pub mod slots;
3pub mod traits;
diff --git a/src/bin/client/games/slots.rs b/src/bin/client/games/slots.rs
deleted file mode 100644
index ff55274..0000000
--- a/src/bin/client/games/slots.rs
+++ /dev/null
@@ -1,61 +0,0 @@
1use crate::games::traits::{Game, GameState};
2
3pub struct SlotsGame;
4
5impl SlotsGame {
6 pub fn new() -> Self {
7 Self
8 }
9
10 fn render_reels(&self, reels: &str) -> String {
11 if reels.is_empty() {
12 "[ ? ] [ ? ] [ ? ]".to_string()
13 } else {
14 let chars: Vec<char> = reels.chars().collect();
15 format!(
16 "[ {} ] [ {} ] [ {} ]\r",
17 chars.get(0).unwrap_or(&'?'),
18 chars.get(1).unwrap_or(&'?'),
19 chars.get(2).unwrap_or(&'?')
20 )
21 }
22 }
23}
24
25impl Game for SlotsGame {
26 fn get_name(&self) -> &'static str {
27 "Slots"
28 }
29
30 fn render(&self, state: &GameState) -> String {
31 let reels = state.data.get("reels").cloned().unwrap_or_default();
32
33 format!(
34 "=== SLOTS ===\r\nBet: {} PLN\r\n{}\r\n\r\nPress [SPACE] to spin!\r\n\r\nControls: [+/-] Adjust Bet [Q] Quit",
35 state.bet_amount,
36 self.render_reels(&reels)
37 )
38 }
39
40 fn render_lines(&self, _state: &GameState) -> usize {
41 6
42 }
43
44 fn get_help(&self) -> String {
45 "SLOTS GAME\n\
46 =========\n\
47 Match symbols to win!\n\
48 \n\
49 Payouts:\n\
50 Three of a kind: 2x-28x bet\n\
51 Two of a kind: 1x-3x bet\n\
52 \n\
53 Symbols: C=Cherry, L=Lemon, O=Orange, G=Grapes, D=Diamond, S=Seven\n\
54 \n\
55 Controls:\n\
56 SPACE - Spin the reels\n\
57 +/- - Adjust bet amount\n\
58 Q - Exit the game"
59 .to_string()
60 }
61}
diff --git a/src/bin/client/games/traits.rs b/src/bin/client/games/traits.rs
deleted file mode 100644
index aed0c24..0000000
--- a/src/bin/client/games/traits.rs
+++ /dev/null
@@ -1,32 +0,0 @@
1use std::collections::HashMap;
2
3#[derive(Debug, Clone)]
4pub enum GameError {
5 InvalidAction,
6 NetworkError,
7}
8
9#[derive(Debug, Clone, PartialEq)]
10pub enum GamePhase {
11 WaitingToStart,
12 Playing,
13 Finished,
14}
15
16#[derive(Debug, Clone)]
17pub struct GameState {
18 pub game_type: String,
19 pub bet_amount: u64,
20 pub phase: GamePhase,
21 pub data: HashMap<String, String>,
22}
23
24pub trait Game: Send + Sync {
25 fn get_name(&self) -> &'static str;
26
27 fn render(&self, state: &GameState) -> String;
28
29 fn render_lines(&self, state: &GameState) -> usize;
30
31 fn get_help(&self) -> String;
32}
diff --git a/src/bin/client/main.rs b/src/bin/client/main.rs
index 4e03381..1b701d8 100644
--- a/src/bin/client/main.rs
+++ b/src/bin/client/main.rs
@@ -1,8 +1,6 @@
1#[macro_use] 1#[macro_use]
2mod macros; 2mod macros;
3mod connection; 3mod connection;
4mod database;
5mod games;
6mod repl; 4mod repl;
7mod tls; 5mod tls;
8 6
diff --git a/src/bin/client/mod.rs b/src/bin/client/mod.rs
deleted file mode 100644
index 5921c41..0000000
--- a/src/bin/client/mod.rs
+++ /dev/null
@@ -1,6 +0,0 @@
1pub mod database;
2mod connection;
3mod games;
4pub mod platform;
5pub mod tls;
6pub mod repl;
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs
index a6f0331..24605e2 100644
--- a/src/bin/client/repl.rs
+++ b/src/bin/client/repl.rs
@@ -4,9 +4,9 @@ use std::sync::{Arc, Mutex};
4use std::time::Instant; 4use std::time::Instant;
5 5
6use crate::connection::TlsStream; 6use crate::connection::TlsStream;
7use crate::database; 7use zagan_bet::client::database;
8use crate::games::manager::GameManager; 8use zagan_bet::client::games::manager::GameManager;
9use crate::games::traits::GamePhase; 9use zagan_bet::client::games::traits::GamePhase;
10 10
11use crossterm::{ 11use crossterm::{
12 cursor::{Hide, MoveToColumn, Show}, 12 cursor::{Hide, MoveToColumn, Show},
diff --git a/src/bin/server/database.rs b/src/bin/server/database.rs
deleted file mode 100644
index c73aec5..0000000
--- a/src/bin/server/database.rs
+++ /dev/null
@@ -1,74 +0,0 @@
1use rusqlite::Connection;
2
3pub fn init_db(conn: &Connection) {
4 conn.execute(
5 "CREATE TABLE IF NOT EXISTS users (
6 username TEXT PRIMARY KEY,
7 hashed_password TEXT NOT NULL,
8 account_id INTEGER UNIQUE,
9 balance INTEGER DEFAULT 1000
10 )",
11 [],
12 )
13 .unwrap();
14 // Add column if not exists (for migration)
15 let _ = conn.execute("ALTER TABLE users ADD COLUMN account_id INTEGER UNIQUE", []);
16 let _ = conn.execute(
17 "ALTER TABLE users ADD COLUMN balance INTEGER DEFAULT 1000",
18 [],
19 );
20}
21
22pub fn add_user(
23 conn: &Connection,
24 username: &str,
25 hashed_password: &str,
26) -> Result<(), Box<dyn std::error::Error>> {
27 loop {
28 let account_id: u32 = rand::random::<u32>();
29 match conn.execute(
30 "INSERT INTO users (username, hashed_password, account_id, balance) VALUES (?1, ?2, ?3, ?4)",
31 [username, hashed_password, &account_id.to_string(), "1000"],
32 ) {
33 Ok(_) => break,
34 Err(_) => continue, // Retry if account_id conflict
35 }
36 }
37 Ok(())
38}
39
40pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> {
41 let mut stmt = conn
42 .prepare("SELECT hashed_password FROM users WHERE username = ?1")
43 .unwrap();
44 let mut rows = stmt.query([username]).unwrap();
45 if let Ok(Some(row)) = rows.next() {
46 row.get(0).ok()
47 } else {
48 None
49 }
50}
51
52pub fn get_account_id(conn: &Connection, username: &str) -> Option<u32> {
53 let mut stmt = conn
54 .prepare("SELECT account_id FROM users WHERE username = ?1")
55 .unwrap();
56 let mut rows = stmt.query([username]).unwrap();
57 if let Ok(Some(row)) = rows.next() {
58 Some(row.get::<_, u32>(0).ok()?)
59 } else {
60 None
61 }
62}
63
64pub fn get_balance(conn: &Connection, username: &str) -> Option<i64> {
65 let mut stmt = conn
66 .prepare("SELECT balance FROM users WHERE username = ?1")
67 .unwrap();
68 let mut rows = stmt.query([username]).unwrap();
69 if let Ok(Some(row)) = rows.next() {
70 Some(row.get::<_, i64>(0).ok()?)
71 } else {
72 None
73 }
74}
diff --git a/src/bin/server/games/mod.rs b/src/bin/server/games/mod.rs
deleted file mode 100644
index 4c62760..0000000
--- a/src/bin/server/games/mod.rs
+++ /dev/null
@@ -1,4 +0,0 @@
1pub mod registry;
2pub mod session;
3pub mod slots;
4pub mod traits;
diff --git a/src/bin/server/games/registry.rs b/src/bin/server/games/registry.rs
deleted file mode 100644
index e77a138..0000000
--- a/src/bin/server/games/registry.rs
+++ /dev/null
@@ -1,55 +0,0 @@
1#![allow(dead_code)]
2
3use crate::games::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 crate::games::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/bin/server/games/session.rs b/src/bin/server/games/session.rs
deleted file mode 100644
index 595a745..0000000
--- a/src/bin/server/games/session.rs
+++ /dev/null
@@ -1,77 +0,0 @@
1#![allow(dead_code)]
2
3use crate::games::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/bin/server/games/slots.rs b/src/bin/server/games/slots.rs
deleted file mode 100644
index 04ac175..0000000
--- a/src/bin/server/games/slots.rs
+++ /dev/null
@@ -1,179 +0,0 @@
1use crate::games::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/bin/server/games/traits.rs b/src/bin/server/games/traits.rs
deleted file mode 100644
index d56b334..0000000
--- a/src/bin/server/games/traits.rs
+++ /dev/null
@@ -1,67 +0,0 @@
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}
diff --git a/src/bin/server/handler.rs b/src/bin/server/handler.rs
index 0a4934f..06ed5cb 100644
--- a/src/bin/server/handler.rs
+++ b/src/bin/server/handler.rs
@@ -1,7 +1,7 @@
1use crate::database; 1use zagan_bet::server::database;
2use crate::games::registry::get_game_registry; 2use zagan_bet::server::games::registry::get_game_registry;
3use crate::games::session::SessionManager; 3use zagan_bet::server::games::session::SessionManager;
4use crate::games::traits::{GameError, GamePhase}; 4use zagan_bet::server::games::traits::GameError;
5use crate::tls::TlsStream; 5use crate::tls::TlsStream;
6use once_cell::sync::Lazy; 6use once_cell::sync::Lazy;
7use parking_lot::Mutex; 7use parking_lot::Mutex;
diff --git a/src/bin/server/main.rs b/src/bin/server/main.rs
index 4328559..8dba415 100644
--- a/src/bin/server/main.rs
+++ b/src/bin/server/main.rs
@@ -1,6 +1,4 @@
1mod args; 1mod args;
2mod database;
3mod games;
4mod handler; 2mod handler;
5mod tls; 3mod tls;
6 4
diff --git a/src/bin/server/mod.rs b/src/bin/server/mod.rs
deleted file mode 100644
index b2d622e..0000000
--- a/src/bin/server/mod.rs
+++ /dev/null
@@ -1,3 +0,0 @@
1pub mod args;
2pub mod handler;
3pub mod database;