package casino import ( "strings" "testing" ) func uxBlackjack() *blackjackTable { t := newBlackjackTable(1, TableConfig{ID: "bj", Game: GameBlackjack, Blackjack: BlackjackConfig{ActionTicks: 8}}, newTestRand()) t.phase = blackjackPlaying t.dealer.cards = []card{{rank: "10", suit: "spades"}, {rank: "7", suit: "hearts"}} t.players.add("alice", &blackjackPlayer{Participant: Participant{Name: "alice", Connected: true, HasBet: true, Wager: Wager{Total: 10}}, hands: []*blackjackHand{{bet: 10, cards: []card{{rank: "10"}, {rank: "6"}}}}}) t.prompt() return t } func TestTableStatusDoesNotExtendAnyDeadlineOrRevealHoleCard(t *testing.T) { table := uxBlackjack() table.tick() beforeTimer, beforeCeiling := table.timer, table.ceiling for i := 0; i < 3; i++ { events := table.status("alice") snap := events[0].Snapshot.(*BlackjackSnapshot) if !snap.Dealer[0].Hidden || snap.Dealer[0].Rank != "?" { t.Fatal("status revealed hole card") } if !snap.Players[0].Active || snap.Players[0].Hand != 1 || snap.Players[0].Bet != 10 { t.Fatalf("missing active hand: %+v", snap.Players[0]) } } if table.timer != beforeTimer || table.ceiling != beforeCeiling { t.Fatal("status extended deadline") } } func TestBlackjackWarnsOnceThenAutoStands(t *testing.T) { for _, multi := range []bool{false, true} { table := uxBlackjack() if multi { table.players.add("bob", &blackjackPlayer{Participant: Participant{Name: "bob", Connected: true, HasBet: true}, hands: []*blackjackHand{{bet: 10, stood: true, cards: []card{{rank: "10"}, {rank: "8"}}}}}) } warnings := 0 timeout := false for i := 0; i < 81; i++ { for _, e := range table.tick() { if e.PrivateTo == "alice" && strings.Contains(e.Message, "No response means stand") { warnings++ if e.Color != "casino_warning" { t.Fatal("warning not colored") } } if strings.Contains(e.Message, "standing.") { timeout = true if e.Color != "casino_warning" { t.Fatal("timeout not colored") } } } } if warnings != 1 || !timeout { t.Fatalf("multi=%v warnings=%d timeout=%v", multi, warnings, timeout) } } } func TestInsuranceRequiresExplicitDecisionAndWarns(t *testing.T) { table := uxBlackjack() table.phase = blackjackInsurance table.timer.start(8) before := len(table.players.get("alice").hands[0].cards) events := table.action("alice", "hit") if !hasMessage(events, "No card action") || table.players.get("alice").declinedInsurance || len(table.players.get("alice").hands[0].cards) != before { t.Fatal("hit silently declined or took card") } warnings := 0 for i := 0; i < 8; i++ { for _, e := range table.tick() { if strings.Contains(e.Message, "Insurance closes") { warnings++ if e.Color != "casino_warning" { t.Fatal("insurance warning color") } } } } if warnings != 1 || table.phase != blackjackPlaying { t.Fatalf("warnings=%d phase=%s", warnings, table.phase) } } func TestBettingWarningOnlyTargetsPlayersWhoNeedToBet(t *testing.T) { table := newBaccaratTestTable(TableConfig{ID: "bac", Game: GameBaccarat, BettingTicks: 8}) for _, n := range []string{"alice", "bob", "carol"} { table.join(n) } table.action("carol", "sitout") table.bet("alice", "banker", 10, &testWallet{credits: 100}) warnings := 0 for i := 0; i < 8; i++ { for _, e := range table.tick() { if e.Color == "casino_warning" { warnings++ if e.PrivateTo != "bob" { t.Fatalf("warning sent to %s", e.PrivateTo) } } } } if warnings != 1 { t.Fatalf("warnings=%d", warnings) } } func TestSitoutDoesNotBlockOthersAndBetRejoins(t *testing.T) { table := newBaccaratTestTable(TableConfig{ID: "bac", Game: GameBaccarat}) table.join("alice") table.join("bob") table.bet("alice", "banker", 10, &testWallet{credits: 100}) events := table.action("bob", "sitout") if !hasMessage(events, "New baccarat hand!") || !table.players.get("bob").SittingOut { t.Fatal("sitout held up round") } table.action("alice", "sitout") events = table.bet("bob", "player", 10, &testWallet{credits: 100}) if !hasMessage(events, "New baccarat hand!") || table.players.get("bob").SittingOut { t.Fatal("bet failed to rejoin") } } func TestQueuedLeaveAndSitoutPreserveCommittedBlackjackHands(t *testing.T) { for _, action := range []string{"leave_after", "sitout"} { table := uxBlackjack() hand := table.players.get("alice").hands[0] table.action("alice", action) if !table.hasParticipant("alice") || hand.stood || hand.surrendered { t.Fatalf("%s altered committed hand", action) } events := table.action("alice", "stand") if !hasMessage(events, "{casino_payout}-10 credits{/}") { t.Fatalf("missing settlement: %+v", events) } if action == "leave_after" && table.hasParticipant("alice") { t.Fatal("queued leave retained seat") } if action == "sitout" && !table.players.get("alice").SittingOut { t.Fatal("queued sitout not applied") } } } func TestSplitAndInsuranceRoundNetIncludesEveryStake(t *testing.T) { table := uxBlackjack() p := table.players.get("alice") p.hands = []*blackjackHand{{bet: 20, stood: true, cards: []card{{rank: "10"}, {rank: "10"}}}, {bet: 10, bust: true, cards: []card{{rank: "10"}, {rank: "10"}, {rank: "5"}}}} p.insurance = 5 events := table.settle() for _, text := range []string{ "{casino_result_win}WIN{/}", "{casino_payout}+5 credits{/}", "Hand 1/2:{/} {casino_result_win}+20 credits{/}", "Hand 2/2:{/} {casino_result_loss}-10 credits{/}", "Insurance:{/} {casino_result_loss}-5 credits{/}", } { if !hasMessage(events, text) { t.Fatalf("missing %q in %+v", text, events) } } } func TestSingleHandSettlementKeepsSummaryQuiet(t *testing.T) { // A connected player with one hand and no insurance already saw the net in // the per-hand result; the next bet menu carries the balance. table := uxBlackjack() table.dealer.cards = []card{{rank: "10", suit: "spades"}, {rank: "7", suit: "hearts"}} events := table.settle() if hasMessage(events, "Round complete") { t.Fatalf("single-hand settlement repeated the net: %+v", events) } for _, e := range events { if e.ShowBalance { t.Fatalf("balance attached to a settlement whose next menu carries it: %+v", e) } } } func TestDepartingSettlementCarriesBalanceSummary(t *testing.T) { table := uxBlackjack() table.dealer.cards = []card{{rank: "10", suit: "spades"}, {rank: "7", suit: "hearts"}} table.players.get("alice").LeaveAfter = true events := table.settle() found := false for _, e := range events { if strings.Contains(e.PrivateMessage, "LOSS") && e.ShowBalance { found = true } } if !found { t.Fatalf("departing player lost the balance summary: %+v", events) } } func TestRebetMenusAndMidRoundJoin(t *testing.T) { table := uxBlackjack() events := table.join("bob") if !hasMessage(events, "Round in progress") { t.Fatal("mid-round join offered betting") } bac := newBaccaratTestTable(TableConfig{ID: "bac", Game: GameBaccarat}) bac.join("alice") first := bac.menuEvent("alice") if first.Menu.Commands[0].Command == "bet " { t.Fatal("first baccarat bet omitted target") } bac.bet("alice", "banker", 25, &testWallet{credits: 100}) menu := bac.menuEvent("alice").Menu if !menu.Repeat || !strings.Contains(menu.Commands[0].Desc, "Repeat 25 on banker") { t.Fatalf("missing rebet details: %+v", menu) } }