aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/pai_gow_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/pai_gow_test.go')
-rw-r--r--internal/casino/pai_gow_test.go163
1 files changed, 139 insertions, 24 deletions
diff --git a/internal/casino/pai_gow_test.go b/internal/casino/pai_gow_test.go
index f258b04..02ea187 100644
--- a/internal/casino/pai_gow_test.go
+++ b/internal/casino/pai_gow_test.go
@@ -181,17 +181,19 @@ func TestPaiGowLifecyclePrivacyAndUniqueDeck(t *testing.T) {
wallets := map[string]*testWallet{}
for _, name := range []string{"a", "b", "c", "d", "e", "f"} {
tab.join(name)
- w := &testWallet{chips: 100}
- wallets[name] = w
- tab.bet(name, "main", 10, w)
+ wallets[name] = &testWallet{chips: 100}
+ }
+ var events []Event
+ for _, name := range []string{"a", "b", "c", "d", "e", "f"} {
+ w := wallets[name]
+ // Side bets settle before the main wager; the last main bet deals.
tab.bet(name, "fortune", 5, w)
+ events = tab.bet(name, "main", 10, w)
}
tab.join("overflow")
if tab.hasParticipant("overflow") {
t.Fatal("seventh seat")
}
- tab.tick()
- events := tab.tick()
if tab.phase != "setting" {
t.Fatal(tab.phase)
}
@@ -214,6 +216,9 @@ func TestPaiGowLifecyclePrivacyAndUniqueDeck(t *testing.T) {
t.Fatal(len(seen))
}
for _, e := range events {
+ if e.Snapshot == nil {
+ continue
+ }
if e.Public || e.PrivateTo == "" {
t.Fatal("private cards leaked")
}
@@ -265,11 +270,10 @@ func TestPaiGowAtomicRetryAndEnvy(t *testing.T) {
b := &pgFailWallet{testWallet: testWallet{chips: 100}}
tab.join("a")
tab.join("b")
- tab.bet("a", "main", 20, a)
tab.bet("a", "fortune", 5, a)
+ tab.bet("a", "main", 20, a)
+ // b's main wager completes the table and deals immediately.
tab.bet("b", "main", 20, b)
- tab.action("a", "deal")
- tab.action("b", "deal")
// Script the dealt cards before either player sets. a loses Fortune but
// collects b's fixed quad Envy, even though b didn't bet Fortune.
tab.player("a").cards = pgCards("2s 4h 6c 8d Js Qh Ac")
@@ -303,8 +307,8 @@ func TestPaiGowRejectedFoulDoesNotLock(t *testing.T) {
tab := pgTestTable(false)
w := &testWallet{chips: 100}
tab.join("a")
+ // A lone main wager deals immediately, so the cards are already out.
tab.bet("a", "", 10, w)
- tab.action("a", "deal")
p := tab.player("a")
p.cards = pgCards("As Ah 2c 3d 4s 7h 9c")
events := tab.action("a", "12")
@@ -326,16 +330,19 @@ func TestPaiGowEnvyEligibilityAndMultipleAwards(t *testing.T) {
stake, envy int
}{{true, 5, 25}, {true, 4, 0}, {false, 5, 0}} {
tab := pgTestTable(tt.enabled)
+ wallets := map[string]*testWallet{}
for _, name := range []string{"a", "b", "c"} {
tab.join(name)
- w := &testWallet{chips: 100}
- tab.bet(name, "main", 20, w)
+ wallets[name] = &testWallet{chips: 100}
+ }
+ for _, name := range []string{"a", "b", "c"} {
+ w := wallets[name]
if name == "a" {
tab.bet(name, "fortune", tt.stake, w)
}
+ tab.bet(name, "main", 20, w)
}
- tab.tick()
- tab.tick()
+ // The last main wager deals; script the hands before setting.
// Own royal and dealer seven-card straight flush must NOT earn Envy.
tab.player("a").cards = pgCards("10s Js Qs Ks As 2h 3d")
tab.player("b").cards = pgCards("Ks Kh Kc Kd 2h 3c 7h")
@@ -401,16 +408,17 @@ func TestPaiGowFaceUpAndPushAceHigh(t *testing.T) {
w := &testWallet{chips: 500}
tab.join("a")
tab.bet("a", "pushace", 10, w)
- if len(w.open) > 0 {
- t.Fatal("pushace before main")
+ if len(w.open) != 1 {
+ t.Fatal("pushace before main should be accepted")
}
- tab.bet("a", "main", 20, w)
- if events := tab.bet("a", "pushace", 11, w); len(w.open) != 1 || !strings.Contains(events[0].Message, "half the main wager") {
+ tab.bet("a", "fortune", 5, w)
+ // The cap is checked when the main wager lands.
+ if events := tab.bet("a", "main", 10, w); len(events) == 0 || !strings.Contains(events[0].Message, "half the main wager") {
t.Fatal("pushace cap not enforced", events, w.open)
}
- tab.bet("a", "pushace", 10, w)
- tab.bet("a", "fortune", 5, w)
- tab.action("a", "deal")
+ tab.bet("a", "main", 20, w)
+ pushaceStake := tab.player("a").pushace.Total
+ // A lone main wager deals immediately; the dealer is revealed in setting.
snap := tab.status("a")[0].Snapshot.(*PaiGowSnapshot)
// The dealer acts first and the reveal happens in the setting phase.
if len(snap.DealerHigh) != 5 || len(snap.DealerLow) != 2 {
@@ -432,7 +440,7 @@ func TestPaiGowFaceUpAndPushAceHigh(t *testing.T) {
expect += 5 * (row.Odds + 1) // the Fortune stake, before settlement resets it
}
if class := pgPushAceClass(tab.dealer, p.cards, [3]int{5, 15, 40}); class.klass != "none" {
- expect += p.pushace.Total * (class.rate + 1)
+ expect += pushaceStake * (class.rate + 1)
}
if p.payout != expect {
t.Fatalf("payout %d want %d (dealer %v, result %s)", p.payout, expect, tab.dealer, p.result)
@@ -452,13 +460,120 @@ func TestPaiGowConfigurationAndRefund(t *testing.T) {
w := &testWallet{chips: 100}
tab.join("a")
tab.bet("a", "fortune", 5, w)
- if len(w.open) > 0 {
- t.Fatal("side before main")
+ if len(w.open) != 1 {
+ t.Fatal("side before main should be accepted")
}
tab.bet("a", "main", 10, w)
- tab.bet("a", "fortune", 5, w)
tab.refundAll()
if w.chips != 100 || len(w.open) > 0 {
t.Fatal("refund failed")
}
}
+
+func TestPaiGowA2345Option(t *testing.T) {
+ if !(PaiGowConfig{}).A2345High() {
+ t.Fatal("A2345 must default to the second-highest straight")
+ }
+ wheel := pgCards("As 2h 3c 4d 5s")
+ if got := pgEvaluate(wheel); got != (pgRank{4, 15}) {
+ t.Fatalf("default wheel rank %v", got)
+ }
+ if got := pgEvaluateRules(wheel, false); got != (pgRank{4, 5}) {
+ t.Fatalf("demoted wheel rank %v", got)
+ }
+ six := pgCards("2h 3c 4d 5s 6h")
+ if pgCompare(pgEvaluateRules(wheel, false), pgEvaluateRules(six, false)) >= 0 {
+ t.Fatal("a demoted wheel must be the lowest straight")
+ }
+ jokerWheel := pgCards("Ah 2c 3d 4s JK")
+ if got := pgEvaluateRules(jokerWheel, false); got[0] != 4 || got[1] != 5 {
+ t.Fatalf("demoted Joker wheel rank %v", got)
+ }
+}
+
+func TestPaiGowSoloMainDealsAfterSideBets(t *testing.T) {
+ tab := pgTestTable(false)
+ w := &testWallet{chips: 100}
+ tab.join("a")
+ // Side bets come first; they are accepted before any main wager.
+ tab.bet("a", "fortune", 5, w)
+ if len(w.open) != 1 {
+ t.Fatalf("fortune before main rejected: %v", w.open)
+ }
+ if tab.phase != "waiting" {
+ t.Fatalf("a side bet alone opened the betting window: %s", tab.phase)
+ }
+ // The main wager deals a lone table immediately.
+ tab.bet("a", "main", 10, w)
+ if tab.phase != "setting" {
+ t.Fatalf("solo main did not deal: %s", tab.phase)
+ }
+ if events := tab.bet("a", "fortune", 5, w); len(events) == 0 || !hasMessage(events, "Betting is closed") {
+ t.Fatalf("bet accepted after the deal: %+v", events)
+ }
+}
+
+func TestPaiGowWindowDealsWhenEveryBettorIsIn(t *testing.T) {
+ tab := pgTestTable(false)
+ wa, wb := &testWallet{chips: 100}, &testWallet{chips: 100}
+ tab.join("a")
+ tab.join("b")
+ tab.bet("a", "main", 10, wa)
+ if tab.phase != "betting" {
+ t.Fatalf("window did not open for a second seat: %s", tab.phase)
+ }
+ tab.bet("b", "main", 10, wb)
+ if tab.phase != "setting" {
+ t.Fatalf("all-bet table did not deal: %s", tab.phase)
+ }
+}
+
+func TestPaiGowUnbetAndStopRefundBeforeDeal(t *testing.T) {
+ tab := pgTestTable(false)
+ wa := &testWallet{chips: 100}
+ tab.join("a")
+ tab.join("b")
+ tab.bet("a", "main", 10, wa)
+ if events := tab.action("a", "unbet main"); !hasMessage(events, "take back") {
+ t.Fatalf("unbet main failed: %+v", events)
+ }
+ if wa.chips+wa.paid != 100 || len(wa.open) != 0 || tab.phase != "waiting" || tab.player("a").main.Total != 0 {
+ t.Fatalf("unbet main left state: chips=%d paid=%d phase=%s", wa.chips, wa.paid, tab.phase)
+ }
+ // A side bet placed and then abandoned is returned when the seat leaves.
+ tab.bet("a", "fortune", 5, wa)
+ if wa.chips+wa.paid != 95 {
+ t.Fatalf("fortune not charged: chips=%d paid=%d", wa.chips, wa.paid)
+ }
+ if events := tab.leave("a"); !hasMessage(events, "You leave Pai Gow Poker.") {
+ t.Fatalf("leave during betting: %+v", events)
+ }
+ if wa.chips+wa.paid != 100 || len(wa.open) != 0 || tab.hasParticipant("a") {
+ t.Fatalf("stop did not refund/remove: chips=%d paid=%d seated=%v", wa.chips, wa.paid, tab.hasParticipant("a"))
+ }
+}
+
+func TestPaiGowPushAceCapAtMainWager(t *testing.T) {
+ cfg := TableConfig{ID: "pg", Game: GamePaiGow, PaiGow: PaiGowConfig{Variant: PaiGowFaceUp, PushAceHigh: "5_15_40"}}.Normalize()
+ tab := newPaiGowTable(1, cfg, rand.New(rand.NewSource(9))).(*paiGowTable)
+ w := &testWallet{chips: 100}
+ tab.join("a")
+ tab.bet("a", "pushace", 10, w)
+ if events := tab.bet("a", "main", 10, w); len(events) == 0 || !hasMessage(events, "half the main wager") {
+ t.Fatalf("pushace cap not checked when the main lands: %+v", events)
+ }
+ if w.chips != 90 || tab.player("a").main.Total != 0 {
+ t.Fatalf("rejected main changed state: chips=%d main=%d", w.chips, tab.player("a").main.Total)
+ }
+}
+
+func TestPaiGowRefundAllReturnsSideBetsWithoutMain(t *testing.T) {
+ tab := pgTestTable(false)
+ w := &testWallet{chips: 100}
+ tab.join("a")
+ tab.bet("a", "fortune", 5, w)
+ tab.refundAll()
+ if w.chips != 100 || len(w.open) != 0 {
+ t.Fatalf("side-only refund failed: chips=%d open=%d", w.chips, len(w.open))
+ }
+}