diff options
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/casino_commands_test.go | 6 | ||||
| -rw-r--r-- | internal/game/casino_ux.go | 77 | ||||
| -rw-r--r-- | internal/game/casino_ux_test.go | 8 | ||||
| -rw-r--r-- | internal/game/cmd_registry.go | 4 | ||||
| -rw-r--r-- | internal/game/craps_test.go | 8 | ||||
| -rw-r--r-- | internal/game/look_casino.go | 2 | ||||
| -rw-r--r-- | internal/game/pai_gow_test.go | 13 | ||||
| -rw-r--r-- | internal/game/render_casino_help.go | 4 | ||||
| -rw-r--r-- | internal/game/render_craps.go | 2 | ||||
| -rw-r--r-- | internal/game/render_help.go | 2 | ||||
| -rw-r--r-- | internal/game/render_pai_gow.go | 11 | ||||
| -rw-r--r-- | internal/game/render_roulette.go | 46 | ||||
| -rw-r--r-- | internal/game/render_roulette_test.go | 2 | ||||
| -rw-r--r-- | internal/game/render_roulette_tracker_test.go | 18 | ||||
| -rw-r--r-- | internal/game/roulette_test.go | 10 |
15 files changed, 98 insertions, 115 deletions
diff --git a/internal/game/casino_commands_test.go b/internal/game/casino_commands_test.go index 5e05d4e..cd558ea 100644 --- a/internal/game/casino_commands_test.go +++ b/internal/game/casino_commands_test.go @@ -12,9 +12,9 @@ import ( func TestCasinoCommandsWrapBeforeSessionWithoutLosingIndent(t *testing.T) { commands := []casino.MenuCommand{ - {Command: "bet <amount> on <target>", Desc: "Place bets until the dice roll; table rules lists targets"}, + {Command: "bet <amount> on <target>", Desc: "Place bets until the dice roll; list gives targets"}, {Command: "roll / autoroll / stop", Desc: "Shoot / repeat on your turn / return to manual play"}, - {Command: "unbet <target> / unbet all", Desc: "Remove one bet / every optional bet"}, + {Command: "unbet", Desc: "Remove all your bets"}, {Command: "table", Desc: "View the felt"}, {Command: "insurance", Desc: "Buy insurance; a deliberately long description which requires a hanging continuation line"}, {Command: "decline", Desc: "Continue without insurance (also the timeout default)"}, @@ -58,7 +58,7 @@ func TestCasinoCommandsHangingIndentAndShortDescription(t *testing.T) { sess := &net.Session{Player: player.New("a")} g := &Game{} commands := []casino.MenuCommand{ - {Command: "bet <amount> on <target>", Desc: "Place a wager; targets: table rules"}, + {Command: "bet <amount> on <target>", Desc: "Place a wager; targets: list"}, {Command: "table", Desc: "View the felt"}, } text := plainRoulette(g.casinoMenuCommands(sess, commands)) diff --git a/internal/game/casino_ux.go b/internal/game/casino_ux.go index 1d66f43..6d666f7 100644 --- a/internal/game/casino_ux.go +++ b/internal/game/casino_ux.go @@ -109,14 +109,26 @@ func (g *Game) showCasinoChoices(sess *net.Session, choices []casinoChoice) { sess.WriteLine(g.renderCasinoCommands(sess, "", "Choose a game (use an unambiguous name or abbreviation):", commands)) } +// renderCasinoFelt draws a table's felt (layout, payouts, board) without the +// prose rule text. The table command shows this; rules opens the help page. +// Craps and roulette already render their felt through the live status +// snapshot, so they return nothing here. +func (g *Game) renderCasinoFelt(sess *net.Session, cfg casino.TableConfig) string { + switch cfg.Game { + case casino.GameBlackjack: + return g.renderCasinoSnapshot(sess, &casino.BlackjackRulesView{Config: cfg}) + case casino.GameBaccarat: + return g.renderCasinoSnapshot(sess, &casino.BaccaratRulesView{Config: cfg}) + case casino.GamePaiGow: + return renderPaiGowFelt(cfg, g.colorMode(sess), sess.Player.OptionBool("unicode")) + } + return "" +} + func (g *Game) executeTableStatus(sess *net.Session, args []string, _ string) { if sess.Player == nil { return } - if len(args) > 0 && args[0] != "rules" && args[0] != "help" { - sess.WriteLine("Use table, table rules, or table help.") - return - } t, playing := g.Casino.Participation(sess.Player.Name) if !playing || t == nil { if len(args) > 0 && args[0] == "help" { @@ -132,41 +144,36 @@ 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. - if t.Config.Game == casino.GameVideoPoker { - g.executeTableAction(sess, "paytable") - } else { - sess.WriteLine(g.renderCasinoRules(sess, t.Config)) - } - return - } + // table help remains a shortcut to the seated game's help page; the old + // "table rules" subcommand is gone, so bare table always shows the felt. if len(args) > 0 && args[0] == "help" { 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: rules. Exact payouts: table rules.")) + if t.Config.Game != casino.GameVideoPoker { + commands := []casino.MenuCommand{ + {Command: "rules", Desc: "Help page for this game variant"}, + {Command: "list", Desc: "Every bet available right now, with payouts"}, + {Command: "option casino_display compact", Desc: "Compact action hints (detailed for full command menus)"}, + {Command: "help casino", Desc: "Quick guides to all casino games"}, + } + sess.WriteLine(g.renderCasinoCommands(sess, "", "Table controls", commands)) } + return } - if len(args) > 0 { - // Reuse the game's current-state menu instead of advertising actions - // (insurance, split, rebet, etc.) that may not be legal right now. - g.emitCasinoEvents(t.Status(sess.Player.Name)) - if t.Config.Game == casino.GameVideoPoker { - return - } - 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: "help casino", Desc: "Quick guides to all casino games"}, - } - sess.WriteLine(g.renderCasinoCommands(sess, "", "Table controls", commands)) + // The table command always shows the live felt. The rules command opens + // the game's help page; there is no separate "table rules" command. + if t.Config.Game == casino.GameVideoPoker { + // A video poker cabinet's felt is its paytable. + g.executeTableAction(sess, "paytable") + return + } + g.emitCasinoEvents(t.Status(sess.Player.Name)) + if felt := g.renderCasinoFelt(sess, t.Config); felt != "" { + sess.WriteLine(felt) + } + if t.Config.Game == casino.GameRoulette { + sess.WriteLine(g.rouletteReminder(sess, t.Config)) + } else { + sess.WriteLine(g.colorize(sess, "command", "Learn to play: rules.")) } } diff --git a/internal/game/casino_ux_test.go b/internal/game/casino_ux_test.go index 5313484..415df63 100644 --- a/internal/game/casino_ux_test.go +++ b/internal/game/casino_ux_test.go @@ -168,10 +168,10 @@ func TestTableStatusShowsStateAndRules(t *testing.T) { if !strings.Contains(text, "LIMITS") || !strings.Contains(text, "Waiting for bets") { t.Fatalf("status missing overview: %q", text) } - conn.data = nil - g.executeTableStatus(sess, []string{"rules"}, "table rules") - if !strings.Contains(stripANSI(string(conn.data)), "BLACKJACK PAYS") { - t.Fatalf("table rules missing: %q", stripANSI(string(conn.data))) + // The table command carries the blackjack felt; the rules command (not a + // "table rules" subcommand) opens the help page. + if !strings.Contains(text, "BLACKJACK PAYS") { + t.Fatalf("table felt missing: %q", text) } } diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go index acac68b..8aad30b 100644 --- a/internal/game/cmd_registry.go +++ b/internal/game/cmd_registry.go @@ -56,8 +56,8 @@ var commandRegistry = map[string]commandDef{ }, ClassInstant}, "roll": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "roll") }, ClassActive}, "autoroll": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "autoroll") }, ClassInstant}, - "unbet": {func(g *Game, s *net.Session, args []string, _ string) { - g.executeTableAction(s, "unbet "+strings.Join(args, " ")) + "unbet": {func(g *Game, s *net.Session, _ []string, _ string) { + g.executeTableAction(s, "unbet") }, ClassActive}, "table": {(*Game).executeTableStatus, ClassInstant}, "status": {(*Game).executeTableStatus, ClassInstant}, diff --git a/internal/game/craps_test.go b/internal/game/craps_test.go index 569b4db..fd2e4b9 100644 --- a/internal/game/craps_test.go +++ b/internal/game/craps_test.go @@ -5,12 +5,14 @@ import ( "testing" "thehouseoficarus/internal/casino" "thehouseoficarus/internal/color" + "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" ) func TestCrapsCommandsAndRendering(t *testing.T) { - g := &Game{Casino: casino.NewManager(1)} + g := &Game{Casino: casino.NewManager(1), Combat: combat.NewTracker(), Deps: Deps{World: world.New(t.TempDir())}} cfg := casino.TableConfig{ID: "craps", Game: casino.GameCraps} g.Casino.Join(1, cfg, "alice") conn := &casinoTestConn{} @@ -24,8 +26,8 @@ func TestCrapsCommandsAndRendering(t *testing.T) { } } conn.data = nil - g.executeTableStatus(sess, []string{"rules"}, "table rules") - if !strings.Contains(string(conn.data), "Bank Craps") || !strings.Contains(string(conn.data), "PASS LINE") { + g.executeTableStatus(sess, nil, "table") + if !strings.Contains(string(conn.data), "PASS LINE") { t.Fatal(string(conn.data)) } for _, cmd := range []string{"roll", "autoroll", "unbet"} { diff --git a/internal/game/look_casino.go b/internal/game/look_casino.go index 6fa2959..1e91ced 100644 --- a/internal/game/look_casino.go +++ b/internal/game/look_casino.go @@ -93,7 +93,7 @@ func (g *Game) renderCasinoTableDetails(sess *net.Session, cfg casino.TableConfi } // casinoTableSummary is the compact per-table rule description used by the -// look details block. The full wording stays in RulesText. +// look details block. The full wording lives in the rules help page. func casinoTableSummary(cfg casino.TableConfig) string { switch cfg.Game { case casino.GameBlackjack: diff --git a/internal/game/pai_gow_test.go b/internal/game/pai_gow_test.go index 9b37a68..b8a9c22 100644 --- a/internal/game/pai_gow_test.go +++ b/internal/game/pai_gow_test.go @@ -18,7 +18,7 @@ func TestPaiGowCommandsLookAndSettlement(t *testing.T) { t.Fatal("alias failed") } conn.data = nil - g.executeTableStatus(sess, []string{"rules"}, "table rules") + g.executeTableStatus(sess, nil, "table") text := stripANSI(string(conn.data)) for _, want := range []string{"FORTUNE", "ENVY", "750:1", "Three pair"} { if !strings.Contains(text, want) { @@ -147,6 +147,15 @@ func TestPaiGowShowdownOrderNamesAndColors(t *testing.T) { t.Fatalf("showdown missing %q:\n%s", want, clean) } } + // The hand labels are left-justified normal text, never centered over the + // card art. + for _, line := range strings.Split(clean, "\n") { + if strings.Contains(line, "Your high") || strings.Contains(line, "Dealer high") { + if strings.HasPrefix(line, " ") { + t.Fatalf("showdown label is indented, not left justified: %q", line) + } + } + } // Winning hand strengths are bold yellow; losing ones are bold white. if !strings.Contains(raw, "38;5;220m") { t.Fatalf("winning strength not yellow:\n%q", raw) @@ -296,7 +305,7 @@ func TestPaiGowContextHintsFollowState(t *testing.T) { conn.data = nil g.executeTableStatus(sess, nil, "table") text := stripANSI(string(conn.data)) - for _, want := range []string{"bet <amount> fortune", "bet <amount>", "table rules", "rules", "stop"} { + for _, want := range []string{"bet <amount> fortune", "bet <amount>", "table", "rules", "stop"} { if !strings.Contains(text, want) { t.Fatalf("missing %q at a fresh table: %s", want, text) } diff --git a/internal/game/render_casino_help.go b/internal/game/render_casino_help.go index 26927ae..248a6dd 100644 --- a/internal/game/render_casino_help.go +++ b/internal/game/render_casino_help.go @@ -40,7 +40,7 @@ func (g *Game) casinoBetTable(sess *net.Session, title string, columns []string, return strings.Join(t.Render(unicode, width), "\n") } -// rouletteReminder is shown by table/table rules, not on ambient boards. +// rouletteReminder is shown by the table command, not on ambient boards. // Hit chances count equally likely pockets; all ratios are NET profit odds. func (g *Game) rouletteReminder(sess *net.Session, cfg casino.TableConfig) string { pockets, basketCount, basketOdds := 37, 4, "8:1" @@ -83,6 +83,6 @@ func (g *Game) rouletteReminder(sess *net.Session, cfg casino.TableConfig) strin "Profit excludes your returned stake: 1:1 on 10 returns 20.\n"+ "Low: 1-18; high: 19-36. Zero pockets lose outside bets.\n"+ "Use bet <amount> on <target>, e.g. bet 10 on split 1 2.\n"+ - "Learn more: help roulette / help casino; table rules for this table.") + "Learn more: help roulette / help casino; table for the felt.") return strings.Join(blocks, "\n") } diff --git a/internal/game/render_craps.go b/internal/game/render_craps.go index 12714dc..80f05ee 100644 --- a/internal/game/render_craps.go +++ b/internal/game/render_craps.go @@ -359,7 +359,7 @@ func renderCrapsSnapshot(s *casino.CrapsSnapshot, mode string, unicode bool, sty } c := newCrapsCanvas(height) c.box(0, 0, 76, height-1) - c.center(1, 75, 1, "HOUSE EDITION — table rules for payouts", crapsGold) + c.center(1, 75, 1, "HOUSE EDITION — rules teaches this edition", crapsGold) switch variant { case "crapless": c.center(1, 75, 3, "POINTS: 2 3 4 5 6 8 9 10 11 12 | PASS / COME / ODDS", crapsInk) diff --git a/internal/game/render_help.go b/internal/game/render_help.go index 36f73f7..6887e3e 100644 --- a/internal/game/render_help.go +++ b/internal/game/render_help.go @@ -64,7 +64,7 @@ var commandList = []cmdEntry{ {"look / l", "Instant", "Look around or examine things"}, {"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"}, + {"table / status", "Instant", "Redisplay the table felt; rules for the help page"}, {"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"}, diff --git a/internal/game/render_pai_gow.go b/internal/game/render_pai_gow.go index 4b31373..b125a68 100644 --- a/internal/game/render_pai_gow.go +++ b/internal/game/render_pai_gow.go @@ -72,8 +72,9 @@ func paiGowNumberLabels(nums []int, mode string) []string { return labels } -// paiGowCardBlock renders one labeled run of cards: an optional centered -// header over the card art and, when nums is set, position numbers below. +// paiGowCardBlock renders one labeled run of cards: an optional header over +// the card art and, when nums is set, position numbers below. The header is +// plain left-justified text, not centered, so the hand labels read normally. func paiGowCardBlock(header string, cards []casino.CardView, nums []int, mode string, unicode bool, size int, style string, store *cardart.Store) []string { if len(cards) == 0 { return nil @@ -86,11 +87,7 @@ func paiGowCardBlock(header string, cards []casino.CardView, nums []int, mode st if header == "" { return lines } - width := 0 - for _, line := range lines { - width = max(width, color.VisibleLen(line)) - } - return append([]string{rouletteCenter(header, width)}, lines...) + return append([]string{header}, lines...) } // paiGowJoinBlocks sets two card blocks side by side with a gutter between diff --git a/internal/game/render_roulette.go b/internal/game/render_roulette.go index 9ffd355..218a91f 100644 --- a/internal/game/render_roulette.go +++ b/internal/game/render_roulette.go @@ -9,7 +9,6 @@ import ( "thehouseoficarus/internal/casino" "thehouseoficarus/internal/color" - "thehouseoficarus/internal/net" ) // Roulette art is rendered per recipient, just like cards. Never embed ANSI @@ -595,48 +594,3 @@ func renderRouletteResult(pockets [5]string, mode string, unicode bool, size int } return strings.Join(lines, "\n") } - -func (g *Game) renderCasinoRules(sess *net.Session, cfg casino.TableConfig) string { - if cfg.Game == casino.GameBlackjack { - // The whole rule card is a rendered felt. - return g.renderCasinoSnapshot(sess, &casino.BlackjackRulesView{Config: cfg}) - } - if cfg.Game == casino.GamePaiGow { - return cfg.RulesText() + "\n" + renderPaiGowFelt(cfg, g.colorMode(sess), sess.Player.OptionBool("unicode")) - } - if cfg.Game == casino.GameBaccarat { - return g.renderCasinoSnapshot(sess, &casino.BaccaratRulesView{Config: cfg}) - } - rules := g.colorize(sess, "casino_action", cfg.RulesText()) - if cfg.Game == casino.GameCraps { - snap := &casino.CrapsSnapshot{Phase: "waiting"} - if g.Casino != nil && sess.Player != nil { - if table := g.Casino.Table(sess.Player.RoomID, cfg.ID); table != nil { - for _, event := range table.Status(sess.Player.Name) { - if view, ok := event.Snapshot.(*casino.CrapsSnapshot); ok { - snap = view - break - } - } - } - } - return rules + "\n" + g.renderCasinoSnapshot(sess, snap) - } - if cfg.Game == casino.GameRoulette { - snap := &casino.RouletteSnapshot{DoubleZero: cfg.Roulette.DoubleZero, Racetrack: cfg.Roulette.Racetrack, Phase: "betting", Wheel: casino.WheelOrder(cfg.Roulette.DoubleZero)} - // Rules views draw a betting board too; use the live table's history - // rather than presenting an empty tracker after completed spins. - if g.Casino != nil && sess.Player != nil { - if table := g.Casino.Table(sess.Player.RoomID, cfg.ID); table != nil { - for _, event := range table.Status(sess.Player.Name) { - if view, ok := event.Snapshot.(*casino.RouletteSnapshot); ok { - snap.History = view.History - break - } - } - } - } - rules += "\n" + g.renderCasinoSnapshot(sess, snap) + "\n" + g.rouletteReminder(sess, cfg) - } - return rules -} diff --git a/internal/game/render_roulette_test.go b/internal/game/render_roulette_test.go index fcfd9c7..72da225 100644 --- a/internal/game/render_roulette_test.go +++ b/internal/game/render_roulette_test.go @@ -406,7 +406,7 @@ func TestRouletteSettlementShowsBalanceOnce(t *testing.T) { if action != "" { g.Casino.Action(1, "r", p.Name, action) } - g.Casino.Tick() + // A lone player's spin starts on the bet, so the first tick settles. g.emitCasinoEvents(g.Casino.Tick()) if got := strings.Count(string(conn.data), "Available "); got != 1 { t.Fatalf("action=%q: balance printed %d times\n%s", action, got, conn.data) diff --git a/internal/game/render_roulette_tracker_test.go b/internal/game/render_roulette_tracker_test.go index a629b57..b05a2be 100644 --- a/internal/game/render_roulette_tracker_test.go +++ b/internal/game/render_roulette_tracker_test.go @@ -105,7 +105,7 @@ func TestRouletteTrackerAboveBettingBoardsOnly(t *testing.T) { } } -func TestRouletteRulesUseLiveHistory(t *testing.T) { +func TestRouletteTableShowsLiveHistory(t *testing.T) { g := &Game{Casino: casino.NewManager(7)} p := player.New("alice") p.RoomID, p.Credits = 1, 100 @@ -116,8 +116,20 @@ func TestRouletteRulesUseLiveHistory(t *testing.T) { g.Casino.Bet(1, "r", p.Name, "red", 1, casinoWallet{g: g, p: p, saveCharacter: func(*player.Player) error { return nil }}) g.Casino.Tick() g.Casino.Tick() - art := g.renderCasinoRules(sess, cfg) + // The table command renders the live status snapshot, which carries the + // tracker's completed-spin history. + var snap *casino.RouletteSnapshot + for _, event := range g.Casino.Table(1, "r").Status(p.Name) { + if view, ok := event.Snapshot.(*casino.RouletteSnapshot); ok && view.Phase != "result" { + snap = view + break + } + } + if snap == nil { + t.Fatal("no live roulette snapshot") + } + art := g.renderCasinoSnapshot(sess, snap) if !strings.Contains(art, "History: last 1 spin") || strings.Contains(art, "awaiting the first spin") { - t.Fatalf("rules did not show live history:\n%s", art) + t.Fatalf("table did not show live history:\n%s", art) } } diff --git a/internal/game/roulette_test.go b/internal/game/roulette_test.go index 0c5b62e..9eaea8e 100644 --- a/internal/game/roulette_test.go +++ b/internal/game/roulette_test.go @@ -5,13 +5,15 @@ import ( "testing" "thehouseoficarus/internal/casino" + "thehouseoficarus/internal/combat" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" + "thehouseoficarus/internal/world" ) func TestRouletteNumericTargetsAndRendering(t *testing.T) { for _, double := range []bool{false, true} { - g := &Game{Casino: casino.NewManager(1)} + g := &Game{Casino: casino.NewManager(1), Combat: combat.NewTracker(), Deps: Deps{World: world.New(t.TempDir())}} cfg := casino.TableConfig{ID: "roulette", Game: casino.GameRoulette, Roulette: casino.RouletteConfig{DoubleZero: double}} g.Casino.Join(1, cfg, "alice") conn := &casinoTestConn{} @@ -29,9 +31,9 @@ func TestRouletteNumericTargetsAndRendering(t *testing.T) { } } conn.data = nil - g.executeTableStatus(sess, []string{"rules"}, "table rules") - if !strings.Contains(string(conn.data), "Straight numbers pay 35:1") || strings.Contains(string(conn.data), "Blackjack") { - t.Fatal("wrong rules", string(conn.data)) + g.executeTableStatus(sess, nil, "table") + if !strings.Contains(string(conn.data), "35:1") || strings.Contains(string(conn.data), "Blackjack") { + t.Fatal("wrong felt", string(conn.data)) } snapshot := &casino.RouletteSnapshot{DoubleZero: double, Phase: "playing", Players: []string{"alice"}} if got := g.renderCasinoSnapshot(sess, snapshot); got != renderRouletteSnapshot(snapshot, g.colorMode(sess), sess.Player.OptionBool("unicode"), sess.Player.OptionInt("card_size")) { |
