summaryrefslogtreecommitdiffstats
path: root/tests/roulette.rs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/roulette.rs')
-rw-r--r--tests/roulette.rs334
1 files changed, 252 insertions, 82 deletions
diff --git a/tests/roulette.rs b/tests/roulette.rs
index 932bace..8f9155c 100644
--- a/tests/roulette.rs
+++ b/tests/roulette.rs
@@ -1,4 +1,7 @@
1use zagan_bet::roulette::{color, is_black, is_even, is_odd, is_red, rtp, Bets, NumberBet, WHEEL}; 1use zagan_bet::client::games::roulette::{Cursor, Direction};
2use zagan_bet::roulette::{
3 color, is_black, is_even, is_odd, is_red, rtp, BetTarget, Bets, Outside, WHEEL,
4};
2use zagan_bet::server::games::roulette::RouletteGame; 5use zagan_bet::server::games::roulette::RouletteGame;
3use zagan_bet::server::games::traits::Game; 6use zagan_bet::server::games::traits::Game;
4 7
@@ -31,102 +34,184 @@ fn european_rtp_is_36_over_37() {
31} 34}
32 35
33#[test] 36#[test]
34fn bets_total_and_winning_number_payouts() { 37fn inside_bets_pay_their_true_odds() {
35 let bets = Bets { 38 let straight = BetTarget::Straight { number: 17 };
36 red: 10, 39 assert_eq!(straight.return_multiplier(17), 36);
37 black: 10, 40 assert_eq!(straight.return_multiplier(18), 0);
38 even: 10, 41
39 odd: 10, 42 let split = BetTarget::Split { a: 1, b: 2 };
40 numbers: vec![NumberBet { 43 assert_eq!(split.return_multiplier(1), 18);
41 number: 17, 44 assert_eq!(split.return_multiplier(2), 18);
42 amount: 5, 45 assert_eq!(split.return_multiplier(3), 0);
43 }], 46 assert_eq!(split.numbers(), vec![1, 2]);
47
48 let corner = BetTarget::Corner {
49 numbers: [1, 2, 4, 5],
44 }; 50 };
51 for number in [1, 2, 4, 5] {
52 assert_eq!(corner.return_multiplier(number), 9);
53 }
54 assert_eq!(corner.return_multiplier(3), 0);
55 assert_eq!(corner.numbers(), vec![1, 2, 4, 5]);
56}
57
58#[test]
59fn dozens_and_columns_pay_three_for_one_and_zero_loses() {
60 let first_dozen = BetTarget::Dozen { index: 0 };
61 assert_eq!(first_dozen.return_multiplier(1), 3);
62 assert_eq!(first_dozen.return_multiplier(12), 3);
63 assert_eq!(first_dozen.return_multiplier(13), 0);
64 assert_eq!(first_dozen.return_multiplier(0), 0);
65
66 let second_dozen = BetTarget::Dozen { index: 1 };
67 assert_eq!(second_dozen.return_multiplier(13), 3);
68 assert_eq!(second_dozen.return_multiplier(24), 3);
69
70 let first_column = BetTarget::Column { index: 0 };
71 assert_eq!(first_column.return_multiplier(1), 3);
72 assert_eq!(first_column.return_multiplier(34), 3);
73 assert_eq!(first_column.return_multiplier(2), 0);
74
75 let third_column = BetTarget::Column { index: 2 };
76 assert_eq!(third_column.return_multiplier(36), 3);
77 assert_eq!(third_column.return_multiplier(0), 0, "0 is not in a column");
45 78
46 assert_eq!(bets.total(), 45); 79 for index in 0..3 {
47 // 17 is black and odd: 10*2 + 10*2 + 5*36. 80 let dozen = BetTarget::Dozen { index };
48 assert_eq!(bets.winnings(17), 220); 81 let winning = index as u8 * 12 + 1;
49 // 18 is red and even. 82 assert_eq!(dozen.return_multiplier(winning), 3);
50 assert_eq!(bets.winnings(18), 40); 83 }
51} 84}
52 85
53#[test] 86#[test]
54fn zero_loses_all_outside_bets_but_pays_a_straight_zero() { 87fn outside_bets_pay_even_money_and_zero_loses_everything() {
55 let outside = Bets { 88 let cases = [
56 red: 10, 89 (Outside::Low, 18u8, 19u8),
57 black: 10, 90 (Outside::High, 19, 18),
58 even: 10, 91 (Outside::Even, 2, 3),
59 odd: 10, 92 (Outside::Odd, 3, 2),
60 numbers: vec![], 93 (Outside::Red, 1, 2),
61 }; 94 (Outside::Black, 2, 1),
62 assert_eq!(outside.winnings(0), 0); 95 ];
63 96 for (kind, winning, losing) in cases {
64 let straight = Bets { 97 let bet = BetTarget::Outside { kind };
65 red: 10, 98 assert_eq!(bet.return_multiplier(winning), 2, "{kind:?} should win");
66 black: 10, 99 assert_eq!(bet.return_multiplier(losing), 0, "{kind:?} should lose");
67 even: 10, 100 assert_eq!(bet.return_multiplier(0), 0, "0 loses every outside bet");
68 odd: 10, 101 }
69 numbers: vec![NumberBet { 102}
70 number: 0, 103
71 amount: 5, 104#[test]
72 }], 105fn chips_on_the_same_target_merge() {
73 }; 106 let mut bets = Bets::default();
74 assert_eq!(straight.total(), 45); 107 assert!(bets.is_empty());
75 assert_eq!(straight.winnings(0), 180); 108
109 bets.add(BetTarget::Straight { number: 17 }, 10);
110 bets.add(BetTarget::Straight { number: 17 }, 5);
111 bets.add(BetTarget::Outside { kind: Outside::Red }, 10);
112
113 assert_eq!(bets.chips.len(), 2);
114 assert_eq!(
115 bets.amount_on(&BetTarget::Straight { number: 17 }),
116 15
117 );
118 assert_eq!(bets.total(), 25);
119 assert_eq!(bets.winnings(17), 15 * 36);
76} 120}
77 121
78#[test] 122#[test]
79fn bets_validate_empty_balance_and_invalid_numbers() { 123fn bets_settle_every_chip_independently() {
124 let mut bets = Bets::default();
125 bets.add(BetTarget::Split { a: 1, b: 2 }, 10);
126 bets.add(BetTarget::Outside { kind: Outside::Red }, 10);
127 bets.add(BetTarget::Straight { number: 1 }, 5);
128
129 // 2 is black, so the red chip loses; the split pays 18x.
130 assert_eq!(bets.winnings(2), 180);
131 // 3 is red, but the straight is on 1, so only the red chip pays.
132 assert_eq!(bets.winnings(3), 20);
133 // 1 is red, matches the straight, and is half of the split.
134 assert_eq!(bets.winnings(1), 5 * 36 + 10 * 2 + 10 * 18);
135}
136
137#[test]
138fn a_straight_zero_is_paid_while_outside_bets_lose() {
139 let mut bets = Bets::default();
140 bets.add(BetTarget::Straight { number: 0 }, 5);
141 bets.add(BetTarget::Outside { kind: Outside::Red }, 10);
142 bets.add(BetTarget::Dozen { index: 0 }, 10);
143
144 assert_eq!(bets.total(), 25);
145 assert_eq!(bets.winnings(0), 180);
146}
147
148#[test]
149fn bets_validate_balance_and_targets() {
80 assert!(Bets::default().validate(1000).is_err()); 150 assert!(Bets::default().validate(1000).is_err());
81 151
82 let too_big = Bets { 152 let mut too_big = Bets::default();
83 red: 2000, 153 too_big.add(BetTarget::Outside { kind: Outside::Black }, 2000);
84 ..Bets::default()
85 };
86 assert!(too_big.validate(1000).is_err()); 154 assert!(too_big.validate(1000).is_err());
87 155
88 let bad_number = Bets { 156 let bad_number = Bets {
89 numbers: vec![NumberBet { 157 chips: vec![zagan_bet::roulette::Chip {
90 number: 37, 158 target: BetTarget::Straight { number: 37 },
91 amount: 10, 159 amount: 10,
92 }], 160 }],
93 ..Bets::default()
94 }; 161 };
95 assert!(bad_number.validate(1000).is_err()); 162 assert!(bad_number.validate(1000).is_err());
96}
97 163
98#[test] 164 let bad_split = Bets {
99fn the_server_settles_a_split_bet() { 165 chips: vec![zagan_bet::roulette::Chip {
100 let bets = Bets { 166 target: BetTarget::Split { a: 4, b: 4 },
101 red: 10, 167 amount: 10,
102 black: 10,
103 even: 10,
104 odd: 10,
105 numbers: vec![NumberBet {
106 number: 17,
107 amount: 5,
108 }], 168 }],
109 }; 169 };
110 let payload = serde_json::to_string(&bets).unwrap(); 170 assert!(bad_split.validate(1000).is_err());
111 let game = RouletteGame::with_result(17); 171
112 let state = game.prepare_state("alice", bets.total()); 172 let bad_corner = Bets {
173 chips: vec![zagan_bet::roulette::Chip {
174 target: BetTarget::Corner {
175 numbers: [1, 2, 3, 3],
176 },
177 amount: 10,
178 }],
179 };
180 assert!(bad_corner.validate(1000).is_err());
113 181
182 let bad_dozen = Bets {
183 chips: vec![zagan_bet::roulette::Chip {
184 target: BetTarget::Dozen { index: 3 },
185 amount: 10,
186 }],
187 };
188 assert!(bad_dozen.validate(1000).is_err());
189}
190
191#[test]
192fn the_server_settles_split_and_outside_chips() {
193 let mut bets = Bets::default();
194 bets.add(BetTarget::Split { a: 1, b: 2 }, 10);
195 bets.add(BetTarget::Outside { kind: Outside::Even }, 10);
196 let payload = bets.to_json();
197
198 let game = RouletteGame::with_result(2);
199 let state = game.prepare_state("alice", bets.total());
114 let state = game.process_action(&state, "spin", &[&payload]).unwrap(); 200 let state = game.process_action(&state, "spin", &[&payload]).unwrap();
115 let result = game.calculate_result(&state).unwrap(); 201 let result = game.calculate_result(&state).unwrap();
116 202
117 assert_eq!(result.winnings, 220); 203 // 2 is even: 10*18 (split) + 10*2 (even money).
118 assert_eq!(result.result_data.get("number").unwrap(), "17"); 204 assert_eq!(result.winnings, 200);
205 assert_eq!(result.result_data.get("number").unwrap(), "2");
119 assert_eq!(result.result_data.get("color").unwrap(), "black"); 206 assert_eq!(result.result_data.get("color").unwrap(), "black");
120 assert_eq!(result.result_data.get("bets").unwrap(), &payload); 207 assert_eq!(result.result_data.get("bets").unwrap(), &payload);
121} 208}
122 209
123#[test] 210#[test]
124fn the_server_rejects_a_total_that_does_not_match_the_bets() { 211fn the_server_rejects_a_total_that_does_not_match_the_bets() {
125 let payload = serde_json::to_string(&Bets { 212 let mut bets = Bets::default();
126 red: 10, 213 bets.add(BetTarget::Outside { kind: Outside::Red }, 10);
127 ..Bets::default() 214 let payload = bets.to_json();
128 })
129 .unwrap();
130 let game = RouletteGame::with_result(17); 215 let game = RouletteGame::with_result(17);
131 let state = game.prepare_state("alice", 99); 216 let state = game.prepare_state("alice", 99);
132 217
@@ -134,31 +219,116 @@ fn the_server_rejects_a_total_that_does_not_match_the_bets() {
134} 219}
135 220
136#[test] 221#[test]
137fn the_server_rejects_an_invalid_number() { 222fn the_server_rejects_an_invalid_target() {
138 let payload = serde_json::to_string(&Bets { 223 let payload = r#"{"chips":[{"target":{"bet":"straight","number":37},"amount":10}]}"#;
139 numbers: vec![NumberBet {
140 number: 37,
141 amount: 10,
142 }],
143 ..Bets::default()
144 })
145 .unwrap();
146 let game = RouletteGame::with_result(17); 224 let game = RouletteGame::with_result(17);
147 let state = game.prepare_state("alice", 10); 225 let state = game.prepare_state("alice", 10);
148 226
149 assert!(game.process_action(&state, "spin", &[&payload]).is_err()); 227 assert!(game.process_action(&state, "spin", &[payload]).is_err());
228}
229
230// --- betting cursor -------------------------------------------------------
231
232fn target_at(x: usize, y: usize) -> BetTarget {
233 Cursor::Lattice { x, y }.target().unwrap()
234}
235
236#[test]
237fn lattice_points_become_straights_splits_and_corners() {
238 assert_eq!(
239 target_at(0, 3),
240 BetTarget::Straight { number: 1 },
241 "bottom-left corner is a straight"
242 );
243 assert_eq!(target_at(0, 0), BetTarget::Straight { number: 3 });
244 assert_eq!(target_at(12, 0), BetTarget::Straight { number: 36 });
245 assert_eq!(target_at(12, 3), BetTarget::Straight { number: 34 });
246
247 assert_eq!(target_at(0, 1), BetTarget::Split { a: 2, b: 3 });
248 assert_eq!(target_at(12, 1), BetTarget::Split { a: 35, b: 36 });
249 assert_eq!(target_at(1, 0), BetTarget::Split { a: 3, b: 6 });
250
251 assert_eq!(
252 target_at(1, 1),
253 BetTarget::Corner {
254 numbers: [2, 3, 5, 6]
255 }
256 );
257 assert_eq!(
258 target_at(6, 2),
259 BetTarget::Corner {
260 numbers: [16, 17, 19, 20]
261 }
262 );
263}
264
265#[test]
266fn the_cursor_walks_the_felt_and_the_outside_rows() {
267 let bottom = Cursor::Lattice { x: 0, y: 3 };
268 assert_eq!(bottom.moved(Direction::Down), Cursor::Dozen(0));
269 assert_eq!(
270 Cursor::Lattice { x: 12, y: 3 }.moved(Direction::Down),
271 Cursor::Dozen(2)
272 );
273 assert_eq!(
274 Cursor::Lattice { x: 0, y: 0 }.moved(Direction::Up),
275 Cursor::Zero
276 );
277 assert_eq!(Cursor::Zero.moved(Direction::Down), Cursor::Lattice { x: 0, y: 0 });
278
279 assert_eq!(Cursor::Dozen(0).moved(Direction::Up), Cursor::Lattice { x: 1, y: 3 });
280 assert_eq!(Cursor::Dozen(0).moved(Direction::Down), Cursor::Column(0));
281 assert_eq!(Cursor::Column(2).moved(Direction::Up), Cursor::Dozen(2));
282 assert_eq!(Cursor::Column(1).moved(Direction::Down), Cursor::Outside(2));
283 assert_eq!(Cursor::Outside(3).moved(Direction::Up), Cursor::Column(1));
284
285 // Edges stay put instead of wrapping.
286 assert_eq!(Cursor::Dozen(0).moved(Direction::Left), Cursor::Dozen(0));
287 assert_eq!(Cursor::Outside(5).moved(Direction::Right), Cursor::Outside(5));
288 assert_eq!(
289 Cursor::Lattice { x: 0, y: 2 }.moved(Direction::Left),
290 Cursor::Lattice { x: 0, y: 2 }
291 );
292}
293
294#[test]
295fn every_cursor_round_trips_through_its_encoded_form() {
296 let cursors = [
297 Cursor::Zero,
298 Cursor::Lattice { x: 7, y: 2 },
299 Cursor::Dozen(1),
300 Cursor::Column(2),
301 Cursor::Outside(4),
302 ];
303 for cursor in cursors {
304 assert_eq!(Cursor::parse(&cursor.encoded()), cursor);
305 }
306 assert_eq!(Cursor::parse("nonsense"), Cursor::default());
307 assert_eq!(Cursor::parse("lattice:99:0"), Cursor::default());
308 assert_eq!(Cursor::parse("dozen:7"), Cursor::default());
309}
310
311#[test]
312fn the_outside_cursor_targets_match_the_labels() {
313 let labels: Vec<String> = (0..6)
314 .map(|index| {
315 let target = Cursor::Outside(index).target().unwrap();
316 target.label()
317 })
318 .collect();
319 assert_eq!(labels, vec!["1-18", "EVEN", "RED", "BLACK", "ODD", "19-36"]);
150} 320}
151 321
152#[test] 322#[test]
153fn parses_a_roulette_result() { 323fn parses_a_roulette_result() {
154 let bets = r#"{"red":10,"black":0,"even":10,"odd":0,"numbers":[{"number":17,"amount":10}]}"#; 324 let bets = r#"{"chips":[{"target":{"bet":"outside","kind":"red"},"amount":30}]}"#;
155 let response = format!("ROULETTE_RESULT session-1 360 30 1330 17 black {bets}"); 325 let response = format!("ROULETTE_RESULT session-1 60 30 1030 17 black {bets}");
156 let result = zagan_bet::client::games::response::parse_roulette_response(&response).unwrap(); 326 let result = zagan_bet::client::games::response::parse_roulette_response(&response).unwrap();
157 327
158 assert_eq!(result.session_id, "session-1"); 328 assert_eq!(result.session_id, "session-1");
159 assert_eq!(result.winnings, 360); 329 assert_eq!(result.winnings, 60);
160 assert_eq!(result.bet_amount, 30); 330 assert_eq!(result.bet_amount, 30);
161 assert_eq!(result.balance, 1330); 331 assert_eq!(result.balance, 1030);
162 assert_eq!(result.number, 17); 332 assert_eq!(result.number, 17);
163 assert_eq!(result.color, "black"); 333 assert_eq!(result.color, "black");
164 assert_eq!(result.bets, bets); 334 assert_eq!(result.bets, bets);
@@ -168,7 +338,7 @@ fn parses_a_roulette_result() {
168fn rejects_bad_roulette_results() { 338fn rejects_bad_roulette_results() {
169 use zagan_bet::client::games::response::parse_roulette_response; 339 use zagan_bet::client::games::response::parse_roulette_response;
170 340
171 let valid_bets = r#"{"red":10}"#; 341 let valid_bets = r#"{"chips":[]}"#;
172 assert!( 342 assert!(
173 parse_roulette_response(&format!("ROULETTE_RESULT s 1 1 1 37 green {valid_bets}")).is_err() 343 parse_roulette_response(&format!("ROULETTE_RESULT s 1 1 1 37 green {valid_bets}")).is_err()
174 ); 344 );