diff options
| author | historia <[not public]> | 2026-09-30 20:50:45 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-30 20:50:45 -0400 |
| commit | e3ff06fc79682673194c8913f0feb9e90920e7d0 (patch) | |
| tree | 86e25670b5bd02d9868130afcc7a69ec1b7b9ed1 /internal/game/casino_rules_list_test.go | |
| parent | a5b9c1052c4640ba519e608e0c97e603ce6aa5ef (diff) | |
| download | thehouseoficarus-e3ff06fc79682673194c8913f0feb9e90920e7d0.tar.gz | |
feat: look, rules, and list commands updated for casino games
Diffstat (limited to 'internal/game/casino_rules_list_test.go')
| -rw-r--r-- | internal/game/casino_rules_list_test.go | 245 |
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) + } + } +} |
