summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/bin/client/repl.rs27
-rw-r--r--src/bin/client/tls.rs17
-rw-r--r--src/bin/server/args.rs5
-rw-r--r--src/bin/server/main.rs6
-rw-r--r--src/bin/server/tls.rs5
-rw-r--r--src/client/core.rs43
-rw-r--r--src/client/database.rs7
-rw-r--r--src/client/games/blackjack.rs11
-rw-r--r--src/client/games/manager.rs8
-rw-r--r--src/client/games/response.rs4
-rw-r--r--src/client/games/roulette.rs12
-rw-r--r--src/client/games/slots.rs6
-rw-r--r--src/client/ui.rs13
-rw-r--r--src/crash.rs4
-rw-r--r--src/roulette.rs10
-rw-r--r--src/server/core.rs45
-rw-r--r--src/server/games/blackjack.rs4
-rw-r--r--src/server/games/roulette.rs10
-rw-r--r--src/server/games/slots.rs4
-rw-r--r--src/server/setup.rs20
-rw-r--r--src/slots.rs10
-rw-r--r--src/tofu.rs5
22 files changed, 88 insertions, 188 deletions
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs
index 7fa462a..9af4ea9 100644
--- a/src/bin/client/repl.rs
+++ b/src/bin/client/repl.rs
@@ -1,14 +1,14 @@
1use rusqlite::Connection; 1use rusqlite::Connection;
2use std::io::{stdout, Write}; 2use std::io::{Write, stdout};
3use std::sync::{Arc, Mutex}; 3use std::sync::{Arc, Mutex};
4use std::time::Instant; 4use std::time::Instant;
5 5
6use crossterm::{ 6use crossterm::{
7 cursor::{MoveToColumn, MoveUp, Show}, 7 cursor::{MoveToColumn, MoveUp, Show},
8 event::{read, Event, KeyCode, KeyEventKind, KeyModifiers}, 8 event::{Event, KeyCode, KeyEventKind, KeyModifiers, read},
9 execute, 9 execute,
10 style::Stylize, 10 style::Stylize,
11 terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, 11 terminal::{Clear, ClearType, disable_raw_mode, enable_raw_mode},
12}; 12};
13 13
14use sha2::{Digest, Sha256}; 14use sha2::{Digest, Sha256};
@@ -20,7 +20,7 @@ use zagan_bet::client::ui::{
20 visual_line_count, 20 visual_line_count,
21}; 21};
22use zagan_bet::protocol::io_failure_message; 22use zagan_bet::protocol::io_failure_message;
23use zagan_bet::roulette::{color as roulette_color, WHEEL}; 23use zagan_bet::roulette::{WHEEL, color as roulette_color};
24 24
25use crate::connection::TlsStream; 25use crate::connection::TlsStream;
26 26
@@ -71,8 +71,7 @@ impl ClientState {
71 self.core.set_connected(true); 71 self.core.set_connected(true);
72 self.core.set_logged_in(authed_user); 72 self.core.set_logged_in(authed_user);
73 let fingerprint = database::get_fingerprint(&self.conn, addr); 73 let fingerprint = database::get_fingerprint(&self.conn, addr);
74 self.core 74 self.core.set_host_info(Some(addr.to_string()), fingerprint);
75 .set_host_info(Some(addr.to_string()), fingerprint);
76 database::save_host(&self.conn, addr); 75 database::save_host(&self.conn, addr);
77 } 76 }
78 Err(e) => { 77 Err(e) => {
@@ -178,12 +177,9 @@ impl ClientState {
178 Effect::Print(lines) => { 177 Effect::Print(lines) => {
179 self.commit_prompt_line(); 178 self.commit_prompt_line();
180 for line in lines { 179 for line in lines {
181 let rendered = if line.starts_with("ERROR") 180 let rendered = if line.starts_with("ERROR") || line.contains("failed") {
182 || line.contains("failed")
183 {
184 line.as_str().red().to_string() 181 line.as_str().red().to_string()
185 } else if line.starts_with("Logged in") 182 } else if line.starts_with("Logged in") || line.starts_with("Successfully")
186 || line.starts_with("Successfully")
187 { 183 {
188 line.as_str().green().to_string() 184 line.as_str().green().to_string()
189 } else if line.starts_with("Available commands:") 185 } else if line.starts_with("Available commands:")
@@ -249,10 +245,7 @@ impl ClientState {
249 Effect::ForgetHost(target) => { 245 Effect::ForgetHost(target) => {
250 self.commit_prompt_line(); 246 self.commit_prompt_line();
251 match database::forget_host(&self.conn, &target) { 247 match database::forget_host(&self.conn, &target) {
252 Ok(_) => println!( 248 Ok(_) => println!("Cleared trust and stored credentials for {}.", target),
253 "Cleared trust and stored credentials for {}.",
254 target
255 ),
256 Err(error) => println!("Failed to forget host: {}", error), 249 Err(error) => println!("Failed to forget host: {}", error),
257 } 250 }
258 } 251 }
@@ -297,9 +290,7 @@ impl ClientState {
297 let roulette = lines 290 let roulette = lines
298 .first() 291 .first()
299 .is_some_and(|first| first.contains("ROULETTE")); 292 .is_some_and(|first| first.contains("ROULETTE"));
300 let crash = lines 293 let crash = lines.first().is_some_and(|first| first.contains("CRASH"));
301 .first()
302 .is_some_and(|first| first.contains("CRASH"));
303 let rendered: Vec<String> = lines 294 let rendered: Vec<String> = lines
304 .iter() 295 .iter()
305 .map(|line| { 296 .map(|line| {
diff --git a/src/bin/client/tls.rs b/src/bin/client/tls.rs
index b46f52b..e14ed98 100644
--- a/src/bin/client/tls.rs
+++ b/src/bin/client/tls.rs
@@ -17,10 +17,10 @@ use std::net::TcpStream;
17use std::sync::Arc; 17use std::sync::Arc;
18 18
19use parking_lot::Mutex; 19use parking_lot::Mutex;
20use rustls::client::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
21use rustls::DigitallySignedStruct; 20use rustls::DigitallySignedStruct;
21use rustls::client::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
22use zagan_bet::protocol::LineBuffer; 22use zagan_bet::protocol::LineBuffer;
23use zagan_bet::tofu::{decide, spki_fingerprint, TrustDecision}; 23use zagan_bet::tofu::{TrustDecision, decide, spki_fingerprint};
24 24
25pub struct TlsConnector { 25pub struct TlsConnector {
26 config: Arc<rustls::ClientConfig>, 26 config: Arc<rustls::ClientConfig>,
@@ -53,7 +53,10 @@ impl TlsConnector {
53 }) 53 })
54 } 54 }
55 55
56 pub fn connect(&self, stream: TcpStream) -> Result<TlsStream, Box<dyn std::error::Error + Send + Sync>> { 56 pub fn connect(
57 &self,
58 stream: TcpStream,
59 ) -> Result<TlsStream, Box<dyn std::error::Error + Send + Sync>> {
57 let mut conn = 60 let mut conn =
58 rustls::ClientConnection::new(Arc::clone(&self.config), self.server_name.clone())?; 61 rustls::ClientConnection::new(Arc::clone(&self.config), self.server_name.clone())?;
59 let mut sock = stream; 62 let mut sock = stream;
@@ -200,10 +203,7 @@ impl TlsStream {
200 } 203 }
201 204
202 /// Apply a read timeout to the underlying TCP socket. 205 /// Apply a read timeout to the underlying TCP socket.
203 pub fn set_read_timeout( 206 pub fn set_read_timeout(&self, duration: Option<std::time::Duration>) -> std::io::Result<()> {
204 &self,
205 duration: Option<std::time::Duration>,
206 ) -> std::io::Result<()> {
207 let mut s = self.session.lock(); 207 let mut s = self.session.lock();
208 s.read_timeout = duration; 208 s.read_timeout = duration;
209 s.sock.set_read_timeout(duration) 209 s.sock.set_read_timeout(duration)
@@ -235,8 +235,7 @@ impl TlsStream {
235 Err(error) 235 Err(error)
236 if matches!( 236 if matches!(
237 error.kind(), 237 error.kind(),
238 std::io::ErrorKind::WouldBlock 238 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
239 | std::io::ErrorKind::TimedOut
240 ) => 239 ) =>
241 { 240 {
242 return Ok(()); 241 return Ok(());
diff --git a/src/bin/server/args.rs b/src/bin/server/args.rs
index 1babd1d..1db2a28 100644
--- a/src/bin/server/args.rs
+++ b/src/bin/server/args.rs
@@ -31,10 +31,7 @@ pub fn parse_args() -> Args {
31 if i + 1 < args.len() { 31 if i + 1 < args.len() {
32 ip = args[i + 1].clone(); 32 ip = args[i + 1].clone();
33 if ip != "127.0.0.1" && ip != "0.0.0.0" { 33 if ip != "127.0.0.1" && ip != "0.0.0.0" {
34 eprintln!( 34 eprintln!("Invalid IP address: {}. Must be 127.0.0.1 or 0.0.0.0", ip);
35 "Invalid IP address: {}. Must be 127.0.0.1 or 0.0.0.0",
36 ip
37 );
38 std::process::exit(1); 35 std::process::exit(1);
39 } 36 }
40 i += 2; 37 i += 2;
diff --git a/src/bin/server/main.rs b/src/bin/server/main.rs
index 85e3134..f42574d 100644
--- a/src/bin/server/main.rs
+++ b/src/bin/server/main.rs
@@ -40,9 +40,9 @@ fn main() {
40 let db_path = std::env::var("SERVER_DB").unwrap_or_else(|_| "server.db".to_string()); 40 let db_path = std::env::var("SERVER_DB").unwrap_or_else(|_| "server.db".to_string());
41 match zagan_bet::server::core::recover_orphaned_reservations(&db_path) { 41 match zagan_bet::server::core::recover_orphaned_reservations(&db_path) {
42 Ok(0) => {} 42 Ok(0) => {}
43 Ok(count) => println!( 43 Ok(count) => {
44 "Settled {count} orphaned reservation(s) as abandoned after a restart" 44 println!("Settled {count} orphaned reservation(s) as abandoned after a restart")
45 ), 45 }
46 Err(error) => { 46 Err(error) => {
47 eprintln!("Failed to settle orphaned reservations in {db_path}: {error}"); 47 eprintln!("Failed to settle orphaned reservations in {db_path}: {error}");
48 std::process::exit(1); 48 std::process::exit(1);
diff --git a/src/bin/server/tls.rs b/src/bin/server/tls.rs
index 5d84589..420b5b4 100644
--- a/src/bin/server/tls.rs
+++ b/src/bin/server/tls.rs
@@ -80,10 +80,7 @@ impl TlsStream {
80 80
81 /// Read timeout for the socket. The connection loop uses a short one so it 81 /// Read timeout for the socket. The connection loop uses a short one so it
82 /// can advance crash rounds while no request is in flight. 82 /// can advance crash rounds while no request is in flight.
83 pub fn set_read_timeout( 83 pub fn set_read_timeout(&self, duration: Option<std::time::Duration>) -> std::io::Result<()> {
84 &self,
85 duration: Option<std::time::Duration>,
86 ) -> std::io::Result<()> {
87 self.stream.get_ref().set_read_timeout(duration) 84 self.stream.get_ref().set_read_timeout(duration)
88 } 85 }
89} 86}
diff --git a/src/client/core.rs b/src/client/core.rs
index 5f8c513..06d9c42 100644
--- a/src/client/core.rs
+++ b/src/client/core.rs
@@ -3,8 +3,8 @@ use crate::client::games::response::{
3 parse_blackjack_response, parse_crash_response, parse_roulette_response, parse_spin_response, 3 parse_blackjack_response, parse_crash_response, parse_roulette_response, parse_spin_response,
4}; 4};
5use crate::client::ui::{ 5use crate::client::ui::{
6 command_names, completion_lines, display_width, help_lines, input_cursor_column, 6 command_names, completion_lines, display_width, help_lines, input_cursor_column, parse_bet_arg,
7 parse_bet_arg, record_history, 7 record_history,
8}; 8};
9use crate::crash::{multiplier_at, CrashState}; 9use crate::crash::{multiplier_at, CrashState};
10use crate::roulette::{Bets, NumberBet}; 10use crate::roulette::{Bets, NumberBet};
@@ -225,10 +225,7 @@ impl ClientCore {
225 "Game exited. Active hand forfeited ({} PLN lost).", 225 "Game exited. Active hand forfeited ({} PLN lost).",
226 state.bet 226 state.bet
227 ); 227 );
228 vec![ 228 vec![Effect::ClearRegion, Effect::Print(vec![message])]
229 Effect::ClearRegion,
230 Effect::Print(vec![message]),
231 ]
232 } 229 }
233 Err(message) => { 230 Err(message) => {
234 self.game.set_error(message); 231 self.game.set_error(message);
@@ -258,10 +255,7 @@ impl ClientCore {
258 Ok(state) => { 255 Ok(state) => {
259 self.crash_started = None; 256 self.crash_started = None;
260 self.game.end_game(); 257 self.game.end_game();
261 let message = format!( 258 let message = format!("Game exited. Round forfeited ({} PLN lost).", state.bet);
262 "Game exited. Round forfeited ({} PLN lost).",
263 state.bet
264 );
265 vec![Effect::ClearRegion, Effect::Print(vec![message])] 259 vec![Effect::ClearRegion, Effect::Print(vec![message])]
266 } 260 }
267 Err(_) if self.game.crash_status() != "running" => { 261 Err(_) if self.game.crash_status() != "running" => {
@@ -286,9 +280,7 @@ impl ClientCore {
286 self.logged_in = None; 280 self.logged_in = None;
287 vec![ 281 vec![
288 Effect::Disconnect, 282 Effect::Disconnect,
289 Effect::Print(vec![ 283 Effect::Print(vec!["Connection lost".to_string()]),
290 "Connection lost".to_string(),
291 ]),
292 ] 284 ]
293 } 285 }
294 } 286 }
@@ -531,27 +523,20 @@ impl ClientCore {
531 } 523 }
532 } 524 }
533 Key::Char(' ') => self.start_blackjack(), 525 Key::Char(' ') => self.start_blackjack(),
534 Key::Char('h') 526 Key::Char('h') if hand.as_ref().is_some_and(|hand| hand.phase == "player") => {
535 if hand.as_ref().is_some_and(|hand| hand.phase == "player") =>
536 {
537 self.blackjack_action("hit") 527 self.blackjack_action("hit")
538 } 528 }
539 Key::Char('s') 529 Key::Char('s') if hand.as_ref().is_some_and(|hand| hand.phase == "player") => {
540 if hand.as_ref().is_some_and(|hand| hand.phase == "player") =>
541 {
542 self.blackjack_action("stand") 530 self.blackjack_action("stand")
543 } 531 }
544 Key::Char('d') 532 Key::Char('d')
545 if hand.as_ref().is_some_and(|hand| { 533 if hand.as_ref().is_some_and(|hand| {
546 hand.phase == "player" 534 hand.phase == "player" && hand.actions.iter().any(|action| action == "double")
547 && hand.actions.iter().any(|action| action == "double")
548 }) => 535 }) =>
549 { 536 {
550 self.blackjack_action("double") 537 self.blackjack_action("double")
551 } 538 }
552 Key::Char('+') | Key::Char('=') 539 Key::Char('+') | Key::Char('=') if matches!(status.as_str(), "ready" | "done") => {
553 if matches!(status.as_str(), "ready" | "done") =>
554 {
555 let _ = self.game.adjust_bet(10); 540 let _ = self.game.adjust_bet(10);
556 self.board_effects() 541 self.board_effects()
557 } 542 }
@@ -602,9 +587,7 @@ impl ClientCore {
602 if hand.phase != "player" { 587 if hand.phase != "player" {
603 return Vec::new(); 588 return Vec::new();
604 } 589 }
605 if action == "double" 590 if action == "double" && !hand.actions.iter().any(|available| available == "double") {
606 && !hand.actions.iter().any(|available| available == "double")
607 {
608 return Vec::new(); 591 return Vec::new();
609 } 592 }
610 self.pending = Some(Pending::BlackjackAction); 593 self.pending = Some(Pending::BlackjackAction);
@@ -914,9 +897,9 @@ impl ClientCore {
914 Some(fingerprint) => { 897 Some(fingerprint) => {
915 effects.push(Effect::Print(vec![format!("Fingerprint: {fingerprint}")])) 898 effects.push(Effect::Print(vec![format!("Fingerprint: {fingerprint}")]))
916 } 899 }
917 None => effects.push(Effect::Print(vec![ 900 None => effects.push(Effect::Print(
918 "Fingerprint: (not trusted)".to_string(), 901 vec!["Fingerprint: (not trusted)".to_string()],
919 ])), 902 )),
920 } 903 }
921 904
922 self.pending = Some(Pending::Info); 905 self.pending = Some(Pending::Info);
diff --git a/src/client/database.rs b/src/client/database.rs
index e1fa42d..e1cedaf 100644
--- a/src/client/database.rs
+++ b/src/client/database.rs
@@ -110,9 +110,8 @@ pub fn get_all_hosts(conn: &Connection) -> Vec<(String, Option<String>)> {
110 let mut stmt = conn 110 let mut stmt = conn
111 .prepare("SELECT host, fingerprint FROM hosts ORDER BY last_used DESC") 111 .prepare("SELECT host, fingerprint FROM hosts ORDER BY last_used DESC")
112 .unwrap(); 112 .unwrap();
113 let rows = stmt.query_map([], |row| { 113 let rows = stmt
114 Ok((row.get(0)?, row.get(1)?)) 114 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))
115 }) 115 .unwrap();
116 .unwrap();
117 rows.filter_map(|r| r.ok()).collect() 116 rows.filter_map(|r| r.ok()).collect()
118} 117}
diff --git a/src/client/games/blackjack.rs b/src/client/games/blackjack.rs
index bad1781..d5f45b3 100644
--- a/src/client/games/blackjack.rs
+++ b/src/client/games/blackjack.rs
@@ -56,15 +56,14 @@ impl Game for BlackjackGame {
56 .get("status") 56 .get("status")
57 .map(String::as_str) 57 .map(String::as_str)
58 .unwrap_or("ready"); 58 .unwrap_or("ready");
59 let balance = state 59 let balance = state.data.get("balance").map(String::as_str).unwrap_or("?");
60 .data
61 .get("balance")
62 .map(String::as_str)
63 .unwrap_or("?");
64 let hand = Self::state(state); 60 let hand = Self::state(state);
65 61
66 if let Some(error) = state.data.get("error") { 62 if let Some(error) = state.data.get("error") {
67 let bet = hand.as_ref().map(|hand| hand.bet).unwrap_or(state.bet_amount); 63 let bet = hand
64 .as_ref()
65 .map(|hand| hand.bet)
66 .unwrap_or(state.bet_amount);
68 return format!( 67 return format!(
69 "=== BLACKJACK ===\r\nBalance: {balance} PLN Bet: {bet} PLN\r\n\r\n\r\nERROR: {error}\r\n[Q] Quit" 68 "=== BLACKJACK ===\r\nBalance: {balance} PLN Bet: {bet} PLN\r\n\r\n\r\nERROR: {error}\r\n[Q] Quit"
70 ); 69 );
diff --git a/src/client/games/manager.rs b/src/client/games/manager.rs
index 4d9e6a8..b20ff44 100644
--- a/src/client/games/manager.rs
+++ b/src/client/games/manager.rs
@@ -138,9 +138,7 @@ impl GameManager {
138 session.bet_amount = result.bet_amount; 138 session.bet_amount = result.bet_amount;
139 session.data.remove("error"); 139 session.data.remove("error");
140 session.data.insert("reels".to_string(), result.reels); 140 session.data.insert("reels".to_string(), result.reels);
141 session 141 session.data.insert("win_type".to_string(), result.win_type);
142 .data
143 .insert("win_type".to_string(), result.win_type);
144 session.data.insert( 142 session.data.insert(
145 "win_lines".to_string(), 143 "win_lines".to_string(),
146 result 144 result
@@ -339,7 +337,9 @@ impl GameManager {
339 for key in ["crash_point", "payout"] { 337 for key in ["crash_point", "payout"] {
340 session.data.remove(key); 338 session.data.remove(key);
341 } 339 }
342 session.data.insert("status".to_string(), state.phase.clone()); 340 session
341 .data
342 .insert("status".to_string(), state.phase.clone());
343 session 343 session
344 .data 344 .data
345 .insert("session_id".to_string(), state.session_id); 345 .insert("session_id".to_string(), state.session_id);
diff --git a/src/client/games/response.rs b/src/client/games/response.rs
index 71e1fed..88df459 100644
--- a/src/client/games/response.rs
+++ b/src/client/games/response.rs
@@ -128,8 +128,8 @@ pub fn parse_roulette_response(response: &str) -> Result<RouletteResult, String>
128 return Err(format!("Invalid roulette color: {color}")); 128 return Err(format!("Invalid roulette color: {color}"));
129 } 129 }
130 let bets = parts[7].to_string(); 130 let bets = parts[7].to_string();
131 let _: crate::roulette::Bets = serde_json::from_str(&bets) 131 let _: crate::roulette::Bets =
132 .map_err(|error| format!("Invalid roulette bets: {error}"))?; 132 serde_json::from_str(&bets).map_err(|error| format!("Invalid roulette bets: {error}"))?;
133 133
134 Ok(RouletteResult { 134 Ok(RouletteResult {
135 session_id: parts[1].to_string(), 135 session_id: parts[1].to_string(),
diff --git a/src/client/games/roulette.rs b/src/client/games/roulette.rs
index 3379317..e80eca8 100644
--- a/src/client/games/roulette.rs
+++ b/src/client/games/roulette.rs
@@ -53,11 +53,7 @@ impl RouletteGame {
53 53
54 match state.data.get("status").map(String::as_str) { 54 match state.data.get("status").map(String::as_str) {
55 Some("result") => { 55 Some("result") => {
56 let number = state 56 let number = state.data.get("number").map(String::as_str).unwrap_or("?");
57 .data
58 .get("number")
59 .map(String::as_str)
60 .unwrap_or("?");
61 let color = state 57 let color = state
62 .data 58 .data
63 .get("color") 59 .get("color")
@@ -102,11 +98,7 @@ impl Game for RouletteGame {
102 fn render(&self, state: &GameState) -> String { 98 fn render(&self, state: &GameState) -> String {
103 let bets = Self::bets(state); 99 let bets = Self::bets(state);
104 let chip = state.data.get("chip").map(String::as_str).unwrap_or("10"); 100 let chip = state.data.get("chip").map(String::as_str).unwrap_or("10");
105 let balance = state 101 let balance = state.data.get("balance").map(String::as_str).unwrap_or("?");
106 .data
107 .get("balance")
108 .map(String::as_str)
109 .unwrap_or("?");
110 let input = state 102 let input = state
111 .data 103 .data
112 .get("number_input") 104 .get("number_input")
diff --git a/src/client/games/slots.rs b/src/client/games/slots.rs
index d34bf49..2ec354e 100644
--- a/src/client/games/slots.rs
+++ b/src/client/games/slots.rs
@@ -109,11 +109,7 @@ impl Game for SlotsGame {
109 109
110 fn render(&self, state: &GameState) -> String { 110 fn render(&self, state: &GameState) -> String {
111 let reels = state.data.get("reels").cloned().unwrap_or_default(); 111 let reels = state.data.get("reels").cloned().unwrap_or_default();
112 let balance = state 112 let balance = state.data.get("balance").map(String::as_str).unwrap_or("?");
113 .data
114 .get("balance")
115 .map(String::as_str)
116 .unwrap_or("?");
117 let winning = Self::highlighted_cells(state); 113 let winning = Self::highlighted_cells(state);
118 let grid = self.render_grid(&reels, &winning); 114 let grid = self.render_grid(&reels, &winning);
119 let paytable = paytable_lines(); 115 let paytable = paytable_lines();
diff --git a/src/client/ui.rs b/src/client/ui.rs
index 0fe78ae..4ab0759 100644
--- a/src/client/ui.rs
+++ b/src/client/ui.rs
@@ -94,10 +94,7 @@ pub fn command_names() -> Vec<&'static str> {
94pub fn help_lines() -> Vec<String> { 94pub fn help_lines() -> Vec<String> {
95 let mut lines = vec!["Available commands:".to_string()]; 95 let mut lines = vec!["Available commands:".to_string()];
96 for command in COMMANDS { 96 for command in COMMANDS {
97 lines.push(format!( 97 lines.push(format!(" {:<28} - {}", command.usage, command.description));
98 " {:<28} - {}",
99 command.usage, command.description
100 ));
101 } 98 }
102 lines 99 lines
103} 100}
@@ -291,16 +288,12 @@ pub fn host_lines(hosts: &[(String, Option<String>)]) -> Vec<String> {
291/// on the current line. `render()` has no trailing newline, so a 7-line board 288/// on the current line. `render()` has no trailing newline, so a 7-line board
292/// needs `MoveUp(6)`. 289/// needs `MoveUp(6)`.
293pub fn lines_to_move_up(previous_lines: usize) -> u16 { 290pub fn lines_to_move_up(previous_lines: usize) -> u16 {
294 previous_lines 291 previous_lines.saturating_sub(1).min(u16::MAX as usize) as u16
295 .saturating_sub(1)
296 .min(u16::MAX as usize) as u16
297} 292}
298 293
299/// Column (0-based) to move to after the given prompt and input text. 294/// Column (0-based) to move to after the given prompt and input text.
300pub fn input_cursor_column(prompt_len: usize, input_len: usize) -> u16 { 295pub fn input_cursor_column(prompt_len: usize, input_len: usize) -> u16 {
301 prompt_len 296 prompt_len.saturating_add(input_len).min(u16::MAX as usize) as u16
302 .saturating_add(input_len)
303 .min(u16::MAX as usize) as u16
304} 297}
305 298
306/// Number of terminal rows a block of logical lines occupies after wrapping. 299/// Number of terminal rows a block of logical lines occupies after wrapping.
diff --git a/src/crash.rs b/src/crash.rs
index 55aab19..2936b99 100644
--- a/src/crash.rs
+++ b/src/crash.rs
@@ -24,9 +24,7 @@ pub fn multiplier_at_secs(seconds: f64) -> f64 {
24 if !seconds.is_finite() || seconds <= 0.0 { 24 if !seconds.is_finite() || seconds <= 0.0 {
25 return 1.0; 25 return 1.0;
26 } 26 }
27 (GROWTH_RATE * seconds) 27 (GROWTH_RATE * seconds).exp().clamp(1.0, MAX_MULTIPLIER)
28 .exp()
29 .clamp(1.0, MAX_MULTIPLIER)
30} 28}
31 29
32/// How long a round runs before reaching `multiplier`. 30/// How long a round runs before reaching `multiplier`.
diff --git a/src/roulette.rs b/src/roulette.rs
index b59f92e..426ecc3 100644
--- a/src/roulette.rs
+++ b/src/roulette.rs
@@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize};
4 4
5/// European wheel clockwise from zero. 5/// European wheel clockwise from zero.
6pub const WHEEL: [u8; 37] = [ 6pub const WHEEL: [u8; 37] = [
7 0, 32, 15, 19, 4, 21, 2, 25, 17, 34, 6, 27, 13, 36, 11, 30, 8, 23, 10, 5, 7 0, 32, 15, 19, 4, 21, 2, 25, 17, 34, 6, 27, 13, 36, 11, 30, 8, 23, 10, 5, 24, 16, 33, 1, 20,
8 24, 16, 33, 1, 20, 14, 31, 9, 22, 18, 29, 7, 28, 12, 35, 3, 26, 8 14, 31, 9, 22, 18, 29, 7, 28, 12, 35, 3, 26,
9]; 9];
10 10
11pub const RED_NUMBERS: [u8; 18] = [ 11pub const RED_NUMBERS: [u8; 18] = [
@@ -92,8 +92,7 @@ impl Bets {
92 if is_red(number) { 92 if is_red(number) {
93 winnings = winnings.saturating_add(self.red.saturating_mul(EVEN_MONEY_RETURN)); 93 winnings = winnings.saturating_add(self.red.saturating_mul(EVEN_MONEY_RETURN));
94 } else { 94 } else {
95 winnings = 95 winnings = winnings.saturating_add(self.black.saturating_mul(EVEN_MONEY_RETURN));
96 winnings.saturating_add(self.black.saturating_mul(EVEN_MONEY_RETURN));
97 } 96 }
98 if is_even(number) { 97 if is_even(number) {
99 winnings = winnings.saturating_add(self.even.saturating_mul(EVEN_MONEY_RETURN)); 98 winnings = winnings.saturating_add(self.even.saturating_mul(EVEN_MONEY_RETURN));
@@ -104,8 +103,7 @@ impl Bets {
104 103
105 for bet in &self.numbers { 104 for bet in &self.numbers {
106 if bet.number == number { 105 if bet.number == number {
107 winnings = winnings 106 winnings = winnings.saturating_add(bet.amount.saturating_mul(STRAIGHT_RETURN));
108 .saturating_add(bet.amount.saturating_mul(STRAIGHT_RETURN));
109 } 107 }
110 } 108 }
111 109
diff --git a/src/server/core.rs b/src/server/core.rs
index e0f0ff2..4b68ccb 100644
--- a/src/server/core.rs
+++ b/src/server/core.rs
@@ -49,11 +49,7 @@ impl ServerCore {
49 Self::with_registry(conn, GameRegistry::with_defaults(), DEFAULT_COST) 49 Self::with_registry(conn, GameRegistry::with_defaults(), DEFAULT_COST)
50 } 50 }
51 51
52 pub fn with_registry( 52 pub fn with_registry(conn: Connection, registry: GameRegistry, bcrypt_cost: u32) -> Self {
53 conn: Connection,
54 registry: GameRegistry,
55 bcrypt_cost: u32,
56 ) -> Self {
57 Self { 53 Self {
58 conn, 54 conn,
59 sessions: SessionManager::new(), 55 sessions: SessionManager::new(),
@@ -115,8 +111,7 @@ impl ServerCore {
115 } 111 }
116 "GAME_INFO" => { 112 "GAME_INFO" => {
117 if parts.len() != 2 { 113 if parts.len() != 2 {
118 "ERROR Invalid GAME_INFO format. Usage: GAME_INFO <game_type>" 114 "ERROR Invalid GAME_INFO format. Usage: GAME_INFO <game_type>".to_string()
119 .to_string()
120 } else { 115 } else {
121 match self.registry.get_game(parts[1]) { 116 match self.registry.get_game(parts[1]) {
122 Some(game) => format!( 117 Some(game) => format!(
@@ -143,16 +138,14 @@ impl ServerCore {
143 } 138 }
144 "CRASH_CASHOUT" => { 139 "CRASH_CASHOUT" => {
145 if parts.len() != 2 { 140 if parts.len() != 2 {
146 "ERROR Invalid CRASH_CASHOUT format. Usage: CRASH_CASHOUT <session>" 141 "ERROR Invalid CRASH_CASHOUT format. Usage: CRASH_CASHOUT <session>".to_string()
147 .to_string()
148 } else { 142 } else {
149 self.handle_crash_end(state, parts[1], false) 143 self.handle_crash_end(state, parts[1], false)
150 } 144 }
151 } 145 }
152 "CRASH_FORFEIT" => { 146 "CRASH_FORFEIT" => {
153 if parts.len() != 2 { 147 if parts.len() != 2 {
154 "ERROR Invalid CRASH_FORFEIT format. Usage: CRASH_FORFEIT <session>" 148 "ERROR Invalid CRASH_FORFEIT format. Usage: CRASH_FORFEIT <session>".to_string()
155 .to_string()
156 } else { 149 } else {
157 self.handle_crash_end(state, parts[1], true) 150 self.handle_crash_end(state, parts[1], true)
158 } 151 }
@@ -351,12 +344,8 @@ impl ServerCore {
351 if hand.player.len() != 2 { 344 if hand.player.len() != 2 {
352 return "ERROR Double is only allowed on the first two cards".to_string(); 345 return "ERROR Double is only allowed on the first two cards".to_string();
353 } 346 }
354 if database::extend_reservation( 347 if database::extend_reservation(&mut self.conn, session_id, hand.base_bet as i64)
355 &mut self.conn, 348 .is_err()
356 session_id,
357 hand.base_bet as i64,
358 )
359 .is_err()
360 { 349 {
361 return "ERROR Insufficient balance".to_string(); 350 return "ERROR Insufficient balance".to_string();
362 } 351 }
@@ -515,7 +504,12 @@ impl ServerCore {
515 504
516 /// Cash out or forfeit. The multiplier comes from the server clock at the 505 /// Cash out or forfeit. The multiplier comes from the server clock at the
517 /// moment the request is handled; the client never supplies one. 506 /// moment the request is handled; the client never supplies one.
518 fn handle_crash_end(&mut self, state: &ConnectionState, session_id: &str, forfeit: bool) -> String { 507 fn handle_crash_end(
508 &mut self,
509 state: &ConnectionState,
510 session_id: &str,
511 forfeit: bool,
512 ) -> String {
519 let Some(user) = state.user.as_deref() else { 513 let Some(user) = state.user.as_deref() else {
520 return "ERROR Not logged in".to_string(); 514 return "ERROR Not logged in".to_string();
521 }; 515 };
@@ -698,11 +692,7 @@ impl ServerCore {
698 .get("number") 692 .get("number")
699 .cloned() 693 .cloned()
700 .unwrap_or_default(); 694 .unwrap_or_default();
701 let color = result 695 let color = result.result_data.get("color").cloned().unwrap_or_default();
702 .result_data
703 .get("color")
704 .cloned()
705 .unwrap_or_default();
706 let bets = result 696 let bets = result
707 .result_data 697 .result_data
708 .get("bets") 698 .get("bets")
@@ -712,11 +702,7 @@ impl ServerCore {
712 "ROULETTE_RESULT {session_id} {winnings} {bet_amount} {new_balance} {number} {color} {bets}" 702 "ROULETTE_RESULT {session_id} {winnings} {bet_amount} {new_balance} {number} {color} {bets}"
713 ) 703 )
714 } else { 704 } else {
715 let reels = result 705 let reels = result.result_data.get("reels").cloned().unwrap_or_default();
716 .result_data
717 .get("reels")
718 .cloned()
719 .unwrap_or_default();
720 let win_type = result 706 let win_type = result
721 .result_data 707 .result_data
722 .get("win_type") 708 .get("win_type")
@@ -753,8 +739,7 @@ impl ServerCore {
753 if let Ok(mut hand) = BlackjackHand::from_json(raw) { 739 if let Ok(mut hand) = BlackjackHand::from_json(raw) {
754 if !hand.is_done() { 740 if !hand.is_done() {
755 hand.forfeit(); 741 hand.forfeit();
756 let balance = 742 let balance = database::get_balance(&self.conn, user).unwrap_or(0);
757 database::get_balance(&self.conn, user).unwrap_or(0);
758 let final_state = hand.state(&session.session_id, balance); 743 let final_state = hand.state(&session.session_id, balance);
759 let details = serde_json::to_string(&final_state) 744 let details = serde_json::to_string(&final_state)
760 .unwrap_or_else(|_| "{}".to_string()); 745 .unwrap_or_else(|_| "{}".to_string());
diff --git a/src/server/games/blackjack.rs b/src/server/games/blackjack.rs
index df5a6ac..cea817a 100644
--- a/src/server/games/blackjack.rs
+++ b/src/server/games/blackjack.rs
@@ -1,7 +1,5 @@
1use super::traits::{Game, GameError, GamePhase, GameResult, GameState}; 1use super::traits::{Game, GameError, GamePhase, GameResult, GameState};
2use crate::blackjack::{ 2use crate::blackjack::{card_value, fresh_shoe, hand_total, is_blackjack, BlackjackState};
3 card_value, fresh_shoe, hand_total, is_blackjack, BlackjackState,
4};
5use rand::seq::SliceRandom; 3use rand::seq::SliceRandom;
6use rand::thread_rng; 4use rand::thread_rng;
7use serde::{Deserialize, Serialize}; 5use serde::{Deserialize, Serialize};
diff --git a/src/server/games/roulette.rs b/src/server/games/roulette.rs
index 7beb8c9..0a58323 100644
--- a/src/server/games/roulette.rs
+++ b/src/server/games/roulette.rs
@@ -65,17 +65,15 @@ impl Game for RouletteGame {
65 65
66 match action { 66 match action {
67 "spin" => { 67 "spin" => {
68 if state.phase != GamePhase::WaitingToStart 68 if state.phase != GamePhase::WaitingToStart && state.phase != GamePhase::Playing {
69 && state.phase != GamePhase::Playing
70 {
71 return Err(GameError::InvalidAction( 69 return Err(GameError::InvalidAction(
72 "Cannot spin in current phase".to_string(), 70 "Cannot spin in current phase".to_string(),
73 )); 71 ));
74 } 72 }
75 73
76 let payload = params 74 let payload = params.first().ok_or(GameError::InvalidAction(
77 .first() 75 "Missing roulette bets".to_string(),
78 .ok_or(GameError::InvalidAction("Missing roulette bets".to_string()))?; 76 ))?;
79 let bets: Bets = serde_json::from_str(payload) 77 let bets: Bets = serde_json::from_str(payload)
80 .map_err(|error| GameError::InvalidAction(format!("Invalid bets: {error}")))?; 78 .map_err(|error| GameError::InvalidAction(format!("Invalid bets: {error}")))?;
81 79
diff --git a/src/server/games/slots.rs b/src/server/games/slots.rs
index 298cd4c..caced24 100644
--- a/src/server/games/slots.rs
+++ b/src/server/games/slots.rs
@@ -175,9 +175,7 @@ impl Game for SlotsGame {
175 175
176 match action { 176 match action {
177 "spin" => { 177 "spin" => {
178 if state.phase != GamePhase::WaitingToStart 178 if state.phase != GamePhase::WaitingToStart && state.phase != GamePhase::Playing {
179 && state.phase != GamePhase::Playing
180 {
181 return Err(GameError::InvalidAction( 179 return Err(GameError::InvalidAction(
182 "Cannot spin in current phase".to_string(), 180 "Cannot spin in current phase".to_string(),
183 )); 181 ));
diff --git a/src/server/setup.rs b/src/server/setup.rs
index 96982c5..023e6ed 100644
--- a/src/server/setup.rs
+++ b/src/server/setup.rs
@@ -47,19 +47,8 @@ pub fn init_identity(dir: &Path, common_name: &str) -> Result<SetupReport, Strin
47 run_openssl( 47 run_openssl(
48 dir, 48 dir,
49 &[ 49 &[
50 "req", 50 "req", "-x509", "-newkey", "rsa:2048", "-keyout", "key.pem", "-out", "cert.pem",
51 "-x509", 51 "-days", "3650", "-nodes", "-subj", &subject,
52 "-newkey",
53 "rsa:2048",
54 "-keyout",
55 "key.pem",
56 "-out",
57 "cert.pem",
58 "-days",
59 "3650",
60 "-nodes",
61 "-subj",
62 &subject,
63 ], 52 ],
64 "generate the certificate", 53 "generate the certificate",
65 &manual, 54 &manual,
@@ -98,9 +87,8 @@ pub fn init_identity(dir: &Path, common_name: &str) -> Result<SetupReport, Strin
98 .map_err(|error| format!("Could not inspect {}: {error}", path.display()))?; 87 .map_err(|error| format!("Could not inspect {}: {error}", path.display()))?;
99 let mut permissions = metadata.permissions(); 88 let mut permissions = metadata.permissions();
100 permissions.set_mode(0o600); 89 permissions.set_mode(0o600);
101 std::fs::set_permissions(path, permissions).map_err(|error| { 90 std::fs::set_permissions(path, permissions)
102 format!("Could not protect {}: {error}", path.display()) 91 .map_err(|error| format!("Could not protect {}: {error}", path.display()))?;
103 })?;
104 } 92 }
105 } 93 }
106 94
diff --git a/src/slots.rs b/src/slots.rs
index 9e41f05..443444e 100644
--- a/src/slots.rs
+++ b/src/slots.rs
@@ -16,14 +16,8 @@ pub const PAYLINES: [[usize; REELS]; 5] = [
16pub const LINE_NAMES: [&str; 5] = ["top", "middle", "bottom", "V", "inverted V"]; 16pub const LINE_NAMES: [&str; 5] = ["top", "middle", "bottom", "V", "inverted V"];
17 17
18/// Symbol value multipliers. 18/// Symbol value multipliers.
19pub const SYMBOL_MULTIPLIERS: [(char, u64); 6] = [ 19pub const SYMBOL_MULTIPLIERS: [(char, u64); 6] =
20 ('C', 1), 20 [('C', 1), ('L', 2), ('O', 3), ('G', 4), ('D', 6), ('S', 10)];
21 ('L', 2),
22 ('O', 3),
23 ('G', 4),
24 ('D', 6),
25 ('S', 10),
26];
27 21
28/// (match length, shape multiplier): 3, 4 or 5 of a kind. 22/// (match length, shape multiplier): 3, 4 or 5 of a kind.
29pub const SHAPE_MULTIPLIERS: [(usize, u64); 3] = [(3, 1), (4, 3), (5, 14)]; 23pub const SHAPE_MULTIPLIERS: [(usize, u64); 3] = [(3, 1), (4, 3), (5, 14)];
diff --git a/src/tofu.rs b/src/tofu.rs
index 6308813..15f38d1 100644
--- a/src/tofu.rs
+++ b/src/tofu.rs
@@ -17,10 +17,7 @@ pub enum TrustDecision {
17 /// Stored fingerprint matches: proceed. 17 /// Stored fingerprint matches: proceed.
18 Trusted, 18 Trusted,
19 /// Key changed since first use: reject with details. 19 /// Key changed since first use: reject with details.
20 Mismatch { 20 Mismatch { expected: String, actual: String },
21 expected: String,
22 actual: String,
23 },
24} 21}
25 22
26/// Hex-encoded SHA-256 of the SPKI inside a DER-encoded X.509 certificate. 23/// Hex-encoded SHA-256 of the SPKI inside a DER-encoded X.509 certificate.