summaryrefslogtreecommitdiffstats
path: root/tests/server_core.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--tests/server_core.rs245
1 files changed, 245 insertions, 0 deletions
diff --git a/tests/server_core.rs b/tests/server_core.rs
index 08a2bf2..695cfe4 100644
--- a/tests/server_core.rs
+++ b/tests/server_core.rs
@@ -1,9 +1,12 @@
1use std::path::PathBuf; 1use std::path::PathBuf;
2use std::sync::Arc;
2 3
3use rusqlite::Connection; 4use rusqlite::Connection;
4use zagan_bet::blackjack::BlackjackState; 5use zagan_bet::blackjack::BlackjackState;
6use zagan_bet::crash::CrashState;
5use zagan_bet::server::core::{ConnectionState, ServerCore}; 7use zagan_bet::server::core::{ConnectionState, ServerCore};
6use zagan_bet::server::database; 8use zagan_bet::server::database;
9use zagan_bet::server::games::crash::ManualClock;
7use zagan_bet::server::games::registry::GameRegistry; 10use zagan_bet::server::games::registry::GameRegistry;
8use zagan_bet::server::games::roulette::RouletteGame; 11use zagan_bet::server::games::roulette::RouletteGame;
9use zagan_bet::server::games::slots::SlotsGame; 12use zagan_bet::server::games::slots::SlotsGame;
@@ -73,6 +76,31 @@ fn blackjack_state(response: &str) -> BlackjackState {
73 serde_json::from_str(raw).expect("valid blackjack state") 76 serde_json::from_str(raw).expect("valid blackjack state")
74} 77}
75 78
79fn setup_crash(name: &str, uniform: f64) -> (ServerCore, String, ManualClock) {
80 let path = PathBuf::from(env!("CARGO_TARGET_TMPDIR"))
81 .join(format!("server-core-{name}.db"))
82 .to_string_lossy()
83 .into_owned();
84 let _ = std::fs::remove_file(&path);
85
86 let conn = Connection::open(&path).unwrap();
87 database::init_db(&conn);
88
89 let clock = ManualClock::new();
90 let core = ServerCore::with_registry(conn, GameRegistry::with_defaults(), 4)
91 .with_crash_uniform(uniform)
92 .with_clock(Arc::new(clock.clone()));
93 (core, path, clock)
94}
95
96fn crash_state(response: &str) -> CrashState {
97 let raw = ["CRASH_START ", "CRASH_STATE ", "CRASH_BUSTED "]
98 .iter()
99 .find_map(|prefix| response.strip_prefix(prefix))
100 .unwrap_or_else(|| panic!("not a crash response: {response}"));
101 serde_json::from_str(raw).expect("valid crash state")
102}
103
76fn register(core: &mut ServerCore, state: &mut ConnectionState, user: &str) { 104fn register(core: &mut ServerCore, state: &mut ConnectionState, user: &str) {
77 assert_eq!( 105 assert_eq!(
78 core.handle_message(state, &format!("REGISTER {user} credential")), 106 core.handle_message(state, &format!("REGISTER {user} credential")),
@@ -496,3 +524,220 @@ fn blackjack_disconnect_forfeits_the_reserved_bet() {
496 assert_eq!(core.play_count("alice"), 1); 524 assert_eq!(core.play_count("alice"), 1);
497 assert_eq!(core.session_count(), 0); 525 assert_eq!(core.session_count(), 0);
498} 526}
527
528// 0.67 crashes at 3.00x: (1 - 0.01) / (1 - 0.67) = 3.0.
529const CRASH_AT_3: f64 = 0.67;
530
531#[test]
532fn crash_start_reserves_the_bet_and_hides_the_crash_point() {
533 let (mut core, _path, _clock) = setup_crash("crash-start", CRASH_AT_3);
534 let mut state = ConnectionState::default();
535 register(&mut core, &mut state, "alice");
536 login(&mut core, &mut state, "alice");
537
538 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10"));
539
540 assert_eq!(started.phase, "running");
541 assert_eq!(started.bet, 10);
542 assert_eq!(started.balance, 990);
543 assert_eq!(started.multiplier, 1.0);
544 assert_eq!(started.potential, 10);
545 assert_eq!(started.payout, 0);
546 assert_eq!(
547 started.crash_point, None,
548 "the crash point must not leak before the round ends"
549 );
550 assert_eq!(core.balance("alice"), Some(990));
551 assert_eq!(core.session_count(), 1);
552 assert_eq!(core.play_count("alice"), 0);
553}
554
555#[test]
556fn crash_cashout_pays_the_multiplier_from_the_server_clock() {
557 let (mut core, path, clock) = setup_crash("crash-cashout", CRASH_AT_3);
558 let mut state = ConnectionState::default();
559 register(&mut core, &mut state, "alice");
560 login(&mut core, &mut state, "alice");
561
562 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10"));
563
564 // Five seconds of round time: 2.00x, still below the 3.00x crash point.
565 clock.advance(5.0);
566 let cashed = crash_state(&core.handle_message(
567 &mut state,
568 &format!("CRASH_CASHOUT {}", started.session_id),
569 ));
570
571 assert_eq!(cashed.phase, "cashed");
572 assert_eq!(cashed.multiplier, 2.0);
573 assert_eq!(cashed.potential, 20);
574 assert_eq!(cashed.payout, 20);
575 assert_eq!(cashed.balance, 1010);
576 let crash_point = cashed.crash_point.unwrap();
577 assert!((crash_point - 3.0).abs() < 1e-9, "got {crash_point}");
578 assert_eq!(core.balance("alice"), Some(1010));
579 assert_eq!(core.play_count("alice"), 1);
580 assert_eq!(core.session_count(), 0);
581
582 let conn = Connection::open(&path).unwrap();
583 let (game_type, bet, winnings, balance_after, details): (String, i64, i64, i64, String) = conn
584 .query_row(
585 "SELECT game_type, bet_amount, winnings, balance_after, details
586 FROM plays WHERE username = 'alice'",
587 [],
588 |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, row.get(4)?)),
589 )
590 .unwrap();
591 assert_eq!(game_type, "crash");
592 assert_eq!(bet, 10);
593 assert_eq!(winnings, 20);
594 assert_eq!(balance_after, 1010);
595 let details: serde_json::Value = serde_json::from_str(&details).unwrap();
596 assert_eq!(details["phase"], "cashed");
597 let audited_crash_point = details["crash_point"].as_f64().unwrap();
598 assert!(
599 (audited_crash_point - 3.0).abs() < 1e-9,
600 "got {audited_crash_point}"
601 );
602 assert_eq!(details["multiplier"], 2.0);
603}
604
605#[test]
606fn a_cashout_after_the_crash_time_loses_the_reserved_bet() {
607 let (mut core, _path, clock) = setup_crash("crash-too-late", CRASH_AT_3);
608 let mut state = ConnectionState::default();
609 register(&mut core, &mut state, "alice");
610 login(&mut core, &mut state, "alice");
611
612 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10"));
613
614 clock.advance(20.0);
615 let late = crash_state(&core.handle_message(
616 &mut state,
617 &format!("CRASH_CASHOUT {}", started.session_id),
618 ));
619
620 assert_eq!(late.phase, "crashed");
621 assert_eq!(late.payout, 0);
622 assert_eq!(late.balance, 990);
623 let crash_point = late.crash_point.unwrap();
624 assert!((crash_point - 3.0).abs() < 1e-9, "got {crash_point}");
625 assert_eq!(core.balance("alice"), Some(990));
626 assert_eq!(core.play_count("alice"), 1);
627 assert_eq!(core.session_count(), 0);
628}
629
630#[test]
631fn crash_poll_pushes_the_bust_exactly_once() {
632 let (mut core, _path, clock) = setup_crash("crash-poll", CRASH_AT_3);
633 let mut state = ConnectionState::default();
634 register(&mut core, &mut state, "alice");
635 login(&mut core, &mut state, "alice");
636
637 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10"));
638
639 clock.advance(1.0);
640 assert!(
641 core.poll(&state).is_empty(),
642 "a running round must not push anything"
643 );
644
645 clock.advance(15.0);
646 let pushed = core.poll(&state);
647 assert_eq!(pushed.len(), 1);
648 let busted = crash_state(&pushed[0]);
649 assert_eq!(busted.session_id, started.session_id);
650 assert_eq!(busted.phase, "crashed");
651 assert_eq!(busted.payout, 0);
652 assert_eq!(busted.balance, 990);
653
654 assert!(core.poll(&state).is_empty(), "the bust must not repeat");
655 assert_eq!(core.play_count("alice"), 1);
656 assert_eq!(core.session_count(), 0);
657}
658
659#[test]
660fn crash_forfeit_keeps_the_reserved_bet() {
661 let (mut core, _path, _clock) = setup_crash("crash-forfeit", CRASH_AT_3);
662 let mut state = ConnectionState::default();
663 register(&mut core, &mut state, "alice");
664 login(&mut core, &mut state, "alice");
665
666 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10"));
667 let forfeited = crash_state(&core.handle_message(
668 &mut state,
669 &format!("CRASH_FORFEIT {}", started.session_id),
670 ));
671
672 assert_eq!(forfeited.phase, "forfeit");
673 assert_eq!(forfeited.payout, 0);
674 assert_eq!(core.balance("alice"), Some(990));
675 assert_eq!(core.play_count("alice"), 1);
676 assert_eq!(core.session_count(), 0);
677}
678
679#[test]
680fn crash_disconnect_forfeits_the_reserved_bet() {
681 let (mut core, _path, _clock) = setup_crash("crash-disconnect", CRASH_AT_3);
682 let mut state = ConnectionState::default();
683 register(&mut core, &mut state, "alice");
684 login(&mut core, &mut state, "alice");
685
686 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10"));
687 assert_eq!(started.balance, 990);
688
689 core.disconnect(&state);
690
691 assert_eq!(core.balance("alice"), Some(990));
692 assert_eq!(core.play_count("alice"), 1);
693 assert_eq!(core.session_count(), 0);
694}
695
696#[test]
697fn crash_rejects_a_second_round_and_bad_cashout_formats() {
698 let (mut core, _path, _clock) = setup_crash("crash-guards", CRASH_AT_3);
699 let mut state = ConnectionState::default();
700 register(&mut core, &mut state, "alice");
701 login(&mut core, &mut state, "alice");
702
703 let started = crash_state(&core.handle_message(&mut state, "PLAY_START crash 10"));
704 assert_eq!(
705 core.handle_message(&mut state, "PLAY_START crash 10"),
706 "ERROR Crash round already in progress"
707 );
708 assert_eq!(
709 core.handle_message(
710 &mut state,
711 &format!("CRASH_CASHOUT {} 50.0", started.session_id)
712 ),
713 "ERROR Invalid CRASH_CASHOUT format. Usage: CRASH_CASHOUT <session>"
714 );
715 assert_eq!(
716 core.handle_message(&mut state, "CRASH_CASHOUT nope"),
717 "ERROR Session not found"
718 );
719}
720
721#[test]
722fn crash_rounds_are_owned_by_the_player_who_started_them() {
723 let (mut core, _path, clock) = setup_crash("crash-owner", CRASH_AT_3);
724 let mut alice = ConnectionState::default();
725 let mut bob = ConnectionState::default();
726 register(&mut core, &mut alice, "alice");
727 register(&mut core, &mut bob, "bob");
728 login(&mut core, &mut alice, "alice");
729 login(&mut core, &mut bob, "bob");
730
731 let started = crash_state(&core.handle_message(&mut alice, "PLAY_START crash 10"));
732
733 clock.advance(5.0);
734 assert_eq!(
735 core.handle_message(
736 &mut bob,
737 &format!("CRASH_CASHOUT {}", started.session_id)
738 ),
739 "ERROR Invalid session"
740 );
741 assert_eq!(core.balance("bob"), Some(1000));
742 assert_eq!(core.session_count(), 1, "alice's round must survive");
743}