From c6d4702c0be4d2b37e1d82ff96265c52847c3f9a Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sat, 3 Oct 2026 17:36:06 -0400 Subject: feat: partial commands now always work --- internal/game/cmd_prefix_test.go | 125 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 internal/game/cmd_prefix_test.go (limited to 'internal/game/cmd_prefix_test.go') diff --git a/internal/game/cmd_prefix_test.go b/internal/game/cmd_prefix_test.go new file mode 100644 index 0000000..8f42a6f --- /dev/null +++ b/internal/game/cmd_prefix_test.go @@ -0,0 +1,125 @@ +package game + +import ( + "testing" + + "thehouseoficarus/internal/player" +) + +// TestResolveCommandPrefix covers the general (non-casino) prefix expansion: +// unique prefixes run, exact commands and ambiguous prefixes do not. +func TestResolveCommandPrefix(t *testing.T) { + plain, sess, _ := houseEdgeGame(t, "name: Nowhere\n") + + if got, ok := plain.resolveCommandPrefix(sess, "inven"); !ok || got != "inventory" { + t.Fatalf("inven -> (%q,%v), want inventory", got, ok) + } + if got, ok := plain.resolveCommandPrefix(sess, "scor"); !ok || got != "score" { + t.Fatalf("scor -> (%q,%v), want score", got, ok) + } + // An exact command is dispatched by the registry and must not be rewritten. + if _, ok := plain.resolveCommandPrefix(sess, "look"); ok { + t.Fatal("exact command was rewritten") + } + // The play gateway is runnable anywhere, so it breaks the play/plant tie + // even without a casino fixture. + if got, ok := plain.resolveCommandPrefix(sess, "pl"); !ok || got != "play" { + t.Fatalf("pl -> (%q,%v), want play", got, ok) + } + if got, ok := plain.resolveCommandPrefix(sess, "plan"); !ok || got != "plant" { + t.Fatalf("plan -> (%q,%v), want plant", got, ok) + } + // A unique one-letter prefix runs: "k" can only mean kill. + if got, ok := plain.resolveCommandPrefix(sess, "k"); !ok || got != "kill" { + t.Fatalf("k -> (%q,%v), want kill", got, ok) + } + // An exact one-letter command is still handled by the registry. + if _, ok := plain.resolveCommandPrefix(sess, "i"); ok { + t.Fatal("exact single-letter command was rewritten") + } + // A many-way one-letter prefix stays ambiguous. + if _, ok := plain.resolveCommandPrefix(sess, "q"); ok { + t.Fatal("ambiguous one-letter prefix resolved") + } + // Nothing matches. + if _, ok := plain.resolveCommandPrefix(sess, "zz"); ok { + t.Fatal("unknown prefix resolved") + } +} + +// TestCommandPrefixCasinoTieBreak verifies that the room's advertised verbs +// break a general prefix tie, and that the play gateway resolves whether or +// not the room actually holds a table. +func TestCommandPrefixCasinoTieBreak(t *testing.T) { + g, sess, _ := houseEdgeGame(t, "name: Wheel\ncasino_tables:\n - id: big6\n game: big6\n") + for _, in := range []string{"pl", "pla"} { + if got, ok := g.resolveCommandPrefix(sess, in); !ok || got != "play" { + t.Fatalf("%s at a casino -> (%q,%v), want play", in, got, ok) + } + } + // The full word still resolves to the other command. + if got, ok := g.resolveCommandPrefix(sess, "plan"); !ok || got != "plant" { + t.Fatalf("plan -> (%q,%v), want plant", got, ok) + } + + // The same break happens on the real dispatch path. + g.queue = NewCommandQueue() + g.handleGameCommand(sess, "pl") + if qc, ok := g.queue.PeekActive("alice"); !ok || qc.Command != "play" { + t.Fatalf("pl queued %+v (ok=%v), want play", qc, ok) + } +} + +// TestCommandPrefixSingleLetterDispatch checks the user-facing case that a +// one-letter prefix works when it can only mean one command. +func TestCommandPrefixSingleLetterDispatch(t *testing.T) { + g, sess, _ := houseEdgeGame(t, "name: Nowhere\n") + g.queue = NewCommandQueue() + g.handleGameCommand(sess, "k man") + qc, ok := g.queue.PeekActive("alice") + if !ok || qc.Command != "kill" || qc.Args != "man" { + t.Fatalf("k man queued %+v (ok=%v), want kill man", qc, ok) + } +} + +// TestCommandPrefixRespectsAliases pins the alias/prefix relationship: +// a prefix never expands an alias, but an exact alias always wins before any +// prefix resolution. +func TestCommandPrefixRespectsAliases(t *testing.T) { + // Alias "kim" only shares a prefix with "ki". Typing "ki man" must not + // run the alias, and must not let it block the kill prefix either. + g, sess, _ := houseEdgeGame(t, "name: Nowhere\n") + g.queue = NewCommandQueue() + sess.Account = &player.Account{Name: "alice", Aliases: map[string]string{"kim": "drop sword"}} + g.handleGameCommand(sess, "ki man") + qc, ok := g.queue.PeekActive("alice") + if !ok || qc.Command != "kill" || qc.Args != "man" { + t.Fatalf("ki man with alias kim queued %+v (ok=%v), want kill man", qc, ok) + } + + // Exact alias "ki" wins outright: the kill prefix is never considered. + g2, sess2, _ := houseEdgeGame(t, "name: Nowhere\n") + g2.queue = NewCommandQueue() + sess2.Account = &player.Account{Name: "alice", Aliases: map[string]string{"ki": "drop sword"}} + g2.handleGameCommand(sess2, "ki man") + qc2, ok := g2.queue.PeekActive("alice") + if !ok || qc2.Command != "drop" || qc2.Args != "sword man" { + t.Fatalf("exact alias ki queued %+v (ok=%v), want drop sword man", qc2, ok) + } +} + +// TestCommandPrefixSeatedCasinoStaysScoped guards the existing rule that a +// seated casino player only abbreviates their own game's verbs: "spl" must +// not fall through the general resolver to blackjack's split at Big 6. +func TestCommandPrefixSeatedCasinoStaysScoped(t *testing.T) { + g, sess, _ := houseEdgeGame(t, "name: Wheel\ncasino_tables:\n - id: big6\n game: big6\n") + g.queue = NewCommandQueue() + g.executePlay(sess, []string{"big6"}, "play big6") + if _, playing := g.Casino.Participation(sess.Player.Name); !playing { + t.Fatal("join failed") + } + g.handleGameCommand(sess, "spl") + if qc, ok := g.queue.PeekActive("alice"); ok { + t.Fatalf("seated spl queued %+v, want nothing", qc) + } +} -- cgit v1.2.3