aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-10-01 05:05:11 -0400
committerhistoria <[not public]>2026-10-01 05:05:11 -0400
commitd4eae333a8d8a96452bbf3617aa85b50466c11a7 (patch)
tree90d4cf075fe5467b35e5cc53ff3ee30fc4ee1feb
parent299b5b67aa80ba2539d7a91c60943060b0426b45 (diff)
downloadthehouseoficarus-d4eae333a8d8a96452bbf3617aa85b50466c11a7.tar.gz
feat: concise table/machine names, casino command shortcuts, remove payout video poker command
-rw-r--r--building_guide/video_poker.md32
-rw-r--r--data/help/bonus_poker.yaml2
-rw-r--r--data/help/bonus_poker_deluxe.yaml2
-rw-r--r--data/help/deuces_wild.yaml2
-rw-r--r--data/help/double_bonus.yaml2
-rw-r--r--data/help/double_double_bonus.yaml2
-rw-r--r--data/help/hot_roll.yaml4
-rw-r--r--data/help/jacks_or_better.yaml2
-rw-r--r--data/help/joker_poker.yaml2
-rw-r--r--data/help/super_times_pay.yaml4
-rw-r--r--data/help/ultimate_x_multi_hand.yaml4
-rw-r--r--data/help/ultimate_x_single_line.yaml4
-rw-r--r--data/help/video_poker.yaml17
-rw-r--r--data/rooms/casino/6192.yaml33
-rw-r--r--data/rooms/casino/6193.yaml46
-rw-r--r--data/rooms/casino/6194.yaml45
-rw-r--r--internal/casino/video_poker.go21
-rw-r--r--internal/casino/video_poker_config.go18
-rw-r--r--internal/casino/video_poker_lifecycle_test.go14
-rw-r--r--internal/casino/video_poker_regression_test.go16
-rw-r--r--internal/game/casino_ux.go3
-rw-r--r--internal/game/casino_ux_test.go74
-rw-r--r--internal/game/cmd_registry.go39
-rw-r--r--internal/game/game.go9
-rw-r--r--internal/game/vesta_world_test.go5
-rw-r--r--internal/game/video_poker_cabinet_test.go21
26 files changed, 336 insertions, 87 deletions
diff --git a/building_guide/video_poker.md b/building_guide/video_poker.md
index ca2ece7..0faf8f3 100644
--- a/building_guide/video_poker.md
+++ b/building_guide/video_poker.md
@@ -1,6 +1,8 @@
# Video poker cabinets
-Room 6192, east of Penny Reels (6118), contains exactly five single-seat
+The video poker hall east of Penny Reels (6118) is three rooms. The parlor
+(6192) runs low pay schedules, the high-limit room (6193) normal schedules, and
+the VIP room (6194) full pay schedules. Each holds the same five single-seat
cabinets: Classic, Ultimate X Single Line, Ultimate X Multi-Hand, Super Times
Pay, and Hot Roll. Choose a game **inside** a cabinet; schedules are not games.
The shared table lifecycle adapter provides room routing, wallet settlement,
@@ -8,7 +10,7 @@ disconnect and eviction handling, so author cabinets under `casino_tables`:
```yaml
casino_tables:
-- id: ultimate_x_multi
+- id: uxm
game: video_poker
max_players: 1
video_poker:
@@ -17,16 +19,19 @@ casino_tables:
```
`cabinet` accepts `classic`, `ultimate_x_single`, `ultimate_x_multi`,
-`super_times_pay`, `hot_roll`. `payout` accepts `low`, `normal`, `full`.
-Defaults are classic/full. Do not author variant/pay_table/feature/single_line;
-strict YAML validation rejects those obsolete fields. Do not use table wager
-limits to set coin denominations. Each cabinet has one occupant, but multiple
-cabinets may share a room and are addressed by ID.
+`super_times_pay`, `hot_roll`. `payout` accepts `low`, `normal`, `full` and is
+the house's setting: players cannot change it. Defaults are classic/full. Do
+not author variant/pay_table/feature/single_line; strict YAML validation
+rejects those obsolete fields. Do not use table wager limits to set coin
+denominations. Each cabinet has one occupant, but multiple cabinets may share a
+room and are addressed by ID. Short room ids (`vp`, `ux`, `uxm`, `stp`, `hr`)
+and cabinet aliases (`poker`, `ultimate_x`, `super_times`, `hot_roll`) keep
+`play` concise.
## Games and quality tiers
-Each row below lists **low / normal / full** schedules. Tier changes apply to
-all games on that cabinet; selecting a different game preserves the tier.
+Each row below lists **low / normal / full** schedules. The authored tier
+applies to all games on that cabinet; selecting a different game preserves it.
These are published schedules, not interpolated payouts. The tier names rank
the offered schedules within a game/cabinet; they do not imply equal RTP across
games. Some classic full schedules exceed 100% with optimal play.
@@ -50,8 +55,7 @@ games. Some classic full schedules exceed 100% with optimal play.
than inventing a third tier. Other omissions are deliberately conservative,
not claims that no physical edition exists.
-The runtime tier selection persists across occupants of the live cabinet.
-A restart or authored configuration replacement restores the authored tier.
+The authored tier persists across occupants of the live cabinet.
Multiplier banks for *all* tiers remain durable and separate.
| Edition | Supported hands |
@@ -66,7 +70,11 @@ Multiplier banks for *all* tiers remain durable and separate.
## Controls and presentation
- `play <id>` seats you; `game` lists games; `game jacks` (or a full game name)
- selects one. `payout low|normal|full` selects cabinet-wide quality.
+ selects one. The house sets the payout tier when the cabinet is authored in
+ YAML; players cannot change it.
+- While seated at any casino game, casino verbs may be shortened to an
+ unambiguous prefix: `b 1000` is `bet 1000`, and `be 1000` also works.
+ `stop` and `rules` must be typed in full.
- `denom 1|10|50` / `denomination` sets credits per coin.
- `hands` lists counts; `hands <count>` selects one.
- `bet 1-4` **adds** coins, capped at five per hand. `bet max` loads five.
diff --git a/data/help/bonus_poker.yaml b/data/help/bonus_poker.yaml
index 65d949b..74c3fae 100644
--- a/data/help/bonus_poker.yaml
+++ b/data/help/bonus_poker.yaml
@@ -8,7 +8,7 @@ description: |
Four aces pay 80, four 2s-4s pay 40, and four 5s-Ks pay 25 per coin.
Select this game inside a compatible video poker cabinet with game bonus_poker.
- payout low|normal|full selects a published pay schedule, not a different game.
+ The house sets the cabinet's published pay schedule, not a different game.
paytable shows exact returns and any next-hand multiplier awards.
Type 531 or 1 3 5 to toggle holds, then draw or d. See help video poker
for denominations, loading coins, supported hands and bonus cabinets.
diff --git a/data/help/bonus_poker_deluxe.yaml b/data/help/bonus_poker_deluxe.yaml
index 40f5234..cd8e21a 100644
--- a/data/help/bonus_poker_deluxe.yaml
+++ b/data/help/bonus_poker_deluxe.yaml
@@ -8,7 +8,7 @@ description: |
pay 80 per coin; two pair pays only 1 per coin.
Select this game inside a compatible video poker cabinet with game bonus_poker_deluxe.
- payout low|normal|full selects a published pay schedule, not a different game.
+ The house sets the cabinet's published pay schedule, not a different game.
paytable shows exact returns and any next-hand multiplier awards.
Type 531 or 1 3 5 to toggle holds, then draw or d. See help video poker
for denominations, loading coins, supported hands and bonus cabinets.
diff --git a/data/help/deuces_wild.yaml b/data/help/deuces_wild.yaml
index 39725cb..e45343e 100644
--- a/data/help/deuces_wild.yaml
+++ b/data/help/deuces_wild.yaml
@@ -9,7 +9,7 @@ description: |
categories. Wild cards form the highest-paying hand on the selected table.
Select this game inside a compatible video poker cabinet with game deuces_wild.
- payout low|normal|full selects a published pay schedule, not a different game.
+ The house sets the cabinet's published pay schedule, not a different game.
paytable shows exact returns and any next-hand multiplier awards.
Type 531 or 1 3 5 to toggle holds, then draw or d. See help video poker
for denominations, loading coins, supported hands and bonus cabinets.
diff --git a/data/help/double_bonus.yaml b/data/help/double_bonus.yaml
index d09b373..3b606ae 100644
--- a/data/help/double_bonus.yaml
+++ b/data/help/double_bonus.yaml
@@ -9,7 +9,7 @@ description: |
Straight, full-house and flush rates depend on the selected schedule.
Select this game inside a compatible video poker cabinet with game double_bonus.
- payout low|normal|full selects a published pay schedule, not a different game.
+ The house sets the cabinet's published pay schedule, not a different game.
paytable shows exact returns and any next-hand multiplier awards.
Type 531 or 1 3 5 to toggle holds, then draw or d. See help video poker
for denominations, loading coins, supported hands and bonus cabinets.
diff --git a/data/help/double_double_bonus.yaml b/data/help/double_double_bonus.yaml
index 4365694..3b3a383 100644
--- a/data/help/double_double_bonus.yaml
+++ b/data/help/double_double_bonus.yaml
@@ -9,7 +9,7 @@ description: |
otherwise 80. Four 5s-Ks pay 50; two pair pays 1 per coin.
Select this game inside a compatible video poker cabinet with game double_double_bonus.
- payout low|normal|full selects a published pay schedule, not a different game.
+ The house sets the cabinet's published pay schedule, not a different game.
paytable shows exact returns and any next-hand multiplier awards.
Type 531 or 1 3 5 to toggle holds, then draw or d. See help video poker
for denominations, loading coins, supported hands and bonus cabinets.
diff --git a/data/help/hot_roll.yaml b/data/help/hot_roll.yaml
index 0e6f483..2e9d6a1 100644
--- a/data/help/hot_roll.yaml
+++ b/data/help/hot_roll.yaml
@@ -5,8 +5,8 @@ description: |
Hot Roll
Choose hands 3, 5 or 10. Games: jacks, ddb.
- game lists games; game <name> selects one. payout low|normal|full sets
- the same quality tier across games. paytable shows the exact schedule.
+ 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).
Before dealing, a 1-in-6 trigger rolls two fair dice. Their sum multiplies
diff --git a/data/help/jacks_or_better.yaml b/data/help/jacks_or_better.yaml
index 4ece68d..941c98e 100644
--- a/data/help/jacks_or_better.yaml
+++ b/data/help/jacks_or_better.yaml
@@ -8,7 +8,7 @@ description: |
2 per coin; four of a kind pays 25. Natural royals pay 4000 at five coins.
Select this game inside a compatible video poker cabinet with game jacks_or_better.
- payout low|normal|full selects a published pay schedule, not a different game.
+ The house sets the cabinet's published pay schedule, not a different game.
paytable shows exact returns and any next-hand multiplier awards.
Type 531 or 1 3 5 to toggle holds, then draw or d. See help video poker
for denominations, loading coins, supported hands and bonus cabinets.
diff --git a/data/help/joker_poker.yaml b/data/help/joker_poker.yaml
index 8783507..7ea583a 100644
--- a/data/help/joker_poker.yaml
+++ b/data/help/joker_poker.yaml
@@ -9,7 +9,7 @@ description: |
possible. This is Kings or Better, not the Two Pair version.
Select this game inside a compatible video poker cabinet with game joker_poker.
- payout low|normal|full selects a published pay schedule, not a different game.
+ The house sets the cabinet's published pay schedule, not a different game.
paytable shows exact returns and any next-hand multiplier awards.
Type 531 or 1 3 5 to toggle holds, then draw or d. See help video poker
for denominations, loading coins, supported hands and bonus cabinets.
diff --git a/data/help/super_times_pay.yaml b/data/help/super_times_pay.yaml
index 0d9f92b..10dd5c8 100644
--- a/data/help/super_times_pay.yaml
+++ b/data/help/super_times_pay.yaml
@@ -5,8 +5,8 @@ description: |
Super Times Pay
Choose hands 1, 3, 5 or 10. Games: jacks, bonus, double, ddb, deuces.
- game lists games; game <name> selects one. payout low|normal|full sets
- the same quality tier across games. paytable shows the exact schedule.
+ 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).
Before dealing, a 1-in-15 trigger awards one multiplier for ALL lines.
diff --git a/data/help/ultimate_x_multi_hand.yaml b/data/help/ultimate_x_multi_hand.yaml
index cffdc33..6656647 100644
--- a/data/help/ultimate_x_multi_hand.yaml
+++ b/data/help/ultimate_x_multi_hand.yaml
@@ -5,8 +5,8 @@ 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. payout low|normal|full sets
- the same quality tier across games. paytable shows the exact schedule.
+ 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
line earns its own NEXT-hand multiplier. The NEXT X column changes with
diff --git a/data/help/ultimate_x_single_line.yaml b/data/help/ultimate_x_single_line.yaml
index 139fe2a..5a9c821 100644
--- a/data/help/ultimate_x_single_line.yaml
+++ b/data/help/ultimate_x_single_line.yaml
@@ -5,8 +5,8 @@ description: |
Ultimate X Single Line
One hand. Games: jacks, bonus, deluxe, double, ddb, deuces, joker.
- game lists games; game <name> selects one. payout low|normal|full sets
- the same quality tier across games. paytable shows the exact schedule.
+ 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
hand's multiplier; current multipliers never multiply new awards. The NEXT X
diff --git a/data/help/video_poker.yaml b/data/help/video_poker.yaml
index 5cd8c77..808edd2 100644
--- a/data/help/video_poker.yaml
+++ b/data/help/video_poker.yaml
@@ -5,16 +5,19 @@ related: [casino, ultimate x single line, ultimate x multi hand, super times pay
description: |
Video Poker — five cabinet types
- The lounge east of Penny Reels has Classic Video Poker, Ultimate X Single
- Line, Ultimate X Multi-Hand, Super Times Pay and Hot Roll. Each has one seat.
- play lists cabinets; play <id> seats you. game lists its supported games.
+ The video poker hall east of Penny Reels is three rooms. The parlor runs low
+ pay schedules, the high-limit room normal schedules, and the VIP room full
+ pay schedules. Each holds Classic Video Poker, Ultimate X Single Line,
+ Ultimate X Multi-Hand, Super Times Pay and Hot Roll. Each cabinet has one
+ seat. play lists cabinets; play <id> seats you. Short cabinet names are vp,
+ ux, uxm, stp and hr. game lists its supported games.
game jacks selects Jacks or Better. Other short names: bonus, deluxe,
double, ddb, deuces, joker. Full game names and underscore names also work.
Not every feature supports every game. Joker Poker is Kings or Better.
- payout low|normal|full sets the cabinet-wide pay quality for ALL its games.
- These select published schedules, not different games or guaranteed RTPs.
- paytable shows the selected game, schedule and exact coin payouts.
+ 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.
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,
@@ -25,6 +28,8 @@ description: |
Bare bet repeats your previous selection, or five initially, without
reprinting the pay table. Explicit bets reprint it with a red coin column.
Loading coins does not charge money or reveal cards. deal pays and deals.
+ While seated at any casino game, casino verbs accept an unambiguous prefix:
+ b 1000 and be 1000 both mean bet 1000; stop and rules need the full word.
Type 531, 1 3 5, or hold 531 to TOGGLE those cards. Type a number again
to release it. Repeated digits in one command toggle only once.
diff --git a/data/rooms/casino/6192.yaml b/data/rooms/casino/6192.yaml
index 76635eb..2f47b9e 100644
--- a/data/rooms/casino/6192.yaml
+++ b/data/rooms/casino/6192.yaml
@@ -1,40 +1,47 @@
-name: Video Poker Lounge
-description: Five single-seat poker cabinets glow blue beneath yellow pay-table borders. Each offers a choice of poker games; the selected coin column shines red. Classic poker, two Ultimate X editions, Super Times Pay and Hot Roll stand side by side.
+name: Video Poker Parlor
+description: A row of low-limit single-seat poker cabinets glows blue beneath yellow pay-table borders.
+ Classic poker, two Ultimate X editions, Super Times Pay and Hot Roll stand side by side. The high-limit
+ room waits north; the reels are back west.
exits:
west:
room: 6118
+ north:
+ room: 6193
casino_tables:
-- id: poker
+- id: vp
game: video_poker
max_players: 1
video_poker:
cabinet: classic
- payout: full
-- id: ultimate_x_single
+ payout: low
+- id: ux
game: video_poker
max_players: 1
video_poker:
cabinet: ultimate_x_single
- payout: full
-- id: ultimate_x_multi
+ payout: low
+- id: uxm
game: video_poker
max_players: 1
video_poker:
cabinet: ultimate_x_multi
- payout: full
-- id: super_times_pay
+ payout: low
+- id: stp
game: video_poker
max_players: 1
video_poker:
cabinet: super_times_pay
- payout: full
-- id: hot_roll
+ payout: low
+- id: hr
game: video_poker
max_players: 1
video_poker:
cabinet: hot_roll
- payout: full
+ payout: low
objects:
- name: poker plaque
- description: 'Use play to list cabinets, then play <id>. game lists the games inside a cabinet; game jacks selects Jacks or Better. payout low|normal|full sets its pay quality across games. denom 1|10|50 sets credits per coin. bet max loads five coins; deal pays and deals. 531 toggles holds 1, 3 and 5; draw or d finishes. Ultimate X multipliers stay on the machine for the next player. rules explains your cabinet; paytable shows exact payouts.'
+ description: 'Use play to list cabinets, then play vp, ux, uxm, stp or hr. game lists the games inside
+ a cabinet; game jacks selects Jacks or Better. denom 1|10|50 sets credits per coin. bet max loads five
+ coins; deal pays and deals. 531 toggles holds 1, 3 and 5; draw or d finishes. The house sets each
+ cabinet''s pay schedule. rules explains your cabinet; paytable shows exact payouts.'
inroom_description: A brass poker plaque explains the five cabinets.
diff --git a/data/rooms/casino/6193.yaml b/data/rooms/casino/6193.yaml
new file mode 100644
index 0000000..f07db50
--- /dev/null
+++ b/data/rooms/casino/6193.yaml
@@ -0,0 +1,46 @@
+name: High Limit Video Poker
+description: The pace slows among mid-limit single-seat cabinets, each with a normal pay schedule and
+ a higher ceiling. The VIP room waits north; the low-limit parlor is back south.
+exits:
+ south:
+ room: 6192
+ north:
+ room: 6194
+casino_tables:
+- id: vp
+ game: video_poker
+ max_players: 1
+ video_poker:
+ cabinet: classic
+ payout: normal
+- id: ux
+ game: video_poker
+ max_players: 1
+ video_poker:
+ cabinet: ultimate_x_single
+ payout: normal
+- id: uxm
+ game: video_poker
+ max_players: 1
+ video_poker:
+ cabinet: ultimate_x_multi
+ payout: normal
+- id: stp
+ game: video_poker
+ max_players: 1
+ video_poker:
+ cabinet: super_times_pay
+ payout: normal
+- id: hr
+ game: video_poker
+ max_players: 1
+ video_poker:
+ cabinet: hot_roll
+ payout: normal
+objects:
+- name: poker plaque
+ description: 'Use play to list cabinets, then play vp, ux, uxm, stp or hr. game lists the games inside
+ a cabinet; game jacks selects Jacks or Better. denom 1|10|50 sets credits per coin. bet max loads five
+ coins; deal pays and deals. 531 toggles holds 1, 3 and 5; draw or d finishes. The house sets each
+ cabinet''s pay schedule. rules explains your cabinet; paytable shows exact payouts.'
+ inroom_description: A brass poker plaque explains the five cabinets.
diff --git a/data/rooms/casino/6194.yaml b/data/rooms/casino/6194.yaml
new file mode 100644
index 0000000..18426a4
--- /dev/null
+++ b/data/rooms/casino/6194.yaml
@@ -0,0 +1,45 @@
+name: VIP Video Poker
+description: Behind a velvet rope, five full-pay single-seat cabinets run at the house's best schedules.
+ This is the quiet end of the video poker hall; the high-limit room is back south.
+exits:
+ south:
+ room: 6193
+casino_tables:
+- id: vp
+ game: video_poker
+ max_players: 1
+ video_poker:
+ cabinet: classic
+ payout: full
+- id: ux
+ game: video_poker
+ max_players: 1
+ video_poker:
+ cabinet: ultimate_x_single
+ payout: full
+- id: uxm
+ game: video_poker
+ max_players: 1
+ video_poker:
+ cabinet: ultimate_x_multi
+ payout: full
+- id: stp
+ game: video_poker
+ max_players: 1
+ video_poker:
+ cabinet: super_times_pay
+ payout: full
+- id: hr
+ game: video_poker
+ max_players: 1
+ video_poker:
+ cabinet: hot_roll
+ payout: full
+objects:
+- name: poker plaque
+ description: 'Use play to list cabinets, then play vp, ux, uxm, stp or hr. game lists the games inside
+ a cabinet; game jacks selects Jacks or Better. denom 1|10|50 sets credits per coin. bet max loads five
+ coins; deal pays and deals. 531 toggles holds 1, 3 and 5; draw or d finishes. The house sets each
+ cabinet''s pay schedule, and these are the best in the house. rules explains your cabinet; paytable
+ shows exact payouts.'
+ inroom_description: A brass poker plaque explains the five cabinets.
diff --git a/internal/casino/video_poker.go b/internal/casino/video_poker.go
index 36fdc35..8c9f66c 100644
--- a/internal/casino/video_poker.go
+++ b/internal/casino/video_poker.go
@@ -128,7 +128,6 @@ func (t *videoPokerTable) hintsLocked(name string) []MenuCommand {
} else {
commands = append(commands,
MenuCommand{Command: "game <name>", Desc: t.settings.GameChoices()},
- MenuCommand{Command: "payout low|normal|full", Desc: "Set the cabinet-wide pay quality"},
MenuCommand{Command: "denom <1|10|50>", Desc: "Credits per coin"},
MenuCommand{Command: "hands <count>", Desc: "Supported hand counts are announced by hands"},
)
@@ -406,24 +405,6 @@ func (t *videoPokerTable) action(name, action string) []Event {
p.config, p.variant, p.hands = cfg, variant, CabinetHandCounts(cfg.Cabinet, variant)[0]
p.next, p.current, p.lines, p.remaining, p.result = nil, nil, nil, nil, ""
return t.statusLocked(name, true)
- case "payout":
- if len(fields) != 2 {
- break
- }
- tier := strings.ToLower(fields[1])
- if tier != "low" && tier != "normal" && tier != "full" {
- break
- }
- cfg := (VideoPokerConfig{Cabinet: p.config.Cabinet, Payout: tier, Variant: p.variant}).Normalize()
- if err := cfg.Validate(); err != nil {
- return []Event{t.private(name, err.Error())}
- }
- t.settings.Payout = tier
- t.settings.PayTable = ""
- t.settings = t.settings.Normalize()
- p.config = cfg
- p.next, p.current, p.lines, p.remaining, p.result = nil, nil, nil, nil, ""
- return t.statusLocked(name, true)
case "feature":
if len(fields) != 2 || cabinetFeature(t.settings.Cabinet) == "" || fields[1] != "on" && fields[1] != "off" {
break
@@ -431,7 +412,7 @@ func (t *videoPokerTable) action(name, action string) []Event {
p.featureOn = fields[1] == "on"
return t.statusLocked(name, false)
}
- return []Event{t.private(name, "Use bet [1-4|max], deal, game <name>, payout low|normal|full, 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], 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/casino/video_poker_config.go b/internal/casino/video_poker_config.go
index 78a772d..8d67252 100644
--- a/internal/casino/video_poker_config.go
+++ b/internal/casino/video_poker_config.go
@@ -145,6 +145,24 @@ func (c VideoPokerConfig) CabinetName() string {
return "Classic Video Poker"
}
+// CabinetAliases lists the single-token names that address this cabinet type
+// in the play menu, beyond the room's own table id. "video poker" itself means
+// the classic cabinet so a lone lounge stays reachable without a specific
+// machine name.
+func (c VideoPokerConfig) CabinetAliases() []string {
+ switch c.Normalize().Cabinet {
+ case CabinetUXSingle:
+ return []string{"ux", "ultimate_x", "ultimate_x_single"}
+ case CabinetUXMulti:
+ return []string{"uxm", "ultimate_x_multi", "ultimate_x_multi_hand"}
+ case CabinetSuperTimesPay:
+ return []string{"stp", "super_times", "super_times_pay"}
+ case CabinetHotRoll:
+ return []string{"hr", "hot_roll"}
+ }
+ return []string{"vp", "poker", "classic", "video_poker"}
+}
+
func (c VideoPokerConfig) CabinetLabel() string {
c = c.Normalize()
return c.CabinetName() + " / " + c.Payout + " pay"
diff --git a/internal/casino/video_poker_lifecycle_test.go b/internal/casino/video_poker_lifecycle_test.go
index 3107a93..f817225 100644
--- a/internal/casino/video_poker_lifecycle_test.go
+++ b/internal/casino/video_poker_lifecycle_test.go
@@ -199,20 +199,20 @@ func TestPokerHoldToggleAndMultiHand(t *testing.T) {
t.Fatal("seat not released")
}
}
-func TestPokerGameAndPayoutSwitching(t *testing.T) {
- table, w, _ := pokerTestCabinet(t, VideoPokerConfig{Cabinet: CabinetClassic})
+func TestPokerGameSwitching(t *testing.T) {
+ table, w, _ := pokerTestCabinet(t, VideoPokerConfig{Cabinet: CabinetClassic, Payout: "low"})
p := table.players["alice"]
+ // Payout is house-owned: the retired verb no longer changes the schedule.
+ if !table.action("alice", "payout full")[0].MessageStarts("Use bet") || p.config.Payout != "low" {
+ t.Fatal("payout action still changes the schedule")
+ }
if !table.action("alice", "game jacks")[0].Snapshot.(*VideoPokerSnapshot).ShowPayTable {
t.Fatal("explicit switch did not show the pay table")
}
table.action("alice", "game ddb")
- if p.config.Variant != "double_double_bonus" || p.config.PayTable != "10/6" {
+ if p.config.Variant != "double_double_bonus" || p.config.PayTable != "8/5" {
t.Fatalf("alias switch: %+v", p.config)
}
- table.action("alice", "payout low")
- if p.config.PayTable != "8/5" {
- t.Fatalf("tier not applied to the current game: %+v", p.config)
- }
table.action("alice", "game joker")
if p.config.PayTable != "50/15/7/5/3" {
t.Fatalf("tier lost switching to joker: %+v", p.config)
diff --git a/internal/casino/video_poker_regression_test.go b/internal/casino/video_poker_regression_test.go
index 634ebc9..90caf91 100644
--- a/internal/casino/video_poker_regression_test.go
+++ b/internal/casino/video_poker_regression_test.go
@@ -8,9 +8,8 @@ import (
func TestPokerEveryCabinetTierGameAndAward(t *testing.T) {
for cabinet, games := range cabinetGames {
- table, _, _ := pokerTestCabinet(t, VideoPokerConfig{Cabinet: cabinet})
for _, tier := range []string{"low", "normal", "full"} {
- table.action("alice", "payout "+tier)
+ table, _, _ := pokerTestCabinet(t, VideoPokerConfig{Cabinet: cabinet, Payout: tier})
for _, variant := range games {
events := table.action("alice", "game "+variant)
p := table.players["alice"]
@@ -32,12 +31,13 @@ func TestPokerEveryCabinetTierGameAndAward(t *testing.T) {
}
}
}
- }
- table.action("alice", "payout low")
- table.leave("alice")
- table.join("bob")
- if table.players["bob"].config.Payout != "low" {
- t.Fatal("machine tier changed with occupant")
+ // The authored tier is machine state, not a player setting: a new
+ // occupant inherits it unchanged.
+ table.leave("alice")
+ table.join("bob")
+ if table.players["bob"].config.Payout != tier {
+ t.Fatalf("%s/%s: tier changed with occupant", cabinet, tier)
+ }
}
}
}
diff --git a/internal/game/casino_ux.go b/internal/game/casino_ux.go
index 750d05d..fc37e7d 100644
--- a/internal/game/casino_ux.go
+++ b/internal/game/casino_ux.go
@@ -69,7 +69,8 @@ func (g *Game) casinoChoices(roomID int) []casinoChoice {
var choices []casinoChoice
for _, c := range room.CasinoTables {
if c.Game == casino.GameVideoPoker {
- choices = append(choices, casinoChoice{address: c.ID, names: []string{c.ID, "video_poker", "vp", c.VideoPoker.Normalize().Variant}, table: true})
+ names := append([]string{c.ID}, c.VideoPoker.CabinetAliases()...)
+ choices = append(choices, casinoChoice{address: c.ID, names: names, table: true})
continue
}
names := []string{string(c.Game)}
diff --git a/internal/game/casino_ux_test.go b/internal/game/casino_ux_test.go
index aa98c87..89363b0 100644
--- a/internal/game/casino_ux_test.go
+++ b/internal/game/casino_ux_test.go
@@ -47,6 +47,80 @@ func uxCasinoGame(t *testing.T) (*Game, *net.Session, *casinoTestConn) {
return g, sess, sess.Conn.(*casinoTestConn)
}
+func TestPayoutCommandRemoved(t *testing.T) {
+ if class := classifyCommand("payout"); class != ClassUnknown {
+ t.Fatalf("payout is still registered as %v", class)
+ }
+}
+
+func TestPlaySoleChoiceNeedsNoName(t *testing.T) {
+ g, sess, _ := casinoCrapsGame(t, "name: Solo\ncasino_tables:\n - id: solo\n game: blackjack\n min_bet: 1\n max_bet: 100\n")
+ g.executePlay(sess, nil, "play")
+ table, ok := g.Casino.Participation("alice")
+ if !ok || table == nil || table.Config.ID != "solo" {
+ t.Fatal("play with a sole choice did not seat the player")
+ }
+}
+
+func TestVideoPokerConciseAliases(t *testing.T) {
+ room := "name: Parlor\ncasino_tables:\n" +
+ "- id: vp\n game: video_poker\n max_players: 1\n video_poker: {cabinet: classic, payout: low}\n" +
+ "- id: ux\n game: video_poker\n max_players: 1\n video_poker: {cabinet: ultimate_x_single, payout: low}\n" +
+ "- id: uxm\n game: video_poker\n max_players: 1\n video_poker: {cabinet: ultimate_x_multi, payout: low}\n" +
+ "- id: stp\n game: video_poker\n max_players: 1\n video_poker: {cabinet: super_times_pay, payout: low}\n" +
+ "- id: hr\n game: video_poker\n max_players: 1\n video_poker: {cabinet: hot_roll, payout: low}\n"
+ g, _, _ := casinoCrapsGame(t, room)
+ choices := g.casinoChoices(1)
+ for input, want := range map[string]string{
+ "vp": "vp", "poker": "vp", "classic": "vp", "video_poker": "vp",
+ "ux": "ux", "ultimate_x": "ux", "ultimate_x_single": "ux",
+ "uxm": "uxm", "ultimate_x_multi": "uxm", "ultimate_x_multi_hand": "uxm",
+ "stp": "stp", "super_times": "stp", "super_times_pay": "stp",
+ "hr": "hr", "hot_roll": "hr",
+ } {
+ choice, ok := resolveCasinoChoice(choices, input)
+ if !ok || choice.address != want {
+ t.Fatalf("play %s resolved to %q (ok=%v), want %q", input, choice.address, ok, want)
+ }
+ }
+}
+
+func TestCasinoCommandAbbreviations(t *testing.T) {
+ g, sess, _ := uxCasinoGame(t)
+ // Unseated players get no casino shortcuts.
+ if got, ok := g.casinoAbbreviation(sess, "b"); ok {
+ t.Fatalf("unseated abbreviation resolved to %q", got)
+ }
+ g.executePlay(sess, []string{"bj"}, "play bj")
+ for input, want := range map[string]string{
+ "b": "bet", "be": "bet", "spl": "split", "su": "surrender",
+ "ins": "insurance", "ro": "roll", "wa": "watch", "ta": "table",
+ "li": "list", "ga": "game",
+ } {
+ got, ok := g.casinoAbbreviation(sess, input)
+ if !ok || got != want {
+ t.Fatalf("%q resolved to %q (ok=%v), want %q", input, got, ok, want)
+ }
+ }
+ // stop and rules are full-word only, and movement keeps its own keys.
+ for _, input := range []string{"sto", "r", "ru", "n", "s"} {
+ if got, ok := g.casinoAbbreviation(sess, input); ok {
+ t.Fatalf("%q unexpectedly abbreviated to %q", input, got)
+ }
+ }
+}
+
+func TestAbbreviatedCasinoCommandReachesQueue(t *testing.T) {
+ g, sess, _ := uxCasinoGame(t)
+ g.queue = NewCommandQueue()
+ g.executePlay(sess, []string{"bj", "10"}, "play bj 10")
+ g.handleGameCommand(sess, "b 25")
+ qc, ok := g.queue.PeekActive("alice")
+ if !ok || qc.Command != "bet" || qc.Args != "25" {
+ t.Fatalf("b 25 queued %+v (ok=%v), want bet 25", qc, ok)
+ }
+}
+
func TestPlayAliasesJoinGames(t *testing.T) {
for input, game := range map[string]string{"bj": "blackjack", "black": "blackjack", "bac": "baccarat", "baccarat": "baccarat"} {
g, sess, conn := uxCasinoGame(t)
diff --git a/internal/game/cmd_registry.go b/internal/game/cmd_registry.go
index d7b808b..fbb401c 100644
--- a/internal/game/cmd_registry.go
+++ b/internal/game/cmd_registry.go
@@ -54,9 +54,6 @@ var commandRegistry = map[string]commandDef{
"game": {func(g *Game, s *net.Session, args []string, _ string) {
g.executeTableAction(s, "game "+strings.Join(args, " "))
}, ClassInstant},
- "payout": {func(g *Game, s *net.Session, args []string, _ string) {
- g.executeTableAction(s, "payout "+strings.Join(args, " "))
- }, ClassInstant},
"roll": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "roll") }, ClassActive},
"autoroll": {func(g *Game, s *net.Session, _ []string, _ string) { g.executeTableAction(s, "autoroll") }, ClassInstant},
"unbet": {func(g *Game, s *net.Session, args []string, _ string) {
@@ -241,6 +238,42 @@ func classifyCommand(cmd string) CommandClass {
return ClassUnknown
}
+// casinoShortVerbs are the casino verbs a seated player may abbreviate to an
+// unambiguous prefix. "stop" and "rules" are deliberately absent: their
+// prefixes (st, r, ru) would otherwise shadow stand and movement, so those two
+// commands must be typed in full.
+var casinoShortVerbs = []string{
+ "bet", "deal", "draw", "hold", "paytable", "denom", "denomination",
+ "hands", "feature", "game", "roll", "autoroll", "unbet", "table",
+ "status", "list", "watch", "sitout", "decline", "hit", "stand",
+ "double", "split", "insurance", "surrender", "spin", "autobet",
+ "autospin", "rebet",
+}
+
+// casinoAbbreviation resolves a seated casino player's unambiguous command
+// prefix to its full verb. An exact command (including the h/st/dd/sp aliases
+// and video poker's d) always wins, so a prefix never shadows a real command;
+// an ambiguous prefix resolves to nothing and falls through unchanged.
+func (g *Game) casinoAbbreviation(sess *net.Session, cmd string) (string, bool) {
+ if sess == nil || sess.Player == nil || cmd == "" || !g.inCasinoGame(sess.Player.Name) {
+ return "", false
+ }
+ if _, ok := commandRegistry[cmd]; ok {
+ return "", false
+ }
+ match := ""
+ for _, verb := range casinoShortVerbs {
+ if !strings.HasPrefix(verb, cmd) {
+ continue
+ }
+ if match != "" {
+ return "", false
+ }
+ match = verb
+ }
+ return match, match != ""
+}
+
func (g *Game) executeCommand(sess *net.Session, cmd string, args []string, rawInput string) {
p := sess.Player
diff --git a/internal/game/game.go b/internal/game/game.go
index 6066179..f1855dc 100644
--- a/internal/game/game.go
+++ b/internal/game/game.go
@@ -615,6 +615,15 @@ func (g *Game) handleGameCommand(sess *net.Session, input string) {
}
cmd := parts[0]
+ // Seated casino players may shorten any casino verb to an unambiguous
+ // prefix (b 1000 -> bet 1000). Exact commands always win, so movement and
+ // the existing short aliases are never shadowed.
+ if canon, ok := g.casinoAbbreviation(sess, cmd); ok {
+ cmd = canon
+ parts[0] = canon
+ input = strings.Join(parts, " ")
+ }
+
if len(parts) == 1 {
switch cmd {
case "equip", "eq", "equipment", "wear", "wield":
diff --git a/internal/game/vesta_world_test.go b/internal/game/vesta_world_test.go
index b719e24..3a2e320 100644
--- a/internal/game/vesta_world_test.go
+++ b/internal/game/vesta_world_test.go
@@ -293,7 +293,8 @@ func TestVestaCasinoFloorsAndVariants(t *testing.T) {
future++
}
count := len(r.CasinoTables) + len(r.CasinoMachines)
- // Individually addressed video-poker cabinets share a lounge.
+ // Individually addressed video-poker cabinets share their lounge;
+ // the parlor, high-limit and VIP rooms each hold the cabinet set.
// Multiplayer games still require their own salons.
for _, cfg := range r.CasinoTables {
if cfg.Game == casino.GameVideoPoker {
@@ -320,7 +321,7 @@ func TestVestaCasinoFloorsAndVariants(t *testing.T) {
classic = classic || machine.Variant == ""
}
}
- if games["blackjack"] != 5 || games["baccarat"] != 3 || games["roulette"] != 3 || games["craps"] != 7 || games["slots"] != 5 || games["video_poker"] != 5 || !soft17 || !surrender || !singleDeck || !huff || !classic {
+ if games["blackjack"] != 5 || games["baccarat"] != 3 || games["roulette"] != 3 || games["craps"] != 7 || games["slots"] != 5 || games["video_poker"] != 15 || !soft17 || !surrender || !singleDeck || !huff || !classic {
t.Errorf("casino variant coverage lost: %v", games)
}
if future != 0 {
diff --git a/internal/game/video_poker_cabinet_test.go b/internal/game/video_poker_cabinet_test.go
index a327bce..0e46c82 100644
--- a/internal/game/video_poker_cabinet_test.go
+++ b/internal/game/video_poker_cabinet_test.go
@@ -120,6 +120,27 @@ func TestPokerCorruptBankRefusesDeal(t *testing.T) {
}
}
+// TestVestaVideoPokerPayoutTiers pins the authored tiering: the parlor, high-limit
+// and VIP rooms each hold the full cabinet set at one house-set payout, and no
+// player command can change it.
+func TestVestaVideoPokerPayoutTiers(t *testing.T) {
+ _, rooms, _ := loadVesta(t)
+ for id, tier := range map[int]string{6192: "low", 6193: "normal", 6194: "full"} {
+ r, ok := rooms[id]
+ if !ok {
+ t.Fatalf("missing video poker room %d", id)
+ }
+ if len(r.CasinoTables) != 5 {
+ t.Fatalf("room %d has %d cabinets, want the full set of 5", id, len(r.CasinoTables))
+ }
+ for _, cfg := range r.CasinoTables {
+ if cfg.Game != casino.GameVideoPoker || cfg.VideoPoker.Payout != tier {
+ t.Fatalf("room %d/%s = %s/%s, want video_poker/%s", id, cfg.ID, cfg.Game, cfg.VideoPoker.Payout, tier)
+ }
+ }
+ }
+}
+
func TestPokerInterruptedTransactionRecovery(t *testing.T) {
g, sess, _ := casinoCrapsGame(t, uxTestRoom)
g.DataDir = t.TempDir()