diff options
| author | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
| commit | 153acb7f644d755c45533b7c5311e3ffa47e8973 (patch) | |
| tree | 7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/game/correct_quality_merge_test.go | |
| parent | 87d08f0151e643951769e323995f1c5ed89ee200 (diff) | |
| download | thehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz | |
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/game/correct_quality_merge_test.go')
| -rw-r--r-- | internal/game/correct_quality_merge_test.go | 201 |
1 files changed, 201 insertions, 0 deletions
diff --git a/internal/game/correct_quality_merge_test.go b/internal/game/correct_quality_merge_test.go new file mode 100644 index 0000000..d54c8c9 --- /dev/null +++ b/internal/game/correct_quality_merge_test.go @@ -0,0 +1,201 @@ +package game + +// Regression: every stack-creation and stack-merge site must agree on the +// Quality/MaxQuality a fresh stack gets (newInventorySlot / stackProps). +// Pickups merge only into a stack with those exact properties, so a stack +// produced by crafting, harvesting, or ammo removal with Quality 0 could no +// longer be merged into - a full inventory then refused a pickup of an item +// the player already carried. + +import ( + "path/filepath" + "testing" + + "thehouseoficarus/internal/item" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +const correctQualityItemYAML = `id: correct_quality_gem +name: correct quality gem +stackable: true +max_quality: 5 +craft: + - type: crafting + ingredients: + - items: + - correct_quality_shard + quantity: 1 + xp: 1 + ticks_per_cycle: 1 +` + +func seedCorrectQualityItems(t *testing.T, h *harness) { + t.Helper() + writeSeedFile(t, filepath.Join(h.dir, "items", "correct_quality_gem.yaml"), []byte(correctQualityItemYAML)) + writeSeedFile(t, filepath.Join(h.dir, "items", "correct_quality_shard.yaml"), + []byte("id: correct_quality_shard\nname: correct quality shard\nstackable: true\nmax_quality: 5\nvalue: 1\n")) +} + +// fillInventoryExcept occupies every slot but slot 0 with filler. +func fillInventoryExcept(p *player.Player) { + for i := 0; i < 28; i++ { + if i != 0 { + p.SetInvSlot(i, &player.InventorySlot{ItemID: "filler", Quantity: 1}) + } + } +} + +// TestStackPropsMatchNewInventorySlot guards the one rule that stack creation +// and stack merging both depend on: the helper the craft/harvest/ammo sites +// use must stamp what newInventorySlot stamps. +func TestStackPropsMatchNewInventorySlot(t *testing.T) { + h := newHarness(t) + seedCorrectQualityItems(t, h) + h.g.RunOnOwner(func() { + h.g.ItemStore.Reload(h.dir) + gem := h.g.newInventorySlot("correct_quality_gem", 1) + if q, mq := stackProps(mustLoadItem(t, h.g, "correct_quality_gem")); q != gem.Quality || mq != gem.MaxQuality { + t.Errorf("stackProps = (%d,%d), newInventorySlot = (%d,%d)", q, mq, gem.Quality, gem.MaxQuality) + } + shard := h.g.newInventorySlot("correct_quality_shard", 1) + if q, mq := stackProps(mustLoadItem(t, h.g, "correct_quality_shard")); q != shard.Quality || mq != shard.MaxQuality { + t.Errorf("quality-free stackProps = (%d,%d), newInventorySlot = (%d,%d)", q, mq, shard.Quality, shard.MaxQuality) + } + }) +} + +func TestCraftOutputMergesIntoQualityStack(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + p := h.seedCharacter("alice", "alice") + seedCorrectQualityItems(t, h) + + h.g.RunOnOwner(func() { + h.g.ItemStore.Reload(h.dir) + def := mustLoadItem(t, h.g, "correct_quality_gem") + p.SetInvSlot(0, &player.InventorySlot{ItemID: def.ID, Quantity: 10, Quality: 5, MaxQuality: 5}) + fillInventoryExcept(p) + + if !craftOutputFits(p, def.ID, def) { + t.Fatal("a full inventory must accept a craft output that merges into an identical stack") + } + if !placeCraftOutput(p, def.ID, def, 2) { + t.Fatal("craft output was not placed") + } + if slot := p.InvSlot(0); slot.Quantity != 12 || slot.Quality != 5 || slot.MaxQuality != 5 { + t.Errorf("merged stack = %+v, want quantity 12 at quality 5", slot) + } + + // A differently qualified stack is not a merge target: the same + // full inventory must report that nothing fits. + p.SetInvSlot(0, &player.InventorySlot{ItemID: def.ID, Quantity: 10, Quality: 1, MaxQuality: 5}) + if craftOutputFits(p, def.ID, def) { + t.Error("craft output would merge into a stack with a different quality") + } + if placeCraftOutput(p, def.ID, def, 2) { + t.Error("craft output was placed with no free slot and no matching stack") + } + }) +} + +func TestCraftOutputCreatesStackWithFreshProps(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + p := h.seedCharacter("alice", "alice") + seedCorrectQualityItems(t, h) + + h.g.RunOnOwner(func() { + h.g.ItemStore.Reload(h.dir) + def := mustLoadItem(t, h.g, "correct_quality_gem") + if !placeCraftOutput(p, def.ID, def, 3) { + t.Fatal("craft output was not placed in an empty inventory") + } + slot := p.InvSlot(0) + if slot == nil || slot.ItemID != def.ID { + t.Fatalf("slot 0 = %+v", slot) + } + if q, mq := h.g.pickedStackProps(def.ID); slot.Quality != q || slot.MaxQuality != mq { + t.Fatalf("crafted stack %+v does not match what a pickup receives (%d,%d)", slot, q, mq) + } + }) +} + +func TestPickupMergesIntoHarvestedStackWhenFull(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + p := h.seedCharacter("alice", "alice") + writeSeedFile(t, filepath.Join(h.dir, "items", hardeningCropID+".yaml"), + []byte("id: test_potato\nname: test potato\nstackable: true\nmax_quality: 5\n")) + + conn := newChanConn() + sess := &net.Session{Conn: conn, Player: p} + sess.SetState(net.StateGame) + + h.g.RunOnOwner(func() { + h.g.ItemStore.Reload(h.dir) + readyHarvest(p, 3, 3) + h.g.advanceHarvest(sess, p) + + stack := p.InvSlot(0) + if stack == nil || stack.ItemID != hardeningCropID { + t.Fatalf("harvested stack = %+v", stack) + } + quality, maxQuality := h.g.pickedStackProps(hardeningCropID) + if stack.Quality != quality || stack.MaxQuality != maxQuality { + t.Fatalf("harvested stack %+v does not match what a pickup receives (%d,%d)", stack, quality, maxQuality) + } + + // A full inventory must still merge a ground pickup into the stack + // the harvest just created. + fillInventoryExcept(p) + h.g.World.AddGroundItem(p.RoomID, hardeningCropID, 2) + h.g.doGet(sess, "test potato") + + if got := p.CountItem(hardeningCropID); got != 5 { + t.Errorf("potatoes carried = %d, want 5", got) + } + if got := h.g.World.GroundItems(p.RoomID)[hardeningCropID]; got != 0 { + t.Errorf("ground potatoes = %d, want 0 (they fit the existing stack)", got) + } + }) +} + +func TestPickupMergesIntoAmmoReturnedStack(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + p := h.seedCharacter("alice", "alice") + seedCorrectQualityItems(t, h) + + conn := newChanConn() + sess := &net.Session{Conn: conn, Player: p} + sess.SetState(net.StateGame) + + h.g.RunOnOwner(func() { + h.g.ItemStore.Reload(h.dir) + p.Equipment[item.SlotAmmo] = "correct_quality_shard" + p.AmmoQty = 4 + if !h.g.unequipAmmo(p) { + t.Fatal("unequipAmmo refused an empty inventory") + } + stack := p.InvSlot(0) + if stack == nil || stack.ItemID != "correct_quality_shard" { + t.Fatalf("returned ammo stack = %+v", stack) + } + quality, maxQuality := h.g.pickedStackProps("correct_quality_shard") + if stack.Quality != quality || stack.MaxQuality != maxQuality { + t.Fatalf("returned ammo stack %+v does not match what a pickup receives (%d,%d)", stack, quality, maxQuality) + } + + fillInventoryExcept(p) + h.g.World.AddGroundItem(p.RoomID, "correct_quality_shard", 3) + h.g.doGet(sess, "correct quality shard") + + if got := p.CountItem("correct_quality_shard"); got != 7 { + t.Errorf("shards carried = %d, want 7", got) + } + if got := h.g.World.GroundItems(p.RoomID)["correct_quality_shard"]; got != 0 { + t.Errorf("ground shards = %d, want 0 (they fit the returned stack)", got) + } + }) +} |
