From 62017c86392b046d746fb4fafef6742840477c5f Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 2 Oct 2026 02:16:44 -0400 Subject: feat: polish 'list' output, drop sitout command --- internal/casino/baccarat.go | 2 +- internal/casino/blackjack.go | 2 +- internal/casino/craps.go | 7 +++ internal/casino/craps_rules.go | 2 +- internal/casino/craps_variants.go | 4 +- internal/casino/roulette.go | 9 ++- internal/casino/table.go | 13 ++++- internal/casino/table_hints.go | 7 +-- internal/casino/table_ux.go | 1 - internal/casino/video_poker.go | 13 +++++ internal/game/casino_house_edge.go | 10 ++-- internal/game/casino_ux.go | 28 ++++------ internal/game/casino_ux_test.go | 15 +++-- internal/game/cmd_registry.go | 3 +- internal/game/pai_gow_test.go | 16 ++++-- internal/game/render_baccarat_felt.go | 32 +++++++---- internal/game/render_baccarat_felt_test.go | 9 ++- internal/game/render_casino_help.go | 88 ------------------------------ internal/game/render_casino_help_test.go | 62 --------------------- internal/game/render_help.go | 1 - internal/game/roulette_test.go | 6 +- internal/net/server.go | 4 ++ internal/net/server_test.go | 15 +++++ 23 files changed, 136 insertions(+), 213 deletions(-) delete mode 100644 internal/game/render_casino_help.go (limited to 'internal') diff --git a/internal/casino/baccarat.go b/internal/casino/baccarat.go index e30de5b..c37870d 100644 --- a/internal/casino/baccarat.go +++ b/internal/casino/baccarat.go @@ -122,7 +122,7 @@ func (t *baccaratTable) menuEvent(name string) Event { MenuCommand{Command: fmt.Sprintf("bet %d banker", t.Config.MinBet)}, MenuCommand{Command: "bet on "}, MenuCommand{Command: "stop", Desc: "Leave; during a round, leave afterward"}, - MenuCommand{Command: "table / sitout", Desc: "Check the table / skip betting until you rejoin"}) + MenuCommand{Command: "table", Desc: "Check the table"}) commands = withTableHints(commands, t.wagerRows()) e := t.betMenuEvent(name, commands) if p != nil { diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go index a059bdb..eee5023 100644 --- a/internal/casino/blackjack.go +++ b/internal/casino/blackjack.go @@ -120,7 +120,7 @@ func (t *blackjackTable) menuEvent(name string) Event { {Command: "bet", Desc: fmt.Sprintf("%s: %d credits (also: rebet)", desc, amount)}, {Command: "bet ", Desc: "Change wager; min/max accepted"}, {Command: "stop", Desc: "Leave; during a round, leave afterward"}, - {Command: "table / sitout", Desc: "Check the table / skip betting until you rejoin"}, + {Command: "table", Desc: "Check the table"}, }) // Blackjack has a single wager target, so no list hint; rules rides along. e.Menu.Commands = withTableHints(e.Menu.Commands, nil) diff --git a/internal/casino/craps.go b/internal/casino/craps.go index 69a38ed..8322192 100644 --- a/internal/casino/craps.go +++ b/internal/casino/craps.go @@ -102,6 +102,13 @@ func (t *crapsTable) view() *CrapsSnapshot { } return s } + +// board is the table-only view for the table command. +func (t *crapsTable) board(_ string) Snapshot { + t.mu.Lock() + defer t.mu.Unlock() + return t.view() +} func (t *crapsTable) menu(name string) Event { e := t.betMenuEvent(name, t.commands(name)) e.Snapshot = t.view() diff --git a/internal/casino/craps_rules.go b/internal/casino/craps_rules.go index 6292b4a..88649a2 100644 --- a/internal/casino/craps_rules.go +++ b/internal/casino/craps_rules.go @@ -97,7 +97,7 @@ func (c TableConfig) CrapsRulesText() string { "Bets remain open until the roll. Dice rotate on seven-out.\n"+ "autoroll: repeat every 3 seconds on your turn (multiplayer waits 10 seconds).\n"+ "unbet: remove every non-contract bet and all free odds; pass/come contracts stay.\n"+ - "stop cancels autoroll without leaving; in manual mode stop/sitout wait for bets.\n"+ + "stop cancels autoroll without leaving; in manual mode stop waits for live bets.\n"+ "Disconnect forfeits live bets.\n"+ "Winning bets return stake and are removed (no automatic pressing).", odds, r.Field12) } diff --git a/internal/casino/craps_variants.go b/internal/casino/craps_variants.go index dcaf1cd..096bfc5 100644 --- a/internal/casino/craps_variants.go +++ b/internal/casino/craps_variants.go @@ -221,12 +221,12 @@ func (c CrapsConfig) variantRulesText() string { cadence := "Solo roll anytime; multiplayer waits 10 seconds, then dealer rolls after 60 seconds." autoroll := "Bets stay open until the roll. autoroll repeats every 3 seconds, respecting the betting window." betLine := "Bet: bet on pass; the felt is authoritative for this edition." - stopLine := "stop cancels autoroll; manual stop/sitout waits for live bets." + stopLine := "stop cancels autoroll; manual stop waits for live bets." if c.variant() == "simplified" { betLine = "Bet: bet — always on pass; bare bet rebets your last wager." cadence = "The dice roll automatically 3 seconds after the first bet." autoroll = "" - stopLine = "stop/sitout waits for live bets." + stopLine = "stop waits for live bets." } lines := []string{c.title() + " — house rules", rules, betLine, fmt.Sprintf("Free odds: %d (0 = 3-4-5x on ordinary points, don't 6x laid). No odds on Simplified, High Point or Die Rich.", c.Odds), diff --git a/internal/casino/roulette.go b/internal/casino/roulette.go index 8949ff8..de6536f 100644 --- a/internal/casino/roulette.go +++ b/internal/casino/roulette.go @@ -502,6 +502,13 @@ func (t *rouletteTable) view() *RouletteSnapshot { } return s } + +// board is the table-only view for the table command. +func (t *rouletteTable) board(_ string) Snapshot { + t.mu.Lock() + defer t.mu.Unlock() + return t.view() +} func (t *rouletteTable) menuEvent(name string) Event { commands := []MenuCommand{} p := t.players.get(name) @@ -512,7 +519,7 @@ func (t *rouletteTable) menuEvent(name string) Event { } } if p != nil { - commands = append(commands, MenuCommand{Command: "sitout / stop", Desc: "Sit out / leave when your wager settles"}) + commands = append(commands, MenuCommand{Command: "stop", Desc: "Leave when your wager settles"}) } commands = append(commands, MenuCommand{Command: "table", Desc: "View the layout"}) if p != nil { diff --git a/internal/casino/table.go b/internal/casino/table.go index 2f4473b..08c18bf 100644 --- a/internal/casino/table.go +++ b/internal/casino/table.go @@ -129,7 +129,18 @@ func (t *Table) idle() bool { return t.game.idle() } // Status is a read-only view: it never restarts a deadline or changes a wager. func (t *Table) Status(name string) []Event { return t.game.status(name) } -func (t *Table) Overview() string { return t.game.overview() } + +// Board returns the game's table-only board for the table command: the live +// wheel, dice layout or paytable, with no balance, command menu, or state +// prose. Games whose table is a printed felt return nil so the caller can +// render the felt instead. +func (t *Table) Board(name string) Snapshot { + if b, ok := t.game.(interface{ board(name string) Snapshot }); ok { + return b.board(name) + } + return nil +} +func (t *Table) Overview() string { return t.game.overview() } // Seats lists the seated players in seat order for the look command. func (t *Table) Seats() []string { return t.game.seats() } diff --git a/internal/casino/table_hints.go b/internal/casino/table_hints.go index 51dda1a..d44deb7 100644 --- a/internal/casino/table_hints.go +++ b/internal/casino/table_hints.go @@ -24,7 +24,7 @@ func (t *rouletteTable) hints(name string) []MenuCommand { commands = append(commands, MenuCommand{Command: "bet / rebet", Desc: "Repeat your previous wager"}) } } - commands = append(commands, MenuCommand{Command: "sitout / stop", Desc: "Sit out / leave when your wager settles"}) + commands = append(commands, MenuCommand{Command: "stop", Desc: "Leave when your wager settles"}) commands = append(commands, MenuCommand{Command: "table", Desc: "View the layout"}) return withTableHints(commands, t.wagerRows()) } @@ -34,7 +34,6 @@ func (t *rouletteTable) hints(name string) []MenuCommand { func blackjackHintsIdle() []MenuCommand { return []MenuCommand{ {Command: "table", Desc: "View the table"}, - {Command: "sitout", Desc: "Sit out after this round"}, {Command: "stop", Desc: "Leave after this round"}, } } @@ -71,7 +70,7 @@ func baccaratHintCommands(t *baccaratTable, name string) []MenuCommand { return []MenuCommand{ {Command: "unbet", Desc: "Take back all your wagers before the round"}, {Command: "stop", Desc: "Leave; during a round, leave afterward"}, - {Command: "table / sitout", Desc: "Check the table / skip betting until you rejoin"}, + {Command: "table", Desc: "Check the table"}, } } commands := []MenuCommand{} @@ -85,6 +84,6 @@ func baccaratHintCommands(t *baccaratTable, name string) []MenuCommand { MenuCommand{Command: fmt.Sprintf("bet %d banker", t.Config.MinBet)}, MenuCommand{Command: "bet on "}, MenuCommand{Command: "stop", Desc: "Leave; during a round, leave afterward"}, - MenuCommand{Command: "table / sitout", Desc: "Check the table / skip betting until you rejoin"}, + MenuCommand{Command: "table", Desc: "Check the table"}, ) } diff --git a/internal/casino/table_ux.go b/internal/casino/table_ux.go index c7b9fd1..d16d434 100644 --- a/internal/casino/table_ux.go +++ b/internal/casino/table_ux.go @@ -112,7 +112,6 @@ func (c *tableCore[P]) committedMenu(name string) Event { e.Color = "casino_menu" commands := []MenuCommand{ {Command: "table", Desc: "View the table"}, - {Command: "sitout", Desc: "Sit out after this round"}, {Command: "stop", Desc: "Leave after this round"}, } // Until the round is dealt or spun, the committed wager can still be diff --git a/internal/casino/video_poker.go b/internal/casino/video_poker.go index a22d3c8..7e79b58 100644 --- a/internal/casino/video_poker.go +++ b/internal/casino/video_poker.go @@ -217,6 +217,19 @@ func (t *videoPokerTable) statusLocked(name string, showTable bool) []Event { return []Event{e} } +// board is the table-only view for the table command: the paytable and game +// state without the balance line or command menu. +func (t *videoPokerTable) board(name string) Snapshot { + t.mu.Lock() + defer t.mu.Unlock() + for _, e := range t.statusLocked(name, true) { + if e.Snapshot != nil { + return e.Snapshot + } + } + return nil +} + func (t *videoPokerTable) join(name string) []Event { t.mu.Lock() defer t.mu.Unlock() diff --git a/internal/game/casino_house_edge.go b/internal/game/casino_house_edge.go index 8bc3a5b..19d8cc2 100644 --- a/internal/game/casino_house_edge.go +++ b/internal/game/casino_house_edge.go @@ -90,8 +90,9 @@ func (g *Game) casinoListRows(sess *net.Session, t *casino.Table, wagers []casin // Explicit list-only presentation. Wrap plain cells before applying color and // handing them to the existing ANSI-aware table renderer; never truncate rules. -// The table stands alone: list names no heading and no balance above it. -func (g *Game) showCasinoListTable(sess *net.Session, hint string, rows []casinoListRow) { +// The table stands alone: list names no heading and no balance above it, and +// the first column header carries the bet syntax instead of a hint line below. +func (g *Game) showCasinoListTable(sess *net.Session, betHeader string, rows []casinoListRow) { precision, hasItem := g.casinoEdgePrecision(sess) // The item adds a column only when at least one listed wager has a real // analysis; decision actions such as hit/stand have none. @@ -104,7 +105,7 @@ func (g *Game) showCasinoListTable(sess *net.Session, hint string, rows []casino } } } - headers := []string{"Bet", "Odds", "Win / action"} + headers := []string{betHeader, "Odds", "Win / action"} categories := []string{"command", "casino_payout", ""} if rtp { headers = append(headers, "RTP") @@ -183,8 +184,5 @@ func (g *Game) showCasinoListTable(sess *net.Session, hint string, rows []casino } } out := table.Render(unicode, width) - if hint != "" { - out = append(out, g.colorize(sess, "command", hint)) - } sess.WriteLine(strings.Join(out, "\n")) } diff --git a/internal/game/casino_ux.go b/internal/game/casino_ux.go index 28fc361..d65cfb9 100644 --- a/internal/game/casino_ux.go +++ b/internal/game/casino_ux.go @@ -159,22 +159,18 @@ func (g *Game) executeTableStatus(sess *net.Session, args []string, _ string) { } return } - // 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") + // The table command shows only the table: the live board for roulette, + // craps and video poker, or the printed felt for blackjack, baccarat and + // Pai Gow. No balance, seat state, command menu, or reminder rides along. + if board := t.Board(sess.Player.Name); board != nil { + if rendered := g.renderCasinoSnapshot(sess, board); rendered != "" { + sess.WriteLine(rendered) + } 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.")) - } } func (g *Game) executeWatch(sess *net.Session, args []string, _ string) { @@ -369,7 +365,7 @@ func (g *Game) executeList(sess *net.Session, args []string, rawInput string) { if _, enabled := g.casinoEdgePrecision(sess); enabled { row.edge = machine.Config.HouseEdge() } - g.showCasinoListTable(sess, "", []casinoListRow{row}) + g.showCasinoListTable(sess, "Bet", []casinoListRow{row}) return } if table, playing := g.Casino.Participation(p.Name); playing && table != nil { @@ -391,13 +387,13 @@ func (g *Game) showCasinoWagerList(sess *net.Session, t *casino.Table) { } rows := g.casinoListRows(sess, t, []casino.WagerRow{{Bet: "main", Payout: "paytable", Win: "deal"}}) rows[0].bet = "bet [1-4|max]" - g.showCasinoListTable(sess, "", rows) + g.showCasinoListTable(sess, "Bet", rows) return } rows := t.Wagers(sess.Player.Name) // Blackjack rows are actions, not targets for a new bet. if t.Config.Game == casino.GameBlackjack && len(rows) > 0 { - g.showCasinoListTable(sess, "", g.casinoListRows(sess, t, rows)) + g.showCasinoListTable(sess, "Action", g.casinoListRows(sess, t, rows)) return } if len(rows) == 0 { @@ -417,10 +413,10 @@ func (g *Game) showCasinoWagerList(sess *net.Session, t *casino.Table) { } list := g.casinoListRows(sess, t, []casino.WagerRow{wager}) list[0].bet = "bet " - g.showCasinoListTable(sess, "", list) + g.showCasinoListTable(sess, "Bet", list) return } - g.showCasinoListTable(sess, "bet on ", g.casinoListRows(sess, t, rows)) + g.showCasinoListTable(sess, "bet on", g.casinoListRows(sess, t, rows)) } // showCasinoGameLook appends the live game state and hint menu for the diff --git a/internal/game/casino_ux_test.go b/internal/game/casino_ux_test.go index 415df63..1a06f89 100644 --- a/internal/game/casino_ux_test.go +++ b/internal/game/casino_ux_test.go @@ -163,16 +163,19 @@ func TestPlayWithAmountJoinsAndBets(t *testing.T) { func TestTableStatusShowsStateAndRules(t *testing.T) { g, sess, conn := uxCasinoGame(t) g.executePlay(sess, []string{"bj"}, "play bj") + conn.data = nil g.executeTableStatus(sess, nil, "table") text := stripANSI(string(conn.data)) - if !strings.Contains(text, "LIMITS") || !strings.Contains(text, "Waiting for bets") { - t.Fatalf("status missing overview: %q", text) - } - // 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") { + // The table command carries the blackjack felt and nothing else: no + // balance, seat status, command menu, or rules reminder. + if !strings.Contains(text, "LIMITS") || !strings.Contains(text, "BLACKJACK PAYS") { t.Fatalf("table felt missing: %q", text) } + for _, clutter := range []string{"Available ", "Waiting for bets", "Learn to play", "stop", "rules"} { + if strings.Contains(text, clutter) { + t.Fatalf("table showed clutter %q: %q", clutter, text) + } + } } func TestWatchAndSpectatorDetailFlow(t *testing.T) { diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go index 8aad30b..710dd92 100644 --- a/internal/game/cmd_registry.go +++ b/internal/game/cmd_registry.go @@ -64,7 +64,6 @@ var commandRegistry = map[string]commandDef{ "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}, "h": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "hit") }, ClassActive}, "st": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "stand") }, ClassActive}, @@ -248,7 +247,7 @@ func classifyCommand(cmd string) CommandClass { var casinoShortVerbs = []string{ "bet", "deal", "draw", "hold", "paytable", "denom", "denomination", "hands", "feature", "game", "roll", "autoroll", "unbet", "table", - "status", "list", "watch", "sitout", "decline", "hit", "stand", + "status", "list", "watch", "decline", "hit", "stand", "double", "split", "insurance", "surrender", "spin", "autobet", "autospin", "rebet", } diff --git a/internal/game/pai_gow_test.go b/internal/game/pai_gow_test.go index f0440b7..5ba367e 100644 --- a/internal/game/pai_gow_test.go +++ b/internal/game/pai_gow_test.go @@ -300,17 +300,23 @@ func TestPaiGowContextHintsFollowState(t *testing.T) { g, sess, conn := casinoCrapsGame(t, "name: Pai Gow\ncasino_tables:\n - id: fortune\n game: paigow\n min_bet: 5\n max_bet: 1000\n pai_gow:\n envy: true\n") g.executePlay(sess, []string{"pai"}, "play pai") - // A fresh bettor is told about the side bet and the main wager, plus the - // shared tail. - conn.data = nil - g.executeTableStatus(sess, nil, "table") + // The join menu carries the context hints: the side bet, the main wager, + // and the shared tail. text := stripANSI(string(conn.data)) for _, want := range []string{"bet fortune", "bet ", "table", "rules", "stop"} { if !strings.Contains(text, want) { - t.Fatalf("missing %q at a fresh table: %s", want, text) + t.Fatalf("missing %q in the join menu: %s", want, text) } } + // The table command shows only the felt, without the hint menu. + conn.data = nil + g.executeTableStatus(sess, nil, "table") + text = stripANSI(string(conn.data)) + if !strings.Contains(text, "PAI GOW POKER") || strings.Contains(text, "bet ") { + t.Fatalf("table should be the felt alone: %s", text) + } + // A lone main wager deals immediately; the live menu moves to setting. conn.data = nil g.executeBet(sess, []string{"20"}, "bet 20") diff --git a/internal/game/render_baccarat_felt.go b/internal/game/render_baccarat_felt.go index 4757764..608ef93 100644 --- a/internal/game/render_baccarat_felt.go +++ b/internal/game/render_baccarat_felt.go @@ -12,7 +12,7 @@ import ( // table across the bottom. Reuses the shared felt canvas and palette. func renderBaccaratRulesFelt(cfg casino.TableConfig, mode string, unicode bool) string { b := cfg.Baccarat.Normalize() - c := newCrapsCanvas(22) + c := newCrapsCanvas(26) // Title block: variant, scoring, naturals. c.box(0, 0, 76, 4) c.center(1, 75, 1, baccaratVariantTitle(b), crapsGold) @@ -60,17 +60,29 @@ func renderBaccaratRulesFelt(cfg casino.TableConfig, mode string, unicode bool) for j, row := range house { c.bjRule(38, 76, 10+j, row[0], row[1]) } - // Drawing table across the bottom. - c.box(0, 15, 76, 21) + // Drawing table across the bottom: one row per hand total, with the + // player rule and each banker situation in its own column. + c.box(0, 15, 76, 25) c.center(1, 75, 16, "DRAWING — AUTOMATIC PUNTO BANCO", crapsGold) - draw := []string{ - "PLAYER: DRAWS 0-5; STANDS 6-7. BANKER VS A STANDING PLAYER: DRAWS 0-5.", - "BANKER VS A DRAWING PLAYER: 0-2 ALWAYS DRAWS; 3 DRAWS UNLESS VS 8;", - "4 DRAWS VS 2-7; 5 DRAWS VS 4-7; 6 DRAWS VS 6-7; 7 STANDS.", - "ONE MAIN BET (PLAYER/BANKER/TIE) PLUS SIDE BETS; SIDE BETS FIRST.", + c.text(3, 17, "HAND", crapsGold) + c.text(9, 17, "PLAYER", crapsGold) + c.text(18, 17, "BANKER (PLAYER STANDS)", crapsGold) + c.text(44, 17, "BANKER (PLAYER DRAWS)", crapsGold) + c.box(1, 18, 75, 18) + drawRows := [][4]string{ + {"0-2", "draw", "draw", "draw"}, + {"3", "draw", "draw", "draw unless vs 8"}, + {"4", "draw", "draw", "draw vs 2-7"}, + {"5", "draw", "draw", "draw vs 4-7"}, + {"6", "stand", "stand", "draw vs 6-7"}, + {"7", "stand", "stand", "stand"}, } - for i, line := range draw { - c.text(3, 17+i, line, crapsInk) + for i, row := range drawRows { + y := 19 + i + c.text(3, y, row[0], crapsInk) + c.text(9, y, row[1], crapsInk) + c.text(18, y, row[2], crapsInk) + c.text(44, y, row[3], crapsInk) } return c.render(mode, unicode) } diff --git a/internal/game/render_baccarat_felt_test.go b/internal/game/render_baccarat_felt_test.go index a3bb01c..0b2a942 100644 --- a/internal/game/render_baccarat_felt_test.go +++ b/internal/game/render_baccarat_felt_test.go @@ -14,7 +14,7 @@ func TestBaccaratRulesFeltGeometryAndPhrases(t *testing.T) { for _, unicode := range []bool{false, true} { art := renderBaccaratRulesFelt(cfg, "none", unicode) rows := strings.Split(strings.TrimSuffix(art, "\n"), "\n") - if len(rows) != 22 { + if len(rows) != 26 { t.Fatalf("felt height %d", len(rows)) } for _, row := range rows { @@ -29,13 +29,16 @@ func TestBaccaratRulesFeltGeometryAndPhrases(t *testing.T) { "SIDE BETS", "HOUSE", "PLAYER PAIR", "BANKER PAIR", "11:1", "DRAGON 7", "PANDA 8", "SUPER 6", "NOT OFFERED", "DECKS", "SHUFFLE AT", "BET LIMITS", "1-100 CREDITS", "35 TICKS", - "DRAWING — AUTOMATIC PUNTO BANCO", "DRAWS 0-5", "6 DRAWS VS 6-7", - "ONE MAIN BET (PLAYER/BANKER/TIE) PLUS SIDE BETS", + "DRAWING — AUTOMATIC PUNTO BANCO", "HAND", "BANKER (PLAYER STANDS)", + "BANKER (PLAYER DRAWS)", "draw unless vs 8", "draw vs 2-7", "draw vs 4-7", "draw vs 6-7", } { if !strings.Contains(art, want) { t.Fatalf("missing %q in unicode=%v felt", want, unicode) } } + if strings.Contains(art, "ONE MAIN BET") { + t.Fatal("drawing table kept the removed main-bet line") + } // The raw dump's phrases must not leak onto the felt. for _, stale := range []string{"Number of decks", "Drawing: Automatic", "Fractional credits:"} { if strings.Contains(art, stale) { diff --git a/internal/game/render_casino_help.go b/internal/game/render_casino_help.go deleted file mode 100644 index 248a6dd..0000000 --- a/internal/game/render_casino_help.go +++ /dev/null @@ -1,88 +0,0 @@ -package game - -import ( - "fmt" - "strings" - - "thehouseoficarus/internal/casino" - "thehouseoficarus/internal/net" - "thehouseoficarus/internal/ui" -) - -// casinoBetTable uses the shared ANSI-aware table renderer, rather than -// letting the session wrap a long, pipe-separated payout paragraph. -func (g *Game) casinoBetTable(sess *net.Session, title string, columns []string, rows [][]string) string { - t := ui.Table{Title: g.colorize(sess, "casino_action", title)} - for _, col := range columns { - t.Columns = append(t.Columns, g.colorize(sess, "casino_action", col)) - } - for _, row := range rows { - cells := append([]string(nil), row...) - for i, cell := range cells { - category := "casino_action" - if i == 0 { - category = "command" - } - if i == len(cells)-1 { - category = "casino_payout" - } - cells[i] = g.colorize(sess, category, cell) - } - t.Rows = append(t.Rows, cells) - } - unicode, width := true, 78 - if p := sess.Player; p != nil { - unicode = p.OptionBool("unicode") - if wrap := p.OptionInt("wrap_width"); wrap > 0 { - width = min(width, wrap) - } - } - return strings.Join(t.Render(unicode, width), "\n") -} - -// 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" - if cfg.Roulette.DoubleZero { - pockets, basketCount, basketOdds = 38, 5, "6:1" - } - chance := func(n int) string { return fmt.Sprintf("%d/%d", n, pockets) } - rows := [][]string{ - {"Number", "17 (or 0)", chance(1), "35:1"}, - {"Split", "split 1 2", chance(2), "17:1"}, - {"Street", "street 1 2 3", chance(3), "11:1"}, - {"Trio", "trio 0 1 2", chance(3), "11:1"}, - {"Corner", "corner 1 2 4 5", chance(4), "8:1"}, - {"Six line", "line 1 2 3 4 5 6", chance(6), "5:1"}, - {"Dozen", "1to12/13to24/25to36", chance(12), "2:1"}, - {"Row", "row1/row2/row3", chance(12), "2:1"}, - {"Color", "red / black", chance(18), "1:1"}, - {"Parity", "odd / even", chance(18), "1:1"}, - {"Low / high", "low / high", chance(18), "1:1"}, - {"Basket", "basket", chance(basketCount), basketOdds}, - } - if cfg.Roulette.DoubleZero { - rows[0][1] = "17 (or 0, 00)" - } - blocks := []string{g.casinoBetTable(sess, "Roulette payouts", []string{"Bet", "Target / example", "Chance", "Profit"}, rows)} - if cfg.Roulette.Racetrack && !cfg.Roulette.DoubleZero { - blocks = append(blocks, g.casinoBetTable(sess, "Racetrack payouts", []string{"Target", "Winning pockets", "Chance", "Profit"}, [][]string{ - {"voisins", "0, 2, 3", chance(3), "5:3"}, - {"voisins", "Other covered", chance(14), "1:1"}, - {"tiers", "Any covered", chance(12), "2:1"}, - {"orphelins", "1, 17", chance(2), "31:5"}, - {"orphelins", "Other covered", chance(6), "13:5"}, - {"zero / jeu zero", "26", chance(1), "8:1"}, - {"zero / jeu zero", "Other covered", chance(6), "7:2"}, - {"neighbors ", "Any covered", chance(9), "3:1"}, - })) - blocks = append(blocks, "Profit is on the total stake; each row lists its winning pockets.\nCall bets split your stake; fractional returns round down.\nUse help racetrack for the chip placements.") - } - blocks = append(blocks, - "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 on , e.g. bet 10 on split 1 2.\n"+ - "Learn more: help roulette / help casino; table for the felt.") - return strings.Join(blocks, "\n") -} diff --git a/internal/game/render_casino_help_test.go b/internal/game/render_casino_help_test.go index 830186c..2b23119 100644 --- a/internal/game/render_casino_help_test.go +++ b/internal/game/render_casino_help_test.go @@ -5,72 +5,10 @@ import ( "testing" "thehouseoficarus/internal/casino" - "thehouseoficarus/internal/color" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" ) -func TestRoulettePayoutTables(t *testing.T) { - for _, mode := range []string{"none", "ansi", "xterm256"} { - for _, unicode := range []bool{false, true} { - for _, double := range []bool{false, true} { - for _, width := range []int{60, 78} { - g := &Game{} - p := player.New("alice") - p.Options = map[string]any{"color": mode, "unicode": unicode, "wrap_width": width} - sess := &net.Session{Player: p} - cfg := casino.TableConfig{Game: casino.GameRoulette, Roulette: casino.RouletteConfig{DoubleZero: double, Racetrack: !double}} - art := g.rouletteReminder(sess, cfg) - plain := stripANSI(art) - for _, want := range []string{"Roulette payouts", "Target / example", "Chance", "Profit", "35:1", "17:1", "11:1", "8:1", "5:1", "2:1", "1:1", "help roulette"} { - if !strings.Contains(plain, want) { - t.Fatalf("missing %q:\n%s", want, plain) - } - } - if double { - for _, want := range []string{"1/38", "5/38", "6:1", "0, 00"} { - if !strings.Contains(plain, want) { - t.Fatalf("missing %q", want) - } - } - if strings.Contains(plain, "Racetrack payouts") { - t.Fatal("American wheel has call bets") - } - } else { - for _, want := range []string{"1/37", "2/37", "3/37", "4/37", "6/37", "12/37", "9/37", "14/37", "Racetrack payouts", "5:3", "13:5", "31:5", "7:2", "zero / jeu zero"} { - if !strings.Contains(plain, want) { - t.Fatalf("missing %q:\n%s", want, plain) - } - } - } - if (mode == "none") == strings.Contains(art, "\x1b[") { - t.Fatalf("wrong color mode %s", mode) - } - tableWidth := 0 - for _, line := range strings.Split(art, "\n") { - plainLine := stripANSI(line) - if plainLine != "" && strings.ContainsRune("╔╟╚+", []rune(plainLine)[0]) { - tableWidth = color.VisibleLen(line) - } - if plainLine != "" && strings.ContainsRune("╔║╟╚+|", []rune(plainLine)[0]) { - if got := color.VisibleLen(line); got != tableWidth || got > width { - t.Errorf("uneven table width %d (want %d, max %d): %q", got, tableWidth, width, plainLine) - } - } - if !unicode { - for _, r := range plainLine { - if r > 127 { - t.Errorf("non-ASCII %q", r) - } - } - } - } - } - } - } - } -} - func TestCasinoHelpTopics(t *testing.T) { g := &Game{Deps: Deps{DataDir: "../../data"}} conn := &casinoTestConn{} diff --git a/internal/game/render_help.go b/internal/game/render_help.go index 6887e3e..28f2c10 100644 --- a/internal/game/render_help.go +++ b/internal/game/render_help.go @@ -67,7 +67,6 @@ var commandList = []cmdEntry{ {"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"}, {"bet / rebet", "Active", "Repeat a wager, or bet [target]; min/max accepted"}, {"autobet / autospin", "Active", "Automatically repeat a casino wager"}, {"spin", "Active", "Spin the current casino wager"}, diff --git a/internal/game/roulette_test.go b/internal/game/roulette_test.go index 9eaea8e..481044d 100644 --- a/internal/game/roulette_test.go +++ b/internal/game/roulette_test.go @@ -32,8 +32,10 @@ func TestRouletteNumericTargetsAndRendering(t *testing.T) { } conn.data = nil 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)) + // The table command shows the wheel board alone: no payout reminder. + text := string(conn.data) + if !strings.Contains(text, "History") || strings.Contains(text, "Blackjack") || strings.Contains(text, "35:1") { + t.Fatal("wrong felt", text) } 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")) { diff --git a/internal/net/server.go b/internal/net/server.go index c715074..d983161 100644 --- a/internal/net/server.go +++ b/internal/net/server.go @@ -502,6 +502,10 @@ func (h *Hub) hardRemoveLocked(s *Session) func(*Session) { func (h *Hub) EnterRoom(s *Session, roomID int) { h.mu.Lock() defer h.mu.Unlock() + // A detailed table watch is scoped to the room it was set in: entering any + // room stops it, so leaving and returning does not silently resume it. + s.CasinoWatchRoom = 0 + s.CasinoWatchTable = "" h.leaveRoomLocked(s) if h.rooms[roomID] == nil { h.rooms[roomID] = make(map[*Session]bool) diff --git a/internal/net/server_test.go b/internal/net/server_test.go index da8084a..6a3b376 100644 --- a/internal/net/server_test.go +++ b/internal/net/server_test.go @@ -99,6 +99,21 @@ func (c *fakeNetConn) readDeadline() time.Time { return c.deadline } +// TestEnterRoomClearsCasinoWatch: detailed table watch is scoped to the room +// it was set in, so entering a room stops a stale watch instead of silently +// resuming it when the player returns. +func TestEnterRoomClearsCasinoWatch(t *testing.T) { + h := NewHub() + s := &Session{} + h.Add(s) + s.CasinoWatchRoom = 7 + s.CasinoWatchTable = "bj1" + h.EnterRoom(s, 9) + if s.CasinoWatchRoom != 0 || s.CasinoWatchTable != "" { + t.Fatalf("watch survived a room change: room=%d table=%q", s.CasinoWatchRoom, s.CasinoWatchTable) + } +} + // TestConnectionCapRefusalKeepsTheServerDrained: at maxConnections a new // connection is refused without holding a slot (the counter stays at the cap), // and the refused peer is told why. Once the admitted sessions end, the live -- cgit v1.2.3