summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-11-01 16:17:19 +0100
committersalmonik <salmonik@tuta.io>2026-11-01 16:17:19 +0100
commitf07221b4e1d8aa464a44fe566884ccc0565406e9 (patch)
tree48020a68cee1c95521d594dc6803c1f7ec01c801 /tests
parentadd nginx + php-fpm deployment example (diff)
downloadzagan-bet-f07221b4e1d8aa464a44fe566884ccc0565406e9.tar.gz
zagan-bet-f07221b4e1d8aa464a44fe566884ccc0565406e9.zip
v3.2.0: straight and split bets anywhere on the felt; add history command
Diffstat (limited to '')
-rw-r--r--tests/client_core.rs90
-rw-r--r--tests/crash.rs2
-rw-r--r--tests/roulette.rs97
-rw-r--r--tests/server_core.rs57
4 files changed, 201 insertions, 45 deletions
diff --git a/tests/client_core.rs b/tests/client_core.rs
index bb8772e..b0bba6f 100644
--- a/tests/client_core.rs
+++ b/tests/client_core.rs
@@ -608,24 +608,29 @@ fn roulette_cursor_places_bets_and_spins() {
608 assert!(h.board()[1].contains("Balance: 1000 Kr")); 608 assert!(h.board()[1].contains("Balance: 1000 Kr"));
609 assert!(h.board()[1].contains("Chip: 10 Kr")); 609 assert!(h.board()[1].contains("Chip: 10 Kr"));
610 assert!(h.board()[9].contains("Bets: (none)")); 610 assert!(h.board()[9].contains("Bets: (none)"));
611 // The cursor starts on number 1, the bottom-left corner. 611 // The cursor starts on number 1, the bottom-left cell.
612 assert!(h.board()[5].contains("[ 1 ]")); 612 assert!(h.board()[5].contains("[ 1 ]"));
613 assert!(h.board()[10].contains("Bet: 1 · pays 36x"));
613 614
614 // Up twice: straight 2, then the split between 2 and 3. 615 // One step up is the split between 1 and 2, two steps is number 2.
615 h.key(Key::Up); 616 h.key(Key::Up);
617 assert!(h.board()[5].contains("[ 1 ]"));
618 assert!(h.board()[4].contains("[ 2 ]"));
619 assert!(h.board()[10].contains("Bet: 1/2 · pays 18x"));
620
616 h.key(Key::Up); 621 h.key(Key::Up);
622 assert!(!h.board()[5].contains("[ 1 ]"));
617 assert!(h.board()[4].contains("[ 2 ]")); 623 assert!(h.board()[4].contains("[ 2 ]"));
618 assert!(h.board()[3].contains("[ 3 ]")); 624 assert!(h.board()[10].contains("Bet: 2 · pays 36x"));
619 625
620 h.key(Key::Enter); 626 h.key(Key::Enter);
621 assert!(h.board()[9].contains("2/3 10")); 627 assert!(h.board()[9].contains("2 10"));
622 assert!(h.board()[9].contains("Total: 10 Kr"));
623 628
624 // Back down to number 1 and place a straight. 629 // Right from 2 is the split with 5.
625 h.key(Key::Down); 630 h.key(Key::Right);
626 h.key(Key::Down); 631 assert!(h.board()[10].contains("Bet: 2/5 · pays 18x"));
627 h.key(Key::Enter); 632 h.key(Key::Enter);
628 assert!(h.board()[9].contains("1 10")); 633 assert!(h.board()[9].contains("2/5 10"));
629 assert!(h.board()[9].contains("Total: 20 Kr")); 634 assert!(h.board()[9].contains("Total: 20 Kr"));
630 635
631 h.key(Key::Char(' ')); 636 h.key(Key::Char(' '));
@@ -636,18 +641,17 @@ fn roulette_cursor_places_bets_and_spins() {
636 .expect("roulette request"); 641 .expect("roulette request");
637 assert_eq!( 642 assert_eq!(
638 request, 643 request,
639 "PLAY_START roulette 20 {\"chips\":[{\"target\":{\"bet\":\"split\",\"a\":2,\"b\":3},\"amount\":10},{\"target\":{\"bet\":\"straight\",\"number\":1},\"amount\":10}]}" 644 "PLAY_START roulette 20 {\"chips\":[{\"target\":{\"bet\":\"straight\",\"number\":2},\"amount\":10},{\"target\":{\"bet\":\"split\",\"a\":2,\"b\":5},\"amount\":10}]}"
640 ); 645 );
641 646
642 let bets = r#"{"chips":[{"target":{"bet":"split","a":2,"b":3},"amount":10},{"target":{"bet":"straight","number":1},"amount":10}]}"#; 647 let bets = r#"{"chips":[{"target":{"bet":"straight","number":2},"amount":10},{"target":{"bet":"split","a":2,"b":5},"amount":10}]}"#;
643 h.reply(&format!( 648 h.reply(&format!(
644 "ROULETTE_RESULT session-1 180 20 1160 3 red {bets}" 649 "ROULETTE_RESULT session-1 540 20 1520 2 black {bets}"
645 )); 650 ));
646 651
647 assert_eq!(h.board().len(), 12); 652 assert!(h.board()[1].contains("Balance: 1520 Kr"));
648 assert!(h.board()[1].contains("Balance: 1160 Kr")); 653 assert!(h.board()[10].contains("Won 540 Kr at 2 BLACK EVEN"));
649 assert!(h.board()[10].contains("Won 180 Kr at 3 RED ODD")); 654 assert!(h.board()[10].contains("+520"));
650 assert!(h.board()[10].contains("+160"));
651} 655}
652 656
653#[test] 657#[test]
@@ -658,19 +662,18 @@ fn roulette_cursor_reaches_the_outside_rows() {
658 h.key(Key::Down); 662 h.key(Key::Down);
659 assert!(h.board()[6].starts_with("[") && h.board()[6].contains("1st 12")); 663 assert!(h.board()[6].starts_with("[") && h.board()[6].contains("1st 12"));
660 h.key(Key::Down); 664 h.key(Key::Down);
661 assert!(h.board()[7].starts_with("[") && h.board()[7].contains("2:1")); 665 assert!(h.board()[7].starts_with("[") && h.board()[7].contains("col 1 2:1"));
662 h.key(Key::Down); 666 h.key(Key::Down);
663 assert!(h.board()[8].starts_with("[") && h.board()[8].contains("1-18")); 667 assert!(h.board()[8].starts_with("[") && h.board()[8].contains("1-18"));
664 668
665 h.key(Key::Enter); 669 h.key(Key::Enter);
666 assert!(h.board()[9].contains("1-18 10")); 670 assert!(h.board()[9].contains("1-18 10"));
667 671
668 // Back up to the felt: the bottom edge of the first dozen is the 1/4 split. 672 // Back up to the felt: the dozens return to number 1.
669 h.key(Key::Up); 673 h.key(Key::Up);
670 h.key(Key::Up); 674 h.key(Key::Up);
671 h.key(Key::Up); 675 h.key(Key::Up);
672 assert!(h.board()[5].contains("[ 1 ]")); 676 assert!(h.board()[5].contains("[ 1 ]"));
673 assert!(h.board()[5].contains("[ 4 ]"));
674} 677}
675 678
676#[test] 679#[test]
@@ -1146,3 +1149,52 @@ fn a_round_is_live_only_while_money_is_reserved() {
1146 h.push(&crash_line("CRASH_BUSTED", &busted)); 1149 h.push(&crash_line("CRASH_BUSTED", &busted));
1147 assert!(!h.core.is_live_game()); 1150 assert!(!h.core.is_live_game());
1148} 1151}
1152
1153#[test]
1154fn history_prints_recent_plays() {
1155 let mut h = Harness::new();
1156 h.line("history");
1157 assert_eq!(h.sent.last().map(String::as_str), Some("HISTORY"));
1158
1159 let json = r#"[{"game":"roulette","bet":10,"winnings":20,"balance_after":1010,"at":"2026-10-30 01:20:11","details":{"number":"17","color":"black"}}]"#;
1160 h.reply(&format!("HISTORY {json}"));
1161
1162 assert!(h
1163 .screen
1164 .lines
1165 .iter()
1166 .any(|line| line.starts_with("Recent plays (1)")));
1167 let row = h
1168 .screen
1169 .lines
1170 .iter()
1171 .find(|line| line.contains("roulette"))
1172 .expect("play row");
1173 assert!(row.contains("2026-10-30 01:20"), "row: {row}");
1174 assert!(row.contains("1010"), "row: {row}");
1175 assert!(row.contains("17 black"), "row: {row}");
1176}
1177
1178#[test]
1179fn history_without_plays_says_so() {
1180 let mut h = Harness::new();
1181 h.line("history");
1182 h.reply("HISTORY []");
1183 assert!(h
1184 .screen
1185 .lines
1186 .iter()
1187 .any(|line| line == "No plays recorded yet."));
1188}
1189
1190#[test]
1191fn history_rejects_a_bad_count_without_sending() {
1192 let mut h = Harness::new();
1193 h.line("history 0");
1194 assert!(!h.sent.iter().any(|line| line == "HISTORY 0"));
1195 assert!(h
1196 .screen
1197 .lines
1198 .iter()
1199 .any(|line| line.contains("Usage: history [1-50]")));
1200}
diff --git a/tests/crash.rs b/tests/crash.rs
index c985ebe..797874b 100644
--- a/tests/crash.rs
+++ b/tests/crash.rs
@@ -38,7 +38,7 @@ fn the_time_for_a_multiplier_is_the_inverse_of_the_curve() {
38} 38}
39 39
40#[test] 40#[test]
41fn cashout_value_is_floored_to_whole_Kr() { 41fn cashout_value_is_floored_to_whole_kr() {
42 assert_eq!(payout(10, 1.0), 10); 42 assert_eq!(payout(10, 1.0), 10);
43 assert_eq!(payout(10, 1.05), 10, "half a Kr is not paid out"); 43 assert_eq!(payout(10, 1.05), 10, "half a Kr is not paid out");
44 assert_eq!(payout(10, 1.10), 11); 44 assert_eq!(payout(10, 1.10), 11);
diff --git a/tests/roulette.rs b/tests/roulette.rs
index 8f9155c..0c5e195 100644
--- a/tests/roulette.rs
+++ b/tests/roulette.rs
@@ -234,28 +234,51 @@ fn target_at(x: usize, y: usize) -> BetTarget {
234} 234}
235 235
236#[test] 236#[test]
237fn lattice_points_become_straights_splits_and_corners() { 237fn cell_centres_are_straights() {
238 assert_eq!( 238 assert_eq!(target_at(1, 5), BetTarget::Straight { number: 1 });
239 target_at(0, 3), 239 assert_eq!(target_at(1, 1), BetTarget::Straight { number: 3 });
240 BetTarget::Straight { number: 1 }, 240 assert_eq!(target_at(23, 1), BetTarget::Straight { number: 36 });
241 "bottom-left corner is a straight" 241 assert_eq!(target_at(23, 5), BetTarget::Straight { number: 34 });
242 ); 242 assert_eq!(target_at(5, 3), BetTarget::Straight { number: 8 });
243 assert_eq!(target_at(0, 0), BetTarget::Straight { number: 3 }); 243
244 assert_eq!(target_at(12, 0), BetTarget::Straight { number: 36 }); 244 // Every one of the 36 numbers can be bet straight.
245 assert_eq!(target_at(12, 3), BetTarget::Straight { number: 34 }); 245 let mut reachable = Vec::new();
246 246 for column in 0..12usize {
247 assert_eq!(target_at(0, 1), BetTarget::Split { a: 2, b: 3 }); 247 for row in 0..3usize {
248 assert_eq!(target_at(12, 1), BetTarget::Split { a: 35, b: 36 }); 248 let cursor = Cursor::Lattice {
249 assert_eq!(target_at(1, 0), BetTarget::Split { a: 3, b: 6 }); 249 x: 2 * column + 1,
250 y: 2 * row + 1,
251 };
252 if let Some(BetTarget::Straight { number }) = cursor.target() {
253 reachable.push(number);
254 }
255 }
256 }
257 reachable.sort_unstable();
258 assert_eq!(reachable, (1..=36).collect::<Vec<_>>());
259}
250 260
261#[test]
262fn lines_between_cells_are_splits_and_crossings_are_corners() {
263 // Vertical lines sit between two columns.
264 assert_eq!(target_at(2, 5), BetTarget::Split { a: 1, b: 4 });
265 assert_eq!(target_at(2, 3), BetTarget::Split { a: 2, b: 5 });
266 assert_eq!(target_at(22, 1), BetTarget::Split { a: 33, b: 36 });
267
268 // Horizontal lines sit between two rows.
269 assert_eq!(target_at(1, 4), BetTarget::Split { a: 1, b: 2 });
270 assert_eq!(target_at(3, 2), BetTarget::Split { a: 5, b: 6 });
271 assert_eq!(target_at(23, 4), BetTarget::Split { a: 34, b: 35 });
272
273 // Crossings cover four cells.
251 assert_eq!( 274 assert_eq!(
252 target_at(1, 1), 275 target_at(2, 4),
253 BetTarget::Corner { 276 BetTarget::Corner {
254 numbers: [2, 3, 5, 6] 277 numbers: [1, 2, 4, 5]
255 } 278 }
256 ); 279 );
257 assert_eq!( 280 assert_eq!(
258 target_at(6, 2), 281 target_at(12, 4),
259 BetTarget::Corner { 282 BetTarget::Corner {
260 numbers: [16, 17, 19, 20] 283 numbers: [16, 17, 19, 20]
261 } 284 }
@@ -264,31 +287,54 @@ fn lattice_points_become_straights_splits_and_corners() {
264 287
265#[test] 288#[test]
266fn the_cursor_walks_the_felt_and_the_outside_rows() { 289fn the_cursor_walks_the_felt_and_the_outside_rows() {
267 let bottom = Cursor::Lattice { x: 0, y: 3 }; 290 let bottom = Cursor::Lattice { x: 1, y: 5 };
291 // One step right is the split between 1 and 4, two steps is number 4.
292 assert_eq!(
293 bottom.moved(Direction::Right),
294 Cursor::Lattice { x: 2, y: 5 }
295 );
296 assert_eq!(
297 bottom.moved(Direction::Right).moved(Direction::Right),
298 Cursor::Lattice { x: 3, y: 5 }
299 );
300 // One step up is the split between 1 and 2.
301 assert_eq!(bottom.moved(Direction::Up), Cursor::Lattice { x: 1, y: 4 });
302
268 assert_eq!(bottom.moved(Direction::Down), Cursor::Dozen(0)); 303 assert_eq!(bottom.moved(Direction::Down), Cursor::Dozen(0));
269 assert_eq!( 304 assert_eq!(
270 Cursor::Lattice { x: 12, y: 3 }.moved(Direction::Down), 305 Cursor::Lattice { x: 23, y: 5 }.moved(Direction::Down),
271 Cursor::Dozen(2) 306 Cursor::Dozen(2)
272 ); 307 );
273 assert_eq!( 308 assert_eq!(
274 Cursor::Lattice { x: 0, y: 0 }.moved(Direction::Up), 309 Cursor::Lattice { x: 1, y: 1 }.moved(Direction::Up),
275 Cursor::Zero 310 Cursor::Zero
276 ); 311 );
277 assert_eq!(Cursor::Zero.moved(Direction::Down), Cursor::Lattice { x: 0, y: 0 }); 312 assert_eq!(
313 Cursor::Zero.moved(Direction::Down),
314 Cursor::Lattice { x: 1, y: 1 }
315 );
278 316
279 assert_eq!(Cursor::Dozen(0).moved(Direction::Up), Cursor::Lattice { x: 1, y: 3 }); 317 assert_eq!(
318 Cursor::Dozen(0).moved(Direction::Up),
319 Cursor::Lattice { x: 1, y: 5 }
320 );
280 assert_eq!(Cursor::Dozen(0).moved(Direction::Down), Cursor::Column(0)); 321 assert_eq!(Cursor::Dozen(0).moved(Direction::Down), Cursor::Column(0));
322 assert_eq!(Cursor::Dozen(2).moved(Direction::Up), Cursor::Lattice { x: 17, y: 5 });
281 assert_eq!(Cursor::Column(2).moved(Direction::Up), Cursor::Dozen(2)); 323 assert_eq!(Cursor::Column(2).moved(Direction::Up), Cursor::Dozen(2));
282 assert_eq!(Cursor::Column(1).moved(Direction::Down), Cursor::Outside(2)); 324 assert_eq!(Cursor::Column(1).moved(Direction::Down), Cursor::Outside(2));
283 assert_eq!(Cursor::Outside(3).moved(Direction::Up), Cursor::Column(1)); 325 assert_eq!(Cursor::Outside(3).moved(Direction::Up), Cursor::Column(1));
284 326
285 // Edges stay put instead of wrapping. 327 // 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!( 328 assert_eq!(
289 Cursor::Lattice { x: 0, y: 2 }.moved(Direction::Left), 329 Cursor::Lattice { x: 1, y: 3 }.moved(Direction::Left),
290 Cursor::Lattice { x: 0, y: 2 } 330 Cursor::Lattice { x: 1, y: 3 }
331 );
332 assert_eq!(
333 Cursor::Lattice { x: 23, y: 3 }.moved(Direction::Right),
334 Cursor::Lattice { x: 23, y: 3 }
291 ); 335 );
336 assert_eq!(Cursor::Dozen(0).moved(Direction::Left), Cursor::Dozen(0));
337 assert_eq!(Cursor::Outside(5).moved(Direction::Right), Cursor::Outside(5));
292} 338}
293 339
294#[test] 340#[test]
@@ -305,6 +351,7 @@ fn every_cursor_round_trips_through_its_encoded_form() {
305 } 351 }
306 assert_eq!(Cursor::parse("nonsense"), Cursor::default()); 352 assert_eq!(Cursor::parse("nonsense"), Cursor::default());
307 assert_eq!(Cursor::parse("lattice:99:0"), Cursor::default()); 353 assert_eq!(Cursor::parse("lattice:99:0"), Cursor::default());
354 assert_eq!(Cursor::parse("lattice:0:1"), Cursor::default());
308 assert_eq!(Cursor::parse("dozen:7"), Cursor::default()); 355 assert_eq!(Cursor::parse("dozen:7"), Cursor::default());
309} 356}
310 357
diff --git a/tests/server_core.rs b/tests/server_core.rs
index 47e80e7..6d6cffd 100644
--- a/tests/server_core.rs
+++ b/tests/server_core.rs
@@ -871,3 +871,60 @@ fn an_insufficient_bet_leaves_no_reservation() {
871 .unwrap(); 871 .unwrap();
872 assert_eq!(pending, 0); 872 assert_eq!(pending, 0);
873} 873}
874
875#[test]
876fn history_requires_login_and_valid_counts() {
877 let (mut core, _path) = setup("history-guards", WIN3);
878 let mut state = ConnectionState::default();
879
880 assert_eq!(
881 core.handle_message(&mut state, "HISTORY"),
882 "ERROR Not logged in"
883 );
884
885 register(&mut core, &mut state, "alice");
886 login(&mut core, &mut state, "alice");
887
888 assert_eq!(
889 core.handle_message(&mut state, "HISTORY nope"),
890 "ERROR Invalid HISTORY count"
891 );
892 assert_eq!(
893 core.handle_message(&mut state, "HISTORY 0"),
894 "ERROR Invalid HISTORY count"
895 );
896 assert_eq!(
897 core.handle_message(&mut state, "HISTORY 1 2"),
898 "ERROR Invalid HISTORY format. Usage: HISTORY [count]"
899 );
900}
901
902#[test]
903fn history_lists_recent_plays_newest_first() {
904 let (mut core, _path) = setup("history-list", WIN3);
905 let mut state = ConnectionState::default();
906 register(&mut core, &mut state, "alice");
907 login(&mut core, &mut state, "alice");
908
909 for _ in 0..3 {
910 assert!(core
911 .handle_message(&mut state, "PLAY_START slots 10")
912 .starts_with("GAME_RESULT"));
913 }
914
915 let response = core.handle_message(&mut state, "HISTORY 2");
916 let json = response.strip_prefix("HISTORY ").expect("history response");
917 let records: Vec<zagan_bet::history::PlayRecord> = serde_json::from_str(json).unwrap();
918
919 assert_eq!(records.len(), 2, "the limit must be honoured");
920 for record in &records {
921 assert_eq!(record.game, "slots");
922 assert_eq!(record.bet, 10);
923 assert_eq!(record.winnings, 10, "three C pay the stake back");
924 assert_eq!(record.summary(), "win");
925 assert_eq!(record.details["win_type"], "win");
926 }
927
928 // Newest first: the timestamps must not increase across the list.
929 assert!(records[0].at >= records[1].at);
930}