aboutsummaryrefslogtreecommitdiff
path: root/internal/game
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 /internal/game
parent299b5b67aa80ba2539d7a91c60943060b0426b45 (diff)
downloadthehouseoficarus-d4eae333a8d8a96452bbf3617aa85b50466c11a7.tar.gz
feat: concise table/machine names, casino command shortcuts, remove payout video poker command
Diffstat (limited to 'internal/game')
-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
6 files changed, 145 insertions, 6 deletions
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()