diff options
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/casino_look_test.go | 90 | ||||
| -rw-r--r-- | internal/game/casino_rules_list_test.go | 245 | ||||
| -rw-r--r-- | internal/game/casino_ux.go | 141 | ||||
| -rw-r--r-- | internal/game/cmd_look.go | 3 | ||||
| -rw-r--r-- | internal/game/cmd_registry.go | 3 | ||||
| -rw-r--r-- | internal/game/render_craps.go | 2 | ||||
| -rw-r--r-- | internal/game/render_help.go | 3 |
7 files changed, 481 insertions, 6 deletions
diff --git a/internal/game/casino_look_test.go b/internal/game/casino_look_test.go new file mode 100644 index 0000000..71db40a --- /dev/null +++ b/internal/game/casino_look_test.go @@ -0,0 +1,90 @@ +package game + +import ( + "os" + "path/filepath" + "strings" + "testing" + "time" + + "thehouseoficarus/internal/casino" +) + +// lookOutput polls the queued session writer until the look output has +// settled (no growth across two samples), then returns it, ANSI-stripped. +func lookOutput(t *testing.T, conn *chanConn) string { + t.Helper() + last := 0 + stable := 0 + for stable < 2 { + n := len(conn.Bytes()) + if n == last { + stable++ + } else { + stable = 0 + last = n + } + time.Sleep(10 * time.Millisecond) + } + return stripANSI(string(conn.Bytes())) +} + +// TestLookAtTableShowsGameStateAndHints covers feature 1 with the full +// production harness: a seated player's look shows the room first, then the +// game table, then the current hint commands — rules always, list when the +// game has multiple wagers. +func TestLookAtTableShowsGameStateAndHints(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + h.seedCharacter("alice", "alice") + sess, conn := h.connect("alice") + + // Add a casino room and move there. + room := "id: 4\nname: Simplified Salon\ncasino_tables:\n - id: simple\n game: craps\n craps:\n variant: simplified\n" + path := filepath.Join(h.dir, "rooms", "4.yaml") + if err := os.WriteFile(path, []byte(room), 0644); err != nil { + t.Fatal(err) + } + h.g.World.AddRoomPath(4, path) + sess.Player.RoomID = 4 + h.hub.EnterRoom(sess, 4) + + cfg := casino.TableConfig{ID: "simple", Game: casino.GameCraps, Craps: casino.CrapsConfig{Variant: "simplified"}} + h.g.Casino.Join(4, cfg, "alice") + h.g.executeLook(sess, nil, "look") + text := lookOutput(t, conn) + if !strings.Contains(text, "Simplified Salon") { + t.Fatalf("look lost the room: %q", text) + } + if !strings.Contains(strings.ToUpper(text), "SIMPLIFIED CRAPS") || !strings.Contains(text, "Limits") { + t.Fatalf("look missing the table state: %q", text) + } + if !strings.Contains(text, "rules") { + t.Fatalf("look missing the rules hint: %q", text) + } +} + +// TestLookStandingShowsFixtureOnly: an unseated player's look keeps the +// plain room listing (no game state block). +func TestLookStandingShowsFixtureOnly(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + h.seedCharacter("alice", "alice") + sess, conn := h.connect("alice") + room := "id: 4\nname: Simplified Salon\ncasino_tables:\n - id: simple\n game: craps\n craps:\n variant: simplified\n" + path := filepath.Join(h.dir, "rooms", "4.yaml") + if err := os.WriteFile(path, []byte(room), 0644); err != nil { + t.Fatal(err) + } + h.g.World.AddRoomPath(4, path) + sess.Player.RoomID = 4 + h.hub.EnterRoom(sess, 4) + h.g.executeLook(sess, nil, "look") + text := lookOutput(t, conn) + if strings.Contains(text, "bet <amount>") { + t.Fatalf("standing look leaked a betting menu: %q", text) + } + if !strings.Contains(text, "play craps") { + t.Fatalf("standing look missing the play hint: %q", text) + } +} 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) + } + } +} diff --git a/internal/game/casino_ux.go b/internal/game/casino_ux.go index a2e7330..2881a49 100644 --- a/internal/game/casino_ux.go +++ b/internal/game/casino_ux.go @@ -8,6 +8,8 @@ import ( "thehouseoficarus/internal/casino" "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" ) type casinoChoice struct { @@ -104,6 +106,8 @@ func (g *Game) executeTableStatus(sess *net.Session, args []string, _ string) { t, playing := g.Casino.Participation(sess.Player.Name) if !playing || t == nil { if len(args) > 0 && args[0] == "help" { + // Machine players get their cabinet's guide; everyone else the + // shared casino one. topic := "casino" if playing { topic = "slots" @@ -115,17 +119,19 @@ func (g *Game) executeTableStatus(sess *net.Session, args []string, _ string) { return } if len(args) > 0 && args[0] == "rules" { + // The felt and the exact, per-table wording stay under table rules; + // bare rules opens the game's own help page. sess.WriteLine(g.renderCasinoRules(sess, t.Config)) return } if len(args) > 0 && args[0] == "help" { - g.doHelp(sess, string(t.Config.Game)) + g.doHelp(sess, t.HelpTopic()) } else { g.emitCasinoEvents(t.Status(sess.Player.Name)) if t.Config.Game == casino.GameRoulette { sess.WriteLine(g.rouletteReminder(sess, t.Config)) } else { - sess.WriteLine(g.colorize(sess, "command", "Learn to play: table help / help casino. Exact payouts: table rules.")) + sess.WriteLine(g.colorize(sess, "command", "Learn to play: rules. Exact payouts: table rules.")) } } if len(args) > 0 { @@ -133,8 +139,10 @@ func (g *Game) executeTableStatus(sess *net.Session, args []string, _ string) { // (insurance, split, rebet, etc.) that may not be legal right now. g.emitCasinoEvents(t.Status(sess.Player.Name)) commands := []casino.MenuCommand{ + {Command: "rules", Desc: "Help page for this game variant"}, + {Command: "list", Desc: "Every bet available right now, with payouts"}, + {Command: "table rules", Desc: "This table’s exact rules and payouts"}, {Command: "option casino_display compact", Desc: "Compact action hints (detailed for full command menus)"}, - {Command: "table rules", Desc: "Review this table’s rules"}, {Command: "help casino", Desc: "Quick guides to all casino games"}, } sess.WriteLine(g.renderCasinoCommands(sess, "", "Table controls", commands)) @@ -236,6 +244,14 @@ func (g *Game) casinoTimeText(text string) string { }) } +// betRangeWords renders the wager range in words for one-line hints. +func betRangeWords(minBet, maxBet int) string { + if maxBet <= 0 || maxBet >= casino.DefaultMaxBet { + return fmt.Sprintf("%d or more", minBet) + } + return fmt.Sprintf("%d-%d", minBet, maxBet) +} + func (g *Game) casinoBalance(sess *net.Session) string { if sess.Player == nil { return "" @@ -243,3 +259,122 @@ func (g *Game) casinoBalance(sess *net.Session) string { p := sess.Player return fmt.Sprintf("Available %s (%s chips + %s credits)", g.colorize(sess, "casino_chips", strconv.Itoa(casinoAffordable(p))), g.colorize(sess, "casino_chips", strconv.Itoa(countChips(p))), g.colorize(sess, "casino_credits", strconv.Itoa(p.Credits))) } + +// executeRules is the seated table's own help entry point: a variant's page +// (Simplified Craps gets its own short page, not a section of the craps +// manual), slots help at a machine, and the room's own game while merely +// standing in a casino room. Explicit topics pass straight through to help. +func (g *Game) executeRules(sess *net.Session, args []string, rawInput string) { + if sess.Player == nil { + return + } + if len(args) > 0 { + g.doHelp(sess, strings.Join(args, " ")) + return + } + if g.Casino == nil { + g.doHelp(sess, "casino") + return + } + if topic := g.casinoRulesTopic(sess.Player.RoomID, sess.Player.Name); topic != "" { + g.doHelp(sess, topic) + return + } + // Not in a casino game or room: the shared guide still answers. + g.doHelp(sess, "casino") +} + +// casinoRulesTopic picks the help page "rules" should open: the seated +// table's variant page first, then a machine's game, then the room's own +// casino games (so standing in the craps pit answers about craps). Mixed +// games, or no casino content at all, fall back to the shared guide. +func (g *Game) casinoRulesTopic(roomID int, name string) string { + if table, playing := g.Casino.Participation(name); playing && table != nil { + return table.HelpTopic() + } + if machine, ok := g.Casino.MachineFor(name); ok { + return string(machine.Config.Game) + } + room, err := g.World.LoadRoom(roomID) + if err != nil { + return "" + } + topic := "" + for _, cfg := range room.CasinoTables { + page := cfg.HelpTopic() + if topic == "" { + topic = page + } else if topic != page { + return "casino" + } + } + for _, m := range room.CasinoMachines { + page := string(m.Game) + if topic == "" { + topic = page + } else if topic != page { + return "casino" + } + } + return topic +} + +// executeList shows the bets a casino game accepts right now — the game's +// own state-driven list, from the come-out pass line to the travelling come +// bets and one-roll props. Everywhere else (shops, plain rooms) "list" +// keeps its shop meaning. +func (g *Game) executeList(sess *net.Session, args []string, rawInput string) { + p := sess.Player + if p == nil { + return + } + if len(args) > 0 || g.Casino == nil { + g.executeShopList(sess, args, rawInput) + return + } + if machine, ok := g.Casino.MachineFor(p.Name); ok { + sess.WriteLine(g.colorize(sess, "casino_action", "A "+casinoGameLabel(machine.Config.Game)+" takes one wager: bet <amount> (or bet max), then spin or autospin.")) + return + } + if table, playing := g.Casino.Participation(p.Name); playing && table != nil { + g.showCasinoWagerList(sess, table) + return + } + g.executeShopList(sess, args, rawInput) +} + +// showCasinoWagerList renders the game's live wager list. Two or more rows +// become a brief table; a single row is one line; an empty list means the +// only bet is the ordinary amount-only wager. +func (g *Game) showCasinoWagerList(sess *net.Session, t *casino.Table) { + rows := t.Wagers(sess.Player.Name) + switch { + case len(rows) == 1: + r := rows[0] + sess.WriteLine(g.colorize(sess, "casino_action", "Your only wager: bet <amount> on "+r.Bet+" — "+r.Payout+" — "+r.Win+".")) + case len(rows) == 0: + sess.WriteLine(g.colorize(sess, "casino_action", "The only wager here is bet <amount>") + " (" + betRangeWords(t.Config.MinBet, t.Config.MaxBet) + ").") + default: + commands := make([]casino.MenuCommand, 0, len(rows)) + for _, r := range rows { + commands = append(commands, casino.MenuCommand{Command: "bet <amount> on " + r.Bet, Desc: r.Payout + " | " + r.Win}) + } + sess.WriteLine(g.renderCasinoCommands(sess, g.casinoBalance(sess), t.Config.Label()+" — available bets", commands)) + } +} + +// showCasinoGameLook appends the live game state and hint menu for the +// table the player is seated at, so look reads room first, then the table, +// then the commands that matter now. Machines are skipped: their menus +// arrive with every spin. +func (g *Game) showCasinoGameLook(sess *net.Session, p *player.Player, room *world.Room) { + if g.Casino == nil || p == nil { + return + } + table, playing := g.Casino.Participation(p.Name) + if !playing || table == nil || table.RoomID != room.ID { + return + } + sess.WriteLine("") + g.emitCasinoEvents(table.Status(p.Name)) +} diff --git a/internal/game/cmd_look.go b/internal/game/cmd_look.go index 6fb80e5..70bc2c2 100644 --- a/internal/game/cmd_look.go +++ b/internal/game/cmd_look.go @@ -58,6 +58,9 @@ func (g *Game) doLook(sess *net.Session) { g.showRoomExits(sess, p, room) g.showRoomPlayers(sess, p, room) + // Seated at a table: look keeps its room-first order, then appends the + // live game state and the current hint menu. + g.showCasinoGameLook(sess, p, room) } func (g *Game) doExits(sess *net.Session) { diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go index 853fb6a..fa3518a 100644 --- a/internal/game/cmd_registry.go +++ b/internal/game/cmd_registry.go @@ -33,6 +33,8 @@ var commandRegistry = map[string]commandDef{ }, ClassActive}, "table": {(*Game).executeTableStatus, ClassInstant}, "status": {(*Game).executeTableStatus, ClassInstant}, + "rules": {(*Game).executeRules, ClassInstant}, + "list": {(*Game).executeList, ClassInstant}, "watch": {(*Game).executeWatch, ClassInstant}, "sitout": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "sitout") }, ClassInstant}, "decline": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "decline") }, ClassActive}, @@ -148,7 +150,6 @@ var commandRegistry = map[string]commandDef{ "inspect": {(*Game).executeInspect, ClassInstant}, "walk": {(*Game).executeWalk, ClassActive}, "bank": {(*Game).executeBank, ClassActive}, - "list": {(*Game).executeShopList, ClassInstant}, "browse": {(*Game).executeShopList, ClassInstant}, "buy": {(*Game).executeShopBuy, ClassInstant}, "sell": {(*Game).executeShopSell, ClassInstant}, diff --git a/internal/game/render_craps.go b/internal/game/render_craps.go index 64c910e..12714dc 100644 --- a/internal/game/render_craps.go +++ b/internal/game/render_craps.go @@ -343,7 +343,7 @@ func renderCrapsSnapshot(s *casino.CrapsSnapshot, mode string, unicode bool, sty } out.WriteString("\n" + title(" "+strings.ToUpper(strings.ReplaceAll(variant, "_", " "))+" CRAPS")) if point != "" { - out.WriteString(subtle(" / "+point)) + out.WriteString(subtle(" / " + point)) } out.WriteString("\n") if variant == "bank" || variant == "diceless" { diff --git a/internal/game/render_help.go b/internal/game/render_help.go index 6066e9a..36f73f7 100644 --- a/internal/game/render_help.go +++ b/internal/game/render_help.go @@ -32,7 +32,7 @@ var commandList = []cmdEntry{ {"attack / kill", "Active", "Attack a mob"}, {"autotrigger / auto", "Instant", "Set a module to auto-trigger"}, {"bank", "Active", "Open bank interface"}, - {"browse / list", "Instant", "Browse a shop's stock"}, + {"browse / list", "Instant", "Browse a shop's stock; at a casino table, list every bet accepted now"}, {"burn", "Active", "Start a fire"}, {"buy", "Instant", "Buy from a shop"}, {"chop / cut", "Active", "Chop trees (Woodcutting)"}, @@ -65,6 +65,7 @@ var commandList = []cmdEntry{ {"map", "Instant", "Display an ASCII map of the area"}, {"play", "Active", "Join a game; play bj / play bac; optional amount and target"}, {"table / status", "Instant", "Redisplay table state; table rules / table help for details"}, + {"rules", "Instant", "Help page for the casino game you're playing (one per house edition)"}, {"watch", "Instant", "Watch detailed table action; watch off to stop"}, {"sitout", "Instant", "Sit out future table rounds; bet to rejoin"}, {"bet / rebet", "Active", "Repeat a wager, or bet <amount> [target]; min/max accepted"}, |
