aboutsummaryrefslogtreecommitdiff
path: root/internal/game/casino_rules_list_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/casino_rules_list_test.go')
-rw-r--r--internal/game/casino_rules_list_test.go245
1 files changed, 245 insertions, 0 deletions
diff --git a/internal/game/casino_rules_list_test.go b/internal/game/casino_rules_list_test.go
new file mode 100644
index 0000000..3d91ecf
--- /dev/null
+++ b/internal/game/casino_rules_list_test.go
@@ -0,0 +1,245 @@
+package game
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "gopkg.in/yaml.v3"
+ "thehouseoficarus/internal/casino"
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/engine"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/object"
+ "thehouseoficarus/internal/player"
+ "thehouseoficarus/internal/world"
+)
+
+// casinoCrapsGame builds a one-room world with the named craps edition and
+// points the help loader at the shipped data dir, so the rules command and
+// the shipped-topic test exercise the real help pages.
+func casinoCrapsGame(t *testing.T, roomYAML string) (*Game, *net.Session, *casinoTestConn) {
+ t.Helper()
+ dir := t.TempDir()
+ if err := os.MkdirAll(filepath.Join(dir, "rooms"), 0755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "rooms", "1.yaml"), []byte(roomYAML), 0644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.MkdirAll(filepath.Join(dir, "players", "characters"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ p := player.New("alice")
+ sess := &net.Session{Player: p, Conn: &casinoTestConn{}}
+ g := &Game{
+ Deps: Deps{World: world.New(dir), ObjectStore: object.NewObjectStore(dir), MobStore: world.NewMobStore(dir), AccountStore: player.NewAccountStore(dir), Ticks: engine.New(), DataDir: "../../data"},
+ Casino: casino.NewManager(1),
+ Combat: combat.NewTracker(),
+ safespot: NewSafespotManager(),
+ restTimers: make(map[string]uint64),
+ loggedInChars: map[string]*net.Session{"alice": sess},
+ }
+ p.Credits = 100
+ p.RoomID = 1
+ return g, sess, sess.Conn.(*casinoTestConn)
+}
+
+// TestRulesAtTableShowsVariantHelpPage pins the "rules" contract: seated at
+// Simplified Craps, rules must open the variant's own short page — not the
+// whole craps manual with the edition buried in a section.
+func TestRulesAtTableShowsVariantHelpPage(t *testing.T) {
+ room := "name: Simplified Salon\ncasino_tables:\n - id: simple\n game: craps\n craps:\n variant: simplified\n"
+ g, sess, conn := casinoCrapsGame(t, room)
+ g.executePlay(sess, []string{"craps"}, "play craps")
+ conn.data = nil
+ g.executeRules(sess, nil, "rules")
+ text := stripANSI(string(conn.data))
+ if !strings.Contains(text, "Simplified Craps") {
+ t.Fatalf("rules missing the variant page: %q", text)
+ }
+ if strings.Contains(text, "HOUSE EDITIONS") || strings.Contains(text, "bet 10 on dontcome") {
+ t.Fatalf("rules leaked the bank manual into the variant page: %q", text)
+ }
+}
+
+// TestRulesStandingInCasinoRoomAnswersRoomGame checks the unseated fallback:
+// standing in a one-table casino room answers that game's page.
+func TestRulesStandingInCasinoRoomAnswersRoomGame(t *testing.T) {
+ room := "name: Crapless Salon\ncasino_tables:\n - id: cl\n game: craps\n craps:\n variant: crapless\n"
+ g, sess, conn := casinoCrapsGame(t, room)
+ g.executeRules(sess, nil, "rules")
+ if text := stripANSI(string(conn.data)); !strings.Contains(text, "Crapless Craps") {
+ t.Fatalf("standing rules did not answer the room's game: %q", text)
+ }
+}
+
+// TestRulesOutsideCasinoFallsBackToGuide checks rooms with no casino content.
+func TestRulesOutsideCasinoFallsBackToGuide(t *testing.T) {
+ g, sess, conn := casinoCrapsGame(t, "name: Void\n")
+ g.executeRules(sess, nil, "rules")
+ if text := stripANSI(string(conn.data)); !strings.Contains(text, "Casino quick guide") {
+ t.Fatalf("rules outside a casino: %q", text)
+ }
+}
+
+// TestListFollowsGameState covers the list contract end to end: come-out
+// lists the line and one-roll bets, a point adds place/odds rows, bonus
+// bets close after the first roll, and single-bet editions print one line.
+func TestListFollowsGameState(t *testing.T) {
+ room := "name: Bank Pit\ncasino_tables:\n - id: bank\n game: craps\n craps:\n variant: bank\n"
+ g, sess, conn := casinoCrapsGame(t, room)
+ g.executePlay(sess, []string{"craps"}, "play craps")
+ g.executeBet(sess, []string{"10", "pass"}, "bet 10 pass")
+ conn.data = nil
+ g.executeList(sess, nil, "list")
+ text := stripANSI(string(conn.data))
+ for _, want := range []string{"bet <amount> on pass", "bet <amount> on dontpass", "bet <amount> on field", "bet <amount> on craps", "bet <amount> on fire"} {
+ if !strings.Contains(text, want) {
+ t.Fatalf("come-out list missing %q: %q", want, text)
+ }
+ }
+ for _, gone := range []string{"place 6", "odds pass"} {
+ if strings.Contains(text, gone) {
+ t.Fatalf("come-out list advertised %q: %q", gone, text)
+ }
+ }
+
+ // Establish a point: place/odds appear, bonus bets close.
+ g.Casino.Action(1, "bank", "alice", "roll")
+ conn.data = nil
+ g.executeList(sess, nil, "list")
+ text = stripANSI(string(conn.data))
+ if !strings.Contains(text, "place 6") || !strings.Contains(text, "odds pass") {
+ t.Fatalf("point list missing place/odds: %q", text)
+ }
+ if strings.Contains(text, "bet <amount> on pass\n") || strings.Contains(text, "fire") {
+ t.Fatalf("point list still advertising closed bets: %q", text)
+ }
+}
+
+// TestListSingleBetGamesStayBrief: editions with one wager (Simplified) and
+// one-wager games (blackjack) answer in one line, no table.
+func TestListSingleBetGamesStayBrief(t *testing.T) {
+ room := "name: Simple\ncasino_tables:\n - id: simple\n game: craps\n craps:\n variant: simplified\n"
+ g, sess, conn := casinoCrapsGame(t, room)
+ g.executePlay(sess, []string{"craps"}, "play craps")
+ conn.data = nil
+ g.executeList(sess, nil, "list")
+ text := stripANSI(string(conn.data))
+ if strings.Contains(text, "Bet") && strings.Contains(text, "Pays") && strings.Contains(text, "Win if") {
+ t.Fatalf("single-bet game rendered a table: %q", text)
+ }
+ if !strings.Contains(text, "bet <amount> on pass") {
+ t.Fatalf("single-bet line missing the wager: %q", text)
+ }
+
+ // Blackjack: one wager, no table, and the list command still answers.
+ room = "name: BJ\ncasino_tables:\n - id: bj\n game: blackjack\n"
+ g, sess, conn = casinoCrapsGame(t, room)
+ g.executePlay(sess, []string{"bj"}, "play bj")
+ conn.data = nil
+ g.executeList(sess, nil, "list")
+ if text := stripANSI(string(conn.data)); !strings.Contains(text, "only wager") {
+ t.Fatalf("blackjack list: %q", text)
+ }
+}
+
+// TestListAtShopKeepsShopMeaning guards the shop fallthrough: an explicit
+// shopmob argument always browses the shop.
+func TestListAtShopKeepsShopMeaning(t *testing.T) {
+ g, sess, _ := casinoCrapsGame(t, "name: Void\n")
+ g.executeList(sess, []string{"shopkeeper"}, "list shopkeeper")
+ if text := stripANSI(string(sess.Conn.(*casinoTestConn).data)); !strings.Contains(text, "no shop") && !strings.Contains(text, "shop") {
+ t.Fatalf("explicit list should reach the shop path: %q", text)
+ }
+}
+
+// TestShippedCasinoHelpTopicsResolve walks every shipped casino table and
+// machine and pins that its rules help topic actually resolves in data/help.
+func TestShippedCasinoHelpTopicsResolve(t *testing.T) {
+ dataDir := filepath.Join("..", "..", "data")
+ helps, err := LoadHelp(dataDir)
+ if err != nil {
+ t.Fatal(err)
+ }
+ resolves := func(topic string) bool {
+ for _, h := range helps {
+ if matchesHelpTopic(h, topic) {
+ return true
+ }
+ }
+ return false
+ }
+ roomsDir := filepath.Join(dataDir, "rooms")
+ err = filepath.WalkDir(roomsDir, func(path string, d os.DirEntry, err error) error {
+ if err != nil || d.IsDir() || !strings.HasSuffix(path, ".yaml") {
+ return err
+ }
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ var room world.Room
+ if err := yaml.Unmarshal(raw, &room); err != nil {
+ return err
+ }
+ for _, cfg := range room.CasinoTables {
+ topic := cfg.HelpTopic()
+ if !resolves(topic) {
+ t.Errorf("%s: table %s (%s) help topic %q does not resolve", path, cfg.ID, cfg.Game, topic)
+ }
+ }
+ for _, m := range room.CasinoMachines {
+ if !resolves(string(m.Game)) {
+ t.Errorf("%s: machine %s game %q does not resolve", path, m.ID, m.Game)
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+// TestRulesAtMachineShowsSlotsHelp: a slot machine player typing rules gets
+// the cabinet's own guide, not a dead end.
+func TestRulesAtMachineShowsSlotsHelp(t *testing.T) {
+ room := "name: Slots Salon\ncasino_machines:\n - id: puff\n game: slots\n"
+ g, sess, conn := casinoCrapsGame(t, room)
+ g.executePlay(sess, []string{"slots"}, "play slots")
+ conn.data = nil
+ g.executeRules(sess, nil, "rules")
+ if text := stripANSI(string(conn.data)); !strings.Contains(text, "Slots quick guide") {
+ t.Fatalf("machine rules: %q", text)
+ }
+}
+
+// TestListRouletteAndBaccarat: roulette lists the whole target catalogue and
+// baccarat lists every outcome and side bet its variant offers.
+func TestListRouletteAndBaccarat(t *testing.T) {
+ room := "name: Wheel Salon\ncasino_tables:\n - id: wheel\n game: roulette\n roulette:\n racetrack: true\n"
+ g, sess, conn := casinoCrapsGame(t, room)
+ g.executePlay(sess, []string{"roulette"}, "play roulette")
+ conn.data = nil
+ g.executeList(sess, nil, "list")
+ text := stripANSI(string(conn.data))
+ for _, want := range []string{"bet <amount> on <number>", "bet <amount> on red / black", "bet <amount> on voisins", "bet <amount> on basket"} {
+ if !strings.Contains(text, want) {
+ t.Fatalf("roulette list missing %q: %q", want, text)
+ }
+ }
+
+ room = "name: Crown Salon\ncasino_tables:\n - id: crown\n game: baccarat\n baccarat:\n variant: ez_baccarat\n dragon_7: true\n panda_8: true\n"
+ g, sess, conn = casinoCrapsGame(t, room)
+ g.executePlay(sess, []string{"bac"}, "play bac")
+ conn.data = nil
+ g.executeList(sess, nil, "list")
+ text = stripANSI(string(conn.data))
+ for _, want := range []string{"bet <amount> on banker (b)", "1:1 (3-card 7 pushes)", "bet <amount> on dragon_7", "bet <amount> on panda_8"} {
+ if !strings.Contains(text, want) {
+ t.Fatalf("baccarat list missing %q: %q", want, text)
+ }
+ }
+}