From d4eae333a8d8a96452bbf3617aa85b50466c11a7 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 1 Oct 2026 05:05:11 -0400 Subject: feat: concise table/machine names, casino command shortcuts, remove payout video poker command --- building_guide/video_poker.md | 32 ++++++----- data/help/bonus_poker.yaml | 2 +- data/help/bonus_poker_deluxe.yaml | 2 +- data/help/deuces_wild.yaml | 2 +- data/help/double_bonus.yaml | 2 +- data/help/double_double_bonus.yaml | 2 +- data/help/hot_roll.yaml | 4 +- data/help/jacks_or_better.yaml | 2 +- data/help/joker_poker.yaml | 2 +- data/help/super_times_pay.yaml | 4 +- data/help/ultimate_x_multi_hand.yaml | 4 +- data/help/ultimate_x_single_line.yaml | 4 +- data/help/video_poker.yaml | 17 +++--- data/rooms/casino/6192.yaml | 33 +++++++----- data/rooms/casino/6193.yaml | 46 ++++++++++++++++ data/rooms/casino/6194.yaml | 45 ++++++++++++++++ internal/casino/video_poker.go | 21 +------- internal/casino/video_poker_config.go | 18 +++++++ internal/casino/video_poker_lifecycle_test.go | 14 ++--- internal/casino/video_poker_regression_test.go | 16 +++--- internal/game/casino_ux.go | 3 +- internal/game/casino_ux_test.go | 74 ++++++++++++++++++++++++++ internal/game/cmd_registry.go | 39 ++++++++++++-- internal/game/game.go | 9 ++++ internal/game/vesta_world_test.go | 5 +- internal/game/video_poker_cabinet_test.go | 21 ++++++++ 26 files changed, 336 insertions(+), 87 deletions(-) create mode 100644 data/rooms/casino/6193.yaml create mode 100644 data/rooms/casino/6194.yaml 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 ` 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 ` 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 selects one. payout low|normal|full sets - the same quality tier across games. paytable shows the exact schedule. + game lists games; game 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 selects one. payout low|normal|full sets - the same quality tier across games. paytable shows the exact schedule. + game lists games; game 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 selects one. payout low|normal|full sets - the same quality tier across games. paytable shows the exact schedule. + game lists games; game 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 selects one. payout low|normal|full sets - the same quality tier across games. paytable shows the exact schedule. + game lists games; game 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 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 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 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 . 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 ", 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 ", 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 , payout low|normal|full, denom 1|10|50, hands , paytable, or feature on|off on a bonus cabinet.")} + return []Event{t.private(name, "Use bet [1-4|max], deal, game , denom 1|10|50, hands , 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() -- cgit v1.2.3