aboutsummaryrefslogtreecommitdiff
path: root/internal/game/bank_transport_regression_test.go
blob: 529df8437950f869f44c31f4836fc9c5abc4bdbe (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
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))
	}
}