diff options
Diffstat (limited to 'internal/casino/blackjack_test.go')
| -rw-r--r-- | internal/casino/blackjack_test.go | 233 |
1 files changed, 233 insertions, 0 deletions
diff --git a/internal/casino/blackjack_test.go b/internal/casino/blackjack_test.go new file mode 100644 index 0000000..e8381fb --- /dev/null +++ b/internal/casino/blackjack_test.go @@ -0,0 +1,233 @@ +package casino + +import ( + "math/rand" + "strings" + "testing" +) + +func TestBlackjackHandTreatsAceAsElevenWhenPossible(t *testing.T) { + hand := &blackjackHand{cards: []blackjackCard{{rank: "A"}, {rank: "6"}}} + total, soft := hand.total() + if total != 17 || !soft { + t.Fatalf("total=%d soft=%v, want soft 17", total, soft) + } + hand.cards = append(hand.cards, blackjackCard{rank: "K"}) + total, soft = hand.total() + if total != 17 || soft { + t.Fatalf("total=%d soft=%v, want hard 17", total, soft) + } +} + +func TestBlackjackSettlementRules(t *testing.T) { + rules := (BlackjackConfig{}).Normalize() + natural := &blackjackHand{bet: 10, cards: []blackjackCard{{rank: "A"}, {rank: "K"}}} + if result, payout := settleHand(natural, 20, false, rules); result != "blackjack" || payout != 25 { + t.Fatalf("natural result=%s payout=%d", result, payout) + } + dealerNatural := &blackjackHand{bet: 10, cards: []blackjackCard{{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) + } + if result, payout := settleHand(&blackjackHand{bet: 10, cards: []blackjackCard{{rank: "10"}, {rank: "8"}}}, 21, true, rules); result != "dealer blackjack" || payout != 0 { + t.Fatalf("dealer blackjack result=%s payout=%d", result, payout) + } + if result, payout := settleHand(&blackjackHand{bet: 10, cards: []blackjackCard{{rank: "10"}, {rank: "8"}}}, 22, false, rules); result != "win" || payout != 20 { + t.Fatalf("dealer bust result=%s payout=%d", result, payout) + } +} + +func TestBlackjackTableStartsAfterAllPlayersBet(t *testing.T) { + table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, MaxPlayers: 2}, newTestRand()) + table.join("alice") + table.join("bob") + wa, wb := &testWallet{credits: 100}, &testWallet{credits: 100} + if events := table.bet("alice", 10, wa); hasMessage(events, "round begins") { + t.Fatal("round started before all players bet") + } + events := table.bet("bob", 10, wb) + if !hasMessage(events, "New blackjack hand") { + t.Fatalf("round did not start after all players bet: %+v", events) + } +} + +func TestSoloBlackjackTurnDoesNotTimeout(t *testing.T) { + table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack, Blackjack: BlackjackConfig{ActionTicks: 1}}, newTestRand()) + table.participants["alice"] = &blackjackPlayer{ + Participant: Participant{Name: "alice", HasBet: true, Connected: true}, + hands: []*blackjackHand{{bet: 10, cards: []blackjackCard{{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) + } + if table.phase != blackjackPlaying { + t.Fatalf("solo table phase changed to %s", table.phase) + } +} + +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} + if got := table.rebetAmount("alice"); got != 25 { + t.Fatalf("rebet=%d, want 25", got) + } +} + +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: []blackjackCard{{rank: "K"}, {rank: "6"}}}}} + table.order = []string{"alice"} + table.activePlayer = 0 + table.activeHand = 0 + table.dealer.cards = []blackjackCard{{rank: "10"}, {rank: "7"}} + event := table.prompt()[0] + if event.Message != "Your turn (K,6).\nActions: hit, stand, double" { + t.Fatalf("prompt=%q", event.Message) + } + if event.Menu == nil || event.Menu.Kind != MenuBlackjackTurn || event.Menu.Title != "Your turn (K,6)" || + len(event.Menu.Actions) != 3 || event.Menu.Actions[0] != "hit" || event.Menu.Actions[1] != "stand" || event.Menu.Actions[2] != "double" { + t.Fatalf("prompt menu=%+v", event.Menu) + } +} + +func TestBlackjackMenuEventCarriesStructuredMenu(t *testing.T) { + table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack}, newTestRand()) + event := table.menuEvent("alice") + if event.Menu == nil || event.Menu.Kind != MenuBlackjackBet { + t.Fatalf("menu event=%+v", event.Menu) + } +} + +func TestBlackjackInsurancePaysOnDealerBlackjack(t *testing.T) { + table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack}, newTestRand()) + w := &testWallet{credits: 100} + table.participants["alice"] = &blackjackPlayer{ + Participant: Participant{Name: "alice", HasBet: true, Connected: true, Wallet: w, Wager: Wager{Total: 10}}, + hands: []*blackjackHand{{bet: 10, cards: []blackjackCard{{rank: "10"}, {rank: "8"}}}}, + } + table.order = []string{"alice"} + table.dealer.cards = []blackjackCard{{rank: "K"}, {rank: "A"}} + table.phase = blackjackInsurance + events := table.action("alice", "insurance") + if w.credits != 95 { + t.Fatalf("insurance stake credits=%d, want 95", w.credits) + } + if w.paid != 15 { + t.Fatalf("insurance payout=%d, want 15 (stake + 2x)", w.paid) + } + if !hasMessage(events, "Insurance pays 15 credits.") { + t.Fatalf("missing insurance payout message: %+v", events) + } + if table.phase != blackjackWaiting { + t.Fatalf("phase=%s, want waiting after dealer blackjack settles", table.phase) + } +} + +func TestBlackjackInsuranceDeclineContinuesRound(t *testing.T) { + table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack}, newTestRand()) + w := &testWallet{credits: 100} + table.participants["alice"] = &blackjackPlayer{ + Participant: Participant{Name: "alice", HasBet: true, Connected: true, Wallet: w, Wager: Wager{Total: 10}}, + hands: []*blackjackHand{{bet: 10, cards: []blackjackCard{{rank: "10"}, {rank: "8"}}}}, + } + table.order = []string{"alice"} + table.dealer.cards = []blackjackCard{{rank: "9"}, {rank: "A"}} + table.phase = blackjackInsurance + events := table.action("alice", "stand") + if !hasMessage(events, "You decline insurance.") { + t.Fatalf("missing decline message: %+v", events) + } + if table.phase != blackjackPlaying { + t.Fatalf("phase=%s, want playing after insurance declined", table.phase) + } + if !hasMessage(events, "Your turn (10,8).") { + t.Fatalf("round did not continue after insurance: %+v", events) + } +} + +func TestBlackjackDoubleSetsBust(t *testing.T) { + table := newBlackjackTable(1, TableConfig{ID: "blackjack", Game: GameBlackjack}, newTestRand()) + // decks 0 disables the shuffle threshold so the forced card is drawn. + table.shoe = &blackjackShoe{cards: []blackjackCard{{rank: "K"}}} + w := &testWallet{credits: 100} + table.participants["alice"] = &blackjackPlayer{ + Participant: Participant{Name: "alice", HasBet: true, Connected: true, Wallet: w, Wager: Wager{Total: 10}}, + hands: []*blackjackHand{{bet: 10, cards: []blackjackCard{{rank: "10"}, {rank: "6"}}}}, + } + table.order = []string{"alice"} + table.dealer.cards = []blackjackCard{{rank: "10"}, {rank: "7"}} + table.phase = blackjackPlaying + hand := table.participants["alice"].hands[0] + events := table.action("alice", "double") + if !hand.bust { + t.Fatal("doubling into 26 did not set the bust flag") + } + if !hasMessage(events, "loses") { + t.Fatalf("busted double did not settle as a loss: %+v", events) + } +} + +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{ + Participant: Participant{Name: "alice", HasBet: true, Connected: true, Wallet: w, Wager: Wager{Total: 10}}, + hands: []*blackjackHand{{bet: 10, fromSplit: true, cards: []blackjackCard{{rank: "A"}, {rank: "A"}}}}, + } + table.order = []string{"alice"} + table.dealer.cards = []blackjackCard{{rank: "10"}, {rank: "7"}} + table.phase = blackjackPlaying + p := table.participants["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) + } + if events := table.action("alice", "hit"); !hasMessage(events, "Split aces cannot be hit.") { + t.Fatalf("hit on split aces not rejected: %+v", events) + } +} + +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") + w := &testWallet{credits: 100} + events := table.bet("alice", 10, w) + if !hasMessage(events, "New blackjack hand") { + t.Fatalf("disconnected 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: []blackjackCard{{rank: "10"}, {rank: "8"}}}}, + } + table.order = []string{"alice"} + table.dealer.cards = []blackjackCard{{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) + } +} + +func hasMessage(events []Event, fragment string) bool { + for _, event := range events { + if contains(event.Message, fragment) || contains(event.PrivateMessage, fragment) { + return true + } + } + return false +} + +func contains(value, fragment string) bool { + return strings.Contains(value, fragment) +} + +func newTestRand() *rand.Rand { return rand.New(rand.NewSource(7)) } |
