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) } }