diff options
Diffstat (limited to 'tests/client_core.rs')
| -rw-r--r-- | tests/client_core.rs | 210 |
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 | ||
| 7 | use zagan_bet::blackjack::BlackjackState; | 7 | use zagan_bet::blackjack::BlackjackState; |
| 8 | use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; | 8 | use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; |
| 9 | use zagan_bet::client::games::response::{parse_blackjack_response, parse_spin_response}; | 9 | use zagan_bet::client::games::response::{ |
| 10 | parse_blackjack_response, parse_crash_response, parse_spin_response, | ||
| 11 | }; | ||
| 10 | use zagan_bet::client::ui::command_names; | 12 | use zagan_bet::client::ui::command_names; |
| 13 | use zagan_bet::crash::CrashState; | ||
| 14 | use std::time::Duration; | ||
| 11 | 15 | ||
| 12 | const WIN3: &str = "CCCCLLCLODGSDGS"; | 16 | const WIN3: &str = "CCCCLLCLODGSDGS"; |
| 13 | const LOSS: &str = "CLOOGDSLCDCOGLS"; | 17 | const 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 | |||
| 878 | fn 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 | |||
| 892 | fn crash_line(prefix: &str, state: &CrashState) -> String { | ||
| 893 | format!("{prefix} {}", serde_json::to_string(state).unwrap()) | ||
| 894 | } | ||
| 895 | |||
| 896 | #[test] | ||
| 897 | fn 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] | ||
| 937 | fn 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] | ||
| 961 | fn 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] | ||
| 987 | fn 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] | ||
| 1000 | fn 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] | ||
| 1020 | fn 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] | ||
| 1043 | fn 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 | } | ||
