aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/video_poker_lifecycle_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/video_poker_lifecycle_test.go')
-rw-r--r--internal/casino/video_poker_lifecycle_test.go327
1 files changed, 327 insertions, 0 deletions
diff --git a/internal/casino/video_poker_lifecycle_test.go b/internal/casino/video_poker_lifecycle_test.go
new file mode 100644
index 0000000..3107a93
--- /dev/null
+++ b/internal/casino/video_poker_lifecycle_test.go
@@ -0,0 +1,327 @@
+package casino
+
+import (
+ "math/rand"
+ "reflect"
+ "strings"
+ "testing"
+)
+
+// pokerTestWallet models atomic settlement failure without filesystem I/O.
+type pokerTestWallet struct {
+ testWallet
+ fail bool
+ bank *pokerTestBank
+}
+
+func (w *pokerTestWallet) LoadPokerBanks(int, string) (map[string][]int, error) { return nil, nil }
+func (w *pokerTestWallet) PayPoker(n int, id string, room int, cabinet string, banks map[string][]int) error {
+ if err := w.PayCredits(n, id); err != nil {
+ return err
+ }
+ return w.bank.save(room, cabinet, banks)
+}
+
+func (w *pokerTestWallet) PayCredits(n int, ids ...string) error {
+ if w.fail {
+ return ErrWalletPersistence
+ }
+ return w.testWallet.PayCredits(n, ids...)
+}
+
+func (e Event) MessageStarts(prefix string) bool { return strings.HasPrefix(e.Message, prefix) }
+
+type pokerTestBank struct{ writes int }
+
+func (b *pokerTestBank) save(int, string, map[string][]int) error { b.writes++; return nil }
+
+func pokerTestCabinet(t *testing.T, cfg VideoPokerConfig) (*videoPokerTable, *pokerTestWallet, *pokerTestBank) {
+ t.Helper()
+ tableCfg := TableConfig{ID: "vp", Game: GameVideoPoker, VideoPoker: cfg}
+ if err := tableCfg.Validate(); err != nil {
+ t.Fatal(err)
+ }
+ table := newVideoPokerTable(1, tableCfg.Normalize(), rand.New(rand.NewSource(1))).(*videoPokerTable)
+ w := &pokerTestWallet{testWallet: testWallet{credits: 10000000}}
+ bank := &pokerTestBank{}
+ w.bank = bank
+ table.join("alice")
+ return table, w, bank
+}
+func pokerDeal(t *testing.T, table *videoPokerTable, w Wallet) {
+ t.Helper()
+ table.action("alice", "coins max")
+ table.bet("alice", "deal", 0, w)
+ if !table.players["alice"].active {
+ t.Fatal("deal failed")
+ }
+}
+func TestPokerCabinetOccupancyAndHints(t *testing.T) {
+ table, _, _ := pokerTestCabinet(t, VideoPokerConfig{Cabinet: CabinetClassic})
+ table.join("bob")
+ if table.hasParticipant("bob") {
+ t.Fatal("cabinet had two occupants")
+ }
+ if table.hints("bob") != nil {
+ t.Fatal("non-occupant saw hints")
+ }
+ for _, h := range table.hints("alice") {
+ if h.Command == "list" {
+ t.Fatal("irrelevant list hint")
+ }
+ }
+ if table.overview() == "" {
+ t.Fatal("empty overview")
+ }
+}
+func TestPokerCoinsDenominationAndRoyal(t *testing.T) {
+ table, w, _ := pokerTestCabinet(t, VideoPokerConfig{Cabinet: CabinetClassic})
+ p := table.players["alice"]
+ for want := 1; want <= 2; want++ {
+ events := table.action("alice", "coins 1")
+ snap := events[0].Snapshot.(*VideoPokerSnapshot)
+ if p.loaded != want || !snap.ShowPayTable || snap.Coins != want {
+ t.Fatalf("load: coins=%d %+v", p.loaded, snap)
+ }
+ }
+ if len(w.open) != 0 || p.remaining != nil {
+ t.Fatal("coin loading charged or dealt")
+ }
+ table.action("alice", "denom 10")
+ table.bet("alice", "deal", 0, w)
+ if p.wager.Total != 20 {
+ t.Fatalf("cost %d", p.wager.Total)
+ }
+ p.cards = pokerHand("Ah Kh Qh Jh 10h")
+ table.action("alice", "12345")
+ table.action("alice", "draw")
+ if w.paid != 5000 {
+ t.Fatalf("two-coin royal: %d", w.paid)
+ }
+ events := table.action("alice", "coins")
+ if p.loaded != 2 || events[0].Snapshot.(*VideoPokerSnapshot).ShowPayTable {
+ t.Fatal("bare bet did not repeat silently")
+ }
+ // A second coin adds to the loaded wager and reprints the pay table.
+ events = table.action("alice", "coins 1")
+ if p.loaded != 3 || !events[0].Snapshot.(*VideoPokerSnapshot).ShowPayTable {
+ t.Fatal("second coin did not reprint the pay table")
+ }
+ table.action("alice", "coins max")
+ table.bet("alice", "deal", 0, w)
+ p.cards = pokerHand("Ah Kh Qh Jh 10h")
+ table.action("alice", "12345")
+ table.action("alice", "draw")
+ if w.paid != 45000 {
+ t.Fatalf("max royal total %d", w.paid)
+ }
+ rows := p.config.PayRows(1)
+ if rows[0].Coins != [5]int{250, 500, 750, 1000, 4000} {
+ t.Fatalf("royal columns %+v", rows[0])
+ }
+ // An uninitialized cabinet repeats the max wager on the first bare bet.
+ table2, _, _ := pokerTestCabinet(t, VideoPokerConfig{Cabinet: CabinetClassic})
+ table2.players["alice"].loaded = 0
+ table2.action("alice", "coins")
+ if table2.players["alice"].loaded != 5 {
+ t.Fatal("initial bare bet not max")
+ }
+}
+func TestPokerHoldToggleAndMultiHand(t *testing.T) {
+ table, w, _ := pokerTestCabinet(t, VideoPokerConfig{Cabinet: CabinetClassic})
+ table.action("alice", "hands 100")
+ pokerDeal(t, table, w)
+ p := table.players["alice"]
+ original := p.cards
+ table.action("alice", "531")
+ if !p.held[0] || p.held[1] || !p.held[2] || p.held[3] || !p.held[4] {
+ t.Fatalf("first toggle: %+v", p.held)
+ }
+ table.action("alice", "1")
+ if p.held[0] {
+ t.Fatal("digit did not toggle back off")
+ }
+ table.action("alice", "1")
+ table.status("alice")
+ table.action("alice", "paytable")
+ if p.remaining == nil || !p.active {
+ t.Fatal("read commands changed deal")
+ }
+ table.action("alice", "denom 50")
+ table.action("alice", "game jacks")
+ if p.denomination != 1 || p.variant != "jacks_or_better" {
+ t.Fatal("settings changed during a hand")
+ }
+ table.action("alice", "draw")
+ if len(p.lines) != 100 || len(w.open) != 0 || p.active {
+ t.Fatal("bad multi-hand settlement")
+ }
+ unique := map[string]bool{}
+ for _, line := range p.lines {
+ for _, i := range []int{0, 2, 4} {
+ if line.Cards[i].Rank != original[i].rank || line.Cards[i].Suit != original[i].suit {
+ t.Fatal("hold lost")
+ }
+ }
+ for _, i := range []int{1, 3} {
+ for _, c := range original {
+ if line.Cards[i].Rank == c.rank && line.Cards[i].Suit == c.suit {
+ t.Fatal("original card returned")
+ }
+ }
+ }
+ if line.Cards[1] == line.Cards[3] {
+ t.Fatal("duplicate within one hand")
+ }
+ unique[line.Cards[1].Rank+line.Cards[1].Suit] = true
+ }
+ if len(unique) < 10 {
+ t.Fatal("lines did not draw independently")
+ }
+ paid := w.paid
+ table.action("alice", "draw")
+ if w.paid != paid {
+ t.Fatal("second draw paid again")
+ }
+ for _, e := range table.status("alice") {
+ if e.Public || e.PrivateTo != "alice" {
+ t.Fatal("private cards leaked")
+ }
+ }
+ // Settings change freely between rounds.
+ table.action("alice", "denom 50")
+ table.action("alice", "hands 1")
+ if p.denomination != 50 || p.hands != 1 {
+ t.Fatal("settings refused between rounds")
+ }
+ table.leave("alice")
+ if !table.idle() {
+ t.Fatal("seat not released")
+ }
+}
+func TestPokerGameAndPayoutSwitching(t *testing.T) {
+ table, w, _ := pokerTestCabinet(t, VideoPokerConfig{Cabinet: CabinetClassic})
+ p := table.players["alice"]
+ if !table.action("alice", "game jacks")[0].Snapshot.(*VideoPokerSnapshot).ShowPayTable {
+ t.Fatal("explicit switch did not show the pay table")
+ }
+ table.action("alice", "game ddb")
+ if p.config.Variant != "double_double_bonus" || p.config.PayTable != "10/6" {
+ t.Fatalf("alias switch: %+v", p.config)
+ }
+ table.action("alice", "payout low")
+ if p.config.PayTable != "8/5" {
+ t.Fatalf("tier not applied to the current game: %+v", p.config)
+ }
+ table.action("alice", "game joker")
+ if p.config.PayTable != "50/15/7/5/3" {
+ t.Fatalf("tier lost switching to joker: %+v", p.config)
+ }
+ events := table.action("alice", "game unknown")
+ if !events[0].MessageStarts("Use bet") || p.variant != "joker_poker" {
+ t.Fatal("unknown game accepted")
+ }
+ table.action("alice", "coins max")
+ pokerDeal(t, table, w)
+ if !table.action("alice", "game jacks")[0].MessageStarts("Finish this hand first.") {
+ t.Fatal("game switched during a hand")
+ }
+ table.action("alice", "draw")
+ // A hundred-hand classic Jacks or Better edition is reachable.
+ table.action("alice", "game jacks")
+ table.action("alice", "hands 100")
+ if p.hands != 100 {
+ t.Fatal("hundred-play unavailable")
+ }
+ table.action("alice", "hands 3")
+ if p.hands != 3 {
+ t.Fatal("multi-hand unavailable")
+ }
+}
+func TestPokerFeatureFeesAndHandCounts(t *testing.T) {
+ for _, tt := range []struct {
+ cabinet string
+ payTable string
+ feature string
+ hands, max int
+ }{{CabinetUXSingle, "9/6", PokerUltimateX, 1, 1}, {CabinetUXMulti, "8/6", PokerUltimateX, 3, 10}, {CabinetSuperTimesPay, "9/6", PokerSuperTimesPay, 1, 10}, {CabinetHotRoll, "9/6", PokerHotRoll, 3, 10}} {
+ table, w, _ := pokerTestCabinet(t, VideoPokerConfig{Cabinet: tt.cabinet})
+ p := table.players["alice"]
+ table.action("alice", "hands 100")
+ if p.hands != tt.hands {
+ t.Fatal("unsupported hand count accepted")
+ }
+ table.action("alice", "coins 1")
+ table.bet("alice", "deal", 0, w)
+ if p.active || len(w.open) != 0 {
+ t.Fatal("feature allowed a partial bet")
+ }
+ pokerDeal(t, table, w)
+ fee := 5
+ if tt.feature == PokerSuperTimesPay {
+ fee = 1
+ }
+ if p.wager.Total != (5+fee)*tt.hands {
+ t.Fatalf("%s fee %d", tt.feature, p.wager.Total)
+ }
+ if p.config.Feature != tt.feature || p.config.PayTable != tt.payTable {
+ t.Fatalf("%s cabinet: %+v", tt.cabinet, p.config)
+ }
+ table.action("alice", "draw")
+ table.action("alice", "feature off")
+ pokerDeal(t, table, w)
+ if p.wager.Total != 5*tt.hands {
+ t.Fatal("feature off still charged a fee")
+ }
+ }
+}
+func TestPokerSettlementRetry(t *testing.T) {
+ table, w, _ := pokerTestCabinet(t, VideoPokerConfig{Cabinet: CabinetClassic})
+ pokerDeal(t, table, w)
+ p := table.players["alice"]
+ p.cards = pokerHand("Ah Kh Qh Jh 10h")
+ table.action("alice", "12345")
+ w.fail = true
+ table.action("alice", "draw")
+ if !p.active || !p.drawn || len(w.open) != 1 {
+ t.Fatal("failed payout lost stake")
+ }
+ final := append([]PokerLine(nil), p.lines...)
+ table.action("alice", "draw")
+ if !reflect.DeepEqual(final, p.lines) || w.paid != 0 {
+ t.Fatal("failed final hands changed or paid")
+ }
+ w.fail = false
+ table.action("alice", "draw")
+ table.action("alice", "draw")
+ if w.paid != 4000 || len(w.open) != 0 || p.active {
+ t.Fatalf("retry: paid=%d", w.paid)
+ }
+}
+func TestPokerDisconnectAndRefund(t *testing.T) {
+ table, w, _ := pokerTestCabinet(t, VideoPokerConfig{Cabinet: CabinetClassic})
+ pokerDeal(t, table, w)
+ table.markDisconnected("alice")
+ if table.hasParticipant("alice") || len(w.open) != 0 {
+ t.Fatal("disconnect failed to settle")
+ }
+ table, w, _ = pokerTestCabinet(t, VideoPokerConfig{Cabinet: CabinetClassic})
+ pokerDeal(t, table, w)
+ table.refundAll()
+ table.refundAll()
+ if w.paid != 5 || len(w.open) != 0 {
+ t.Fatal("refund not exactly once")
+ }
+}
+func TestPokerJokerDeck(t *testing.T) {
+ table, w, _ := pokerTestCabinet(t, VideoPokerConfig{Variant: "joker_poker"})
+ pokerDeal(t, table, w)
+ p := table.players["alice"]
+ seen := map[card]bool{}
+ for _, c := range append(append([]card(nil), p.cards[:]...), p.remaining...) {
+ seen[c] = true
+ }
+ if len(seen) != 53 || !seen[card{rank: "JK", suit: "joker"}] {
+ t.Fatal("not a 53-card deck")
+ }
+}