summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/blackjack.rs139
-rw-r--r--tests/client_core.rs201
-rw-r--r--tests/server_core.rs159
3 files changed, 498 insertions, 1 deletions
diff --git a/tests/blackjack.rs b/tests/blackjack.rs
new file mode 100644
index 0000000..79721d3
--- /dev/null
+++ b/tests/blackjack.rs
@@ -0,0 +1,139 @@
1use zagan_bet::blackjack::{
2 card_label, card_value, fresh_shoe, hand_total, is_blackjack, BlackjackState,
3};
4use zagan_bet::server::games::blackjack::BlackjackGame;
5
6fn hand(deck: &[&str], bet: u64) -> zagan_bet::server::games::blackjack::BlackjackHand {
7 let deck = deck.iter().map(|card| card.to_string()).collect();
8 BlackjackGame::with_deck(deck).deal(bet)
9}
10
11#[test]
12fn card_values_and_soft_totals_are_correct() {
13 assert_eq!(card_value("AS"), 11);
14 assert_eq!(card_value("10H"), 10);
15 assert_eq!(card_value("KD"), 10);
16 assert_eq!(card_label("AS"), "A♠");
17 assert_eq!(card_label("10H"), "10♥");
18
19 let cards = |values: &[&str]| values.iter().map(|card| card.to_string()).collect::<Vec<_>>();
20 assert_eq!(hand_total(&cards(&["AS", "10H"])), 21);
21 assert_eq!(hand_total(&cards(&["AS", "9H"])), 20);
22 assert_eq!(hand_total(&cards(&["AS", "AS", "9H"])), 21);
23 assert!(is_blackjack(&cards(&["AS", "KH"])));
24 assert!(!is_blackjack(&cards(&["AS", "9H"])));
25}
26
27#[test]
28fn a_six_deck_shoe_has_312_cards() {
29 let shoe = fresh_shoe();
30 assert_eq!(shoe.len(), 312);
31 assert_eq!(shoe.iter().filter(|card| card.as_str() == "AS").count(), 6);
32}
33
34#[test]
35fn a_natural_blackjack_settles_immediately() {
36 let hand = hand(&["AS", "9D", "KH", "8C"], 10);
37
38 assert!(hand.is_done());
39 assert_eq!(hand.result, "blackjack");
40 assert_eq!(hand.winnings, 25); // 10 + 15
41 assert_eq!(hand.player_total(), 21);
42}
43
44#[test]
45fn a_player_hand_can_hit_to_21() {
46 let mut hand = hand(&["10S", "9D", "6H", "8C", "5D"], 10);
47 assert_eq!(hand.player_total(), 16);
48 assert_eq!(hand.dealer_total(), 17);
49
50 hand.hit().unwrap();
51
52 assert!(hand.is_done());
53 assert_eq!(hand.player_total(), 21);
54 assert_eq!(hand.result, "win");
55 assert_eq!(hand.winnings, 20);
56}
57
58#[test]
59fn hitting_over_21_busts() {
60 let mut hand = hand(&["10S", "9D", "6H", "8C", "KD"], 10);
61 hand.hit().unwrap();
62
63 assert!(hand.is_done());
64 assert_eq!(hand.result, "bust");
65 assert_eq!(hand.winnings, 0);
66}
67
68#[test]
69fn doubling_adds_the_base_bet_and_draws_one_card() {
70 let mut hand = hand(&["5S", "9D", "5H", "8C", "10D"], 10);
71 assert_eq!(hand.player_total(), 10);
72
73 hand.double().unwrap();
74
75 assert_eq!(hand.player.len(), 3);
76 assert_eq!(hand.bet, 20);
77 assert_eq!(hand.player_total(), 20);
78 assert_eq!(hand.result, "win");
79 assert_eq!(hand.winnings, 40);
80}
81
82#[test]
83fn the_dealer_busts_and_the_player_wins() {
84 let mut hand = hand(&["10S", "7H", "8H", "9C", "10H"], 10);
85 assert_eq!(hand.player_total(), 18);
86 assert_eq!(hand.dealer_total(), 16);
87
88 hand.stand();
89
90 assert_eq!(hand.dealer_total(), 26);
91 assert_eq!(hand.result, "win");
92 assert_eq!(hand.winnings, 20);
93}
94
95#[test]
96fn the_dealer_stands_on_soft_17() {
97 let mut hand = hand(&["10S", "AD", "8H", "6C"], 10);
98 assert_eq!(hand.dealer_total(), 17);
99
100 hand.stand();
101
102 assert_eq!(hand.dealer.len(), 2, "dealer must stand on soft 17");
103 assert_eq!(hand.result, "win");
104 assert_eq!(hand.winnings, 20);
105}
106
107#[test]
108fn equal_totals_push() {
109 let mut hand = hand(&["10S", "10D", "8H", "8C"], 10);
110 hand.stand();
111
112 assert_eq!(hand.result, "push");
113 assert_eq!(hand.winnings, 10);
114}
115
116#[test]
117fn dealer_blackjack_wins_unless_player_also_has_it() {
118 let dealer_blackjack = hand(&["10S", "AS", "8H", "KD"], 10);
119 assert_eq!(dealer_blackjack.result, "lose");
120 assert_eq!(dealer_blackjack.winnings, 0);
121
122 let both = hand(&["AS", "AD", "KH", "KC"], 10);
123 assert_eq!(both.result, "push");
124 assert_eq!(both.winnings, 10);
125}
126
127#[test]
128fn the_player_state_hides_the_hole_card() {
129 let hand = hand(&["10S", "9D", "6H", "8C"], 10);
130 let state: BlackjackState = hand.state("session-1", 990);
131
132 assert_eq!(state.phase, "player");
133 assert_eq!(state.dealer, vec!["9D".to_string(), "?".to_string()]);
134 assert_eq!(state.dealer_total, 9);
135 assert_eq!(state.player_total, 16);
136 assert_eq!(state.actions, vec!["hit", "stand", "double"]);
137 assert_eq!(state.bet, 10);
138 assert_eq!(state.balance, 990);
139}
diff --git a/tests/client_core.rs b/tests/client_core.rs
index 7fb7e29..cfcf608 100644
--- a/tests/client_core.rs
+++ b/tests/client_core.rs
@@ -4,8 +4,9 @@
4//! logical `Effect`s that `ClientCore` emits, so assertions are about what a 4//! logical `Effect`s that `ClientCore` emits, so assertions are about what a
5//! player would see, not about ANSI sequences. 5//! player would see, not about ANSI sequences.
6 6
7use zagan_bet::blackjack::BlackjackState;
7use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key}; 8use zagan_bet::client::core::{ClientCore, Effect, InteractiveCommand, Key};
8use zagan_bet::client::games::response::parse_spin_response; 9use zagan_bet::client::games::response::{parse_blackjack_response, parse_spin_response};
9use zagan_bet::client::ui::command_names; 10use zagan_bet::client::ui::command_names;
10 11
11const WIN3: &str = "CCCCLLCLODGSDGS"; 12const WIN3: &str = "CCCCLLCLODGSDGS";
@@ -139,6 +140,11 @@ impl Harness {
139 self.line("play roulette"); 140 self.line("play roulette");
140 self.reply("INFO alice 1 1000"); 141 self.reply("INFO alice 1 1000");
141 } 142 }
143
144 fn enter_blackjack(&mut self) {
145 self.line("play blackjack");
146 self.reply("INFO alice 1 1000");
147 }
142} 148}
143 149
144#[test] 150#[test]
@@ -647,3 +653,196 @@ fn roulette_rejects_an_out_of_range_number() {
647 assert!(h.board()[4].contains("ERROR: Number must be 0-36")); 653 assert!(h.board()[4].contains("ERROR: Number must be 0-36"));
648 assert!(h.board()[2].contains("Bets: (none)")); 654 assert!(h.board()[2].contains("Bets: (none)"));
649} 655}
656
657fn bj_state(
658 session: &str,
659 phase: &str,
660 player: &[&str],
661 dealer: &[&str],
662 player_total: u8,
663 dealer_total: u8,
664 bet: u64,
665 base_bet: u64,
666 balance: i64,
667 actions: &[&str],
668 result: &str,
669 winnings: u64,
670) -> BlackjackState {
671 BlackjackState {
672 session_id: session.to_string(),
673 phase: phase.to_string(),
674 player: player.iter().map(|card| card.to_string()).collect(),
675 dealer: dealer.iter().map(|card| card.to_string()).collect(),
676 player_total,
677 dealer_total,
678 bet,
679 base_bet,
680 balance,
681 actions: actions.iter().map(|action| action.to_string()).collect(),
682 result: result.to_string(),
683 winnings,
684 }
685}
686
687#[test]
688fn blackjack_deals_hits_and_settles() {
689 let mut h = Harness::new();
690 h.enter_blackjack();
691
692 assert_eq!(h.board().len(), 6);
693 assert!(h.board()[4].contains("Press [SPACE] to deal."));
694
695 h.key(Key::Char(' '));
696 assert!(h
697 .sent
698 .iter()
699 .any(|line| line == "PLAY_START blackjack 10"));
700
701 let player = bj_state(
702 "bj1",
703 "player",
704 &["10S", "6H"],
705 &["9D", "?"],
706 16,
707 9,
708 10,
709 10,
710 990,
711 &["hit", "stand", "double"],
712 "playing",
713 0,
714 );
715 h.reply(&format!(
716 "BLACKJACK_STATE {}",
717 serde_json::to_string(&player).unwrap()
718 ));
719
720 assert_eq!(h.board().len(), 6);
721 assert!(h.board()[2].contains("Dealer: [9♦] [ ? ] 9"));
722 assert!(h.board()[3].contains("You: [10♠] [6♥] 16"));
723 assert!(h.board()[4].contains("Hit, stand or double."));
724 assert!(h.board()[5].contains("[D]ouble"));
725
726 h.key(Key::Char('h'));
727 assert_eq!(
728 h.sent.last().map(String::as_str),
729 Some("BLACKJACK_ACTION bj1 hit")
730 );
731
732 let done = bj_state(
733 "bj1",
734 "done",
735 &["10S", "6H", "5D"],
736 &["9D", "8C"],
737 21,
738 17,
739 10,
740 10,
741 1010,
742 &[],
743 "win",
744 20,
745 );
746 h.reply(&format!(
747 "BLACKJACK_STATE {}",
748 serde_json::to_string(&done).unwrap()
749 ));
750
751 assert!(h.board()[4].contains("Win! Won 20 PLN"));
752 assert!(h.board()[5].contains("[SPACE] New hand"));
753
754 h.key(Key::Char('q'));
755 assert!(h.board().is_empty());
756 assert!(h.screen.lines.iter().any(|line| line == "Game exited."));
757}
758
759#[test]
760fn blackjack_cancel_sends_a_cancel_action() {
761 let mut h = Harness::new();
762 h.enter_blackjack();
763 h.key(Key::Char(' '));
764
765 let player = bj_state(
766 "bj1",
767 "player",
768 &["10S", "6H"],
769 &["9D", "?"],
770 16,
771 9,
772 10,
773 10,
774 990,
775 &["hit", "stand", "double"],
776 "playing",
777 0,
778 );
779 h.reply(&format!(
780 "BLACKJACK_STATE {}",
781 serde_json::to_string(&player).unwrap()
782 ));
783
784 h.key(Key::Char('q'));
785 assert_eq!(
786 h.sent.last().map(String::as_str),
787 Some("BLACKJACK_ACTION bj1 cancel")
788 );
789
790 let cancelled = bj_state(
791 "bj1",
792 "done",
793 &["10S", "6H"],
794 &["9D", "?"],
795 16,
796 9,
797 10,
798 10,
799 1000,
800 &[],
801 "cancelled",
802 0,
803 );
804 h.reply(&format!(
805 "BLACKJACK_STATE {}",
806 serde_json::to_string(&cancelled).unwrap()
807 ));
808
809 assert!(h.board().is_empty());
810 assert!(h.screen.lines.iter().any(|line| line == "Game exited."));
811}
812
813#[test]
814fn blackjack_ready_board_can_change_the_next_bet() {
815 let mut h = Harness::new();
816 h.enter_blackjack();
817
818 h.key(Key::Char('='));
819 assert!(h.board()[1].contains("Bet: 20 PLN"));
820
821 h.key(Key::Char(' '));
822 assert!(h
823 .sent
824 .iter()
825 .any(|line| line == "PLAY_START blackjack 20"));
826}
827
828#[test]
829fn parses_a_blackjack_state() {
830 let state = bj_state(
831 "bj1",
832 "player",
833 &["10S", "6H"],
834 &["9D", "?"],
835 16,
836 9,
837 10,
838 10,
839 990,
840 &["hit", "stand"],
841 "playing",
842 0,
843 );
844 let response = format!("BLACKJACK_STATE {}", serde_json::to_string(&state).unwrap());
845
846 assert_eq!(parse_blackjack_response(&response).unwrap(), state);
847 assert!(parse_blackjack_response("ERROR Insufficient balance").is_err());
848}
diff --git a/tests/server_core.rs b/tests/server_core.rs
index cf05102..3ad277a 100644
--- a/tests/server_core.rs
+++ b/tests/server_core.rs
@@ -1,6 +1,7 @@
1use std::path::PathBuf; 1use std::path::PathBuf;
2 2
3use rusqlite::Connection; 3use rusqlite::Connection;
4use zagan_bet::blackjack::BlackjackState;
4use zagan_bet::server::core::{ConnectionState, ServerCore}; 5use zagan_bet::server::core::{ConnectionState, ServerCore};
5use zagan_bet::server::database; 6use zagan_bet::server::database;
6use zagan_bet::server::games::registry::GameRegistry; 7use zagan_bet::server::games::registry::GameRegistry;
@@ -47,6 +48,31 @@ fn setup_roulette(name: &str, number: u8) -> (ServerCore, String) {
47 (ServerCore::with_registry(conn, registry, 4), path) 48 (ServerCore::with_registry(conn, registry, 4), path)
48} 49}
49 50
51fn setup_blackjack(name: &str, deck: &[&str]) -> (ServerCore, String) {
52 let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR"))
53 .join(format!("server-core-{name}.db"))
54 .to_string_lossy()
55 .into_owned();
56 let _ = std::fs::remove_file(&path);
57
58 let conn = Connection::open(&path).unwrap();
59 database::init_db(&conn);
60
61 let deck = deck.iter().map(|card| card.to_string()).collect();
62 (
63 ServerCore::with_registry(conn, GameRegistry::with_defaults(), 4)
64 .with_blackjack_deck(deck),
65 path,
66 )
67}
68
69fn blackjack_state(response: &str) -> BlackjackState {
70 let raw = response
71 .strip_prefix("BLACKJACK_STATE ")
72 .expect("blackjack state response");
73 serde_json::from_str(raw).expect("valid blackjack state")
74}
75
50fn register(core: &mut ServerCore, state: &mut ConnectionState, user: &str) { 76fn register(core: &mut ServerCore, state: &mut ConnectionState, user: &str) {
51 assert_eq!( 77 assert_eq!(
52 core.handle_message(state, &format!("REGISTER {user} credential")), 78 core.handle_message(state, &format!("REGISTER {user} credential")),
@@ -337,3 +363,136 @@ fn unknown_games_are_rejected_before_a_session_is_created() {
337 ); 363 );
338 assert_eq!(core.session_count(), 0); 364 assert_eq!(core.session_count(), 0);
339} 365}
366
367#[test]
368fn blackjack_reserves_the_bet_and_settles_a_hit_hand() {
369 let (mut core, path) = setup_blackjack(
370 "blackjack-hit",
371 &["10S", "9D", "6H", "8C", "5D"],
372 );
373 let mut state = ConnectionState::default();
374 register(&mut core, &mut state, "alice");
375 login(&mut core, &mut state, "alice");
376
377 let response = core.handle_message(&mut state, "PLAY_START blackjack 10");
378 let current = blackjack_state(&response);
379
380 assert_eq!(current.phase, "player");
381 assert_eq!(current.player_total, 16);
382 assert_eq!(current.dealer, vec!["9D".to_string(), "?".to_string()]);
383 assert_eq!(current.balance, 990);
384 assert_eq!(current.actions, vec!["hit", "stand", "double"]);
385 assert_eq!(core.session_count(), 1);
386
387 let response = core.handle_message(
388 &mut state,
389 &format!("BLACKJACK_ACTION {} hit", current.session_id),
390 );
391 let done = blackjack_state(&response);
392
393 assert_eq!(done.phase, "done");
394 assert_eq!(done.result, "win");
395 assert_eq!(done.winnings, 20);
396 assert_eq!(done.balance, 1010);
397 assert_eq!(core.balance("alice"), Some(1010));
398 assert_eq!(core.play_count("alice"), 1);
399 assert_eq!(core.session_count(), 0);
400
401 let conn = Connection::open(&path).unwrap();
402 let (game_type, details): (String, String) = conn
403 .query_row(
404 "SELECT game_type, details FROM plays WHERE username = 'alice'",
405 [],
406 |row| Ok((row.get(0)?, row.get(1)?)),
407 )
408 .unwrap();
409 assert_eq!(game_type, "blackjack");
410 let details: BlackjackState = serde_json::from_str(&details).unwrap();
411 assert_eq!(details.result, "win");
412 assert_eq!(details.winnings, 20);
413}
414
415#[test]
416fn blackjack_double_reserves_an_extra_bet() {
417 let (mut core, _path) = setup_blackjack(
418 "blackjack-double",
419 &["5S", "9D", "5H", "8C", "10D"],
420 );
421 let mut state = ConnectionState::default();
422 register(&mut core, &mut state, "alice");
423 login(&mut core, &mut state, "alice");
424
425 let current = blackjack_state(&core.handle_message(&mut state, "PLAY_START blackjack 10"));
426 assert_eq!(current.balance, 990);
427
428 let done = blackjack_state(&core.handle_message(
429 &mut state,
430 &format!("BLACKJACK_ACTION {} double", current.session_id),
431 ));
432
433 assert_eq!(done.phase, "done");
434 assert_eq!(done.bet, 20);
435 assert_eq!(done.winnings, 40);
436 assert_eq!(done.balance, 1020);
437 assert_eq!(core.balance("alice"), Some(1020));
438 assert_eq!(core.play_count("alice"), 1);
439}
440
441#[test]
442fn blackjack_natural_settles_immediately() {
443 let (mut core, _path) =
444 setup_blackjack("blackjack-natural", &["AS", "9D", "KH", "8C"]);
445 let mut state = ConnectionState::default();
446 register(&mut core, &mut state, "alice");
447 login(&mut core, &mut state, "alice");
448
449 let done = blackjack_state(&core.handle_message(&mut state, "PLAY_START blackjack 10"));
450
451 assert_eq!(done.phase, "done");
452 assert_eq!(done.result, "blackjack");
453 assert_eq!(done.winnings, 25);
454 assert_eq!(done.balance, 1015);
455 assert_eq!(core.session_count(), 0);
456 assert_eq!(core.play_count("alice"), 1);
457}
458
459#[test]
460fn blackjack_cancel_refunds_the_reserved_bet() {
461 let (mut core, _path) =
462 setup_blackjack("blackjack-cancel", &["10S", "9D", "6H", "8C"]);
463 let mut state = ConnectionState::default();
464 register(&mut core, &mut state, "alice");
465 login(&mut core, &mut state, "alice");
466
467 let current = blackjack_state(&core.handle_message(&mut state, "PLAY_START blackjack 10"));
468 assert_eq!(current.balance, 990);
469
470 let cancelled = blackjack_state(&core.handle_message(
471 &mut state,
472 &format!("BLACKJACK_ACTION {} cancel", current.session_id),
473 ));
474
475 assert_eq!(cancelled.result, "cancelled");
476 assert_eq!(cancelled.balance, 1000);
477 assert_eq!(core.balance("alice"), Some(1000));
478 assert_eq!(core.play_count("alice"), 0);
479 assert_eq!(core.session_count(), 0);
480}
481
482#[test]
483fn blackjack_disconnect_refunds_the_reserved_bet() {
484 let (mut core, _path) =
485 setup_blackjack("blackjack-disconnect", &["10S", "9D", "6H", "8C"]);
486 let mut state = ConnectionState::default();
487 register(&mut core, &mut state, "alice");
488 login(&mut core, &mut state, "alice");
489
490 let current = blackjack_state(&core.handle_message(&mut state, "PLAY_START blackjack 10"));
491 assert_eq!(current.balance, 990);
492
493 core.disconnect(&state);
494
495 assert_eq!(core.balance("alice"), Some(1000));
496 assert_eq!(core.play_count("alice"), 0);
497 assert_eq!(core.session_count(), 0);
498}