summaryrefslogtreecommitdiffstats
path: root/tests/client_core.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-10-29 20:43:09 +0100
committersalmonik <salmonik@tuta.io>2026-10-29 20:43:09 +0100
commit6e2787d792de64c003eff03b62a2df798ef851c0 (patch)
tree0e9bf97b2f71df5fb9eb00d5195d32b8326886ff /tests/client_core.rs
parentv2.2.1: forfeit blackjack bets when leaving or disconnecting (diff)
downloadzagan-bet-6e2787d792de64c003eff03b62a2df798ef851c0.tar.gz
zagan-bet-6e2787d792de64c003eff03b62a2df798ef851c0.zip
v2.3.0: add crash with local multiplier and pushed crash event
Diffstat (limited to 'tests/client_core.rs')
-rw-r--r--tests/client_core.rs210
1 files changed, 209 insertions, 1 deletions
diff --git a/tests/client_core.rs b/tests/client_core.rs
index ec444bd..e8b5646 100644
--- a/tests/client_core.rs
+++ b/tests/client_core.rs
@@ -6,8 +6,12 @@
6 6
7use zagan_bet::blackjack::BlackjackState; 7use zagan_bet::blackjack::BlackjackState;
8use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; 8use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key};
9use zagan_bet::client::games::response::{parse_blackjack_response, parse_spin_response}; 9use zagan_bet::client::games::response::{
10 parse_blackjack_response, parse_crash_response, parse_spin_response,
11};
10use zagan_bet::client::ui::command_names; 12use zagan_bet::client::ui::command_names;
13use zagan_bet::crash::CrashState;
14use std::time::Duration;
11 15
12const WIN3: &str = "CCCCLLCLODGSDGS"; 16const WIN3: &str = "CCCCLLCLODGSDGS";
13const LOSS: &str = "CLOOGDSLCDCOGLS"; 17const LOSS: &str = "CLOOGDSLCDCOGLS";
@@ -145,6 +149,25 @@ impl Harness {
145 self.line("play blackjack"); 149 self.line("play blackjack");
146 self.reply("INFO alice 1 1000"); 150 self.reply("INFO alice 1 1000");
147 } 151 }
152
153 fn enter_crash(&mut self) {
154 self.line("play crash");
155 self.reply("INFO alice 1 1000");
156 }
157
158 fn tick(&mut self, elapsed: Duration) {
159 let effects = self.core.tick_at(elapsed);
160 self.apply(effects);
161 }
162
163 /// Deliver a server-initiated line the way the terminal loop does.
164 fn push(&mut self, line: &str) {
165 let effects = self
166 .core
167 .handle_async_line(line)
168 .expect("a pushed crash line");
169 self.apply(effects);
170 }
148} 171}
149 172
150#[test] 173#[test]
@@ -851,3 +874,188 @@ fn parses_a_blackjack_state() {
851 assert_eq!(parse_blackjack_response(&response).unwrap(), state); 874 assert_eq!(parse_blackjack_response(&response).unwrap(), state);
852 assert!(parse_blackjack_response("ERROR Insufficient balance").is_err()); 875 assert!(parse_blackjack_response("ERROR Insufficient balance").is_err());
853} 876}
877
878fn crash_state(phase: &str, multiplier: f64, crash_point: Option<f64>, payout: u64, balance: i64) -> CrashState {
879 CrashState {
880 session_id: "cr1".to_string(),
881 phase: phase.to_string(),
882 bet: 10,
883 balance,
884 multiplier,
885 potential: (10.0 * multiplier) as u64,
886 crash_point,
887 payout,
888 result: phase.to_string(),
889 }
890}
891
892fn crash_line(prefix: &str, state: &CrashState) -> String {
893 format!("{prefix} {}", serde_json::to_string(state).unwrap())
894}
895
896#[test]
897fn crash_starts_a_round_and_draws_the_multiplier_locally() {
898 let mut h = Harness::new();
899 h.enter_crash();
900
901 assert_eq!(h.board().len(), 6);
902 assert!(h.board()[4].contains("Press [SPACE] to start."));
903 assert!(h.board()[5].contains("[SPACE] Play"));
904
905 h.key(Key::Char(' '));
906 assert!(h.sent.iter().any(|line| line == "PLAY_START crash 10"));
907
908 let running = crash_state("running", 1.0, None, 0, 990);
909 h.reply(&crash_line("CRASH_START", &running));
910
911 assert_eq!(h.board().len(), 6);
912 assert!(h.board()[2].contains("Multiplier: 1.00x"));
913 assert!(h.board()[3].contains("Potential: 10 PLN"));
914 assert!(h.board()[4].contains("Rising"));
915 assert!(h.board()[5].contains("[C] Cash out"));
916
917 // The multiplier is drawn from the local clock, with no server round trip.
918 h.tick(Duration::from_secs(5));
919 assert!(h.board()[2].contains("Multiplier: 2.00x"));
920 assert!(h.board()[3].contains("Potential: 20 PLN"));
921
922 h.key(Key::Char('c'));
923 assert_eq!(
924 h.sent.last().map(String::as_str),
925 Some("CRASH_CASHOUT cr1")
926 );
927
928 let cashed = crash_state("cashed", 2.0, Some(3.0), 20, 1010);
929 h.reply(&crash_line("CRASH_STATE", &cashed));
930
931 assert!(h.board()[4].contains("Cashed out at 2.00x Won 20 PLN"));
932 assert!(h.board()[5].contains("[SPACE] Play"));
933 assert_eq!(h.board()[1], "Balance: 1010 PLN Bet: 10 PLN");
934}
935
936#[test]
937fn a_pushed_bust_ends_the_round_without_a_request() {
938 let mut h = Harness::new();
939 h.enter_crash();
940 h.key(Key::Char(' '));
941
942 let running = crash_state("running", 1.0, None, 0, 990);
943 h.reply(&crash_line("CRASH_START", &running));
944 h.tick(Duration::from_secs(3));
945
946 assert!(h.sent.iter().all(|line| line != "CRASH_CASHOUT cr1"));
947
948 let busted = crash_state("crashed", 1.5, Some(1.5), 0, 990);
949 h.push(&crash_line("CRASH_BUSTED", &busted));
950
951 assert!(h.board()[4].contains("CRASHED at 1.50x Bet lost"));
952 assert!(h.board()[5].contains("[SPACE] Play"));
953
954 // The local clock stops once the round is over.
955 h.tick(Duration::from_secs(30));
956 assert!(h.board()[2].contains("Multiplier: 1.50x"));
957 assert!(!h.core.expects_push());
958}
959
960#[test]
961fn crash_q_forfeits_an_active_round() {
962 let mut h = Harness::new();
963 h.enter_crash();
964 h.key(Key::Char(' '));
965
966 let running = crash_state("running", 1.0, None, 0, 990);
967 h.reply(&crash_line("CRASH_START", &running));
968
969 h.key(Key::Char('q'));
970 assert_eq!(
971 h.sent.last().map(String::as_str),
972 Some("CRASH_FORFEIT cr1")
973 );
974
975 let forfeited = crash_state("forfeit", 1.2, Some(2.0), 0, 990);
976 h.reply(&crash_line("CRASH_STATE", &forfeited));
977
978 assert!(h.board().is_empty());
979 assert!(h
980 .screen
981 .lines
982 .iter()
983 .any(|line| line.contains("Round forfeited (10 PLN lost)")));
984}
985
986#[test]
987fn crash_ready_board_changes_the_next_bet() {
988 let mut h = Harness::new();
989 h.enter_crash();
990
991 h.key(Key::Char('='));
992 assert!(h.board()[1].contains("Bet: 20 PLN"));
993 assert!(h.board()[3].contains("Potential: 20 PLN"));
994
995 h.key(Key::Char(' '));
996 assert!(h.sent.iter().any(|line| line == "PLAY_START crash 20"));
997}
998
999#[test]
1000fn parses_crash_messages_and_errors() {
1001 let running = crash_state("running", 1.0, None, 0, 990);
1002 let cashed = crash_state("cashed", 2.0, Some(3.0), 20, 1010);
1003
1004 assert_eq!(
1005 parse_crash_response(&crash_line("CRASH_START", &running)).unwrap(),
1006 running
1007 );
1008 assert_eq!(
1009 parse_crash_response(&crash_line("CRASH_BUSTED", &cashed)).unwrap(),
1010 cashed
1011 );
1012 assert_eq!(
1013 parse_crash_response("ERROR Round already ended").unwrap_err(),
1014 "Round already ended"
1015 );
1016 assert!(parse_crash_response("GAME_RESULT nope").is_err());
1017}
1018
1019#[test]
1020fn a_late_cashout_does_not_overwrite_a_pushed_bust() {
1021 let mut h = Harness::new();
1022 h.enter_crash();
1023 h.key(Key::Char(' '));
1024
1025 let running = crash_state("running", 1.0, None, 0, 990);
1026 h.reply(&crash_line("CRASH_START", &running));
1027
1028 h.key(Key::Char('c'));
1029 assert_eq!(h.sent.last().map(String::as_str), Some("CRASH_CASHOUT cr1"));
1030
1031 // The round crashes while the cashout is in flight, so the push lands
1032 // before the server's "session not found" reply.
1033 let busted = crash_state("crashed", 1.2, Some(1.2), 0, 990);
1034 h.push(&crash_line("CRASH_BUSTED", &busted));
1035 h.reply("ERROR Session not found");
1036
1037 assert!(h.board()[4].contains("CRASHED at 1.20x"));
1038 assert!(!h.board().iter().any(|line| line.contains("ERROR")));
1039 assert!(h.board()[5].contains("[SPACE] Play"));
1040}
1041
1042#[test]
1043fn a_forfeit_that_arrives_after_the_bust_still_leaves_cleanly() {
1044 let mut h = Harness::new();
1045 h.enter_crash();
1046 h.key(Key::Char(' '));
1047
1048 let running = crash_state("running", 1.0, None, 0, 990);
1049 h.reply(&crash_line("CRASH_START", &running));
1050
1051 h.key(Key::Char('q'));
1052 assert_eq!(h.sent.last().map(String::as_str), Some("CRASH_FORFEIT cr1"));
1053
1054 let busted = crash_state("crashed", 1.5, Some(1.5), 0, 990);
1055 h.push(&crash_line("CRASH_BUSTED", &busted));
1056 h.reply("ERROR Session not found");
1057
1058 assert!(h.board().is_empty());
1059 assert!(h.screen.lines.iter().any(|line| line == "Game exited."));
1060 assert!(!h.screen.lines.iter().any(|line| line.contains("PLN lost")));
1061}