aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_prefix_test.go
blob: 8f42a6faf6cd50abc08e826b33aa7fb69ca9b62a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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)
	}
}