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
|
package game
import (
"os"
"path/filepath"
"strings"
"testing"
"thehouseoficarus/internal/behavior"
"thehouseoficarus/internal/casino"
"thehouseoficarus/internal/combat"
"thehouseoficarus/internal/item"
"thehouseoficarus/internal/net"
"thehouseoficarus/internal/player"
"thehouseoficarus/internal/world"
)
// regressionGame builds a minimal Game with real stores over a temp data dir.
func regressionGame(t *testing.T) (*Game, *net.Session) {
t.Helper()
dir := t.TempDir()
for _, sub := range []string{"items", "rooms"} {
if err := os.MkdirAll(filepath.Join(dir, sub), 0o755); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(dir, "items", "chips.yaml"), []byte("name: chips\nstackable: true\n"), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "rooms", "1.yaml"), []byte("name: test\n"), 0o644); err != nil {
t.Fatal(err)
}
g := &Game{
Deps: Deps{
World: world.New(dir),
ItemStore: item.NewItemStore(dir),
AccountStore: player.NewAccountStore(dir),
},
Casino: casino.NewManager(1),
Combat: combat.NewTracker(),
}
sess := &net.Session{Player: player.New("alice"), Conn: &casinoTestConn{}}
sess.Player.RoomID = 1
return g, sess
}
// Regression: a stackable withdrawal must succeed via stack merging even
// when the inventory has zero free slots.
func TestBankWithdrawMergesStackIntoFullInventory(t *testing.T) {
g, sess := regressionGame(t)
for i := 0; i < 28; i++ {
sess.Player.SetInvSlot(i, &player.InventorySlot{ItemID: "filler", Quantity: 1})
}
sess.Player.SetInvSlot(0, &player.InventorySlot{ItemID: "chips", Quantity: 10})
sess.Player.SetBankSlot(0, &player.InventorySlot{ItemID: "chips", Quantity: 5})
g.doBankWithdraw(sess, "1")
inv := sess.Player.InvSlot(0)
if inv == nil || inv.Quantity != 15 {
t.Fatalf("stack withdrawal blocked by a full inventory: inventory=%+v", inv)
}
if sess.Player.BankSlot(0) != nil {
t.Fatal("bank slot was not cleared after the withdrawal")
}
}
// Regression: stacks of the same item but different quality must not merge,
// which would silently destroy the withdrawn stack's properties.
func TestBankWithdrawKeepsIncompatibleQualitySeparate(t *testing.T) {
g, sess := regressionGame(t)
sess.Player.SetInvSlot(0, &player.InventorySlot{ItemID: "chips", Quantity: 10, Quality: 3, MaxQuality: 10})
sess.Player.SetBankSlot(0, &player.InventorySlot{ItemID: "chips", Quantity: 5, Quality: 8, MaxQuality: 10})
g.doBankWithdraw(sess, "1")
first, second := sess.Player.InvSlot(0), sess.Player.InvSlot(1)
if first == nil || first.Quantity != 10 || first.Quality != 3 {
t.Fatalf("existing stack was disturbed: %+v", first)
}
if second == nil || second.Quantity != 5 || second.Quality != 8 || second.MaxQuality != 10 {
t.Fatalf("incompatible stack was merged or metadata lost: first=%+v second=%+v", first, second)
}
}
// Regression: sequenced transport modules (like transport_home) bypass
// triggerTransport entirely; the casino gate must also apply when the module
// is started.
func TestSequencedTransportBlockedWhilePlaying(t *testing.T) {
g, sess := regressionGame(t)
oldMods, oldIndex := AllMods, modByID
t.Cleanup(func() { AllMods, modByID = oldMods, oldIndex })
mod := &ModDef{
ID: "transport_home", Name: "transport home", Category: ModTransport,
Level: 1, Destination: 2,
Sequence: []behavior.ModTriggerStep{{Message: "wait", Delay: 1}},
}
AllMods = []*ModDef{mod}
modByID = map[string]*ModDef{mod.ID: mod}
g.Casino.JoinMachine(1, casino.MachineConfig{ID: "slots", Game: casino.GameSlots}, sess.Player.Name)
conn := sess.Conn.(*casinoTestConn)
conn.data = nil
g.doTrigger(sess, "transport_home")
if sess.Player.Action != nil {
t.Fatalf("transport sequence scheduled despite casino participation: %+v", sess.Player.Action)
}
if !strings.Contains(string(conn.data), "playing a game") {
t.Fatalf("missing casino-gate message: %q", string(conn.data))
}
}
|