diff options
| author | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
| commit | 153acb7f644d755c45533b7c5311e3ffa47e8973 (patch) | |
| tree | 7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/casino/hardening_casino_disconnect_test.go | |
| parent | 87d08f0151e643951769e323995f1c5ed89ee200 (diff) | |
| download | thehouseoficarus-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.go | 102 |
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) + } +} |
