aboutsummaryrefslogtreecommitdiff
path: root/internal/game/video_poker_cabinet_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/video_poker_cabinet_test.go')
-rw-r--r--internal/game/video_poker_cabinet_test.go177
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")
+ }
+}