diff options
Diffstat (limited to 'internal/casino/blackjack_test.go')
| -rw-r--r-- | internal/casino/blackjack_test.go | 145 |
1 files changed, 112 insertions, 33 deletions
diff --git a/internal/casino/blackjack_test.go b/internal/casino/blackjack_test.go index a6735c0..510077d 100644 --- a/internal/casino/blackjack_test.go +++ b/internal/casino/blackjack_test.go @@ -37,6 +37,10 @@ func TestBlackjackSettlementRules(t *testing.T) { if result, payout := settleHand(natural, 20, false, rules); result != "blackjack" || payout != 25 { t.Fatalf("natural result=%s payout=%d", result, payout) } + smallNatural := &blackjackHand{bet: 1, cards: []card{{rank: "A"}, {rank: "K"}}} + if result, payout := settleHand(smallNatural, 20, false, rules); result != "blackjack" || payout != 2 { + t.Fatalf("small natural result=%s payout=%d, want blackjack/2 (winnings round down)", result, payout) + } dealerNatural := &blackjackHand{bet: 10, cards: []card{{rank: "A"}, {rank: "Q"}}} if result, payout := settleHand(dealerNatural, 21, true, rules); result != "push" || payout != 10 { t.Fatalf("dealer natural result=%s payout=%d", result, payout) @@ -65,11 +69,10 @@ func TestBlackjackTableStartsAfterAllPlayersBet(t *testing.T) { func TestSoloBlackjackTurnDoesNotTimeout(t *testing.T) { table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, Blackjack: BlackjackConfig{ActionTicks: 1}}, newTestRand()) - table.participants["alice"] = &blackjackPlayer{ + table.players.add("alice", &blackjackPlayer{ Participant: Participant{Name: "alice", HasBet: true, Connected: true}, hands: []*blackjackHand{{bet: 10, cards: []card{{rank: "10"}, {rank: "6"}}}}, - } - table.order = []string{"alice"} + }) table.phase = blackjackPlaying if events := table.tick(); len(events) != 0 { t.Fatalf("solo action timed out: %+v", events) @@ -79,9 +82,39 @@ func TestSoloBlackjackTurnDoesNotTimeout(t *testing.T) { } } +func TestBlackjackDisconnectedSoloPlayerTimesOut(t *testing.T) { + table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, Blackjack: BlackjackConfig{ActionTicks: 1}}, newTestRand()) + table.shoe = &shoe{cards: []card{{rank: "9"}}} // dealer stands on 17; spare card just in case + w := &testWallet{credits: 100} + table.players.add("alice", &blackjackPlayer{ + Participant: Participant{Name: "alice", HasBet: true, Connected: false, Wallet: w, Wager: Wager{Total: 10}}, + hands: []*blackjackHand{{bet: 10, cards: []card{{rank: "10"}, {rank: "6"}}}}, + }) + table.dealer.cards = []card{{rank: "10"}, {rank: "7"}} + table.phase = blackjackPlaying + table.timer.start(1) // beginPlay starts the action timer in production + events := table.tick() + if !hasMessage(events, "Your action timed out; standing.") { + t.Fatalf("disconnected player was not timed out: %+v", events) + } + if table.phase != blackjackWaiting { + t.Fatalf("phase=%s, want waiting after the round settles", table.phase) + } + if w.paid != 0 { + t.Fatalf("stood 16 against dealer 17 should lose, paid=%d", w.paid) + } + // The table recovers: a new player can sit and bet. + table.shoe = tableauShoe(card{rank: "10"}, card{rank: "7"}, card{rank: "10"}, card{rank: "8"}) + table.join("bob") + wb := &testWallet{credits: 100} + if events := table.bet("bob", "", 10, wb); !hasSnapshot(events) { + t.Fatalf("table did not recover after the disconnected player timed out: %+v", events) + } +} + func TestBlackjackRebetKeepsLastWager(t *testing.T) { table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, MinBet: 5, MaxBet: 100}, newTestRand()) - table.participants["alice"] = &blackjackPlayer{Participant: Participant{Name: "alice"}, lastBet: 25} + table.players.add("alice", &blackjackPlayer{Participant: Participant{Name: "alice"}, lastBet: 25}) if spot, got := table.rebet("alice"); got != 25 || spot != "" { t.Fatalf("rebet=(%q, %d), want (\"\", 25)", spot, got) } @@ -89,8 +122,7 @@ func TestBlackjackRebetKeepsLastWager(t *testing.T) { func TestBlackjackPromptNamesActiveHandAndOnlyOffersAvailableActions(t *testing.T) { table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack}, newTestRand()) - table.participants["alice"] = &blackjackPlayer{Participant: Participant{Name: "alice", HasBet: true, Connected: true}, hands: []*blackjackHand{{bet: 10, cards: []card{{rank: "K"}, {rank: "6"}}}}} - table.order = []string{"alice"} + table.players.add("alice", &blackjackPlayer{Participant: Participant{Name: "alice", HasBet: true, Connected: true}, hands: []*blackjackHand{{bet: 10, cards: []card{{rank: "K"}, {rank: "6"}}}}}) table.activePlayer = 0 table.activeHand = 0 table.dealer.cards = []card{{rank: "10"}, {rank: "7"}} @@ -130,11 +162,10 @@ func TestBlackjackActionCommandsDescribeEachAction(t *testing.T) { func TestBlackjackInsurancePaysOnDealerBlackjack(t *testing.T) { table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack}, newTestRand()) w := &testWallet{credits: 100} - table.participants["alice"] = &blackjackPlayer{ + table.players.add("alice", &blackjackPlayer{ Participant: Participant{Name: "alice", HasBet: true, Connected: true, Wallet: w, Wager: Wager{Total: 10}}, hands: []*blackjackHand{{bet: 10, cards: []card{{rank: "10"}, {rank: "8"}}}}, - } - table.order = []string{"alice"} + }) table.dealer.cards = []card{{rank: "K"}, {rank: "A"}} table.phase = blackjackInsurance events := table.action("alice", "insurance") @@ -155,11 +186,10 @@ func TestBlackjackInsurancePaysOnDealerBlackjack(t *testing.T) { func TestBlackjackInsuranceDeclineContinuesRound(t *testing.T) { table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack}, newTestRand()) w := &testWallet{credits: 100} - table.participants["alice"] = &blackjackPlayer{ + table.players.add("alice", &blackjackPlayer{ Participant: Participant{Name: "alice", HasBet: true, Connected: true, Wallet: w, Wager: Wager{Total: 10}}, hands: []*blackjackHand{{bet: 10, cards: []card{{rank: "10"}, {rank: "8"}}}}, - } - table.order = []string{"alice"} + }) table.dealer.cards = []card{{rank: "9"}, {rank: "A"}} table.phase = blackjackInsurance events := table.action("alice", "stand") @@ -179,14 +209,13 @@ func TestBlackjackDoubleSetsBust(t *testing.T) { // decks 0 disables the shuffle threshold so the forced card is drawn. table.shoe = &shoe{cards: []card{{rank: "K"}}} w := &testWallet{credits: 100} - table.participants["alice"] = &blackjackPlayer{ + table.players.add("alice", &blackjackPlayer{ Participant: Participant{Name: "alice", HasBet: true, Connected: true, Wallet: w, Wager: Wager{Total: 10}}, hands: []*blackjackHand{{bet: 10, cards: []card{{rank: "10"}, {rank: "6"}}}}, - } - table.order = []string{"alice"} + }) table.dealer.cards = []card{{rank: "10"}, {rank: "7"}} table.phase = blackjackPlaying - hand := table.participants["alice"].hands[0] + hand := table.players.get("alice").hands[0] events := table.action("alice", "double") if !hand.bust { t.Fatal("doubling into 26 did not set the bust flag") @@ -200,14 +229,13 @@ func TestBlackjackSplitAcesOnlyAllowResplit(t *testing.T) { resplit := true table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, Blackjack: BlackjackConfig{ResplitAces: &resplit}}, newTestRand()) w := &testWallet{credits: 100} - table.participants["alice"] = &blackjackPlayer{ + table.players.add("alice", &blackjackPlayer{ Participant: Participant{Name: "alice", HasBet: true, Connected: true, Wallet: w, Wager: Wager{Total: 10}}, hands: []*blackjackHand{{bet: 10, fromSplit: true, cards: []card{{rank: "A"}, {rank: "A"}}}}, - } - table.order = []string{"alice"} + }) table.dealer.cards = []card{{rank: "10"}, {rank: "7"}} table.phase = blackjackPlaying - p := table.participants["alice"] + p := table.players.get("alice") actions := table.availableActions(p, p.hands[0]) if len(actions) != 2 || actions[0] != "stand" || actions[1] != "split" { t.Fatalf("split ace actions=%v, want [stand split]", actions) @@ -221,26 +249,77 @@ func TestBlackjackBettingIgnoresDisconnected(t *testing.T) { table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, MaxPlayers: 2}, newTestRand()) table.join("alice") table.join("bob") - table.markDisconnected("bob") + table.markDisconnected("bob") // removes bob; alice is the only bettor w := &testWallet{credits: 100} events := table.bet("alice", "", 10, w) if !hasSnapshot(events) { - t.Fatalf("disconnected player stalled the round: %+v", events) + t.Fatalf("removed player stalled the round: %+v", events) } } -func TestBlackjackReconnectShowsTableSnapshot(t *testing.T) { - table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack}, newTestRand()) - table.participants["alice"] = &blackjackPlayer{ - Participant: Participant{Name: "alice", HasBet: true, Connected: true}, - hands: []*blackjackHand{{bet: 10, cards: []card{{rank: "10"}, {rank: "8"}}}}, +func TestBlackjackDisconnectActivePassesPlay(t *testing.T) { + // Two players; the active one disconnects. Play passes to the next + // bettor and the forfeited wager is never paid out. + table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, MaxPlayers: 2, Blackjack: BlackjackConfig{ActionTicks: 1}}, newTestRand()) + table.shoe = tableauShoe(card{rank: "10"}, card{rank: "7"}, card{rank: "K"}, card{rank: "Q"}, card{rank: "10"}, card{rank: "6"}) + wa, wb := &testWallet{credits: 100}, &testWallet{credits: 100} + table.join("alice") + table.join("bob") + table.bet("alice", "", 10, wa) + events := table.bet("bob", "", 10, wb) + if !hasSnapshot(events) { + t.Fatalf("round did not start: %+v", events) } - table.order = []string{"alice"} - table.dealer.cards = []card{{rank: "10"}, {rank: "7"}} - table.phase = blackjackPlaying - events := table.join("alice") - if len(events) != 1 || events[0].Blackjack == nil { - t.Fatalf("reconnect during play lacked a table snapshot: %+v", events) + // Both players dealt; alice active. Alice disconnects. + disEvt := table.markDisconnected("alice") + if !hasMessage(disEvt, "alice stands up") || len(disEvt) == 0 { + t.Fatalf("missing disconnect event: %+v", disEvt) + } + // Alice forfeits (never paid). + if wa.paid != 0 { + t.Fatalf("alice's forfeited wager was paid: %d", wa.paid) + } + // Bob is now prompted for his turn (10,6 against dealer 10-up). + if !hasMessage(disEvt, "Your turn (10,6)") { + t.Fatalf("play did not pass to bob after alice left: %+v", disEvt) + } + // Bob stands; round settles. + settle := table.action("bob", "stand") + if !hasMessage(settle, "You lose!") { + t.Fatalf("bob 16 vs dealer 17: expected loss, got %+v", settle) + } + if wb.paid != 0 { + t.Fatalf("bob should lose 16v17, paid=%d", wb.paid) + } +} + +func TestBlackjackDisconnectEarlierSeatPreservesTurn(t *testing.T) { + // Two players; the earlier seat disconnects during play. The active + // player's turn is unaffected (index fixup). + table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, MaxPlayers: 2, Blackjack: BlackjackConfig{ActionTicks: 1}}, newTestRand()) + table.shoe = tableauShoe(card{rank: "10"}, card{rank: "7"}, card{rank: "10"}, card{rank: "9"}, card{rank: "K"}, card{rank: "Q"}) + table.join("alice") + table.join("bob") + wa := &testWallet{credits: 100} + table.bet("alice", "", 10, wa) + events := table.bet("bob", "", 10, &testWallet{credits: 100}) + if !hasSnapshot(events) { + t.Fatalf("round did not start: %+v", events) + } + // Advance past alice's turn (bob is picked up after skipCompletedHands). + // Dealer 10-7. Let alice stand. + table.action("alice", "stand") + // Bob is now active. Remove alice (seat index 0; bob at 1). + disEvt := table.markDisconnected("alice") + // Bob (now seat 0) is still prompted — no "your turn" means finishHand + // skipped straight to settle (one bettor, bob 10-9 stands, 19 vs 17 → win). + // Actually finishHand → skipCompletedHands → bob's hand incomplete → + // prompt. Let's verify: + if !hasMessage(disEvt, "Your turn") { + t.Fatalf("bob's turn not preserved after alice was removed: %+v", disEvt) + } + if wa.paid != 0 { + t.Fatalf("alice should not be paid after removal, got %d", wa.paid) } } |
