aboutsummaryrefslogtreecommitdiff
path: root/internal/game
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game')
-rw-r--r--internal/game/casino_look_test.go90
-rw-r--r--internal/game/casino_rules_list_test.go245
-rw-r--r--internal/game/casino_ux.go141
-rw-r--r--internal/game/cmd_look.go3
-rw-r--r--internal/game/cmd_registry.go3
-rw-r--r--internal/game/render_craps.go2
-rw-r--r--internal/game/render_help.go3
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"},