aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/hardening_casino_disconnect_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 05:31:09 -0400
committerhistoria <[not public]>2026-09-18 05:31:09 -0400
commit153acb7f644d755c45533b7c5311e3ffa47e8973 (patch)
tree7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/casino/hardening_casino_disconnect_test.go
parent87d08f0151e643951769e323995f1c5ed89ee200 (diff)
downloadthehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/casino/hardening_casino_disconnect_test.go')
-rw-r--r--internal/casino/hardening_casino_disconnect_test.go102
1 files changed, 102 insertions, 0 deletions
diff --git a/internal/casino/hardening_casino_disconnect_test.go b/internal/casino/hardening_casino_disconnect_test.go
new file mode 100644
index 0000000..6d25581
--- /dev/null
+++ b/internal/casino/hardening_casino_disconnect_test.go
@@ -0,0 +1,102 @@
+package casino
+
+import "testing"
+
+// A disconnect must only forfeit a hand that is actually in flight. A stake
+// placed in the betting window has had no card dealt for it, so refunding it
+// reveals nothing; destroying it there would let an ordinary transient TCP
+// drop swallow an undelt wager.
+
+// TestBlackjackPreDealDisconnectRefundsStake: alice bets, the betting window
+// is open (bob has not bet, no cards dealt), alice's socket drops. Her stake
+// comes back, the wager record is resolved, and the round that follows pays
+// nothing on top of the refund.
+func TestBlackjackPreDealDisconnectRefundsStake(t *testing.T) {
+ table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, MaxPlayers: 2, BettingTicks: 3}, newTestRand())
+ table.join("alice")
+ table.join("bob")
+ wallet := &testWallet{credits: 100}
+ table.bet("alice", "", 10, wallet)
+ if table.phase != blackjackBetting {
+ t.Fatalf("setup: expected the betting window (phase=%s)", table.phase)
+ }
+ if wallet.credits != 90 {
+ t.Fatalf("setup: stake not committed (credits=%d)", wallet.credits)
+ }
+
+ events := table.markDisconnected("alice")
+ if !hasMessage(events, "alice stands up") {
+ t.Fatalf("missing disconnect event: %+v", events)
+ }
+ if got := wallet.credits + wallet.paid; got != 100 {
+ t.Fatalf("pre-deal disconnect returned %d of 100; the undelt stake must be refunded", got)
+ }
+ if len(wallet.open) != 0 {
+ t.Fatalf("refunded stake left outstanding: %+v", wallet.open)
+ }
+
+ // The window resolves with bob sitting out; nothing may pay the
+ // refunded round afterwards.
+ for i := 0; i < 10; i++ {
+ table.tick()
+ }
+ if got := wallet.credits + wallet.paid; got != 100 {
+ t.Fatalf("refunded pre-deal round paid again: balance %d", got)
+ }
+ if table.phase != blackjackWaiting {
+ t.Fatalf("table did not reset after the window expired: phase=%s", table.phase)
+ }
+}
+
+// TestBlackjackMidHandDisconnectForfeitsStake: the documented mid-hand
+// forfeit policy is unchanged — cards are face up, so the stake stays with
+// the house.
+func TestBlackjackMidHandDisconnectForfeitsStake(t *testing.T) {
+ table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, MaxPlayers: 2}, newTestRand())
+ table.shoe = tableauShoe(
+ card{rank: "10"}, card{rank: "7"}, // dealer
+ card{rank: "10"}, card{rank: "8"}, // alice 18
+ card{rank: "10"}, card{rank: "6"}, // bob 16
+ )
+ wallet := &testWallet{credits: 100}
+ table.join("alice")
+ table.join("bob")
+ table.bet("alice", "", 10, wallet)
+ table.bet("bob", "", 10, &testWallet{credits: 100})
+ if table.phase != blackjackPlaying {
+ t.Fatalf("setup: expected a dealt hand (phase=%s)", table.phase)
+ }
+
+ table.markDisconnected("alice")
+ if wallet.credits != 90 || wallet.paid != 0 {
+ t.Fatalf("mid-hand disconnect refunded a dealt hand: credits=%d paid=%d, want 90/0 (forfeit)", wallet.credits, wallet.paid)
+ }
+}
+
+// TestBaccaratBettingWindowDisconnectRefundsStake: baccarat deals
+// synchronously once every connected seat has bet, so an outstanding wager
+// there has never had a card dealt for it either.
+func TestBaccaratBettingWindowDisconnectRefundsStake(t *testing.T) {
+ table := newBaccaratTable(1, TableConfig{ID: "baccarat", Game: GameBaccarat, MaxPlayers: 2, BettingTicks: 3}, newTestRand())
+ table.join("alice")
+ table.join("bob")
+ wallet := &testWallet{credits: 100}
+ table.bet("alice", baccaratSpotPlayer, 10, wallet)
+ if table.phase != baccaratBetting {
+ t.Fatalf("setup: expected the betting window (phase=%s)", table.phase)
+ }
+
+ table.markDisconnected("alice")
+ if got := wallet.credits + wallet.paid; got != 100 {
+ t.Fatalf("pre-deal baccarat disconnect returned %d of 100; want the stake refunded", got)
+ }
+ if len(wallet.open) != 0 {
+ t.Fatalf("refunded baccarat stake left outstanding: %+v", wallet.open)
+ }
+ for i := 0; i < 10; i++ {
+ table.tick()
+ }
+ if got := wallet.credits + wallet.paid; got != 100 {
+ t.Fatalf("refunded pre-deal coup paid again: balance %d", got)
+ }
+}