aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/help/casino.yaml4
-rw-r--r--data/help/hot_roll.yaml2
-rw-r--r--data/help/pai_gow.yaml2
-rw-r--r--data/help/racetrack.yaml2
-rw-r--r--data/help/super_times_pay.yaml2
-rw-r--r--data/help/ultimate_x_multi_hand.yaml2
-rw-r--r--data/help/ultimate_x_single_line.yaml2
-rw-r--r--data/help/video_poker.yaml2
-rw-r--r--data/rooms/casino/6101.yaml2
-rw-r--r--data/rooms/casino/6102.yaml2
-rw-r--r--data/rooms/casino/6104.yaml2
-rw-r--r--data/rooms/casino/6106.yaml2
-rw-r--r--data/rooms/casino/6108.yaml2
-rw-r--r--internal/admin/api_colors.go2
-rw-r--r--internal/casino/baccarat.go6
-rw-r--r--internal/casino/blackjack.go2
-rw-r--r--internal/casino/blackjack_wagers.go2
-rw-r--r--internal/casino/craps.go2
-rw-r--r--internal/casino/craps_rules.go10
-rw-r--r--internal/casino/craps_ux.go8
-rw-r--r--internal/casino/craps_variants.go6
-rw-r--r--internal/casino/craps_wagers.go8
-rw-r--r--internal/casino/house_edge_craps.go6
-rw-r--r--internal/casino/house_edge_test.go16
-rw-r--r--internal/casino/machine.go13
-rw-r--r--internal/casino/manager.go18
-rw-r--r--internal/casino/pai_gow.go6
-rw-r--r--internal/casino/pai_gow_rules.go2
-rw-r--r--internal/casino/roulette.go2
-rw-r--r--internal/casino/roulette_wagers.go14
-rw-r--r--internal/casino/table.go7
-rw-r--r--internal/casino/table_hints.go4
-rw-r--r--internal/casino/table_ux.go4
-rw-r--r--internal/casino/table_ux_test.go2
-rw-r--r--internal/casino/types.go3
-rw-r--r--internal/casino/video_poker.go14
-rw-r--r--internal/config/config.go1
-rw-r--r--internal/game/casino.go48
-rw-r--r--internal/game/casino_commands.go35
-rw-r--r--internal/game/casino_commands_test.go8
-rw-r--r--internal/game/casino_hints_test.go62
-rw-r--r--internal/game/casino_house_edge.go28
-rw-r--r--internal/game/casino_look_test.go2
-rw-r--r--internal/game/casino_rules_list_test.go4
-rw-r--r--internal/game/casino_test.go40
-rw-r--r--internal/game/casino_ux.go8
-rw-r--r--internal/game/cmd_color.go1
-rw-r--r--internal/game/cmd_verbs.go25
-rw-r--r--internal/game/look_casino_test.go2
-rw-r--r--internal/game/pai_gow_test.go9
-rw-r--r--internal/game/render_craps_test.go2
-rw-r--r--internal/game/render_help.go2
-rw-r--r--internal/game/render_roulette_test.go2
-rw-r--r--internal/game/roulette_test.go3
-rw-r--r--internal/game/video_poker_test.go2
-rw-r--r--internal/player/player.go1
56 files changed, 334 insertions, 136 deletions
diff --git a/data/help/casino.yaml b/data/help/casino.yaml
index 8c0fa73..36058d3 100644
--- a/data/help/casino.yaml
+++ b/data/help/casino.yaml
@@ -6,8 +6,8 @@ description: |
Casino quick guide
play <game> Join a table; use play to list nearby games.
- bet <amount> Wager chips first, then credits.
- bet 10 on <target> Choose a roulette, baccarat or craps bet.
+ bet N Wager chips first, then credits.
+ bet N on BET Choose a roulette, baccarat or craps bet.
bet / rebet Repeat your last table wager.
table Show the table felt; roulette also shows the racetrack,
history and payout tables.
diff --git a/data/help/hot_roll.yaml b/data/help/hot_roll.yaml
index 2e9d6a1..2545c69 100644
--- a/data/help/hot_roll.yaml
+++ b/data/help/hot_roll.yaml
@@ -5,7 +5,7 @@ description: |
Hot Roll
Choose hands 3, 5 or 10. Games: jacks, ddb.
- game lists games; game <name> selects one. The house sets the cabinet's
+ game lists games; game NAME selects one. The house sets the cabinet's
pay schedule; paytable shows the exact schedule.
Feature ON adds five coins per hand to a five-coin base wager (10 total).
diff --git a/data/help/pai_gow.yaml b/data/help/pai_gow.yaml
index b252278..672ffab 100644
--- a/data/help/pai_gow.yaml
+++ b/data/help/pai_gow.yaml
@@ -51,7 +51,7 @@ description: |
you set yours. Dealer's best five cards exactly ace-high (a pair of
aces does not count) automatically pushes every main bet; the optional
Push Ace High side bet pays that push: without Joker 5:1, with Joker
- 15:1, or 40:1 when BOTH sides are ace-high. bet <amount> pushace,
+ 15:1, or 40:1 when BOTH sides are ace-high. bet N pushace,
placed before your main wager and capped at half of it.
Fortune uses ALL seven cards, independent of your split and main result.
diff --git a/data/help/racetrack.yaml b/data/help/racetrack.yaml
index 535285e..cf925bd 100644
--- a/data/help/racetrack.yaml
+++ b/data/help/racetrack.yaml
@@ -30,7 +30,7 @@ description: |
bet 10 on zero (or jeu zero / jeuzero) means Jeu Zero;
bet 10 on 0 means just pocket 0.
- neighbors <n> - the chosen pocket plus four on either side, 9 chips.
+ neighbors N - the chosen pocket plus four on either side, 9 chips.
One straight-number chip per pocket. Profit: 3:1. Chance: 9/37.
Fractions are rounded down after adding winning chip returns.
diff --git a/data/help/super_times_pay.yaml b/data/help/super_times_pay.yaml
index 10dd5c8..4abd7fe 100644
--- a/data/help/super_times_pay.yaml
+++ b/data/help/super_times_pay.yaml
@@ -5,7 +5,7 @@ description: |
Super Times Pay
Choose hands 1, 3, 5 or 10. Games: jacks, bonus, double, ddb, deuces.
- game lists games; game <name> selects one. The house sets the cabinet's
+ game lists games; game NAME selects one. The house sets the cabinet's
pay schedule; paytable shows the exact schedule.
Feature ON adds one coin per hand to a five-coin base wager (6 total).
diff --git a/data/help/ultimate_x_multi_hand.yaml b/data/help/ultimate_x_multi_hand.yaml
index 6656647..0203c5a 100644
--- a/data/help/ultimate_x_multi_hand.yaml
+++ b/data/help/ultimate_x_multi_hand.yaml
@@ -5,7 +5,7 @@ description: |
Ultimate X Multi-Hand
Choose hands 3, 5 or 10. Games: jacks, deluxe, double, ddb, deuces, joker.
- game lists games; game <name> selects one. The house sets the cabinet's
+ game lists games; game NAME selects one. The house sets the cabinet's
pay schedule; paytable shows the exact schedule.
Feature ON adds five coins per hand to a five-coin base wager. Each winning
diff --git a/data/help/ultimate_x_single_line.yaml b/data/help/ultimate_x_single_line.yaml
index 5a9c821..425dc34 100644
--- a/data/help/ultimate_x_single_line.yaml
+++ b/data/help/ultimate_x_single_line.yaml
@@ -5,7 +5,7 @@ description: |
Ultimate X Single Line
One hand. Games: jacks, bonus, deluxe, double, ddb, deuces, joker.
- game lists games; game <name> selects one. The house sets the cabinet's
+ game lists games; game NAME selects one. The house sets the cabinet's
pay schedule; paytable shows the exact schedule.
Feature ON adds five coins to a five-coin wager. Winning hands earn the NEXT
diff --git a/data/help/video_poker.yaml b/data/help/video_poker.yaml
index 067d95b..4ddbfd3 100644
--- a/data/help/video_poker.yaml
+++ b/data/help/video_poker.yaml
@@ -18,7 +18,7 @@ description: |
The house sets each cabinet's pay schedule (low, normal or full) when it is
placed; these are published schedules, not different games or guaranteed
RTPs. paytable shows the selected game, schedule and exact coin payouts.
- hands lists supported counts; hands <count> selects one between rounds.
+ hands lists supported counts; hands COUNT selects one between rounds.
Classic Jacks supports 1/3/5/10/25/50/100 hands; other classic games use 1.
Ultimate X Single uses 1, Ultimate X Multi and Hot Roll 3/5/10,
and Super Times Pay 1/3/5/10. Larger games use smaller card art.
diff --git a/data/rooms/casino/6101.yaml b/data/rooms/casino/6101.yaml
index fb2838a..05cb1fe 100644
--- a/data/rooms/casino/6101.yaml
+++ b/data/rooms/casino/6101.yaml
@@ -33,7 +33,7 @@ casino_tables:
split_unlike_ten_values: false
objects:
- name: rules plaque
- description: Play blackjack; bet min, bet max or bet <amount>. Blackjack pays 3:2; insurance 2:1. 6
+ description: Play blackjack; bet min, bet max or bet N. Blackjack pays 3:2; insurance 2:1. 6
deck(s). Dealer stands on soft 17. No surrender. Double after split allowed. Split 21 pays 1:1; split
identical ranks only.
inroom_description: A rules plaque stands where travelers can read it.
diff --git a/data/rooms/casino/6102.yaml b/data/rooms/casino/6102.yaml
index ae39910..fafb666 100644
--- a/data/rooms/casino/6102.yaml
+++ b/data/rooms/casino/6102.yaml
@@ -28,7 +28,7 @@ casino_tables:
split_unlike_ten_values: false
objects:
- name: rules plaque
- description: Play blackjack; bet min, bet max or bet <amount>. Blackjack pays 3:2; insurance 2:1. 6
+ description: Play blackjack; bet min, bet max or bet N. Blackjack pays 3:2; insurance 2:1. 6
deck(s). Dealer stands on soft 17. No surrender. Double after split allowed. Split 21 pays 1:1; split
identical ranks only.
inroom_description: A rules plaque stands where travelers can read it.
diff --git a/data/rooms/casino/6104.yaml b/data/rooms/casino/6104.yaml
index 2ec6993..b21fea2 100644
--- a/data/rooms/casino/6104.yaml
+++ b/data/rooms/casino/6104.yaml
@@ -26,7 +26,7 @@ casino_tables:
split_unlike_ten_values: false
objects:
- name: rules plaque
- description: Play blackjack; bet min, bet max or bet <amount>. Blackjack pays 6:5; insurance 2:1. 1
+ description: Play blackjack; bet min, bet max or bet N. Blackjack pays 6:5; insurance 2:1. 1
deck(s). Dealer hits soft 17. No surrender. No double after split. Split 21 pays 1:1; split identical
ranks only.
inroom_description: A rules plaque stands where travelers can read it.
diff --git a/data/rooms/casino/6106.yaml b/data/rooms/casino/6106.yaml
index 6cf1345..b17fa9e 100644
--- a/data/rooms/casino/6106.yaml
+++ b/data/rooms/casino/6106.yaml
@@ -28,7 +28,7 @@ casino_tables:
split_unlike_ten_values: false
objects:
- name: rules plaque
- description: Play blackjack; bet min, bet max or bet <amount>. Blackjack pays 3:2; insurance 2:1. 6
+ description: Play blackjack; bet min, bet max or bet N. Blackjack pays 3:2; insurance 2:1. 6
deck(s). Dealer stands on soft 17. Late surrender and re-splitting aces offered. Double after split allowed. Split 21 pays 1:1; split
identical ranks only.
inroom_description: A rules plaque stands where travelers can read it.
diff --git a/data/rooms/casino/6108.yaml b/data/rooms/casino/6108.yaml
index db7beaa..72bb00b 100644
--- a/data/rooms/casino/6108.yaml
+++ b/data/rooms/casino/6108.yaml
@@ -28,7 +28,7 @@ mobs:
- id: casino_patron
objects:
- name: rules plaque
- description: Play blackjack; bet min, bet max or bet <amount>. Blackjack pays 3:2; insurance 2:1. 8
+ description: Play blackjack; bet min, bet max or bet N. Blackjack pays 3:2; insurance 2:1. 8
deck(s). Dealer hits soft 17. No surrender. Double after split allowed. Split 21 pays 1:1; split identical
ranks only.
inroom_description: A rules plaque stands where travelers can read it.
diff --git a/internal/admin/api_colors.go b/internal/admin/api_colors.go
index b6285c4..ddd0270 100644
--- a/internal/admin/api_colors.go
+++ b/internal/admin/api_colors.go
@@ -294,7 +294,7 @@ func buildPreview() []colorSection {
ln(seg(" 2. ", "dialog_options"), seg("Just passing through.", "dialog")),
ln(seg("[enter to continue]", "dialog_options")),
txt(""),
- ln(seg(" ", ""), seg("bet <amount>", "command"), seg(" Set your wager", "")),
+ ln(seg(" ", ""), seg("bet ", "command"), seg("N", "command_variable"), seg(" on ", "command"), seg("BET", "command_variable"), seg(" Set your wager", "")),
ln(seg(" ", ""), seg("spin", "command"), seg(" Spin the reels", "")),
},
})
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 <n> on <bet>"},
+ 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 <n> 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 <n> 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 <amount>", 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 <amount>, 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 <amount>" 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 <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"+
+ "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 <amount>", 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 <amount>", 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 := "<bet>"
+ target := "BET"
if !t.Config.Craps.allows("buy") {
target = "pass"
}
- commands = append(commands, MenuCommand{Command: "bet <n> 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 <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)
+ 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 <n> 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 <amount> — 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 <n>", "true", "This number rolled before 7; 5% commission on wins"},
- {"lay <n>", "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 <a> <b>", "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 <n>", "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 <n>", "odds dontpass", "odds dont <n>", "odds dontcome <n>":
+ case "odds pass", "odds pass / come", "odds come N", "odds dontpass", "odds dont N", "odds dontcome N":
return edge(0)
- case "buy <n>", "lay <n>":
+ 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 <a> <b>":
+ 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}, "<number>", 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}}, "<number>", 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 <n>,
+// 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 <n>",
- "buy <n>", "lay <n>",
+ "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 <n> 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 <n>").Parts {
+ for _, part := range crapless.HouseEdge("lay N").Parts {
if part.Label == want {
found = true
}
}
if !found {
- t.Fatalf("crapless lay <n> 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 <a> <b>")
+ 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 <amount>'.")
+ 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 <amount>'.")
+ 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 <amount>", 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 <amount> 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 <amount> 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 <amount>", 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 <amount> 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 <n> on <bet>", 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{
- {"<number>", "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 <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) + ")"},
+ {"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 <n>", "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 <amount>" 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 <n> on <bet>", 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 <n> on <bet>"},
+ 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 <n> 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 <amount>").
+// 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 <amount>" {
+ 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 <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"},
+ 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 <name>, denom 1|10|50, hands <count>, 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 {
diff --git a/internal/config/config.go b/internal/config/config.go
index ff081c1..8bd505d 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -63,6 +63,7 @@ func DefaultColors() ColorsConfig {
"dialog": "6E",
"dialog_options": "bold",
"command": "0A bold",
+ "command_variable": "B4 bold",
"broadcast": "89",
"sequence": "off",
"fire": "B4",
diff --git a/internal/game/casino.go b/internal/game/casino.go
index 0b16114..8f18614 100644
--- a/internal/game/casino.go
+++ b/internal/game/casino.go
@@ -342,7 +342,7 @@ func (g *Game) executePlay(sess *net.Session, args []string, rawInput string) {
if len(args) > 1 {
amount, err := strconv.Atoi(args[1])
if !choice.table || len(args) > 3 || (args[1] != "min" && args[1] != "max" && (err != nil || amount <= 0)) {
- sess.WriteLine("Use play <game> [amount|min|max] [target]. No wager was placed.")
+ sess.WriteLine("Use play GAME [N|min|max] [on BET]. No wager was placed.")
return
}
}
@@ -406,7 +406,7 @@ func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) {
return
}
}
- sess.WriteLine("Use bet, bet <amount>, or bet max.")
+ sess.WriteLine("Use bet, bet N, or bet max.")
return
}
table, playing := g.Casino.Participation(p.Name)
@@ -424,7 +424,7 @@ func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) {
g.emitCasinoEvents(g.Casino.RebetAll(table.RoomID, table.Config.ID, p.Name, casinoWallet{g: g, p: p}))
return
}
- // Table bet syntax: bet | bet <amount> [on] <target> | bet min|max [on] <target>.
+ // Table bet syntax: bet | bet N [on BET] | bet min|max [on BET].
// BEFORE an "on" keyword, a token that parses as an amount (or min/max)
// sets the wager and any other token is the bet target; AFTER "on", the
// next token is always the target. That makes numeric targets reachable
@@ -438,26 +438,21 @@ func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) {
for i, arg := range args {
if strings.EqualFold(arg, "on") {
if sawOn || spot != "" || i == len(args)-1 {
- sess.WriteLine("Use bet <n> on <bet>. No wager was placed.")
+ sess.WriteLine("Use bet N on BET. No wager was placed.")
return
}
sawOn = true
continue
}
- if !sawOn && !(joinTarget && hasAmount && spot != "") {
+ // Once an amount is set, every remaining token belongs to the target
+ // ("bet 2 2" and "bet 2 on 2" are identical). Before that, a numeric
+ // token sets the amount and anything else starts the target.
+ if !sawOn && !hasAmount {
if strings.EqualFold(arg, "min") {
- if hasAmount {
- sess.WriteLine("Use bet, bet <amount> [on <target>], bet min, or bet max.")
- return
- }
amount, hasAmount = table.Config.MinBet, true
continue
}
if strings.EqualFold(arg, "max") {
- if hasAmount {
- sess.WriteLine("Use bet, bet <amount> [on <target>], bet min, or bet max.")
- return
- }
amount = min(table.Config.MaxBet, casinoAffordable(p))
if amount < 1 {
sess.WriteLine("You don't have any chips or credits to bet.")
@@ -467,10 +462,6 @@ func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) {
continue
}
if value, err := strconv.Atoi(arg); err == nil && value > 0 {
- if hasAmount {
- sess.WriteLine("Use bet, bet <amount> [on <target>], bet min, or bet max.")
- return
- }
amount, hasAmount = value, true
continue
}
@@ -483,7 +474,7 @@ func (g *Game) executeBet(sess *net.Session, args []string, rawInput string) {
spot += " " + strings.ToLower(arg)
continue
}
- sess.WriteLine("Use bet, bet <amount> [on <target>], bet min, or bet max.")
+ sess.WriteLine("Use bet, bet N [on BET], bet min, or bet max.")
return
}
if table.Config.Game == casino.GameBaccarat {
@@ -657,6 +648,9 @@ func (g *Game) casinoMenuParts(sess *net.Session, menu *casino.MenuView, message
heading = message
}
commands = append([]casino.MenuCommand(nil), menu.Commands...)
+ if !g.casinoHintsVisible(sess, menu) {
+ commands = nil
+ }
for i := range commands {
if commands[i].Cost > casinoAffordable(sess.Player) {
commands[i].Desc += " [unavailable: insufficient funds]"
@@ -665,6 +659,24 @@ func (g *Game) casinoMenuParts(sess *net.Session, menu *casino.MenuView, message
return balance, heading, commands
}
+// casinoHintsVisible decides whether a menu's command list prints. "on"
+// always shows hints (the historical behavior); "off" never does; the default
+// "once" shows them only on the menu a player gets when sitting down. Players
+// can always ask for the live list with "what" or "verbs".
+func (g *Game) casinoHintsVisible(sess *net.Session, menu *casino.MenuView) bool {
+ if sess == nil || sess.Player == nil || menu == nil {
+ return true
+ }
+ switch sess.Player.OptionString("casino_hints") {
+ case "off":
+ return false
+ case "once":
+ return menu.ShowHints
+ default:
+ return true
+ }
+}
+
// casinoBoardReminder splits a persistent-board menu (craps, roulette, video
// poker, Pai Gow) so its instructions print under the board they describe.
// Every other game, and compact displays, keep their menu in one block.
diff --git a/internal/game/casino_commands.go b/internal/game/casino_commands.go
index f4ca79f..b6deb21 100644
--- a/internal/game/casino_commands.go
+++ b/internal/game/casino_commands.go
@@ -1,6 +1,7 @@
package game
import (
+ "regexp"
"strings"
"unicode/utf8"
@@ -8,6 +9,34 @@ import (
"thehouseoficarus/internal/net"
)
+// casinoCommandVariable matches a command variable: an uppercase run such as
+// N in "bet N on BET" or MAX in "bet [1-4|MAX]". Literal command text is
+// lowercase, so every uppercase run is a placeholder the player replaces.
+var casinoCommandVariable = regexp.MustCompile(`[A-Z]+`)
+
+// colorizeCommand paints literal command text in the command color and each
+// uppercase variable in the distinct variable color. Mode "none" collapses to
+// plain text, and text without variables gets a single color wrapper.
+func (g *Game) colorizeCommand(sess *net.Session, text string) string {
+ matches := casinoCommandVariable.FindAllStringIndex(text, -1)
+ if len(matches) == 0 {
+ return g.colorize(sess, "command", text)
+ }
+ var sb strings.Builder
+ last := 0
+ for _, m := range matches {
+ if m[0] > last {
+ sb.WriteString(g.colorize(sess, "command", text[last:m[0]]))
+ }
+ sb.WriteString(g.colorize(sess, "command_variable", text[m[0]:m[1]]))
+ last = m[1]
+ }
+ if last < len(text) {
+ sb.WriteString(g.colorize(sess, "command", text[last:]))
+ }
+ return sb.String()
+}
+
// Wrap before Session.WriteLine: generic prose wrapping strips alignment from
// overlong lines. Every casino menu uses the same two-space command indent and
// hanging description column, capped at 80 even when prose wrapping is off.
@@ -35,7 +64,7 @@ func (g *Game) casinoCommandColumns(sess *net.Session, commands []casino.MenuCom
n := utf8.RuneCountInString(c.Command)
if n > limit/2 {
for _, line := range casinoWrapWords(c.Command, max(1, limit-2)) {
- lines = append(lines, " "+g.colorize(sess, "command", line))
+ lines = append(lines, " "+g.colorizeCommand(sess, line))
}
for _, line := range casinoWrapWords(c.Desc, max(1, limit-4)) {
lines = append(lines, " "+line)
@@ -43,13 +72,13 @@ func (g *Game) casinoCommandColumns(sess *net.Session, commands []casino.MenuCom
continue
}
if strings.TrimSpace(c.Desc) == "" {
- lines = append(lines, " "+g.colorize(sess, "command", c.Command))
+ lines = append(lines, " "+g.colorizeCommand(sess, c.Command))
continue
}
indent := width + 4
desc := casinoWrapWords(c.Desc, max(1, limit-indent))
cmd := c.Command + strings.Repeat(" ", width-n)
- lines = append(lines, " "+g.colorize(sess, "command", cmd)+" "+desc[0])
+ lines = append(lines, " "+g.colorizeCommand(sess, cmd)+" "+desc[0])
for _, line := range desc[1:] {
lines = append(lines, strings.Repeat(" ", indent)+line)
}
diff --git a/internal/game/casino_commands_test.go b/internal/game/casino_commands_test.go
index cd558ea..6b41ecc 100644
--- a/internal/game/casino_commands_test.go
+++ b/internal/game/casino_commands_test.go
@@ -12,13 +12,13 @@ import (
func TestCasinoCommandsWrapBeforeSessionWithoutLosingIndent(t *testing.T) {
commands := []casino.MenuCommand{
- {Command: "bet <amount> on <target>", Desc: "Place bets until the dice roll; list gives targets"},
+ {Command: "bet N on BET", Desc: "Place bets until the dice roll; list gives targets"},
{Command: "roll / autoroll / stop", Desc: "Shoot / repeat on your turn / return to manual play"},
{Command: "unbet", Desc: "Remove all your bets"},
{Command: "table", Desc: "View the felt"},
{Command: "insurance", Desc: "Buy insurance; a deliberately long description which requires a hanging continuation line"},
{Command: "decline", Desc: "Continue without insurance (also the timeout default)"},
- {Command: "bet <amount> on player/banker/tie/player_pair/banker_pair", Desc: "One wager per round; 'on' is optional"},
+ {Command: "bet N on player/banker/tie/player_pair/banker_pair", Desc: "One wager per round; 'on' is optional"},
{Command: "option casino_display compact", Desc: "Compact action hints (detailed for full command menus)"},
{Command: "spin", Desc: "Spin using your current wager"},
{Command: "autospin", Desc: "Start automatic betting"},
@@ -58,7 +58,7 @@ func TestCasinoCommandsHangingIndentAndShortDescription(t *testing.T) {
sess := &net.Session{Player: player.New("a")}
g := &Game{}
commands := []casino.MenuCommand{
- {Command: "bet <amount> on <target>", Desc: "Place a wager; targets: list"},
+ {Command: "bet N on BET", Desc: "Place a wager; targets: list"},
{Command: "table", Desc: "View the felt"},
}
text := plainRoulette(g.casinoMenuCommands(sess, commands))
@@ -81,7 +81,7 @@ func TestCasinoCommandsHangingIndentAndShortDescription(t *testing.T) {
}
func TestCasinoCompactHintsUseOnlyAvailableCommands(t *testing.T) {
p := player.New("a")
- p.Options = map[string]any{"casino_display": "compact"}
+ p.Options = map[string]any{"casino_display": "compact", "casino_hints": "on"}
p.Credits = 10
sess := &net.Session{Player: p}
g := &Game{}
diff --git a/internal/game/casino_hints_test.go b/internal/game/casino_hints_test.go
new file mode 100644
index 0000000..21739d2
--- /dev/null
+++ b/internal/game/casino_hints_test.go
@@ -0,0 +1,62 @@
+package game
+
+import (
+ "strings"
+ "testing"
+
+ "thehouseoficarus/internal/casino"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+// The default must be the low-noise "once" mode.
+func TestCasinoHintsDefaultIsOnce(t *testing.T) {
+ if got := player.New("alice").OptionString("casino_hints"); got != "once" {
+ t.Fatalf("casino_hints default=%q, want once", got)
+ }
+}
+
+// casino_hints on shows hints on every menu; once shows them at sit-down only;
+// off never does. "what" always lists the live hints regardless.
+func TestCasinoHintsOption(t *testing.T) {
+ for _, tc := range []struct {
+ mode string
+ join, after bool
+ }{
+ {"on", true, true},
+ {"once", true, false},
+ {"off", false, false},
+ } {
+ t.Run(tc.mode, func(t *testing.T) {
+ g, sess, conn := uxCasinoGame(t)
+ g.Hub = net.NewHub()
+ g.Hub.EnterRoom(sess, 1)
+ sess.Player.Options = map[string]any{"color": "none", "unicode": true, "casino_hints": tc.mode}
+ cfg := casino.TableConfig{ID: "roulette", Game: casino.GameRoulette, Roulette: casino.RouletteConfig{SpinTicks: 8}}
+ g.emitCasinoEvents(g.Casino.Join(1, cfg, sess.Player.Name))
+ if got := strings.Contains(string(conn.data), "bet N on BET"); got != tc.join {
+ t.Fatalf("join hints=%v, want %v: %q", got, tc.join, conn.data)
+ }
+ sess.ClearPrompt()
+ g.executeBet(sess, []string{"2", "2"}, "bet 2 2")
+ if !strings.Contains(string(conn.data), "You bet 2 on 2.") {
+ t.Fatalf("bet N N did not resolve: %q", conn.data)
+ }
+ for i := 0; i < 8; i++ {
+ conn.data = nil
+ g.CasinoTick()
+ }
+ if got := strings.Contains(string(conn.data), "bet N on BET"); got != tc.after {
+ t.Fatalf("post-settlement hints=%v, want %v: %q", got, tc.after, conn.data)
+ }
+ // "what" is an explicit request: it always lists the live hints
+ // and never the room's exits while seated.
+ conn.data = nil
+ g.executeVerbs(sess, nil, "what")
+ text := string(conn.data)
+ if !strings.Contains(text, "bet N on BET") || strings.Contains(text, "Exits:") || strings.Contains(text, "north") {
+ t.Fatalf("what at the table wrong: %q", text)
+ }
+ })
+ }
+}
diff --git a/internal/game/casino_house_edge.go b/internal/game/casino_house_edge.go
index 1abbcac..de0036b 100644
--- a/internal/game/casino_house_edge.go
+++ b/internal/game/casino_house_edge.go
@@ -76,8 +76,8 @@ func (g *Game) casinoListRows(sess *net.Session, t *casino.Table, wagers []casin
// Different target returns need distinct rows, not a list of unlabeled RTPs.
for _, part := range row.edge.Parts {
entry := row
- if strings.Contains(wager.Bet, "<n>") {
- entry.bet = strings.ReplaceAll(wager.Bet, "<n>", part.Label)
+ if bet, ok := expandBetNumber(wager.Bet, part.Label); ok {
+ entry.bet = bet
} else if part.Label != "" {
// The label refines the shared win text (hop's doubles), so append
// it: the base phrase stays "Next roll is a/b (doubles)".
@@ -90,6 +90,24 @@ func (g *Game) casinoListRows(sess *net.Session, t *casino.Table, wagers []casin
return rows
}
+// expandBetNumber replaces the standalone number variable N in a grouped
+// wager row ("buy N", "odds dont N") with one concrete number. It reports
+// false when the row has no N token, leaving the label to refine the win text.
+func expandBetNumber(bet, number string) (string, bool) {
+ fields := strings.Fields(bet)
+ found := false
+ for i, f := range fields {
+ if f == "N" {
+ fields[i] = number
+ found = true
+ }
+ }
+ if !found {
+ return bet, false
+ }
+ return strings.Join(fields, " "), true
+}
+
// 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, and
@@ -174,7 +192,11 @@ func (g *Game) showCasinoListTable(sess *net.Session, betHeader string, rows []c
category = "casino_action"
}
physical[i] = parts[line]
- if category != "" {
+ switch category {
+ case "command":
+ physical[i] = g.colorizeCommand(sess, physical[i])
+ case "":
+ default:
physical[i] = g.colorize(sess, category, physical[i])
}
}
diff --git a/internal/game/casino_look_test.go b/internal/game/casino_look_test.go
index cdb0b59..732bc6b 100644
--- a/internal/game/casino_look_test.go
+++ b/internal/game/casino_look_test.go
@@ -81,7 +81,7 @@ func TestLookStandingShowsFixtureOnly(t *testing.T) {
h.hub.EnterRoom(sess, 4)
h.g.executeLook(sess, nil, "look")
text := lookOutput(t, conn)
- if strings.Contains(text, "bet <amount>") {
+ if strings.Contains(text, "bet N") {
t.Fatalf("standing look leaked a betting menu: %q", text)
}
if !strings.Contains(text, "play craps") {
diff --git a/internal/game/casino_rules_list_test.go b/internal/game/casino_rules_list_test.go
index f0c72d2..f3a18e6 100644
--- a/internal/game/casino_rules_list_test.go
+++ b/internal/game/casino_rules_list_test.go
@@ -170,7 +170,7 @@ func TestListSingleBetGamesRenderOneRow(t *testing.T) {
conn.data = nil
g.executeList(sess, nil, "list")
text = stripANSI(string(conn.data))
- if targets := listTargets(text); !targets["bet <amount>"] {
+ if targets := listTargets(text); !targets["bet N"] {
t.Fatalf("blackjack list: %q", text)
}
}
@@ -255,7 +255,7 @@ func TestListRouletteAndBaccarat(t *testing.T) {
g.executeList(sess, nil, "list")
text := stripANSI(string(conn.data))
targets := listTargets(text)
- for _, want := range []string{"<number>", "red / black", "voisins", "basket"} {
+ for _, want := range []string{"NUMBER", "red / black", "voisins", "basket"} {
if !targets[want] {
t.Fatalf("roulette list missing %q: %q", want, text)
}
diff --git a/internal/game/casino_test.go b/internal/game/casino_test.go
index 2f72d07..0b82945 100644
--- a/internal/game/casino_test.go
+++ b/internal/game/casino_test.go
@@ -63,7 +63,7 @@ func TestExecuteBetRejectsInvalidMachineArgs(t *testing.T) {
for _, args := range [][]string{{"banana"}, {"-5"}, {"10", "extra"}} {
conn.data = nil
g.executeBet(sess, args, "bet "+strings.Join(args, " "))
- if !strings.Contains(string(conn.data), "Use bet, bet <amount>, or bet max.") {
+ if !strings.Contains(string(conn.data), "Use bet, bet N, or bet max.") {
t.Fatalf("bet %v: expected usage error, got %q", args, string(conn.data))
}
}
@@ -110,13 +110,18 @@ func TestExecuteBetParsesBaccaratSpots(t *testing.T) {
if !strings.Contains(string(conn.data), "must be on player, banker, or tie") {
t.Fatalf("bet 5 on 17: expected the table to receive the numeric spot, got %q", string(conn.data))
}
- // Two amounts or two spots are usage errors.
- for _, args := range [][]string{{"10", "20"}, {"player", "banker"}} {
- conn.data = nil
- g.executeBet(sess, args, "bet "+strings.Join(args, " "))
- if !strings.Contains(string(conn.data), "Use bet,") {
- t.Fatalf("bet %v: expected usage error, got %q", args, string(conn.data))
- }
+ // A number after the amount is the target ("bet 10 20" == "bet 10 on
+ // 20"), so baccarat rejects the value rather than the syntax.
+ conn.data = nil
+ g.executeBet(sess, []string{"10", "20"}, "bet 10 20")
+ if !strings.Contains(string(conn.data), "must be on player, banker, or tie") {
+ t.Fatalf("bet 10 20: expected the table to receive the numeric spot, got %q", string(conn.data))
+ }
+ // Two spots remain a usage error.
+ conn.data = nil
+ g.executeBet(sess, []string{"player", "banker"}, "bet player banker")
+ if !strings.Contains(string(conn.data), "Use bet,") {
+ t.Fatalf("bet player banker: expected usage error, got %q", string(conn.data))
}
// A bare first bet asks for a target (via the rebet path).
conn.data = nil
@@ -129,22 +134,29 @@ func TestExecuteBetParsesBaccaratSpots(t *testing.T) {
func TestCasinoMenuRendersAlignedColoredCommands(t *testing.T) {
g := &Game{Casino: casino.NewManager(1)}
conn := &casinoTestConn{}
- sess := &net.Session{Player: player.New("alice"), Conn: conn}
+ p := player.New("alice")
+ p.Options = map[string]any{"casino_hints": "on"}
+ sess := &net.Session{Player: p, Conn: conn}
g.loggedInChars = map[string]*net.Session{"alice": sess}
menu := &casino.MenuView{Heading: "Your slot machine bet is set to 10.", ShowBalance: true, Commands: []casino.MenuCommand{
- {Command: "bet <amount>", Desc: "Set your wager"},
+ {Command: "bet N", Desc: "Set your wager"},
{Command: "spin", Desc: "Spin using your current wager"},
}}
msg := g.casinoPrivateMessage(sess, "casino_menu", menu, "Your slot machine bet is set to 10.")
if !strings.Contains(regexp.MustCompile(`\x1b\[[0-9;]*m`).ReplaceAllString(msg, ""), "Available 0 (0 chips + 0 credits)") {
t.Fatalf("missing balance line: %q", msg)
}
- // Commands are painted with the "command" color (0A bold).
- if !strings.Contains(msg, "\033[1m\033[38;5;10mbet <amount>\033[0m") {
- t.Fatalf("command not colored: %q", msg)
+ // Literal text uses the "command" color (0A bold); the N variable uses the
+ // distinct "command_variable" color (B4 bold).
+ if !strings.Contains(msg, color.FgCode("xterm256", 10)) || !strings.Contains(msg, color.FgCode("xterm256", 180)) {
+ t.Fatalf("command/variable not colored: %q", msg)
+ }
+ plain := regexp.MustCompile(`\x1b\[[0-9;]*m`).ReplaceAllString(msg, "")
+ if !strings.Contains(plain, "bet N") {
+ t.Fatalf("command text missing: %q", plain)
}
// Shorter commands are right-padded to align the descriptions.
- if !strings.Contains(msg, "\033[1m\033[38;5;10mspin \033[0m Spin using your current wager") {
+ if !strings.Contains(msg, "\033[1m\033[38;5;10mspin \033[0m Spin using your current wager") {
t.Fatalf("commands not aligned: %q", msg)
}
}
diff --git a/internal/game/casino_ux.go b/internal/game/casino_ux.go
index 4c9accb..e44da54 100644
--- a/internal/game/casino_ux.go
+++ b/internal/game/casino_ux.go
@@ -355,7 +355,7 @@ func (g *Game) executeList(sess *net.Session, args []string, rawInput string) {
return
}
if machine, ok := g.Casino.MachineFor(p.Name); ok {
- row := casinoListRow{bet: "bet <amount> / max", odds: "paytable", win: "spin / autospin"}
+ row := casinoListRow{bet: "bet N / MAX", odds: "paytable", win: "spin / autospin"}
if _, enabled := g.casinoEdgePrecision(sess); enabled {
row.edge = machine.Config.HouseEdge()
}
@@ -380,7 +380,7 @@ 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]"
+ rows[0].bet = "bet [1-4|MAX]"
g.showCasinoListTable(sess, "Bet", rows)
return
}
@@ -406,11 +406,11 @@ func (g *Game) showCasinoWagerList(sess *net.Session, t *casino.Table) {
wager.Payout = fmt.Sprintf("1:1; BJ %g:1", t.Config.Blackjack.Normalize().BlackjackPayout)
}
list := g.casinoListRows(sess, t, []casino.WagerRow{wager})
- list[0].bet = "bet <amount>"
+ list[0].bet = "bet N"
g.showCasinoListTable(sess, "Bet", list)
return
}
- g.showCasinoListTable(sess, "bet <n> on", g.casinoListRows(sess, t, rows))
+ g.showCasinoListTable(sess, "bet N on", g.casinoListRows(sess, t, rows))
}
// showCasinoGameLook appends the live game state and hint menu for the
diff --git a/internal/game/cmd_color.go b/internal/game/cmd_color.go
index 104d359..304e99f 100644
--- a/internal/game/cmd_color.go
+++ b/internal/game/cmd_color.go
@@ -181,6 +181,7 @@ var colorCategoryOrder = []string{
// Commands
"command",
+ "command_variable",
// Drops / consume / fire
"drop_message",
diff --git a/internal/game/cmd_verbs.go b/internal/game/cmd_verbs.go
index 24957ab..d13b803 100644
--- a/internal/game/cmd_verbs.go
+++ b/internal/game/cmd_verbs.go
@@ -4,6 +4,7 @@ import (
"strings"
"thehouseoficarus/internal/behavior"
+ "thehouseoficarus/internal/casino"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/world"
)
@@ -46,6 +47,19 @@ var alwaysAvailable = []string{
func (g *Game) executeVerbs(sess *net.Session, args []string, rawInput string) {
p := sess.Player
+ // A seated casino player has no room verbs: the only valid commands are
+ // the game's live hints, so what/verbs shows those instead of exits and
+ // objects that cannot be used mid-hand.
+ if g.Casino != nil {
+ if table, playing := g.Casino.Participation(p.Name); playing {
+ if table != nil {
+ g.showCasinoHints(sess, table.Hints(p.Name))
+ } else if machine, ok := g.Casino.MachineFor(p.Name); ok {
+ g.showCasinoHints(sess, machine.Hints())
+ }
+ return
+ }
+ }
showAll := len(args) == 1 && strings.ToLower(args[0]) == "all"
seen := make(map[string]bool)
@@ -208,6 +222,17 @@ func (g *Game) executeVerbs(sess *net.Session, args []string, rawInput string) {
}
}
+// showCasinoHints prints the live command hints for a seated player. It is
+// the casino branch of the what/verbs command, so a casino never lists room
+// exits that are not valid options while seated.
+func (g *Game) showCasinoHints(sess *net.Session, commands []casino.MenuCommand) {
+ if len(commands) == 0 {
+ sess.WriteLine(g.colorize(sess, "casino_action", "No commands are available right now."))
+ return
+ }
+ sess.WriteLine(g.casinoMenuCommands(sess, commands))
+}
+
func addUnique(list *[]string, seen *map[string]bool, cmd string) {
if (*seen)[cmd] {
return
diff --git a/internal/game/look_casino_test.go b/internal/game/look_casino_test.go
index 651c74f..d53e02d 100644
--- a/internal/game/look_casino_test.go
+++ b/internal/game/look_casino_test.go
@@ -42,7 +42,7 @@ func TestLookCasinoTableDetailsAndBoards(t *testing.T) {
t.Fatalf("look craps missing %q: %s", want, text)
}
}
- for _, gone := range []string{"PASS ONLY", "bet <amount> on pass", "HOUSE EDITION"} {
+ for _, gone := range []string{"PASS ONLY", "bet N on pass", "HOUSE EDITION"} {
if strings.Contains(text, gone) {
t.Fatalf("look craps showed invariant content %q: %s", gone, text)
}
diff --git a/internal/game/pai_gow_test.go b/internal/game/pai_gow_test.go
index 5ba367e..830b080 100644
--- a/internal/game/pai_gow_test.go
+++ b/internal/game/pai_gow_test.go
@@ -299,11 +299,16 @@ func TestPaiGowFeltAndJokerAllModes(t *testing.T) {
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")
+ // Isolate the state-driven hint content from the casino_hints option.
+ if sess.Player.Options == nil {
+ sess.Player.Options = map[string]any{}
+ }
+ sess.Player.Options["casino_hints"] = "on"
// 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"} {
+ for _, want := range []string{"bet N fortune", "bet N", "table", "rules", "stop"} {
if !strings.Contains(text, want) {
t.Fatalf("missing %q in the join menu: %s", want, text)
}
@@ -313,7 +318,7 @@ func TestPaiGowContextHintsFollowState(t *testing.T) {
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>") {
+ if !strings.Contains(text, "PAI GOW POKER") || strings.Contains(text, "bet N") {
t.Fatalf("table should be the felt alone: %s", text)
}
diff --git a/internal/game/render_craps_test.go b/internal/game/render_craps_test.go
index b6a70be..4445844 100644
--- a/internal/game/render_craps_test.go
+++ b/internal/game/render_craps_test.go
@@ -207,7 +207,7 @@ func TestCrapsRemindersFollowArt(t *testing.T) {
conn.data = nil
g.emitCasinoEvents(g.Casino.Table(1, "c").Status("alice"))
text := plainRoulette(string(conn.data))
- felt, commands := strings.Index(text, "PASS LINE"), strings.Index(text, "bet <n> on <bet>")
+ felt, commands := strings.Index(text, "PASS LINE"), strings.Index(text, "bet N on BET")
if felt < 0 || commands < felt {
t.Fatal("reminder above graphic", text)
}
diff --git a/internal/game/render_help.go b/internal/game/render_help.go
index 28f2c10..9e8b45a 100644
--- a/internal/game/render_help.go
+++ b/internal/game/render_help.go
@@ -67,7 +67,7 @@ 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"},
- {"bet / rebet", "Active", "Repeat a wager, or bet <amount> [target]; min/max accepted"},
+ {"bet / rebet", "Active", "Repeat a wager, or bet N [on BET]; min/max accepted"},
{"autobet / autospin", "Active", "Automatically repeat a casino wager"},
{"spin", "Active", "Spin the current casino wager"},
{"hit / stand / double / split", "Active", "Play blackjack (shortcuts: h / st / dd / sp)"},
diff --git a/internal/game/render_roulette_test.go b/internal/game/render_roulette_test.go
index e4fe650..cfd8b01 100644
--- a/internal/game/render_roulette_test.go
+++ b/internal/game/render_roulette_test.go
@@ -605,7 +605,7 @@ func TestRouletteMenuPutsReminderAfterBoard(t *testing.T) {
out := strings.ReplaceAll(string(conn.data), "\r\n", "\n")
tracker := strings.Index(out, "History: last 0 spins")
board := strings.Index(out, "R = red, B = black, G = green")
- reminder := strings.Index(out, "bet <n> on <bet>")
+ reminder := strings.Index(out, "bet N on BET")
if tracker < 0 || board < 0 || reminder < 0 {
t.Fatalf("missing tracker, board, or reminder:\n%s", out)
}
diff --git a/internal/game/roulette_test.go b/internal/game/roulette_test.go
index 481044d..fb773a3 100644
--- a/internal/game/roulette_test.go
+++ b/internal/game/roulette_test.go
@@ -57,6 +57,9 @@ func TestRouletteCombinationTargetParsing(t *testing.T) {
g.loggedInChars = map[string]*net.Session{"alice": sess}
// Multi-token targets are joined for roulette, with or without "on".
valid := [][]string{
+ // A numeric target needs no "on": "bet 10 17" == "bet 10 on 17".
+ {"10", "17"},
+ {"10", "on", "17"},
{"10", "on", "split", "1", "2"},
{"10", "on", "split", "1-2"},
{"10", "split", "1-2"},
diff --git a/internal/game/video_poker_test.go b/internal/game/video_poker_test.go
index 8ec44d7..4ea7275 100644
--- a/internal/game/video_poker_test.go
+++ b/internal/game/video_poker_test.go
@@ -73,7 +73,7 @@ func TestBlackjackListUsesRealActionCommands(t *testing.T) {
conn.data = nil
g.executeList(sess, nil, "list")
text := stripANSI(string(conn.data))
- if strings.Contains(text, "bet <amount> on") || len(listTargets(text)) == 0 {
+ if strings.Contains(text, "bet N on") || len(listTargets(text)) == 0 {
t.Fatal(text)
}
}
diff --git a/internal/player/player.go b/internal/player/player.go
index 0c92af3..43f5d7e 100644
--- a/internal/player/player.go
+++ b/internal/player/player.go
@@ -141,6 +141,7 @@ var OptionDefs = []OptionDef{
{"wrap_width", OptInt, 80, nil, "Wrap all output to this many columns (minimum 80)"},
{"unicode", OptBool, true, nil, "Unicode box-drawing characters"},
{"casino_display", OptString, "detailed", []string{"compact", "detailed"}, "Menu style: compact action hints, or detailed command menus"},
+ {"casino_hints", OptString, "once", []string{"on", "once", "off"}, "Casino command hints: on always, once at sit-down, or off"},
{"card_size", OptInt, 9, nil, "Casino card art size (1, 5, 7, or 9; applies at every table)"},
{"card_style", OptString, "dark", []string{"dark", "light"}, "Casino card style (dark or light)"},
{"visual_ticks", OptBool, false, nil, "Display a tick marker every game tick"},