diff options
Diffstat (limited to 'internal/game/video_poker_cabinet_test.go')
| -rw-r--r-- | internal/game/video_poker_cabinet_test.go | 177 |
1 files changed, 177 insertions, 0 deletions
diff --git a/internal/game/video_poker_cabinet_test.go b/internal/game/video_poker_cabinet_test.go new file mode 100644 index 0000000..a327bce --- /dev/null +++ b/internal/game/video_poker_cabinet_test.go @@ -0,0 +1,177 @@ +package game + +import ( + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "gopkg.in/yaml.v3" + "thehouseoficarus/internal/casino" + "thehouseoficarus/internal/persistence" +) + +const uxTestRoom = "name: Cabinets\ncasino_tables:\n - id: ux_multi\n game: video_poker\n video_poker:\n cabinet: ultimate_x_multi\n" + +func TestPokerBankAtomicSettlementAndRetry(t *testing.T) { + g, sess, _ := casinoCrapsGame(t, uxTestRoom) + g.DataDir = t.TempDir() + w := casinoWallet{g: g, p: sess.Player} + stake, err := w.Wager(10) + if err != nil { + t.Fatal(err) + } + banks := map[string][]int{"jacks_or_better/full/1/3": {2, 3, 4}} + // A failure before journal creation cannot apply either half. + if err := os.WriteFile(g.journalDir(), []byte("blocked"), 0600); err != nil { + t.Fatal(err) + } + if err := w.PayPoker(50, stake.ID, 1, "ux_multi", banks); err == nil { + t.Fatal("accepted failed persistence") + } + if sess.Player.Credits != 90 || len(sess.Player.PendingWagers) != 1 { + t.Fatal("failure mutated character") + } + loaded, err := w.LoadPokerBanks(1, "ux_multi") + if err != nil || len(loaded) != 0 { + t.Fatalf("bank changed: %v %v", loaded, err) + } + if err := os.Remove(g.journalDir()); err != nil { + t.Fatal(err) + } + if err := w.PayPoker(50, stake.ID, 1, "ux_multi", banks); err != nil { + t.Fatal(err) + } + disk, err := g.AccountStore.LoadCharacter("alice") + if err != nil { + t.Fatal(err) + } + loaded, err = w.LoadPokerBanks(1, "ux_multi") + if err != nil || disk.Credits != 140 || sess.Player.Credits != 140 || len(disk.PendingWagers) != 0 || !reflect.DeepEqual(loaded, banks) { + t.Fatalf("settlement: %v %+v", err, disk) + } + if err := w.PayPoker(50, stake.ID, 1, "ux_multi", banks); err == nil { + t.Fatal("duplicate stake paid") + } +} + +func TestPokerSeatsPassMachineBankAcrossRestart(t *testing.T) { + g, sess, _ := casinoCrapsGame(t, uxTestRoom) + g.DataDir = t.TempDir() + w := casinoWallet{g: g, p: sess.Player} + stake, err := w.Wager(1) + if err != nil { + t.Fatal(err) + } + banks := map[string][]int{"jacks_or_better/full/1/3": {2, 3, 4}} + if err := w.PayPoker(0, stake.ID, 1, "ux_multi", banks); err != nil { + t.Fatal(err) + } + // Restart the casino manager: the bank comes from disk, not a player map. + g.Casino = casino.NewManager(2) + g.executePlay(sess, []string{"ux_multi"}, "play ux_multi") + table, ok := g.Casino.Participation("alice") + if !ok || table == nil { + t.Fatal("not seated") + } + g.Casino.Join(1, table.Config, "bob") + if _, playing := g.Casino.Participation("bob"); playing { + t.Fatal("second occupant admitted") + } + snap := table.Status("alice")[0].Snapshot.(*casino.VideoPokerSnapshot) + if !reflect.DeepEqual(snap.Next, []int{2, 3, 4}) { + t.Fatalf("bank not displayed on join: %v", snap.Next) + } + if !g.leaveCasinoGames(sess.Player) { + t.Fatal("could not leave") + } + g.Casino.Join(1, table.Config, "bob") + snap = table.Status("bob")[0].Snapshot.(*casino.VideoPokerSnapshot) + if !reflect.DeepEqual(snap.Next, []int{2, 3, 4}) { + t.Fatalf("next occupant did not inherit: %v", snap.Next) + } + // Settings keep separate banks; merely viewing them never consumes them. + g.Casino.Action(1, table.Config.ID, "bob", "denom 10") + if got := table.Status("bob")[0].Snapshot.(*casino.VideoPokerSnapshot).Next; len(got) != 0 { + t.Fatalf("wrong denomination bank: %v", got) + } + g.Casino.Action(1, table.Config.ID, "bob", "denom 1") + if got := table.Status("bob")[0].Snapshot.(*casino.VideoPokerSnapshot).Next; !reflect.DeepEqual(got, []int{2, 3, 4}) { + t.Fatal("bank lost on switch") + } +} + +func TestPokerCorruptBankRefusesDeal(t *testing.T) { + g, sess, conn := casinoCrapsGame(t, uxTestRoom) + g.DataDir = t.TempDir() + path, _ := g.pokerBankPath(1, "ux_multi") + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, []byte("banks: [broken"), 0600); err != nil { + t.Fatal(err) + } + g.executePlay(sess, []string{"ux_multi"}, "play ux_multi") + g.executeBet(sess, []string{"max"}, "bet max") + g.executeTableAction(sess, "deal") + if sess.Player.Credits != 100 || len(sess.Player.PendingWagers) != 0 || !strings.Contains(string(conn.data), "Machine bank unavailable") { + t.Fatal("corrupt bank did not block deal") + } +} + +func TestPokerInterruptedTransactionRecovery(t *testing.T) { + g, sess, _ := casinoCrapsGame(t, uxTestRoom) + g.DataDir = t.TempDir() + w := casinoWallet{g: g, p: sess.Player} + stake, err := w.Wager(10) + if err != nil { + t.Fatal(err) + } + path, _ := g.pokerBankPath(1, "ux_multi") + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + t.Fatal(err) + } + before, _ := yaml.Marshal(pokerBankFile{Banks: map[string][]int{"jacks_or_better/full/1/3": {2, 3, 4}}}) + if err := os.WriteFile(path, before, 0600); err != nil { + t.Fatal(err) + } + next := *sess.Player + next.Credits += 50 + next.PendingWagers = nil + charData, _ := g.AccountStore.MarshalCharacter(&next) + bankData, _ := yaml.Marshal(pokerBankFile{}) + txn := g.newTxn() + if err := txn.Replace(g.AccountStore.CharPath("alice"), charData, 0600); err != nil { + t.Fatal(err) + } + if err := txn.Replace(path, bankData, 0600); err != nil { + t.Fatal(err) + } + journal, err := txn.MarshalJournal() + if err != nil { + t.Fatal(err) + } + if err := os.MkdirAll(g.journalDir(), 0700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(g.journalDir(), "interrupted.json"), journal, 0600); err != nil { + t.Fatal(err) + } + // Process died after applying just the character replacement. + if err := persistence.WriteFile(g.AccountStore.CharPath("alice"), charData, 0600); err != nil { + t.Fatal(err) + } + g.recoverJournals() + restored, err := g.AccountStore.LoadCharacter("alice") + if err != nil { + t.Fatal(err) + } + if restored.Credits != 90 || len(restored.PendingWagers) != 1 || restored.PendingWagers[0].ID != stake.ID { + t.Fatal("character before-image not restored") + } + restoredBank, err := w.LoadPokerBanks(1, "ux_multi") + if err != nil || !reflect.DeepEqual(restoredBank["jacks_or_better/full/1/3"], []int{2, 3, 4}) { + t.Fatal("bank before-image not restored") + } +} |
