summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tests/client_core.rs88
-rw-r--r--tests/roulette.rs184
-rw-r--r--tests/server_core.rs59
3 files changed, 331 insertions, 0 deletions
diff --git a/tests/client_core.rs b/tests/client_core.rs
index 4749d99..7fb7e29 100644
--- a/tests/client_core.rs
+++ b/tests/client_core.rs
@@ -47,6 +47,15 @@ impl Screen {
47 self.region_start = None; 47 self.region_start = None;
48 self.region_len = 0; 48 self.region_len = 0;
49 } 49 }
50 Effect::AnimateRoulette { final_lines, .. } => {
51 if let Some(start) = self.region_start {
52 self.lines.truncate(start);
53 } else {
54 self.region_start = Some(self.lines.len());
55 }
56 self.lines.extend(final_lines.iter().cloned());
57 self.region_len = final_lines.len();
58 }
50 Effect::SetPrompt { text, cursor_col } => { 59 Effect::SetPrompt { text, cursor_col } => {
51 self.prompt = text.clone(); 60 self.prompt = text.clone();
52 self.prompt_cursor = *cursor_col; 61 self.prompt_cursor = *cursor_col;
@@ -125,6 +134,11 @@ impl Harness {
125 self.line(&format!("play slots {bet}")); 134 self.line(&format!("play slots {bet}"));
126 self.reply("INFO alice 1 1000"); 135 self.reply("INFO alice 1 1000");
127 } 136 }
137
138 fn enter_roulette(&mut self) {
139 self.line("play roulette");
140 self.reply("INFO alice 1 1000");
141 }
128} 142}
129 143
130#[test] 144#[test]
@@ -559,3 +573,77 @@ fn rejects_a_grid_with_the_wrong_size() {
559 573
560 assert!(parse_spin_response(response).is_err()); 574 assert!(parse_spin_response(response).is_err());
561} 575}
576
577#[test]
578fn roulette_split_bets_spin_and_show_the_result() {
579 let mut h = Harness::new();
580 h.enter_roulette();
581
582 assert_eq!(h.board().len(), 8);
583 assert_eq!(h.board()[0], "=== ROULETTE ===");
584 assert!(h.board()[1].contains("Balance: 1000 PLN"));
585 assert!(h.board()[1].contains("Chip: 10 PLN"));
586 assert!(h.board()[2].contains("Bets: (none)"));
587
588 h.key(Key::Char('r'));
589 h.key(Key::Char('e'));
590 h.key(Key::Char('1'));
591 h.key(Key::Char('7'));
592 h.key(Key::Enter);
593
594 assert!(h.board()[2].contains("RED 10"));
595 assert!(h.board()[2].contains("EVEN 10"));
596 assert!(h.board()[2].contains("#17 10"));
597 assert!(h.board()[2].contains("Total: 30 PLN"));
598
599 h.key(Key::Char(' '));
600 let request = h
601 .sent
602 .iter()
603 .find(|line| line.starts_with("PLAY_START roulette"))
604 .expect("roulette request");
605 assert_eq!(
606 request,
607 "PLAY_START roulette 30 {\"red\":10,\"black\":0,\"even\":10,\"odd\":0,\"numbers\":[{\"number\":17,\"amount\":10}]}"
608 );
609
610 let bets =
611 r#"{"red":10,"black":0,"even":10,"odd":0,"numbers":[{"number":17,"amount":10}]}"#;
612 h.reply(&format!(
613 "ROULETTE_RESULT session-1 360 30 1330 17 black {bets}"
614 ));
615
616 assert_eq!(h.board().len(), 8);
617 assert!(h.board()[1].contains("Balance: 1330 PLN"));
618 assert!(h.board()[4].contains("Winning: 17 BLACK ODD"));
619 assert!(h.board()[5].contains("Won 360 PLN"));
620}
621
622#[test]
623fn roulette_rejects_an_empty_spin_and_accepts_the_equals_chip_key() {
624 let mut h = Harness::new();
625 h.enter_roulette();
626
627 h.key(Key::Char(' '));
628 assert!(h.board()[4].contains("ERROR: Place at least one bet"));
629 assert!(!h.sent.iter().any(|line| line.starts_with("PLAY_START roulette")));
630
631 h.key(Key::Char('='));
632 assert!(h.board()[1].contains("Chip: 20 PLN"));
633 h.key(Key::Char('r'));
634 assert!(h.board()[2].contains("RED 20"));
635 assert!(h.board()[2].contains("Total: 20 PLN"));
636}
637
638#[test]
639fn roulette_rejects_an_out_of_range_number() {
640 let mut h = Harness::new();
641 h.enter_roulette();
642
643 h.key(Key::Char('9'));
644 h.key(Key::Char('9'));
645 h.key(Key::Enter);
646
647 assert!(h.board()[4].contains("ERROR: Number must be 0-36"));
648 assert!(h.board()[2].contains("Bets: (none)"));
649}
diff --git a/tests/roulette.rs b/tests/roulette.rs
new file mode 100644
index 0000000..6b99d17
--- /dev/null
+++ b/tests/roulette.rs
@@ -0,0 +1,184 @@
1use zagan_bet::roulette::{
2 color, is_black, is_even, is_odd, is_red, rtp, Bets, NumberBet, WHEEL,
3};
4use zagan_bet::server::games::roulette::RouletteGame;
5use zagan_bet::server::games::traits::Game;
6
7#[test]
8fn the_european_wheel_has_all_37_unique_pockets() {
9 let mut numbers = WHEEL.to_vec();
10 numbers.sort_unstable();
11 assert_eq!(numbers, (0..=36).collect::<Vec<_>>());
12 assert_eq!(color(0), "green");
13 assert_eq!(WHEEL.len(), 37);
14}
15
16#[test]
17fn colors_and_even_odd_are_correct() {
18 assert!(is_red(1));
19 assert!(is_black(2));
20 assert!(!is_red(0));
21 assert!(!is_black(0));
22 assert!(is_even(2));
23 assert!(is_odd(3));
24 assert!(!is_even(0));
25 assert!(!is_odd(0));
26 assert_eq!(color(17), "black");
27 assert_eq!(color(18), "red");
28}
29
30#[test]
31fn european_rtp_is_36_over_37() {
32 assert!((rtp() - 36.0 / 37.0).abs() < 1e-12);
33}
34
35#[test]
36fn bets_total_and_winning_number_payouts() {
37 let bets = Bets {
38 red: 10,
39 black: 10,
40 even: 10,
41 odd: 10,
42 numbers: vec![NumberBet {
43 number: 17,
44 amount: 5,
45 }],
46 };
47
48 assert_eq!(bets.total(), 45);
49 // 17 is black and odd: 10*2 + 10*2 + 5*36.
50 assert_eq!(bets.winnings(17), 220);
51 // 18 is red and even.
52 assert_eq!(bets.winnings(18), 40);
53}
54
55#[test]
56fn zero_loses_all_outside_bets_but_pays_a_straight_zero() {
57 let outside = Bets {
58 red: 10,
59 black: 10,
60 even: 10,
61 odd: 10,
62 numbers: vec![],
63 };
64 assert_eq!(outside.winnings(0), 0);
65
66 let straight = Bets {
67 red: 10,
68 black: 10,
69 even: 10,
70 odd: 10,
71 numbers: vec![NumberBet {
72 number: 0,
73 amount: 5,
74 }],
75 };
76 assert_eq!(straight.total(), 45);
77 assert_eq!(straight.winnings(0), 180);
78}
79
80#[test]
81fn bets_validate_empty_balance_and_invalid_numbers() {
82 assert!(Bets::default().validate(1000).is_err());
83
84 let too_big = Bets {
85 red: 2000,
86 ..Bets::default()
87 };
88 assert!(too_big.validate(1000).is_err());
89
90 let bad_number = Bets {
91 numbers: vec![NumberBet {
92 number: 37,
93 amount: 10,
94 }],
95 ..Bets::default()
96 };
97 assert!(bad_number.validate(1000).is_err());
98}
99
100#[test]
101fn the_server_settles_a_split_bet() {
102 let bets = Bets {
103 red: 10,
104 black: 10,
105 even: 10,
106 odd: 10,
107 numbers: vec![NumberBet {
108 number: 17,
109 amount: 5,
110 }],
111 };
112 let payload = serde_json::to_string(&bets).unwrap();
113 let game = RouletteGame::with_result(17);
114 let state = game.prepare_state("alice", bets.total());
115
116 let state = game.process_action(&state, "spin", &[&payload]).unwrap();
117 let result = game.calculate_result(&state).unwrap();
118
119 assert_eq!(result.winnings, 220);
120 assert_eq!(result.result_data.get("number").unwrap(), "17");
121 assert_eq!(result.result_data.get("color").unwrap(), "black");
122 assert_eq!(result.result_data.get("bets").unwrap(), &payload);
123}
124
125#[test]
126fn the_server_rejects_a_total_that_does_not_match_the_bets() {
127 let payload = serde_json::to_string(&Bets {
128 red: 10,
129 ..Bets::default()
130 })
131 .unwrap();
132 let game = RouletteGame::with_result(17);
133 let state = game.prepare_state("alice", 99);
134
135 assert!(game.process_action(&state, "spin", &[&payload]).is_err());
136}
137
138#[test]
139fn the_server_rejects_an_invalid_number() {
140 let payload = serde_json::to_string(&Bets {
141 numbers: vec![NumberBet {
142 number: 37,
143 amount: 10,
144 }],
145 ..Bets::default()
146 })
147 .unwrap();
148 let game = RouletteGame::with_result(17);
149 let state = game.prepare_state("alice", 10);
150
151 assert!(game.process_action(&state, "spin", &[&payload]).is_err());
152}
153
154#[test]
155fn parses_a_roulette_result() {
156 let bets = r#"{"red":10,"black":0,"even":10,"odd":0,"numbers":[{"number":17,"amount":10}]}"#;
157 let response = format!("ROULETTE_RESULT session-1 360 30 1330 17 black {bets}");
158 let result = zagan_bet::client::games::response::parse_roulette_response(&response).unwrap();
159
160 assert_eq!(result.session_id, "session-1");
161 assert_eq!(result.winnings, 360);
162 assert_eq!(result.bet_amount, 30);
163 assert_eq!(result.balance, 1330);
164 assert_eq!(result.number, 17);
165 assert_eq!(result.color, "black");
166 assert_eq!(result.bets, bets);
167}
168
169#[test]
170fn rejects_bad_roulette_results() {
171 use zagan_bet::client::games::response::parse_roulette_response;
172
173 let valid_bets = r#"{"red":10}"#;
174 assert!(parse_roulette_response(&format!(
175 "ROULETTE_RESULT s 1 1 1 37 green {valid_bets}"
176 ))
177 .is_err());
178 assert!(parse_roulette_response(&format!(
179 "ROULETTE_RESULT s 1 1 1 17 purple {valid_bets}"
180 ))
181 .is_err());
182 assert!(parse_roulette_response("ROULETTE_RESULT s 1 1 1 17 black nope").is_err());
183 assert!(parse_roulette_response("").is_err());
184}
diff --git a/tests/server_core.rs b/tests/server_core.rs
index cab74a7..cf05102 100644
--- a/tests/server_core.rs
+++ b/tests/server_core.rs
@@ -4,7 +4,9 @@ use rusqlite::Connection;
4use zagan_bet::server::core::{ConnectionState, ServerCore}; 4use zagan_bet::server::core::{ConnectionState, ServerCore};
5use zagan_bet::server::database; 5use zagan_bet::server::database;
6use zagan_bet::server::games::registry::GameRegistry; 6use zagan_bet::server::games::registry::GameRegistry;
7use zagan_bet::server::games::roulette::RouletteGame;
7use zagan_bet::server::games::slots::SlotsGame; 8use zagan_bet::server::games::slots::SlotsGame;
9use zagan_bet::roulette::Bets;
8 10
9fn setup(name: &str, grid: &str) -> (ServerCore, String) { 11fn setup(name: &str, grid: &str) -> (ServerCore, String) {
10 let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR")) 12 let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR"))
@@ -29,6 +31,22 @@ const LOSS: &str = "CLOOGDSLCDCOGLS";
29/// Top line is C C C D D; it pays 2x total bet. 31/// Top line is C C C D D; it pays 2x total bet.
30const WIN3: &str = "CCCCLLCLODGSDGS"; 32const WIN3: &str = "CCCCLLCLODGSDGS";
31 33
34fn setup_roulette(name: &str, number: u8) -> (ServerCore, String) {
35 let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR"))
36 .join(format!("server-core-{name}.db"))
37 .to_string_lossy()
38 .into_owned();
39 let _ = std::fs::remove_file(&path);
40
41 let conn = Connection::open(&path).unwrap();
42 database::init_db(&conn);
43
44 let registry = GameRegistry::new();
45 registry.register_game(RouletteGame::with_result(number));
46
47 (ServerCore::with_registry(conn, registry, 4), path)
48}
49
32fn register(core: &mut ServerCore, state: &mut ConnectionState, user: &str) { 50fn register(core: &mut ServerCore, state: &mut ConnectionState, user: &str) {
33 assert_eq!( 51 assert_eq!(
34 core.handle_message(state, &format!("REGISTER {user} credential")), 52 core.handle_message(state, &format!("REGISTER {user} credential")),
@@ -164,6 +182,47 @@ fn plays_record_the_game_type_and_game_specific_details() {
164} 182}
165 183
166#[test] 184#[test]
185fn roulette_settles_split_bets_and_records_details() {
186 let (mut core, path) = setup_roulette("roulette", 17);
187 let mut state = ConnectionState::default();
188 register(&mut core, &mut state, "alice");
189 login(&mut core, &mut state, "alice");
190
191 let bets =
192 r#"{"red":10,"black":10,"even":10,"odd":10,"numbers":[{"number":17,"amount":5}]}"#;
193 let response = core.handle_message(&mut state, &format!("PLAY_START roulette 45 {bets}"));
194
195 assert!(
196 response.starts_with("ROULETTE_RESULT"),
197 "response: {response}"
198 );
199 assert!(
200 response.contains(" 220 45 1175 17 black "),
201 "response: {response}"
202 );
203 assert!(response.ends_with(&format!(" {bets}")), "response: {response}");
204 assert_eq!(core.balance("alice"), Some(1175));
205 assert_eq!(core.play_count("alice"), 1);
206 assert_eq!(core.session_count(), 0);
207
208 let conn = Connection::open(&path).unwrap();
209 let (game_type, details): (String, String) = conn
210 .query_row(
211 "SELECT game_type, details FROM plays WHERE username = 'alice'",
212 [],
213 |row| Ok((row.get(0)?, row.get(1)?)),
214 )
215 .unwrap();
216
217 assert_eq!(game_type, "roulette");
218 let details: serde_json::Value = serde_json::from_str(&details).unwrap();
219 assert_eq!(details["number"], "17");
220 assert_eq!(details["color"], "black");
221 let stored: Bets = serde_json::from_str(details["bets"].as_str().unwrap()).unwrap();
222 assert_eq!(stored.total(), 45);
223}
224
225#[test]
167fn a_failed_audit_insert_rolls_back_the_balance_change() { 226fn a_failed_audit_insert_rolls_back_the_balance_change() {
168 let (mut core, path) = setup("rollback", LOSS); 227 let (mut core, path) = setup("rollback", LOSS);
169 let mut state = ConnectionState::default(); 228 let mut state = ConnectionState::default();