summaryrefslogtreecommitdiffstats
path: root/src/server/core.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-29 18:22:40 +0100
committersalmonik <salmonik@tuta.io>2026-10-29 18:22:40 +0100
commitb5a52fc0312afdf5d5fbe53275dc88d6d881c2ff (patch)
treed12e3c7f6c23ae1698199d16ee763551edfd8c12 /src/server/core.rs
parentv2.1.2: ignore ANSI when measuring board height (diff)
downloadzagan-bet-b5a52fc0312afdf5d5fbe53275dc88d6d881c2ff.tar.gz
zagan-bet-b5a52fc0312afdf5d5fbe53275dc88d6d881c2ff.zip
v2.2.0: add blackjack with reserved bets and hit/stand/double
Diffstat (limited to '')
-rw-r--r--src/server/core.rs187
1 files changed, 185 insertions, 2 deletions
diff --git a/src/server/core.rs b/src/server/core.rs
index 2ce8b2b..cf8c4b9 100644
--- a/src/server/core.rs
+++ b/src/server/core.rs
@@ -1,10 +1,13 @@
1use bcrypt::{hash, verify, DEFAULT_COST}; 1use bcrypt::{hash, verify, DEFAULT_COST};
2use rusqlite::Connection; 2use rusqlite::Connection;
3use std::collections::HashMap;
3 4
5use crate::blackjack::BlackjackState;
4use crate::server::database; 6use crate::server::database;
7use crate::server::games::blackjack::{BlackjackGame, BlackjackHand};
5use crate::server::games::registry::GameRegistry; 8use crate::server::games::registry::GameRegistry;
6use crate::server::games::session::SessionManager; 9use crate::server::games::session::SessionManager;
7use crate::server::games::traits::GameError; 10use crate::server::games::traits::{GameError, GamePhase, GameState};
8 11
9#[derive(Debug, Default)] 12#[derive(Debug, Default)]
10pub struct ConnectionState { 13pub struct ConnectionState {
@@ -22,6 +25,7 @@ pub struct ServerCore {
22 sessions: SessionManager, 25 sessions: SessionManager,
23 registry: GameRegistry, 26 registry: GameRegistry,
24 bcrypt_cost: u32, 27 bcrypt_cost: u32,
28 blackjack: BlackjackGame,
25} 29}
26 30
27impl ServerCore { 31impl ServerCore {
@@ -39,9 +43,16 @@ impl ServerCore {
39 sessions: SessionManager::new(), 43 sessions: SessionManager::new(),
40 registry, 44 registry,
41 bcrypt_cost, 45 bcrypt_cost,
46 blackjack: BlackjackGame::new(),
42 } 47 }
43 } 48 }
44 49
50 /// Test/utility hook: use a fixed shoe for the next hand(s).
51 pub fn with_blackjack_deck(mut self, deck: Vec<String>) -> Self {
52 self.blackjack = BlackjackGame::with_deck(deck);
53 self
54 }
55
45 pub fn open(path: &str) -> rusqlite::Result<Self> { 56 pub fn open(path: &str) -> rusqlite::Result<Self> {
46 let conn = Connection::open(path)?; 57 let conn = Connection::open(path)?;
47 database::init_db(&conn); 58 database::init_db(&conn);
@@ -85,13 +96,23 @@ impl ServerCore {
85 } 96 }
86 } 97 }
87 "PLAY_START" => { 98 "PLAY_START" => {
88 if parts.len() != 3 && parts.len() != 4 { 99 if parts.len() == 3 && parts[1] == "blackjack" {
100 self.handle_blackjack_start(state, parts[2])
101 } else if parts.len() != 3 && parts.len() != 4 {
89 "ERROR Invalid PLAY_START format. Usage: PLAY_START <game_type> <bet_amount> [payload]" 102 "ERROR Invalid PLAY_START format. Usage: PLAY_START <game_type> <bet_amount> [payload]"
90 .to_string() 103 .to_string()
91 } else { 104 } else {
92 self.handle_play_start(state, parts[1], parts[2], parts.get(3).copied()) 105 self.handle_play_start(state, parts[1], parts[2], parts.get(3).copied())
93 } 106 }
94 } 107 }
108 "BLACKJACK_ACTION" => {
109 if parts.len() != 3 {
110 "ERROR Invalid BLACKJACK_ACTION format. Usage: BLACKJACK_ACTION <session> <action>"
111 .to_string()
112 } else {
113 self.handle_blackjack_action(state, parts[1], parts[2])
114 }
115 }
95 "REGISTER" => { 116 "REGISTER" => {
96 if parts.len() != 3 { 117 if parts.len() != 3 {
97 "ERROR Invalid REGISTER format".to_string() 118 "ERROR Invalid REGISTER format".to_string()
@@ -182,6 +203,158 @@ impl ServerCore {
182 } 203 }
183 } 204 }
184 205
206 fn handle_blackjack_start(&mut self, state: &ConnectionState, bet_str: &str) -> String {
207 let Some(user) = state.user.as_deref() else {
208 return "ERROR Not logged in".to_string();
209 };
210 if self
211 .sessions
212 .get_user_sessions(user)
213 .iter()
214 .any(|session| session.game_type == "blackjack")
215 {
216 return "ERROR Blackjack hand already in progress".to_string();
217 }
218 let bet: i64 = match bet_str.parse() {
219 Ok(bet) => bet,
220 Err(_) => return "ERROR Invalid bet amount".to_string(),
221 };
222 if bet <= 0 {
223 return "ERROR Invalid bet amount".to_string();
224 }
225 let Some(balance) = database::get_balance(&self.conn, user) else {
226 return "ERROR Could not retrieve balance".to_string();
227 };
228 if balance < bet {
229 return "ERROR Insufficient balance".to_string();
230 }
231 let balance = match database::debit(&mut self.conn, user, bet) {
232 Ok(balance) => balance,
233 Err(_) => return "ERROR Insufficient balance".to_string(),
234 };
235
236 let hand = self.blackjack.deal(bet as u64);
237 let session_id = self.create_blackjack_session(user, &hand);
238 self.blackjack_response(user, &session_id, hand, balance)
239 }
240
241 fn create_blackjack_session(&mut self, user: &str, hand: &BlackjackHand) -> String {
242 let mut data = HashMap::new();
243 data.insert("hand".to_string(), hand.to_json());
244 let game_state = GameState {
245 session_id: String::new(),
246 game_type: "blackjack".to_string(),
247 bet_amount: hand.bet,
248 phase: GamePhase::Playing,
249 data,
250 };
251 self.sessions
252 .create_session(user, "blackjack".to_string(), game_state)
253 }
254
255 fn handle_blackjack_action(
256 &mut self,
257 state: &ConnectionState,
258 session_id: &str,
259 action: &str,
260 ) -> String {
261 let Some(user) = state.user.as_deref() else {
262 return "ERROR Not logged in".to_string();
263 };
264 let Ok(session) = self.sessions.get_session(session_id) else {
265 return "ERROR Session not found".to_string();
266 };
267 if session.username != user || session.game_type != "blackjack" {
268 return "ERROR Invalid session".to_string();
269 }
270 let Some(raw) = session.state.data.get("hand") else {
271 return "ERROR Missing hand".to_string();
272 };
273 let Ok(mut hand) = BlackjackHand::from_json(raw) else {
274 return "ERROR Invalid hand".to_string();
275 };
276 if hand.is_done() {
277 return "ERROR Hand is over".to_string();
278 }
279
280 let balance = database::get_balance(&self.conn, user).unwrap_or(0);
281 match action {
282 "hit" => {
283 if let Err(error) = hand.hit() {
284 return format!("ERROR {error}");
285 }
286 }
287 "stand" => hand.stand(),
288 "double" => {
289 if hand.player.len() != 2 {
290 return "ERROR Double is only allowed on the first two cards".to_string();
291 }
292 if database::debit(&mut self.conn, user, hand.base_bet as i64).is_err() {
293 return "ERROR Insufficient balance".to_string();
294 }
295 if let Err(error) = hand.double() {
296 let _ = database::credit(&mut self.conn, user, hand.base_bet as i64);
297 return format!("ERROR {error}");
298 }
299 }
300 "cancel" => {
301 let refund = session.bet_amount as i64;
302 let balance = database::credit(&mut self.conn, user, refund).unwrap_or(balance);
303 let _ = self.sessions.remove_session(session_id);
304 hand.cancel();
305 let state = hand.state(session_id, balance);
306 return blackjack_wire(&state);
307 }
308 _ => return "ERROR Unknown action".to_string(),
309 }
310
311 let balance = database::get_balance(&self.conn, user).unwrap_or(balance);
312 self.blackjack_response(user, session_id, hand, balance)
313 }
314
315 fn blackjack_response(
316 &mut self,
317 user: &str,
318 session_id: &str,
319 hand: BlackjackHand,
320 balance: i64,
321 ) -> String {
322 if hand.is_done() {
323 let state = hand.state(session_id, balance);
324 let details = serde_json::to_string(&state).unwrap_or_else(|_| "{}".to_string());
325 let balance = match database::settle_play(
326 &mut self.conn,
327 session_id,
328 user,
329 "blackjack",
330 hand.bet as i64,
331 hand.winnings as i64,
332 &details,
333 ) {
334 Ok(balance) => balance,
335 Err(error) => {
336 eprintln!("Blackjack settlement failed for {user}: {error}");
337 let _ = self.sessions.remove_session(session_id);
338 return "ERROR Failed to update balance".to_string();
339 }
340 };
341 let _ = self.sessions.remove_session(session_id);
342 let state = hand.state(session_id, balance);
343 blackjack_wire(&state)
344 } else {
345 if let Ok(mut session) = self.sessions.get_session(session_id) {
346 session.state.bet_amount = hand.bet;
347 session
348 .state
349 .data
350 .insert("hand".to_string(), hand.to_json());
351 let _ = self.sessions.update_session(session_id, session.state);
352 }
353 let state = hand.state(session_id, balance);
354 blackjack_wire(&state)
355 }
356 }
357
185 fn handle_play_start( 358 fn handle_play_start(
186 &mut self, 359 &mut self,
187 state: &ConnectionState, 360 state: &ConnectionState,
@@ -311,6 +484,9 @@ impl ServerCore {
311 pub fn disconnect(&mut self, state: &ConnectionState) { 484 pub fn disconnect(&mut self, state: &ConnectionState) {
312 if let Some(user) = state.user.as_deref() { 485 if let Some(user) = state.user.as_deref() {
313 for session in self.sessions.get_user_sessions(user) { 486 for session in self.sessions.get_user_sessions(user) {
487 if session.game_type == "blackjack" {
488 let _ = database::credit(&mut self.conn, user, session.bet_amount as i64);
489 }
314 let _ = self.sessions.remove_session(&session.session_id); 490 let _ = self.sessions.remove_session(&session.session_id);
315 } 491 }
316 } 492 }
@@ -339,3 +515,10 @@ fn game_error(error: GameError) -> String {
339 GameError::InternalError(message) => format!("ERROR {message}"), 515 GameError::InternalError(message) => format!("ERROR {message}"),
340 } 516 }
341} 517}
518
519fn blackjack_wire(state: &BlackjackState) -> String {
520 format!(
521 "BLACKJACK_STATE {}",
522 serde_json::to_string(state).unwrap_or_else(|_| "{}".to_string())
523 )
524}