diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/casino/video_poker.go | 21 | ||||
| -rw-r--r-- | internal/casino/video_poker_config.go | 18 | ||||
| -rw-r--r-- | internal/casino/video_poker_lifecycle_test.go | 14 | ||||
| -rw-r--r-- | internal/casino/video_poker_regression_test.go | 16 | ||||
| -rw-r--r-- | internal/game/casino_ux.go | 3 | ||||
| -rw-r--r-- | internal/game/casino_ux_test.go | 74 | ||||
| -rw-r--r-- | internal/game/cmd_registry.go | 39 | ||||
| -rw-r--r-- | internal/game/game.go | 9 | ||||
| -rw-r--r-- | internal/game/vesta_world_test.go | 5 | ||||
| -rw-r--r-- | internal/game/video_poker_cabinet_test.go | 21 |
10 files changed, 179 insertions, 41 deletions
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() |
