summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-29 22:16:15 +0100
committersalmonik <salmonik@tuta.io>2026-10-29 22:16:15 +0100
commitbc0de9955c375cf27d52c9934403964205afc182 (patch)
tree42799fb426fb0ec164f44c92a63525003c582f35
parentinitial connection timeout set to 10s (diff)
downloadzagan-bet-bc0de9955c375cf27d52c9934403964205afc182.tar.gz
zagan-bet-bc0de9955c375cf27d52c9934403964205afc182.zip
formatting
-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
-rw-r--r--tests/blackjack.rs7
-rw-r--r--tests/client_core.rs63
-rw-r--r--tests/client_ui.rs45
-rw-r--r--tests/crash.rs6
-rw-r--r--tests/protocol.rs10
-rw-r--r--tests/roulette.rs19
-rw-r--r--tests/server_core.rs92
-rw-r--r--tests/slots.rs7
-rw-r--r--tests/tofu.rs10
31 files changed, 208 insertions, 327 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.
diff --git a/tests/blackjack.rs b/tests/blackjack.rs
index f90ca8d..36ed7cf 100644
--- a/tests/blackjack.rs
+++ b/tests/blackjack.rs
@@ -16,7 +16,12 @@ fn card_values_and_soft_totals_are_correct() {
16 assert_eq!(card_label("AS"), "Aâ™ "); 16 assert_eq!(card_label("AS"), "Aâ™ ");
17 assert_eq!(card_label("10H"), "10♥"); 17 assert_eq!(card_label("10H"), "10♥");
18 18
19 let cards = |values: &[&str]| values.iter().map(|card| card.to_string()).collect::<Vec<_>>(); 19 let cards = |values: &[&str]| {
20 values
21 .iter()
22 .map(|card| card.to_string())
23 .collect::<Vec<_>>()
24 };
20 assert_eq!(hand_total(&cards(&["AS", "10H"])), 21); 25 assert_eq!(hand_total(&cards(&["AS", "10H"])), 21);
21 assert_eq!(hand_total(&cards(&["AS", "9H"])), 20); 26 assert_eq!(hand_total(&cards(&["AS", "9H"])), 20);
22 assert_eq!(hand_total(&cards(&["AS", "AS", "9H"])), 21); 27 assert_eq!(hand_total(&cards(&["AS", "AS", "9H"])), 21);
diff --git a/tests/client_core.rs b/tests/client_core.rs
index e8b5646..176791e 100644
--- a/tests/client_core.rs
+++ b/tests/client_core.rs
@@ -4,6 +4,7 @@
4//! logical `Effect`s that `ClientCore` emits, so assertions are about what a 4//! logical `Effect`s that `ClientCore` emits, so assertions are about what a
5//! player would see, not about ANSI sequences. 5//! player would see, not about ANSI sequences.
6 6
7use std::time::Duration;
7use zagan_bet::blackjack::BlackjackState; 8use zagan_bet::blackjack::BlackjackState;
8use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; 9use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key};
9use zagan_bet::client::games::response::{ 10use zagan_bet::client::games::response::{
@@ -11,7 +12,6 @@ use zagan_bet::client::games::response::{
11}; 12};
12use zagan_bet::client::ui::command_names; 13use zagan_bet::client::ui::command_names;
13use zagan_bet::crash::CrashState; 14use zagan_bet::crash::CrashState;
14use std::time::Duration;
15 15
16const WIN3: &str = "CCCCLLCLODGSDGS"; 16const WIN3: &str = "CCCCLLCLODGSDGS";
17const LOSS: &str = "CLOOGDSLCDCOGLS"; 17const LOSS: &str = "CLOOGDSLCDCOGLS";
@@ -67,10 +67,7 @@ impl Screen {
67 } 67 }
68 Effect::SendLine(line) => sent.push(line.clone()), 68 Effect::SendLine(line) => sent.push(line.clone()),
69 Effect::Interactive(command) => interactive.push(command.clone()), 69 Effect::Interactive(command) => interactive.push(command.clone()),
70 Effect::Quit 70 Effect::Quit | Effect::Disconnect | Effect::ListHosts | Effect::ForgetHost(_) => {}
71 | Effect::Disconnect
72 | Effect::ListHosts
73 | Effect::ForgetHost(_) => {}
74 } 71 }
75 } 72 }
76 } 73 }
@@ -276,7 +273,8 @@ fn a_mega_win_shows_all_winning_lines() {
276 273
277 assert_eq!(h.board().len(), 10); 274 assert_eq!(h.board().len(), 10);
278 assert!(h.board()[7].contains("MEGA WIN! 700 PLN")); 275 assert!(h.board()[7].contains("MEGA WIN! 700 PLN"));
279 assert!(h.board()[8].contains("Winning lines: 1 (top), 2 (middle), 3 (bottom), 4 (V), 5 (inverted V)")); 276 assert!(h.board()[8]
277 .contains("Winning lines: 1 (top), 2 (middle), 3 (bottom), 4 (V), 5 (inverted V)"));
280 assert_eq!(h.board()[2].matches("{ C }").count(), 5); 278 assert_eq!(h.board()[2].matches("{ C }").count(), 5);
281} 279}
282 280
@@ -423,10 +421,7 @@ fn help_lists_every_command() {
423 421
424 let transcript = h.screen.lines.join("\n"); 422 let transcript = h.screen.lines.join("\n");
425 for command in command_names() { 423 for command in command_names() {
426 assert!( 424 assert!(transcript.contains(command), "help is missing '{command}'");
427 transcript.contains(command),
428 "help is missing '{command}'"
429 );
430 } 425 }
431} 426}
432 427
@@ -548,11 +543,7 @@ fn send_prints_the_echo() {
548 h.line("send hello"); 543 h.line("send hello");
549 h.reply("hello"); 544 h.reply("hello");
550 545
551 assert!(h 546 assert!(h.screen.lines.iter().any(|line| line == "Echo: hello"));
552 .screen
553 .lines
554 .iter()
555 .any(|line| line == "Echo: hello"));
556} 547}
557 548
558#[test] 549#[test]
@@ -560,11 +551,7 @@ fn disconnect_clears_connection_state() {
560 let mut h = Harness::new(); 551 let mut h = Harness::new();
561 h.line("disconnect"); 552 h.line("disconnect");
562 553
563 assert!(h 554 assert!(h.screen.lines.iter().any(|line| line == "Disconnected"));
564 .screen
565 .lines
566 .iter()
567 .any(|line| line == "Disconnected"));
568 555
569 h.line("play slots"); 556 h.line("play slots");
570 assert!(h 557 assert!(h
@@ -636,8 +623,7 @@ fn roulette_split_bets_spin_and_show_the_result() {
636 "PLAY_START roulette 30 {\"red\":10,\"black\":0,\"even\":10,\"odd\":0,\"numbers\":[{\"number\":17,\"amount\":10}]}" 623 "PLAY_START roulette 30 {\"red\":10,\"black\":0,\"even\":10,\"odd\":0,\"numbers\":[{\"number\":17,\"amount\":10}]}"
637 ); 624 );
638 625
639 let bets = 626 let bets = r#"{"red":10,"black":0,"even":10,"odd":0,"numbers":[{"number":17,"amount":10}]}"#;
640 r#"{"red":10,"black":0,"even":10,"odd":0,"numbers":[{"number":17,"amount":10}]}"#;
641 h.reply(&format!( 627 h.reply(&format!(
642 "ROULETTE_RESULT session-1 360 30 1330 17 black {bets}" 628 "ROULETTE_RESULT session-1 360 30 1330 17 black {bets}"
643 )); 629 ));
@@ -655,7 +641,10 @@ fn roulette_rejects_an_empty_spin_and_accepts_the_equals_chip_key() {
655 641
656 h.key(Key::Char(' ')); 642 h.key(Key::Char(' '));
657 assert!(h.board()[4].contains("ERROR: Place at least one bet")); 643 assert!(h.board()[4].contains("ERROR: Place at least one bet"));
658 assert!(!h.sent.iter().any(|line| line.starts_with("PLAY_START roulette"))); 644 assert!(!h
645 .sent
646 .iter()
647 .any(|line| line.starts_with("PLAY_START roulette")));
659 648
660 h.key(Key::Char('=')); 649 h.key(Key::Char('='));
661 assert!(h.board()[1].contains("Chip: 20 PLN")); 650 assert!(h.board()[1].contains("Chip: 20 PLN"));
@@ -716,10 +705,7 @@ fn blackjack_deals_hits_and_settles() {
716 assert!(h.board()[4].contains("Press [SPACE] to deal.")); 705 assert!(h.board()[4].contains("Press [SPACE] to deal."));
717 706
718 h.key(Key::Char(' ')); 707 h.key(Key::Char(' '));
719 assert!(h 708 assert!(h.sent.iter().any(|line| line == "PLAY_START blackjack 10"));
720 .sent
721 .iter()
722 .any(|line| line == "PLAY_START blackjack 10"));
723 709
724 let player = bj_state( 710 let player = bj_state(
725 "bj1", 711 "bj1",
@@ -847,10 +833,7 @@ fn blackjack_ready_board_can_change_the_next_bet() {
847 assert!(h.board()[1].contains("Bet: 20 PLN")); 833 assert!(h.board()[1].contains("Bet: 20 PLN"));
848 834
849 h.key(Key::Char(' ')); 835 h.key(Key::Char(' '));
850 assert!(h 836 assert!(h.sent.iter().any(|line| line == "PLAY_START blackjack 20"));
851 .sent
852 .iter()
853 .any(|line| line == "PLAY_START blackjack 20"));
854} 837}
855 838
856#[test] 839#[test]
@@ -875,7 +858,13 @@ fn parses_a_blackjack_state() {
875 assert!(parse_blackjack_response("ERROR Insufficient balance").is_err()); 858 assert!(parse_blackjack_response("ERROR Insufficient balance").is_err());
876} 859}
877 860
878fn crash_state(phase: &str, multiplier: f64, crash_point: Option<f64>, payout: u64, balance: i64) -> CrashState { 861fn crash_state(
862 phase: &str,
863 multiplier: f64,
864 crash_point: Option<f64>,
865 payout: u64,
866 balance: i64,
867) -> CrashState {
879 CrashState { 868 CrashState {
880 session_id: "cr1".to_string(), 869 session_id: "cr1".to_string(),
881 phase: phase.to_string(), 870 phase: phase.to_string(),
@@ -920,10 +909,7 @@ fn crash_starts_a_round_and_draws_the_multiplier_locally() {
920 assert!(h.board()[3].contains("Potential: 20 PLN")); 909 assert!(h.board()[3].contains("Potential: 20 PLN"));
921 910
922 h.key(Key::Char('c')); 911 h.key(Key::Char('c'));
923 assert_eq!( 912 assert_eq!(h.sent.last().map(String::as_str), Some("CRASH_CASHOUT cr1"));
924 h.sent.last().map(String::as_str),
925 Some("CRASH_CASHOUT cr1")
926 );
927 913
928 let cashed = crash_state("cashed", 2.0, Some(3.0), 20, 1010); 914 let cashed = crash_state("cashed", 2.0, Some(3.0), 20, 1010);
929 h.reply(&crash_line("CRASH_STATE", &cashed)); 915 h.reply(&crash_line("CRASH_STATE", &cashed));
@@ -967,10 +953,7 @@ fn crash_q_forfeits_an_active_round() {
967 h.reply(&crash_line("CRASH_START", &running)); 953 h.reply(&crash_line("CRASH_START", &running));
968 954
969 h.key(Key::Char('q')); 955 h.key(Key::Char('q'));
970 assert_eq!( 956 assert_eq!(h.sent.last().map(String::as_str), Some("CRASH_FORFEIT cr1"));
971 h.sent.last().map(String::as_str),
972 Some("CRASH_FORFEIT cr1")
973 );
974 957
975 let forfeited = crash_state("forfeit", 1.2, Some(2.0), 0, 990); 958 let forfeited = crash_state("forfeit", 1.2, Some(2.0), 0, 990);
976 h.reply(&crash_line("CRASH_STATE", &forfeited)); 959 h.reply(&crash_line("CRASH_STATE", &forfeited));
diff --git a/tests/client_ui.rs b/tests/client_ui.rs
index 7a1b675..44febde 100644
--- a/tests/client_ui.rs
+++ b/tests/client_ui.rs
@@ -1,9 +1,9 @@
1use std::time::Duration;
1use zagan_bet::client::ui::{ 2use zagan_bet::client::ui::{
2 command_names, completion_lines, display_width, first_contact_lines, format_pong, 3 command_names, completion_lines, display_width, first_contact_lines, format_pong, help_lines,
3 help_lines, host_lines, input_cursor_column, lines_to_move_up, parse_bet_arg, record_history, 4 host_lines, input_cursor_column, lines_to_move_up, parse_bet_arg, record_history,
4 visual_line_count, 5 visual_line_count,
5}; 6};
6use std::time::Duration;
7 7
8#[test] 8#[test]
9fn help_has_no_source_indentation() { 9fn help_has_no_source_indentation() {
@@ -27,13 +27,7 @@ fn help_documents_every_command() {
27fn completion_knows_every_command() { 27fn completion_knows_every_command() {
28 let commands = command_names(); 28 let commands = command_names();
29 for expected in [ 29 for expected in [
30 "connect", 30 "connect", "games", "play", "hosts", "send", "logout", "quit",
31 "games",
32 "play",
33 "hosts",
34 "send",
35 "logout",
36 "quit",
37 ] { 31 ] {
38 assert!( 32 assert!(
39 commands.contains(&expected), 33 commands.contains(&expected),
@@ -109,19 +103,12 @@ fn display_width_ignores_ansi_sequences() {
109fn visual_line_count_ignores_ansi_sequences() { 103fn visual_line_count_ignores_ansi_sequences() {
110 let styled = format!("\x1b[31m{}\x1b[0m", "x".repeat(75)); 104 let styled = format!("\x1b[31m{}\x1b[0m", "x".repeat(75));
111 assert_eq!(visual_line_count(&[styled], 72), 2); 105 assert_eq!(visual_line_count(&[styled], 72), 2);
112 assert_eq!( 106 assert_eq!(visual_line_count(&["\x1b[31m18\x1b[0m".to_string()], 80), 1);
113 visual_line_count(&["\x1b[31m18\x1b[0m".to_string()], 80),
114 1
115 );
116} 107}
117 108
118#[test] 109#[test]
119fn visual_line_count_accounts_for_terminal_wrapping() { 110fn visual_line_count_accounts_for_terminal_wrapping() {
120 let lines = vec![ 111 let lines = vec!["12345".to_string(), "123456".to_string(), String::new()];
121 "12345".to_string(),
122 "123456".to_string(),
123 String::new(),
124 ];
125 112
126 assert_eq!(visual_line_count(&lines, 5), 4); // 1 + 2 + 1 113 assert_eq!(visual_line_count(&lines, 5), 4); // 1 + 2 + 1
127 assert_eq!(visual_line_count(&lines, 6), 3); // 1 + 1 + 1 114 assert_eq!(visual_line_count(&lines, 6), 3); // 1 + 1 + 1
@@ -146,7 +133,10 @@ fn completions_wrap_within_the_requested_width() {
146 let commands = command_names(); 133 let commands = command_names();
147 let lines = completion_lines(&commands, 40); 134 let lines = completion_lines(&commands, 40);
148 135
149 assert_eq!(lines.first().map(String::as_str), Some("Possible completions:")); 136 assert_eq!(
137 lines.first().map(String::as_str),
138 Some("Possible completions:")
139 );
150 for line in &lines[1..] { 140 for line in &lines[1..] {
151 assert!( 141 assert!(
152 line.len() <= 40, 142 line.len() <= 40,
@@ -208,10 +198,7 @@ fn pong_is_formatted_to_one_decimal() {
208fn host_lines_fit_an_80_column_terminal() { 198fn host_lines_fit_an_80_column_terminal() {
209 let fingerprint = "a4d6f80784154c2cb739a1df61ab3e9038ed731ee2e05c7611457ac741b3834c"; 199 let fingerprint = "a4d6f80784154c2cb739a1df61ab3e9038ed731ee2e05c7611457ac741b3834c";
210 let lines = host_lines(&[ 200 let lines = host_lines(&[
211 ( 201 ("127.0.0.1:11525".to_string(), Some(fingerprint.to_string())),
212 "127.0.0.1:11525".to_string(),
213 Some(fingerprint.to_string()),
214 ),
215 ("unknown.example:443".to_string(), None), 202 ("unknown.example:443".to_string(), None),
216 ]); 203 ]);
217 204
@@ -233,10 +220,7 @@ fn hosts_sharing_a_fingerprint_are_shown_as_one_server() {
233 let lines = host_lines(&[ 220 let lines = host_lines(&[
234 ("10.0.0.100:11525".to_string(), Some(key.to_string())), 221 ("10.0.0.100:11525".to_string(), Some(key.to_string())),
235 ("127.0.0.1:11525".to_string(), Some(key.to_string())), 222 ("127.0.0.1:11525".to_string(), Some(key.to_string())),
236 ( 223 ("other.example:11525".to_string(), Some("ff".repeat(32))),
237 "other.example:11525".to_string(),
238 Some("ff".repeat(32)),
239 ),
240 ]); 224 ]);
241 225
242 assert_eq!( 226 assert_eq!(
@@ -264,7 +248,10 @@ fn hosts_without_a_fingerprint_are_never_merged() {
264 assert!(lines.contains(&" a.example:443".to_string())); 248 assert!(lines.contains(&" a.example:443".to_string()));
265 assert!(lines.contains(&" b.example:443".to_string())); 249 assert!(lines.contains(&" b.example:443".to_string()));
266 assert_eq!( 250 assert_eq!(
267 lines.iter().filter(|line| line.contains("no fingerprint")).count(), 251 lines
252 .iter()
253 .filter(|line| line.contains("no fingerprint"))
254 .count(),
268 2 255 2
269 ); 256 );
270} 257}
diff --git a/tests/crash.rs b/tests/crash.rs
index f59c52f..49eaf89 100644
--- a/tests/crash.rs
+++ b/tests/crash.rs
@@ -18,7 +18,11 @@ fn the_multiplier_doubles_every_five_seconds() {
18 close(multiplier_at(Duration::ZERO), 1.0, 1e-9); 18 close(multiplier_at(Duration::ZERO), 1.0, 1e-9);
19 close(multiplier_at(Duration::from_secs(5)), 2.0, 1e-9); 19 close(multiplier_at(Duration::from_secs(5)), 2.0, 1e-9);
20 close(multiplier_at(Duration::from_secs(10)), 4.0, 1e-6); 20 close(multiplier_at(Duration::from_secs(10)), 4.0, 1e-6);
21 close(multiplier_at(Duration::from_millis(2500)), 1.414_213_562, 1e-6); 21 close(
22 multiplier_at(Duration::from_millis(2500)),
23 1.414_213_562,
24 1e-6,
25 );
22 26
23 // Never below 1x, even for nonsense input. 27 // Never below 1x, even for nonsense input.
24 assert_eq!(multiplier_at_secs(-3.0), 1.0); 28 assert_eq!(multiplier_at_secs(-3.0), 1.0);
diff --git a/tests/protocol.rs b/tests/protocol.rs
index 2962574..bf8a4fd 100644
--- a/tests/protocol.rs
+++ b/tests/protocol.rs
@@ -40,8 +40,14 @@ fn reads_multiple_lines_from_a_reader() {
40 let mut buffer = LineBuffer::new(); 40 let mut buffer = LineBuffer::new();
41 let mut reader = Cursor::new(b"one\ntwo\n".to_vec()); 41 let mut reader = Cursor::new(b"one\ntwo\n".to_vec());
42 42
43 assert_eq!(buffer.read_line(&mut reader).unwrap().as_deref(), Some("one")); 43 assert_eq!(
44 assert_eq!(buffer.read_line(&mut reader).unwrap().as_deref(), Some("two")); 44 buffer.read_line(&mut reader).unwrap().as_deref(),
45 Some("one")
46 );
47 assert_eq!(
48 buffer.read_line(&mut reader).unwrap().as_deref(),
49 Some("two")
50 );
45 assert_eq!(buffer.read_line(&mut reader).unwrap(), None); 51 assert_eq!(buffer.read_line(&mut reader).unwrap(), None);
46} 52}
47 53
diff --git a/tests/roulette.rs b/tests/roulette.rs
index 6b99d17..932bace 100644
--- a/tests/roulette.rs
+++ b/tests/roulette.rs
@@ -1,6 +1,4 @@
1use zagan_bet::roulette::{ 1use zagan_bet::roulette::{color, is_black, is_even, is_odd, is_red, rtp, Bets, NumberBet, WHEEL};
2 color, is_black, is_even, is_odd, is_red, rtp, Bets, NumberBet, WHEEL,
3};
4use zagan_bet::server::games::roulette::RouletteGame; 2use zagan_bet::server::games::roulette::RouletteGame;
5use zagan_bet::server::games::traits::Game; 3use zagan_bet::server::games::traits::Game;
6 4
@@ -171,14 +169,13 @@ fn rejects_bad_roulette_results() {
171 use zagan_bet::client::games::response::parse_roulette_response; 169 use zagan_bet::client::games::response::parse_roulette_response;
172 170
173 let valid_bets = r#"{"red":10}"#; 171 let valid_bets = r#"{"red":10}"#;
174 assert!(parse_roulette_response(&format!( 172 assert!(
175 "ROULETTE_RESULT s 1 1 1 37 green {valid_bets}" 173 parse_roulette_response(&format!("ROULETTE_RESULT s 1 1 1 37 green {valid_bets}")).is_err()
176 )) 174 );
177 .is_err()); 175 assert!(
178 assert!(parse_roulette_response(&format!( 176 parse_roulette_response(&format!("ROULETTE_RESULT s 1 1 1 17 purple {valid_bets}"))
179 "ROULETTE_RESULT s 1 1 1 17 purple {valid_bets}" 177 .is_err()
180 )) 178 );
181 .is_err());
182 assert!(parse_roulette_response("ROULETTE_RESULT s 1 1 1 17 black nope").is_err()); 179 assert!(parse_roulette_response("ROULETTE_RESULT s 1 1 1 17 black nope").is_err());
183 assert!(parse_roulette_response("").is_err()); 180 assert!(parse_roulette_response("").is_err());
184} 181}
diff --git a/tests/server_core.rs b/tests/server_core.rs
index 0350fb6..6b7b415 100644
--- a/tests/server_core.rs
+++ b/tests/server_core.rs
@@ -4,13 +4,13 @@ use std::sync::Arc;
4use rusqlite::Connection; 4use rusqlite::Connection;
5use zagan_bet::blackjack::BlackjackState; 5use zagan_bet::blackjack::BlackjackState;
6use zagan_bet::crash::CrashState; 6use zagan_bet::crash::CrashState;
7use zagan_bet::roulette::Bets;
7use zagan_bet::server::core::{ConnectionState, ServerCore}; 8use zagan_bet::server::core::{ConnectionState, ServerCore};
8use zagan_bet::server::database; 9use zagan_bet::server::database;
9use zagan_bet::server::games::crash::ManualClock; 10use zagan_bet::server::games::crash::ManualClock;
10use zagan_bet::server::games::registry::GameRegistry; 11use zagan_bet::server::games::registry::GameRegistry;
11use zagan_bet::server::games::roulette::RouletteGame; 12use zagan_bet::server::games::roulette::RouletteGame;
12use zagan_bet::server::games::slots::SlotsGame; 13use zagan_bet::server::games::slots::SlotsGame;
13use zagan_bet::roulette::Bets;
14 14
15fn setup(name: &str, grid: &str) -> (ServerCore, String) { 15fn setup(name: &str, grid: &str) -> (ServerCore, String) {
16 let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")) 16 let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR"))
@@ -63,8 +63,7 @@ fn setup_blackjack(name: &str, deck: &[&str]) -> (ServerCore, String) {
63 63
64 let deck = deck.iter().map(|card| card.to_string()).collect(); 64 let deck = deck.iter().map(|card| card.to_string()).collect();
65 ( 65 (
66 ServerCore::with_registry(conn, GameRegistry::with_defaults(), 4) 66 ServerCore::with_registry(conn, GameRegistry::with_defaults(), 4).with_blackjack_deck(deck),
67 .with_blackjack_deck(deck),
68 path, 67 path,
69 ) 68 )
70} 69}
@@ -216,7 +215,10 @@ fn plays_record_the_game_type_and_game_specific_details() {
216 login(&mut core, &mut state, "alice"); 215 login(&mut core, &mut state, "alice");
217 216
218 let response = core.handle_message(&mut state, "PLAY_START slots 10"); 217 let response = core.handle_message(&mut state, "PLAY_START slots 10");
219 assert!(response.starts_with("GAME_RESULT"), "spin failed: {response}"); 218 assert!(
219 response.starts_with("GAME_RESULT"),
220 "spin failed: {response}"
221 );
220 222
221 let conn = Connection::open(&path).unwrap(); 223 let conn = Connection::open(&path).unwrap();
222 let (game_type, details): (String, String) = conn 224 let (game_type, details): (String, String) = conn
@@ -242,8 +244,7 @@ fn roulette_settles_split_bets_and_records_details() {
242 register(&mut core, &mut state, "alice"); 244 register(&mut core, &mut state, "alice");
243 login(&mut core, &mut state, "alice"); 245 login(&mut core, &mut state, "alice");
244 246
245 let bets = 247 let bets = r#"{"red":10,"black":10,"even":10,"odd":10,"numbers":[{"number":17,"amount":5}]}"#;
246 r#"{"red":10,"black":10,"even":10,"odd":10,"numbers":[{"number":17,"amount":5}]}"#;
247 let response = core.handle_message(&mut state, &format!("PLAY_START roulette 45 {bets}")); 248 let response = core.handle_message(&mut state, &format!("PLAY_START roulette 45 {bets}"));
248 249
249 assert!( 250 assert!(
@@ -254,7 +255,10 @@ fn roulette_settles_split_bets_and_records_details() {
254 response.contains(" 220 45 1175 17 black "), 255 response.contains(" 220 45 1175 17 black "),
255 "response: {response}" 256 "response: {response}"
256 ); 257 );
257 assert!(response.ends_with(&format!(" {bets}")), "response: {response}"); 258 assert!(
259 response.ends_with(&format!(" {bets}")),
260 "response: {response}"
261 );
258 assert_eq!(core.balance("alice"), Some(1175)); 262 assert_eq!(core.balance("alice"), Some(1175));
259 assert_eq!(core.play_count("alice"), 1); 263 assert_eq!(core.play_count("alice"), 1);
260 assert_eq!(core.session_count(), 0); 264 assert_eq!(core.session_count(), 0);
@@ -327,7 +331,10 @@ fn a_winning_spin_updates_the_balance_and_the_audit_row() {
327 login(&mut core, &mut state, "alice"); 331 login(&mut core, &mut state, "alice");
328 332
329 let response = core.handle_message(&mut state, "PLAY_START slots 10"); 333 let response = core.handle_message(&mut state, "PLAY_START slots 10");
330 assert!(response.starts_with("GAME_RESULT"), "spin failed: {response}"); 334 assert!(
335 response.starts_with("GAME_RESULT"),
336 "spin failed: {response}"
337 );
331 assert!(response.contains(" 10 10 1000 "), "response: {response}"); 338 assert!(response.contains(" 10 10 1000 "), "response: {response}");
332 assert!(response.ends_with(" win 1 0,3,6"), "response: {response}"); 339 assert!(response.ends_with(" win 1 0,3,6"), "response: {response}");
333 340
@@ -364,11 +371,8 @@ fn a_negative_balance_is_rejected_without_wrapping() {
364 login(&mut core, &mut state, "alice"); 371 login(&mut core, &mut state, "alice");
365 372
366 let conn = Connection::open(&path).unwrap(); 373 let conn = Connection::open(&path).unwrap();
367 conn.execute( 374 conn.execute("UPDATE users SET balance = -5 WHERE username = 'alice'", [])
368 "UPDATE users SET balance = -5 WHERE username = 'alice'", 375 .unwrap();
369 [],
370 )
371 .unwrap();
372 376
373 assert_eq!( 377 assert_eq!(
374 core.handle_message(&mut state, "PLAY_START slots 10"), 378 core.handle_message(&mut state, "PLAY_START slots 10"),
@@ -394,10 +398,7 @@ fn unknown_games_are_rejected_before_a_session_is_created() {
394 398
395#[test] 399#[test]
396fn blackjack_reserves_the_bet_and_settles_a_hit_hand() { 400fn blackjack_reserves_the_bet_and_settles_a_hit_hand() {
397 let (mut core, path) = setup_blackjack( 401 let (mut core, path) = setup_blackjack("blackjack-hit", &["10S", "9D", "6H", "8C", "5D"]);
398 "blackjack-hit",
399 &["10S", "9D", "6H", "8C", "5D"],
400 );
401 let mut state = ConnectionState::default(); 402 let mut state = ConnectionState::default();
402 register(&mut core, &mut state, "alice"); 403 register(&mut core, &mut state, "alice");
403 login(&mut core, &mut state, "alice"); 404 login(&mut core, &mut state, "alice");
@@ -442,10 +443,7 @@ fn blackjack_reserves_the_bet_and_settles_a_hit_hand() {
442 443
443#[test] 444#[test]
444fn blackjack_double_reserves_an_extra_bet() { 445fn blackjack_double_reserves_an_extra_bet() {
445 let (mut core, path) = setup_blackjack( 446 let (mut core, path) = setup_blackjack("blackjack-double", &["5S", "9D", "5H", "8C", "10D"]);
446 "blackjack-double",
447 &["5S", "9D", "5H", "8C", "10D"],
448 );
449 let mut state = ConnectionState::default(); 447 let mut state = ConnectionState::default();
450 register(&mut core, &mut state, "alice"); 448 register(&mut core, &mut state, "alice");
451 login(&mut core, &mut state, "alice"); 449 login(&mut core, &mut state, "alice");
@@ -483,8 +481,7 @@ fn blackjack_double_reserves_an_extra_bet() {
483 481
484#[test] 482#[test]
485fn blackjack_natural_settles_immediately() { 483fn blackjack_natural_settles_immediately() {
486 let (mut core, _path) = 484 let (mut core, _path) = setup_blackjack("blackjack-natural", &["AS", "9D", "KH", "8C"]);
487 setup_blackjack("blackjack-natural", &["AS", "9D", "KH", "8C"]);
488 let mut state = ConnectionState::default(); 485 let mut state = ConnectionState::default();
489 register(&mut core, &mut state, "alice"); 486 register(&mut core, &mut state, "alice");
490 login(&mut core, &mut state, "alice"); 487 login(&mut core, &mut state, "alice");
@@ -501,8 +498,7 @@ fn blackjack_natural_settles_immediately() {
501 498
502#[test] 499#[test]
503fn blackjack_forfeit_keeps_the_reserved_bet() { 500fn blackjack_forfeit_keeps_the_reserved_bet() {
504 let (mut core, _path) = 501 let (mut core, _path) = setup_blackjack("blackjack-cancel", &["10S", "9D", "6H", "8C"]);
505 setup_blackjack("blackjack-cancel", &["10S", "9D", "6H", "8C"]);
506 let mut state = ConnectionState::default(); 502 let mut state = ConnectionState::default();
507 register(&mut core, &mut state, "alice"); 503 register(&mut core, &mut state, "alice");
508 login(&mut core, &mut state, "alice"); 504 login(&mut core, &mut state, "alice");
@@ -524,8 +520,7 @@ fn blackjack_forfeit_keeps_the_reserved_bet() {
524 520
525#[test] 521#[test]
526fn blackjack_disconnect_forfeits_the_reserved_bet() { 522fn blackjack_disconnect_forfeits_the_reserved_bet() {
527 let (mut core, _path) = 523 let (mut core, _path) = setup_blackjack("blackjack-disconnect", &["10S", "9D", "6H", "8C"]);
528 setup_blackjack("blackjack-disconnect", &["10S", "9D", "6H", "8C"]);
529 let mut state = ConnectionState::default(); 524 let mut state = ConnectionState::default();
530 register(&mut core, &mut state, "alice"); 525 register(&mut core, &mut state, "alice");
531 login(&mut core, &mut state, "alice"); 526 login(&mut core, &mut state, "alice");
@@ -578,10 +573,9 @@ fn crash_cashout_pays_the_multiplier_from_the_server_clock() {
578 573
579 // Five seconds of round time: 2.00x, still below the 3.00x crash point. 574 // Five seconds of round time: 2.00x, still below the 3.00x crash point.
580 clock.advance(5.0); 575 clock.advance(5.0);
581 let cashed = crash_state(&core.handle_message( 576 let cashed = crash_state(
582 &mut state, 577 &core.handle_message(&mut state, &format!("CRASH_CASHOUT {}", started.session_id)),
583 &format!("CRASH_CASHOUT {}", started.session_id), 578 );
584 ));
585 579
586 assert_eq!(cashed.phase, "cashed"); 580 assert_eq!(cashed.phase, "cashed");
587 assert_eq!(cashed.multiplier, 2.0); 581 assert_eq!(cashed.multiplier, 2.0);
@@ -600,7 +594,15 @@ fn crash_cashout_pays_the_multiplier_from_the_server_clock() {
600 "SELECT game_type, bet_amount, winnings, balance_after, details 594 "SELECT game_type, bet_amount, winnings, balance_after, details
601 FROM plays WHERE username = 'alice'", 595 FROM plays WHERE username = 'alice'",
602 [], 596 [],
603 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?)), 597 |row| {
598 Ok((
599 row.get(0)?,
600 row.get(1)?,
601 row.get(2)?,
602 row.get(3)?,
603 row.get(4)?,
604 ))
605 },
604 ) 606 )
605 .unwrap(); 607 .unwrap();
606 assert_eq!(game_type, "crash"); 608 assert_eq!(game_type, "crash");
@@ -627,10 +629,9 @@ fn a_cashout_after_the_crash_time_loses_the_reserved_bet() {
627 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10")); 629 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10"));
628 630
629 clock.advance(20.0); 631 clock.advance(20.0);
630 let late = crash_state(&core.handle_message( 632 let late = crash_state(
631 &mut state, 633 &core.handle_message(&mut state, &format!("CRASH_CASHOUT {}", started.session_id)),
632 &format!("CRASH_CASHOUT {}", started.session_id), 634 );
633 ));
634 635
635 assert_eq!(late.phase, "crashed"); 636 assert_eq!(late.phase, "crashed");
636 assert_eq!(late.payout, 0); 637 assert_eq!(late.payout, 0);
@@ -679,10 +680,9 @@ fn crash_forfeit_keeps_the_reserved_bet() {
679 login(&mut core, &mut state, "alice"); 680 login(&mut core, &mut state, "alice");
680 681
681 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10")); 682 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10"));
682 let forfeited = crash_state(&core.handle_message( 683 let forfeited = crash_state(
683 &mut state, 684 &core.handle_message(&mut state, &format!("CRASH_FORFEIT {}", started.session_id)),
684 &format!("CRASH_FORFEIT {}", started.session_id), 685 );
685 ));
686 686
687 assert_eq!(forfeited.phase, "forfeit"); 687 assert_eq!(forfeited.phase, "forfeit");
688 assert_eq!(forfeited.payout, 0); 688 assert_eq!(forfeited.payout, 0);
@@ -747,10 +747,7 @@ fn crash_rounds_are_owned_by_the_player_who_started_them() {
747 747
748 clock.advance(5.0); 748 clock.advance(5.0);
749 assert_eq!( 749 assert_eq!(
750 core.handle_message( 750 core.handle_message(&mut bob, &format!("CRASH_CASHOUT {}", started.session_id)),
751 &mut bob,
752 &format!("CRASH_CASHOUT {}", started.session_id)
753 ),
754 "ERROR Invalid session" 751 "ERROR Invalid session"
755 ); 752 );
756 assert_eq!(core.balance("bob"), Some(1000)); 753 assert_eq!(core.balance("bob"), Some(1000));
@@ -775,10 +772,9 @@ fn a_crash_reservation_is_durable_until_the_round_settles() {
775 drop(conn); 772 drop(conn);
776 773
777 clock.advance(5.0); 774 clock.advance(5.0);
778 let cashed = crash_state(&core.handle_message( 775 let cashed = crash_state(
779 &mut state, 776 &core.handle_message(&mut state, &format!("CRASH_CASHOUT {}", started.session_id)),
780 &format!("CRASH_CASHOUT {}", started.session_id), 777 );
781 ));
782 assert_eq!(cashed.phase, "cashed"); 778 assert_eq!(cashed.phase, "cashed");
783 779
784 let conn = Connection::open(&path).unwrap(); 780 let conn = Connection::open(&path).unwrap();
diff --git a/tests/slots.rs b/tests/slots.rs
index 8bdd9af..7f0b6b5 100644
--- a/tests/slots.rs
+++ b/tests/slots.rs
@@ -1,8 +1,8 @@
1//! Regression tests for the server-side 5x3 slots game. 1//! Regression tests for the server-side 5x3 slots game.
2 2
3use zagan_bet::slots::{paytable_lines, LINE_NAMES, SHAPE_MULTIPLIERS, SYMBOL_MULTIPLIERS};
4use zagan_bet::server::games::slots::{LineWin, SlotsGame, PAYLINES, REELS, ROWS}; 3use zagan_bet::server::games::slots::{LineWin, SlotsGame, PAYLINES, REELS, ROWS};
5use zagan_bet::server::games::traits::{Game, GamePhase}; 4use zagan_bet::server::games::traits::{Game, GamePhase};
5use zagan_bet::slots::{paytable_lines, LINE_NAMES, SHAPE_MULTIPLIERS, SYMBOL_MULTIPLIERS};
6 6
7/// Convert row-major test rows into the column-major wire format. 7/// Convert row-major test rows into the column-major wire format.
8fn grid(rows: [&str; 3]) -> String { 8fn grid(rows: [&str; 3]) -> String {
@@ -62,7 +62,10 @@ fn four_of_a_kind_pays_more_than_three() {
62fn five_of_a_kind_pays_all_lines() { 62fn five_of_a_kind_pays_all_lines() {
63 let (winnings, wins) = payout(["SSSSS", "SSSSS", "SSSSS"], 10); 63 let (winnings, wins) = payout(["SSSSS", "SSSSS", "SSSSS"], 10);
64 64
65 assert_eq!(winnings, 7000, "five SSSSS lines: 5 * 10 * S(10) * shape5(14)"); 65 assert_eq!(
66 winnings, 7000,
67 "five SSSSS lines: 5 * 10 * S(10) * shape5(14)"
68 );
66 assert_eq!(lines(&wins), vec![1, 2, 3, 4, 5]); 69 assert_eq!(lines(&wins), vec![1, 2, 3, 4, 5]);
67 assert_eq!(wins[0].cells, vec![0, 3, 6, 9, 12]); 70 assert_eq!(wins[0].cells, vec![0, 3, 6, 9, 12]);
68 assert_eq!(wins[3].cells, vec![0, 4, 8, 10, 12]); 71 assert_eq!(wins[3].cells, vec![0, 4, 8, 10, 12]);
diff --git a/tests/tofu.rs b/tests/tofu.rs
index 4f59d6d..b644cc4 100644
--- a/tests/tofu.rs
+++ b/tests/tofu.rs
@@ -74,7 +74,10 @@ fn fingerprint_is_sha256_of_spki() {
74 let expected = openssl_spki_sha256(dir.join("primary.pem").to_str().unwrap()); 74 let expected = openssl_spki_sha256(dir.join("primary.pem").to_str().unwrap());
75 75
76 let fp = spki_fingerprint(&der).expect("must parse the test certificate"); 76 let fp = spki_fingerprint(&der).expect("must parse the test certificate");
77 assert_eq!(fp, expected, "fingerprint must equal openssl's SPKI SHA-256"); 77 assert_eq!(
78 fp, expected,
79 "fingerprint must equal openssl's SPKI SHA-256"
80 );
78} 81}
79 82
80#[test] 83#[test]
@@ -90,10 +93,7 @@ fn first_contact_trusts_and_records() {
90 93
91#[test] 94#[test]
92fn matching_key_is_trusted() { 95fn matching_key_is_trusted() {
93 assert_eq!( 96 assert_eq!(decide(Some("ab12cd34"), "ab12cd34"), TrustDecision::Trusted);
94 decide(Some("ab12cd34"), "ab12cd34"),
95 TrustDecision::Trusted
96 );
97} 97}
98 98
99#[test] 99#[test]