diff options
| author | historia <[not public]> | 2026-10-02 02:16:44 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-10-02 02:16:44 -0400 |
| commit | 62017c86392b046d746fb4fafef6742840477c5f (patch) | |
| tree | eab569e29e858ff79b8ce06caa9bacb28bc9ee81 /internal/game | |
| parent | d3a305d1d7962f82af2397760cf8008758bddd1d (diff) | |
| download | thehouseoficarus-62017c86392b046d746fb4fafef6742840477c5f.tar.gz | |
feat: polish 'list' output, drop sitout command
Diffstat (limited to 'internal/game')
| -rw-r--r-- | internal/game/casino_house_edge.go | 10 | ||||
| -rw-r--r-- | internal/game/casino_ux.go | 28 | ||||
| -rw-r--r-- | internal/game/casino_ux_test.go | 15 | ||||
| -rw-r--r-- | internal/game/cmd_registry.go | 3 | ||||
| -rw-r--r-- | internal/game/pai_gow_test.go | 16 | ||||
| -rw-r--r-- | internal/game/render_baccarat_felt.go | 32 | ||||
| -rw-r--r-- | internal/game/render_baccarat_felt_test.go | 9 | ||||
| -rw-r--r-- | internal/game/render_casino_help.go | 88 | ||||
| -rw-r--r-- | internal/game/render_casino_help_test.go | 62 | ||||
| -rw-r--r-- | internal/game/render_help.go | 1 | ||||
| -rw-r--r-- | internal/game/roulette_test.go | 6 |
11 files changed, 69 insertions, 201 deletions
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 <amount>" - g.showCasinoListTable(sess, "", list) + g.showCasinoListTable(sess, "Bet", list) return } - g.showCasinoListTable(sess, "bet <amount> on <bet>", g.casinoListRows(sess, t, rows)) + g.showCasinoListTable(sess, "bet <amount> 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 <amount> fortune", "bet <amount>", "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 <amount>") { + 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 <n>", "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 <amount> on <target>, 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 <amount> [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")) { |
