aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/casino/baccarat.go2
-rw-r--r--internal/casino/blackjack.go2
-rw-r--r--internal/casino/craps.go7
-rw-r--r--internal/casino/craps_rules.go2
-rw-r--r--internal/casino/craps_variants.go4
-rw-r--r--internal/casino/roulette.go9
-rw-r--r--internal/casino/table.go13
-rw-r--r--internal/casino/table_hints.go7
-rw-r--r--internal/casino/table_ux.go1
-rw-r--r--internal/casino/video_poker.go13
-rw-r--r--internal/game/casino_house_edge.go10
-rw-r--r--internal/game/casino_ux.go28
-rw-r--r--internal/game/casino_ux_test.go15
-rw-r--r--internal/game/cmd_registry.go3
-rw-r--r--internal/game/pai_gow_test.go16
-rw-r--r--internal/game/render_baccarat_felt.go32
-rw-r--r--internal/game/render_baccarat_felt_test.go9
-rw-r--r--internal/game/render_casino_help.go88
-rw-r--r--internal/game/render_casino_help_test.go62
-rw-r--r--internal/game/render_help.go1
-rw-r--r--internal/game/roulette_test.go6
-rw-r--r--internal/net/server.go4
-rw-r--r--internal/net/server_test.go15
23 files changed, 136 insertions, 213 deletions
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 <amount> on <bet>"},
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 <amount>", 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 <amount> 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 <amount> — 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 <amount> on <bet>"},
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 <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")) {
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