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