From 6232d41e5639f9c38e47b2bb65266e343ca26e8f Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sat, 3 Oct 2026 02:16:00 -0400 Subject: feat: add casino_hints option and standardize casino command syntax --- internal/casino/baccarat.go | 6 +++--- internal/casino/blackjack.go | 2 +- internal/casino/blackjack_wagers.go | 2 +- internal/casino/craps.go | 2 +- internal/casino/craps_rules.go | 10 +++++----- internal/casino/craps_ux.go | 8 ++++---- internal/casino/craps_variants.go | 6 +++--- internal/casino/craps_wagers.go | 8 ++++---- internal/casino/house_edge_craps.go | 6 +++--- internal/casino/house_edge_test.go | 16 ++++++++-------- internal/casino/machine.go | 13 ++++++++++--- internal/casino/manager.go | 18 ++++++++++++++++-- internal/casino/pai_gow.go | 6 +++--- internal/casino/pai_gow_rules.go | 2 +- internal/casino/roulette.go | 2 +- internal/casino/roulette_wagers.go | 14 +++++++------- internal/casino/table.go | 7 ++++--- internal/casino/table_hints.go | 4 ++-- internal/casino/table_ux.go | 4 ++-- internal/casino/table_ux_test.go | 2 +- internal/casino/types.go | 3 +++ internal/casino/video_poker.go | 14 +++++++------- 22 files changed, 90 insertions(+), 65 deletions(-) (limited to 'internal/casino') diff --git a/internal/casino/baccarat.go b/internal/casino/baccarat.go index 3140fa8..f1ba951 100644 --- a/internal/casino/baccarat.go +++ b/internal/casino/baccarat.go @@ -120,7 +120,7 @@ func (t *baccaratTable) menuEvent(name string) Event { } commands = append(commands, MenuCommand{Command: fmt.Sprintf("bet %d banker", t.Config.MinBet)}, - MenuCommand{Command: "bet on "}, + MenuCommand{Command: "bet N on BET"}, MenuCommand{Command: "stop", Desc: "Leave; during a round, leave afterward"}, MenuCommand{Command: "table", Desc: "Check the table"}) commands = withTableHints(commands, t.wagerRows()) @@ -182,7 +182,7 @@ func (t *baccaratTable) bet(name, spot string, amount int, wallet Wallet) []Even spot = p.mainSpot } if spot == "" { - return []Event{t.private(name, "Bet on what? Try bet on "+t.Rules.betChoices()+".")} + return []Event{t.private(name, "Bet on what? Try bet N on "+t.Rules.betChoices()+".")} } if !t.Rules.allowsSpot(spot) { return []Event{t.private(name, "Baccarat bets must be on player, banker, or tie, or an enabled optional bet. Available: "+t.Rules.betChoices()+".")} @@ -262,7 +262,7 @@ func (t *baccaratTable) rebetAll(name string, wallet Wallet) []Event { return []Event{t.private(name, "You have already bet a main wager this round.")} } if !p.hasLastBet() { - return []Event{t.private(name, "Bet on what? Try bet on "+t.Rules.betChoices()+".")} + return []Event{t.private(name, "Bet on what? Try bet N on "+t.Rules.betChoices()+".")} } var events []Event for _, spot := range t.Rules.spots() { diff --git a/internal/casino/blackjack.go b/internal/casino/blackjack.go index eee5023..5656129 100644 --- a/internal/casino/blackjack.go +++ b/internal/casino/blackjack.go @@ -118,7 +118,7 @@ func (t *blackjackTable) menuEvent(name string) Event { } e := t.betMenuEvent(name, []MenuCommand{ {Command: "bet", Desc: fmt.Sprintf("%s: %d credits (also: rebet)", desc, amount)}, - {Command: "bet ", Desc: "Change wager; min/max accepted"}, + {Command: "bet N", Desc: "Change wager; min/max accepted"}, {Command: "stop", Desc: "Leave; during a round, leave afterward"}, {Command: "table", Desc: "Check the table"}, }) diff --git a/internal/casino/blackjack_wagers.go b/internal/casino/blackjack_wagers.go index 23afac8..211962f 100644 --- a/internal/casino/blackjack_wagers.go +++ b/internal/casino/blackjack_wagers.go @@ -2,7 +2,7 @@ package casino import "fmt" -// Blackjack's only standing wager is bet , so outside the hand's +// Blackjack's only standing wager is bet N, so outside the hand's // decision points its wager list is empty (which also suppresses the "list" // hint). During the insurance window and the player's own turn, the extra // wagers and wagers-that-return-money get listed: insurance, decline, diff --git a/internal/casino/craps.go b/internal/casino/craps.go index 6fa6c79..73c67c4 100644 --- a/internal/casino/craps.go +++ b/internal/casino/craps.go @@ -209,7 +209,7 @@ func (t *crapsTable) bet(name, spot string, amount int, w Wallet) []Event { spot = p.lastSpot } if spot == "" && t.Config.Craps.variant() == "simplified" { - // Pass is the only target, so "bet " needs no target at all. + // Pass is the only target, so "bet N" needs no target at all. spot = "pass" } spot = strings.ToLower(strings.TrimSpace(spot)) diff --git a/internal/casino/craps_rules.go b/internal/casino/craps_rules.go index 60a7c0f..1c5e998 100644 --- a/internal/casino/craps_rules.go +++ b/internal/casino/craps_rules.go @@ -119,11 +119,11 @@ func (c TableConfig) CrapsRulesText() string { odds = fmt.Sprintf("%dx", r.Odds) } return prefix + fmt.Sprintf("Bank Craps | Free odds %s | Field 12 pays %d:1\n"+ - "bet on pass / dontpass / come / dontcome / field\n"+ - "bet on place 6 / buy 4 / lay 10 / hard 8 / hop 2 3\n"+ - "bet on seven / craps / eleven / ace-deuce / aces / boxcars / horn\n"+ - "bet on odds pass / odds come 6 (also dontpass/dontcome)\n"+ - "bet on fire / small / tall / all (before shooter's first roll only)\n"+ + "bet N on pass / dontpass / come / dontcome / field\n"+ + "bet N on place 6 / buy 4 / lay 10 / hard 8 / hop 2 3\n"+ + "bet N on seven / craps / eleven / ace-deuce / aces / boxcars / horn\n"+ + "bet N on odds pass / odds come 6 (also dontpass/dontcome)\n"+ + "bet N on fire / small / tall / all (before shooter's first roll only)\n"+ "Pass/come 1:1; don't bars 12. Place 4/10 9:5, 5/9 7:5, 6/8 7:6.\n"+ "Buy/lay true odds; 5%% commission on buy STAKE / lay WIN, rounded down, wins only.\n"+ "Field 2 pays 2:1; 3/4/9/10/11 pay 1:1. Hard 4/10 7:1; 6/8 9:1.\n"+ diff --git a/internal/casino/craps_ux.go b/internal/casino/craps_ux.go index 0a7932d..e64feb3 100644 --- a/internal/casino/craps_ux.go +++ b/internal/casino/craps_ux.go @@ -17,16 +17,16 @@ func (t *crapsTable) commands(name string) []MenuCommand { if p.lastBet > 0 { commands = append(commands, MenuCommand{Command: "bet", Desc: fmt.Sprintf("Rebet %d on pass (also: rebet)", p.lastBet)}, - MenuCommand{Command: "bet ", Desc: "New amount on pass; the dice roll 3 ticks after a bet"}) + MenuCommand{Command: "bet N", Desc: "New amount on pass; the dice roll 3 ticks after a bet"}) } else { - commands = append(commands, MenuCommand{Command: "bet ", Desc: "Wager on pass; the dice roll 3 ticks after a bet"}) + commands = append(commands, MenuCommand{Command: "bet N", Desc: "Wager on pass; the dice roll 3 ticks after a bet"}) } } else { - target := "" + target := "BET" if !t.Config.Craps.allows("buy") { target = "pass" } - commands = append(commands, MenuCommand{Command: "bet on " + target, Desc: "Place a wager"}) + commands = append(commands, MenuCommand{Command: "bet N on " + target, Desc: "Place a wager"}) } } if p.HasBet { diff --git a/internal/casino/craps_variants.go b/internal/casino/craps_variants.go index bcb06d0..634af36 100644 --- a/internal/casino/craps_variants.go +++ b/internal/casino/craps_variants.go @@ -213,17 +213,17 @@ func (c CrapsConfig) variantRulesText() string { rules += "\nTrue odds on 4/10: 2:1; 5/9: 3:2; 6/8: 6:5. Lay and don't odds invert these ratios. Buy/lay commission: 5% buy STAKE / lay PROFIT, rounded down, on wins only; removals have no commission." } if c.variant() == "crapless" { - rules += fmt.Sprintf("\nPass/come pay 1:1 on their point before 7; 7 loses. Line bets only on come-out; come requires a table point.\nTargets: pass / come / odds pass / odds come / place / buy / lay .\nPlace 4/10 pays 9:5; 5/9 7:5; 6/8 7:6.\nfield: 2 pays 2:1, 12 pays %d:1, 3/4/9/10/11 pay 1:1; others lose.\nhard 4/10 pays 7:1; hard 6/8 pays 9:1; easy number or 7 loses.\nseven 4:1; craps 7:1; eleven / ace-deuce 15:1; aces / boxcars 30:1.\nhorn splits equally among 2/3/11/12; hop pays 15:1 easy / 30:1 double, unordered.\nPlace/buy/hardways and come odds are off on come-out; established come contracts and lay always work.\nNo Fire or ATS. Making a point retains the shooter; seven-out rotates.", c.Field12) + rules += fmt.Sprintf("\nPass/come pay 1:1 on their point before 7; 7 loses. Line bets only on come-out; come requires a table point.\nTargets: pass / come / odds pass / odds come NUMBER / place NUMBER / buy NUMBER / lay NUMBER.\nPlace 4/10 pays 9:5; 5/9 7:5; 6/8 7:6.\nfield: 2 pays 2:1, 12 pays %d:1, 3/4/9/10/11 pay 1:1; others lose.\nhard 4/10 pays 7:1; hard 6/8 pays 9:1; easy number or 7 loses.\nseven 4:1; craps 7:1; eleven / ace-deuce 15:1; aces / boxcars 30:1.\nhorn splits equally among 2/3/11/12; hop A B pays 15:1 easy / 30:1 double, unordered.\nPlace/buy/hardways and come odds are off on come-out; established come contracts and lay always work.\nNo Fire or ATS. Making a point retains the shooter; seven-out rotates.", c.Field12) } if c.variant() == "new_york" { rules += "\nPass come-out 7/11 wins, 2/3/12 loses; 4/5/6/8/9/10 establish a point. Make it before 7 to win, all 1:1. Don't pass is opposite, except come-out 12 pushes. Line bets only on come-out. Making a point retains the shooter; seven-out rotates." } cadence := "Solo roll anytime; multiplayer waits 10 ticks, then dealer rolls after 60 ticks." autoroll := "Bets stay open until the roll. autoroll repeats every 3 ticks, respecting the betting window." - betLine := "Bet: bet on pass; the felt is authoritative for this edition." + betLine := "Bet: bet N on pass; the felt is authoritative for this edition." 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." + betLine = "Bet: bet N — always on pass; bare bet rebets your last wager." cadence = "The dice roll automatically 3 ticks after the first bet." autoroll = "" stopLine = "stop waits for live bets." diff --git a/internal/casino/craps_wagers.go b/internal/casino/craps_wagers.go index a36f43d..beb8722 100644 --- a/internal/casino/craps_wagers.go +++ b/internal/casino/craps_wagers.go @@ -80,8 +80,8 @@ func (t *crapsTable) wagerRows(name string) []WagerRow { // win condition and the 5% vig applies only to the winning wager. func crapsBuyLayRows() []WagerRow { return []WagerRow{ - {"buy ", "true", "This number rolled before 7; 5% commission on wins"}, - {"lay ", "true", "7 rolled before this number; 5% commission on wins"}, + {"buy N", "true", "This number rolled before 7; 5% commission on wins"}, + {"lay N", "true", "7 rolled before this number; 5% commission on wins"}, } } @@ -95,7 +95,7 @@ func crapsPropRows(c CrapsConfig, bonuses bool) []WagerRow { {"eleven / ace-deuce", "15:1", "Next roll totals 11 / 3"}, {"aces / boxcars", "30:1", "Next roll is 2 / 12"}, {"horn", "27:4", "Splits the stake over 2/3/11/12"}, - {"hop ", "15:1/30:1", "Next roll is a/b"}, + {"hop A B", "15:1/30:1", "Next roll is a/b"}, } if bonuses { rows = append(rows, @@ -149,6 +149,6 @@ func crapsPlaceRows(c CrapsConfig) []WagerRow { func crapsOddsRows() []WagerRow { return []WagerRow{ {"odds pass / come", "true", "Free odds behind the point"}, - {"odds dont ", "true", "Laid against the point at inverse odds"}, + {"odds dont N", "true", "Laid against the point at inverse odds"}, } } diff --git a/internal/casino/house_edge_craps.go b/internal/casino/house_edge_craps.go index 6b86571..d634aa6 100644 --- a/internal/casino/house_edge_craps.go +++ b/internal/casino/house_edge_craps.go @@ -10,9 +10,9 @@ func crapsHouseEdge(cfg TableConfig, bet string) HouseEdge { switch bet { case "pass", "come", "dontpass", "dontcome": return edge(-crapsContractEV(c, bet, 0)) - case "odds pass", "odds pass / come", "odds come ", "odds dontpass", "odds dont ", "odds dontcome ": + case "odds pass", "odds pass / come", "odds come N", "odds dontpass", "odds dont N", "odds dontcome N": return edge(0) - case "buy ", "lay ": + case "buy N", "lay N": e := HouseEdge{Note: "5% commission on wins only; bet multiples keep the vig whole"} for _, n := range []int{2, 3, 4, 5, 6, 8, 9, 10, 11, 12} { if !c.number(n) { @@ -33,7 +33,7 @@ func crapsHouseEdge(cfg TableConfig, bet string) HouseEdge { return edge(-crapsResolvedEV(c, "eleven", 0)) case "aces / boxcars": return edge(-crapsResolvedEV(c, "aces", 0)) - case "hop ": + case "hop A B": return HouseEdge{Parts: []EdgePart{{"", -crapsResolvedEV(c, "hop", 12)}, {"(doubles)", -crapsResolvedEV(c, "hop", 11)}}} case "small / tall": return edge(1 - 35*crapsCollectProbability([]int{2, 3, 4, 5, 6})) diff --git a/internal/casino/house_edge_test.go b/internal/casino/house_edge_test.go index 0704edc..bb9c924 100644 --- a/internal/casino/house_edge_test.go +++ b/internal/casino/house_edge_test.go @@ -17,9 +17,9 @@ func TestKnownHouseEdges(t *testing.T) { bet string want float64 // expected loss per unit stake (unrounded fraction) }{ - {"roulette straight", TableConfig{Game: GameRoulette}, "", 1.0 / 37}, + {"roulette straight", TableConfig{Game: GameRoulette}, "NUMBER", 1.0 / 37}, {"roulette even money", TableConfig{Game: GameRoulette}, "red / black", 1.0 / 37}, - {"roulette DZ straight", TableConfig{Game: GameRoulette, Roulette: RouletteConfig{DoubleZero: true}}, "", 2.0 / 38}, + {"roulette DZ straight", TableConfig{Game: GameRoulette, Roulette: RouletteConfig{DoubleZero: true}}, "NUMBER", 2.0 / 38}, {"roulette DZ basket", TableConfig{Game: GameRoulette, Roulette: RouletteConfig{DoubleZero: true}}, "basket", 3.0 / 38}, {"craps pass", TableConfig{Game: GameCraps}, "pass", 0.014141414141414}, {"craps dontpass", TableConfig{Game: GameCraps}, "dontpass", 0.013636363636364}, @@ -108,14 +108,14 @@ func TestUnrecognizedTargetNeverInherits(t *testing.T) { } } -// TestCrapsGroupedTargetsHaveRTP: the list names grouped rows (4/10, buy , +// TestCrapsGroupedTargetsHaveRTP: the list names grouped rows (4/10, buy N, // odds pass / come). Each must resolve to a real edge, not the unknown dash. func TestCrapsGroupedTargetsHaveRTP(t *testing.T) { for _, bet := range []string{ "place 4/10", "place 5/9", "place 6/8", "hard 4/10", "hard 6/8", - "odds pass / come", "odds dont ", - "buy ", "lay ", + "odds pass / come", "odds dont N", + "buy N", "lay N", } { got := (TableConfig{Game: GameCraps}).HouseEdge(bet) if len(got.Parts) == 0 { @@ -132,20 +132,20 @@ func TestCrapsGroupedTargetsHaveRTP(t *testing.T) { // The rows expand to every legal number, including the crapless extras. for _, want := range []string{"2", "3", "11", "12"} { found := false - for _, part := range crapless.HouseEdge("lay ").Parts { + for _, part := range crapless.HouseEdge("lay N").Parts { if part.Label == want { found = true } } if !found { - t.Fatalf("crapless lay missing number %s", want) + t.Fatalf("crapless lay N missing number %s", want) } } } // TestHopBetsTwoParts pins the split hop analysis (easy vs doubles). func TestHopBetsTwoParts(t *testing.T) { - got := (TableConfig{Game: GameCraps}).HouseEdge("hop ") + got := (TableConfig{Game: GameCraps}).HouseEdge("hop A B") if len(got.Parts) != 2 { t.Fatalf("hop analysis: %s", got.Text(2)) } diff --git a/internal/casino/machine.go b/internal/casino/machine.go index 610e745..0733384 100644 --- a/internal/casino/machine.go +++ b/internal/casino/machine.go @@ -103,7 +103,7 @@ func (m *Machine) start(name string, wallet Wallet) []Event { return m.private("You're in the middle of a spin!") } if m.bet <= 0 { - return m.private("Set a wager first with 'bet '.") + return m.private("Set a wager first with 'bet N'.") } return m.beginSpinLocked(m.bet, wallet, fmt.Sprintf("%s pulls the lever on the slots machine.", name), @@ -139,7 +139,7 @@ func (m *Machine) enableAutoBet(name string, wallet Wallet) []Event { return m.private("You're in the middle of a spin!") } if m.bet <= 0 { - return m.private("Set a wager first with 'bet '.") + return m.private("Set a wager first with 'bet N'.") } m.player.Wallet = wallet m.autoBet = true @@ -522,12 +522,19 @@ func formatWins(wins []SlotWin, ways int) string { return message[:len(message)-1] } +// Hints is the machine's live command list for the "what" command. +func (m *Machine) Hints() []MenuCommand { + m.mu.Lock() + defer m.mu.Unlock() + return m.commands() +} + func (m *Machine) commands() []MenuCommand { if m.autoBet { return []MenuCommand{{Command: "stop", Desc: "Stop automatic betting"}} } commands := []MenuCommand{ - {Command: "bet ", Desc: "Set your wager and spin"}, + {Command: "bet N", Desc: "Set your wager and spin"}, {Command: "bet max", Desc: "Bet the maximum and spin"}, } if m.bet > 0 { diff --git a/internal/casino/manager.go b/internal/casino/manager.go index 8a33df6..bdd9249 100644 --- a/internal/casino/manager.go +++ b/internal/casino/manager.go @@ -205,7 +205,7 @@ func (m *Manager) JoinMachine(roomID int, cfg MachineConfig, name string) []Even if machine == nil { return []Event{{RoomID: roomID, TableID: cfg.ID, Type: EventResult, PrivateTo: name, Message: "That game is not available here."}} } - return machine.join(name) + return markHints(machine.join(name)) } func (m *Manager) SetMachineBet(roomID int, machineID, name string, amount int, wallet Wallet) []Event { @@ -247,7 +247,21 @@ func (m *Manager) Join(roomID int, cfg TableConfig, name string) []Event { if table == nil { return []Event{{RoomID: roomID, TableID: cfg.ID, Type: EventResult, PrivateTo: name, Message: "That game is not available here."}} } - return table.join(name) + return markHints(table.join(name)) +} + +// markHints tags the first menu in a join/status result so the presentation +// layer always shows its command hints under the "casino_hints once" default. +// Recurring menus (after a bet or action) leave the flag clear, and other +// events (seat notices, rule felts) are untouched. +func markHints(events []Event) []Event { + for i := range events { + if events[i].Menu != nil { + events[i].Menu.ShowHints = true + break + } + } + return events } func (m *Manager) Leave(roomID int, tableID, name string) []Event { diff --git a/internal/casino/pai_gow.go b/internal/casino/pai_gow.go index 0de79e3..a2dafcf 100644 --- a/internal/casino/pai_gow.go +++ b/internal/casino/pai_gow.go @@ -148,12 +148,12 @@ func (t *paiGowTable) hintsLocked(name string) []MenuCommand { // Side bets are placed before the main wager: the main bet is what // deals, so there is no later chance to add them. if p.fortune.Total == 0 { - commands = append(commands, MenuCommand{Command: "bet fortune", Desc: "Optional Fortune side bet; place before your main bet"}) + commands = append(commands, MenuCommand{Command: "bet N fortune", Desc: "Optional Fortune side bet; place before your main bet"}) } if faceUp && p.pushace.Total == 0 { - commands = append(commands, MenuCommand{Command: "bet pushace", Desc: "Optional Push Ace High side bet; place before your main bet"}) + commands = append(commands, MenuCommand{Command: "bet N pushace", Desc: "Optional Push Ace High side bet; place before your main bet"}) } - commands = append(commands, MenuCommand{Command: "bet ", Desc: "Main wager; deals once every bettor is in"}) + commands = append(commands, MenuCommand{Command: "bet N", Desc: "Main wager; deals once every bettor is in"}) } if p.main.Total > 0 || p.fortune.Total > 0 || p.pushace.Total > 0 { commands = append(commands, MenuCommand{Command: "unbet", Desc: "Take back all your wagers before the deal"}) diff --git a/internal/casino/pai_gow_rules.go b/internal/casino/pai_gow_rules.go index 0539c9f..494cde7 100644 --- a/internal/casino/pai_gow_rules.go +++ b/internal/casino/pai_gow_rules.go @@ -110,7 +110,7 @@ func paiGowRules(c TableConfig) string { case PaiGowNoPush: s += " No commission; split results use two extra cards: player must beat dealer, ties lose; Joker is ace." } - s += "\nHouse way: preserve the best high-hand category, maximize low within it; split two pair, full houses and trips-pairs when this improves low to a pair. Never foul. house arranges the house way and still needs set; autoset on locks any legal arrangement immediately (autoset off restores the confirmation). A multiplayer timeout or disconnect uses the house way; a lone player is never timed out.\nBet main first, optionally bet fortune before dealing; unbet takes every undealt wager back. Fortune uses all seven cards regardless of split; highest listed prize only, profit odds plus stake returned." + s += "\nHouse way: preserve the best high-hand category, maximize low within it; split two pair, full houses and trips-pairs when this improves low to a pair. Never foul. house arranges the house way and still needs set; autoset on locks any legal arrangement immediately (autoset off restores the confirmation). A multiplayer timeout or disconnect uses the house way; a lone player is never timed out.\nBet main first, optionally bet N fortune before dealing; unbet takes every undealt wager back. Fortune uses all seven cards regardless of split; highest listed prize only, profit odds plus stake returned." if r.Envy { s += "\nEnvy: Fortune stake of at least 5 qualifies for fixed bonuses on OTHER active players' hands, never your own or dealer; multiple bonuses add." } else { diff --git a/internal/casino/roulette.go b/internal/casino/roulette.go index 8f45d49..ad0a706 100644 --- a/internal/casino/roulette.go +++ b/internal/casino/roulette.go @@ -518,7 +518,7 @@ func (t *rouletteTable) menuEvent(name string, board bool) Event { commands := []MenuCommand{} p := t.players.get(name) if t.phase != phasePlaying && p != nil && !p.HasBet { - commands = append(commands, MenuCommand{Command: "bet on ", Desc: "Place one wager"}) + commands = append(commands, MenuCommand{Command: "bet N on BET", Desc: "Place one wager"}) if p.lastBet > 0 { commands = append(commands, MenuCommand{Command: "bet / rebet", Desc: "Repeat your previous wager"}) } diff --git a/internal/casino/roulette_wagers.go b/internal/casino/roulette_wagers.go index 7562582..5879f03 100644 --- a/internal/casino/roulette_wagers.go +++ b/internal/casino/roulette_wagers.go @@ -13,17 +13,17 @@ func (t *rouletteTable) wagerRows() []WagerRow { } chance := func(n int) string { return fmt.Sprintf("%d/%d", n, pockets) } rows := []WagerRow{ - {"", "35:1", "One pocket (" + chance(1) + ")"}, + {"NUMBER", "35:1", "One pocket (" + chance(1) + ")"}, {"red / black", "1:1", "The color (" + chance(18) + ")"}, {"odd / even", "1:1", "The parity (" + chance(18) + ")"}, {"low / high", "1:1", "Low 1-18 / high 19-36 (" + chance(18) + ")"}, {"1to12 / 13to24 / 25to36", "2:1", "A dozen (" + chance(12) + ")"}, {"row1 / row2 / row3", "2:1", "A numbered row (" + chance(12) + ")"}, - {"split ", "17:1", "Two touching pockets (" + chance(2) + ")"}, - {"street ", "11:1", "Three numbers in a street (" + chance(3) + ")"}, - {"trio 0 ", "11:1", "A zero-edge trio (" + chance(3) + ")"}, - {"corner ", "8:1", "A 2x2 square (" + chance(4) + ")"}, - {"line … ", "5:1", "Two adjoining streets (" + chance(6) + ")"}, + {"split A B", "17:1", "Two touching pockets (" + chance(2) + ")"}, + {"street A B C", "11:1", "Three numbers in a street (" + chance(3) + ")"}, + {"trio 0 A B", "11:1", "A zero-edge trio (" + chance(3) + ")"}, + {"corner A B C D", "8:1", "A 2x2 square (" + chance(4) + ")"}, + {"line A … F", "5:1", "Two adjoining streets (" + chance(6) + ")"}, {"basket", basketOdds, "0, 1, 2, 3 (+ 00) (" + chance(basketPockets) + ")"}, } if t.Config.Roulette.Racetrack && !t.Config.Roulette.DoubleZero { @@ -32,7 +32,7 @@ func (t *rouletteTable) wagerRows() []WagerRow { WagerRow{"tiers", "2:1", "The 12 pockets opposite zero, 6 chips"}, WagerRow{"orphelins", "31:5 or 13:5", "The two orphan arcs, 5 chips"}, WagerRow{"zero / jeu zero", "8:1 or 7:2", "The 7 pockets nearest zero, 4 chips"}, - WagerRow{"neighbors ", "3:1", "Pocket n plus four numbers each side, 9 chips"}, + WagerRow{"neighbors N", "3:1", "Pocket n plus four numbers each side, 9 chips"}, ) } return rows diff --git a/internal/casino/table.go b/internal/casino/table.go index d93d91e..d9d3525 100644 --- a/internal/casino/table.go +++ b/internal/casino/table.go @@ -127,8 +127,9 @@ func (t *Table) refundAll() []Event { return t.game.refundAll() } // idle on the Table wrapper delegates to the game. 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) } +// Status is a read-only view: it never restarts a deadline or changes a +// wager. It is an explicit look/watch, so its menu always carries hints. +func (t *Table) Status(name string) []Event { return markHints(t.game.status(name)) } // 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 @@ -165,7 +166,7 @@ func (t *Table) RulesText() string { return rulesTextOnTable(t.Config) } func (t *Table) HelpTopic() string { return t.Config.HelpTopic() } // Wagers is the game's live list of bets the player could make right now. -// Empty when nothing is worth naming (a lone "bet " wager, a +// Empty when nothing is worth naming (a lone "bet N" wager, a // committed round) — the same signal that suppresses the "list" hint. func (t *Table) Wagers(name string) []WagerRow { return t.game.wagers(name) } diff --git a/internal/casino/table_hints.go b/internal/casino/table_hints.go index 98eaced..eafb05e 100644 --- a/internal/casino/table_hints.go +++ b/internal/casino/table_hints.go @@ -19,7 +19,7 @@ func (t *rouletteTable) hints(name string) []MenuCommand { } commands := []MenuCommand{} if t.phase != phasePlaying && !p.HasBet { - commands = append(commands, MenuCommand{Command: "bet on ", Desc: "Place one wager"}) + commands = append(commands, MenuCommand{Command: "bet N on BET", Desc: "Place one wager"}) if p.lastBet > 0 { commands = append(commands, MenuCommand{Command: "bet / rebet", Desc: "Repeat your previous wager"}) } @@ -82,7 +82,7 @@ func baccaratHintCommands(t *baccaratTable, name string) []MenuCommand { } return append(commands, MenuCommand{Command: fmt.Sprintf("bet %d banker", t.Config.MinBet)}, - MenuCommand{Command: "bet on "}, + MenuCommand{Command: "bet N on BET"}, MenuCommand{Command: "stop", Desc: "Leave; during a round, leave afterward"}, MenuCommand{Command: "table", Desc: "Check the table"}, ) diff --git a/internal/casino/table_ux.go b/internal/casino/table_ux.go index b570b36..5d70aaf 100644 --- a/internal/casino/table_ux.go +++ b/internal/casino/table_ux.go @@ -6,7 +6,7 @@ import ( ) // WagerRow is one line of a table's "list" of available bets, in the game's -// current state: the bet wording (what follows "bet on"), its +// current state: the bet wording (what follows "bet N on"), its // payout, and a brief description of the win condition. type WagerRow struct { Bet string @@ -21,7 +21,7 @@ const ( // withTableHints finishes any game's hint list: "rules" is always present // while seated at a table, and "list" follows when the game named more than -// one wager a player might want to make (a lone bet is just "bet "). +// one wager a player might want to make (a lone bet is just "bet N"). // Games whose own menus already append these hints are not double-hinted. func withTableHints(commands []MenuCommand, wagerRows []WagerRow) []MenuCommand { for _, c := range commands { diff --git a/internal/casino/table_ux_test.go b/internal/casino/table_ux_test.go index 4d58a5f..f81d537 100644 --- a/internal/casino/table_ux_test.go +++ b/internal/casino/table_ux_test.go @@ -218,7 +218,7 @@ func TestRebetMenusAndMidRoundJoin(t *testing.T) { bac := newBaccaratTestTable(TableConfig{ID: "bac", Game: GameBaccarat}) bac.join("alice") first := bac.menuEvent("alice") - if first.Menu.Commands[0].Command == "bet " { + if first.Menu.Commands[0].Command == "bet N" { t.Fatal("first baccarat bet omitted target") } bac.bet("alice", "banker", 25, &testWallet{credits: 100}) diff --git a/internal/casino/types.go b/internal/casino/types.go index 5f9de1d..570d0ac 100644 --- a/internal/casino/types.go +++ b/internal/casino/types.go @@ -256,6 +256,9 @@ type MenuView struct { ShowBalance bool // prepend the player's chips/credits balance Commands []MenuCommand Repeat bool // returning bettor: compact clients can show a short reminder + // ShowHints marks a menu that always carries its command list under the + // "casino_hints once" option: the join menu and an explicit look/status. + ShowHints bool } // MenuCommand is one selectable command shown in a casino menu: the typed diff --git a/internal/casino/video_poker.go b/internal/casino/video_poker.go index d2578b8..4cb9883 100644 --- a/internal/casino/video_poker.go +++ b/internal/casino/video_poker.go @@ -147,7 +147,7 @@ func (t *videoPokerTable) hintsLocked(name string) []MenuCommand { if t.players[name] == nil { return nil } - commands := []MenuCommand{{Command: "bet [1-4|max]", Desc: "Load coins (up to five per hand)"}, {Command: "deal", Desc: "Pay and deal the loaded wager"}} + commands := []MenuCommand{{Command: "bet [1-4|MAX]", Desc: "Load coins (up to five per hand)"}, {Command: "deal", Desc: "Pay and deal the loaded wager"}} if p := t.players[name]; p != nil && p.active { commands = []MenuCommand{{Command: "1 3 5", Desc: "Toggle held cards (531 also works)"}, {Command: "draw", Desc: "Replace unheld cards on every line"}} if p.drawn { @@ -155,9 +155,9 @@ func (t *videoPokerTable) hintsLocked(name string) []MenuCommand { } } else { commands = append(commands, - MenuCommand{Command: "game ", Desc: t.settings.GameChoices()}, - MenuCommand{Command: "denom <1|10|50>", Desc: "Credits per coin"}, - MenuCommand{Command: "hands ", Desc: "Supported hand counts are announced by hands"}, + MenuCommand{Command: "game NAME", Desc: t.settings.GameChoices()}, + MenuCommand{Command: "denom 1|10|50", Desc: "Credits per coin"}, + MenuCommand{Command: "hands COUNT", Desc: "Supported hand counts are announced by hands"}, ) if cabinetFeature(t.settings.Cabinet) != "" { commands = append(commands, MenuCommand{Command: "feature on|off", Desc: "Toggle the extra-cost bonus"}) @@ -298,7 +298,7 @@ func (t *videoPokerTable) bet(name, spot string, amount int, w Wallet) []Event { return []Event{t.private(name, "Finish this hand with draw first.")} } if p.loaded == 0 { - return []Event{t.private(name, "Load coins first: bet [1-4|max].")} + return []Event{t.private(name, "Load coins first: bet [1-4|MAX].")} } if p.featureOn && p.loaded != 5 { return []Event{t.private(name, "The feature requires five wager coins per hand. Use bet max or feature off.")} @@ -406,7 +406,7 @@ func (t *videoPokerTable) action(name, action string) []Event { default: add, err := strconv.Atoi(fields[1]) if err != nil || add < 1 || add > 4 { - return []Event{t.private(name, "Use bet [1-4|max].")} + return []Event{t.private(name, "Use bet [1-4|MAX].")} } n = min(5, p.loaded+add) } @@ -462,7 +462,7 @@ func (t *videoPokerTable) action(name, action string) []Event { p.clearResult() return t.statusLocked(name, false) } - return []Event{t.private(name, "Use bet [1-4|max], unbet, deal, game , denom 1|10|50, hands , paytable, or feature on|off on a bonus cabinet.")} + return []Event{t.private(name, "Use bet [1-4|MAX], unbet, deal, game NAME, denom 1|10|50, hands COUNT, paytable, or feature on|off on a bonus cabinet.")} } func (t *videoPokerTable) drawLocked(name string, p *pokerPlayer) []Event { -- cgit v1.2.3