aboutsummaryrefslogtreecommitdiff
path: root/internal/world/fix_regressions_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-19 01:55:34 -0400
committerhistoria <[not public]>2026-09-19 01:55:34 -0400
commitcc33072eced3d5460512c09c42ec4531c807d7af (patch)
treeb1b451fa87fa389cb8343a3645372f3b43f5523e /internal/world/fix_regressions_test.go
parent2c38386484cdb507c4687caceb37e0b963e33594 (diff)
downloadthehouseoficarus-cc33072eced3d5460512c09c42ec4531c807d7af.tar.gz
fix: casino stake preservation, world state reconciliation, casino game registration
Diffstat (limited to 'internal/world/fix_regressions_test.go')
-rw-r--r--internal/world/fix_regressions_test.go153
1 files changed, 153 insertions, 0 deletions
diff --git a/internal/world/fix_regressions_test.go b/internal/world/fix_regressions_test.go
new file mode 100644
index 0000000..f2ef795
--- /dev/null
+++ b/internal/world/fix_regressions_test.go
@@ -0,0 +1,153 @@
+package world
+
+import "testing"
+
+// TestEnsureObjectStatesDoesNotDuplicateWanderer: a wandering object leaves its
+// home room's key behind. Re-entering the home room must recognize the state by
+// HomeRoomID and not create a second, independent instance (which would make the
+// object visible/gatherable in two rooms and eventually silently discard one
+// state when they collide).
+func TestEnsureObjectStatesDoesNotDuplicateWanderer(t *testing.T) {
+ w := New(t.TempDir())
+ w.AddRoomPath(1, "room-1")
+ w.AddRoomPath(2, "room-2")
+ w.EnsureObjectStates(1, []string{"fishing_spot"})
+ st := w.GetObjState(1, "fishing_spot", 0)
+ if st == nil {
+ t.Fatal("setup: object state was not created")
+ }
+ st.WanderRooms = []int{2}
+ st.WanderInterval = 1
+ w.TickObjWander()
+
+ if w.GetObjState(2, "fishing_spot", 0) == nil {
+ t.Fatal("setup: the object did not wander to room 2")
+ }
+
+ // Re-enter the home room: it must not spawn a duplicate.
+ w.EnsureObjectStates(1, []string{"fishing_spot"})
+ if got := len(w.AllObjInstances(1)); got != 0 {
+ t.Fatalf("home room shows %d instances while the object wanders; want 0", got)
+ }
+ if got := len(w.AllObjInstances(2)); got != 1 {
+ t.Fatalf("wandered room shows %d instances; want 1", got)
+ }
+}
+
+// TestRemoveObjInstanceIndexRemovesOnlyNamed: a room may declare the same object
+// twice. Destroying one instance must remove exactly that index, not every
+// state for the defID (which erased an untouched sibling).
+func TestRemoveObjInstanceIndexRemovesOnlyNamed(t *testing.T) {
+ w := New(t.TempDir())
+ w.EnsureObjectStates(1, []string{"safespot", "safespot"})
+ if got := len(w.AllObjInstances(1)); got != 2 {
+ t.Fatalf("setup: expected two instances, got %d", got)
+ }
+
+ w.RemoveObjInstanceIndex(1, "safespot", 1)
+ got := w.AllObjInstances(1)
+ if len(got) != 1 {
+ t.Fatalf("removing index 1 left %d instances; want 1", len(got))
+ }
+ if got[0].Index != 0 {
+ t.Fatalf("removed the wrong instance: surviving index=%d, want 0", got[0].Index)
+ }
+}
+
+// TestReconcileRoomStateAddsPrunesAndPreserves: a live content edit must add
+// newly declared objects/spawns and drop removed ones, while preserving
+// dynamic instances (fires) and player-dropped ground items, and must not
+// double a spawn on a second reconcile.
+func TestReconcileRoomStateAddsPrunesAndPreserves(t *testing.T) {
+ w := New(t.TempDir())
+ w.EnsureObjectStates(1, []string{"anvil", "anvil"})
+ w.AddObjInstance(1, "fire", 5) // dynamic instance, not in the room file
+ w.AddGroundItem(1, "loot", 1) // player-dropped, not a spawn
+ w.mu.Lock()
+ w.seeded[1] = true
+ w.mu.Unlock()
+
+ room := &Room{
+ ID: 1,
+ Objects: []RoomObject{{ID: "anvil"}},
+ ItemSpawns: []SpawnDef{{ID: "ore", Quantity: 3, RespawnTicks: 5}},
+ }
+ w.ReconcileRoomState(1, room)
+
+ anvils, fires := 0, 0
+ for _, st := range w.AllObjInstances(1) {
+ switch st.DefID {
+ case "anvil":
+ anvils++
+ case "fire":
+ fires++
+ }
+ }
+ if anvils != 1 {
+ t.Fatalf("prune left %d anvils; want 1", anvils)
+ }
+ if fires != 1 {
+ t.Fatalf("reconcile pruned the dynamic fire instance (fires=%d)", fires)
+ }
+ ground := w.AccessibleGround(1, "alice")
+ if ground["ore"] != 3 {
+ t.Fatalf("new item spawn quantity = %d, want 3", ground["ore"])
+ }
+ if ground["loot"] != 1 {
+ t.Fatalf("reconcile destroyed a player-dropped item (loot=%d)", ground["loot"])
+ }
+
+ // A second reconcile must be idempotent (no doubled spawn).
+ w.ReconcileRoomState(1, room)
+ if got := w.AccessibleGround(1, "alice")["ore"]; got != 3 {
+ t.Fatalf("second reconcile doubled the spawn: ore=%d, want 3", got)
+ }
+
+ // Removing the spawn from the definition drops its ground entry.
+ w.ReconcileRoomState(1, &Room{ID: 1, Objects: []RoomObject{{ID: "anvil"}}})
+ if got := w.AccessibleGround(1, "alice")["ore"]; got != 0 {
+ t.Fatalf("removed item spawn still on the ground: ore=%d", got)
+ }
+}
+
+// TestReconcileRoomStateLeavesVisitingWanderer: reconciling a room must not
+// prune an object that merely wandered in from a different home room.
+func TestReconcileRoomStateLeavesVisitingWanderer(t *testing.T) {
+ w := New(t.TempDir())
+ w.AddRoomPath(1, "room-1")
+ w.AddRoomPath(2, "room-2")
+ w.EnsureObjectStates(1, []string{"fishing_spot"})
+ st := w.GetObjState(1, "fishing_spot", 0)
+ st.WanderRooms = []int{2}
+ st.WanderInterval = 1
+ w.TickObjWander()
+ if w.GetObjState(2, "fishing_spot", 0) == nil {
+ t.Fatal("setup: the object did not wander to room 2")
+ }
+
+ // Room 2 declares nothing; its reconcile must leave the visitor alone.
+ w.ReconcileRoomState(2, &Room{ID: 2})
+ if w.GetObjState(2, "fishing_spot", 0) == nil {
+ t.Fatal("reconcile pruned a wanderer visiting from another home room")
+ }
+}
+
+// TestRekeyRoomsRemapsHomeRoomID: identity is anchored to HomeRoomID, so a room
+// rekey must remap it alongside RoomID or a wanderer can be duplicated after
+// the rename.
+func TestRekeyRoomsRemapsHomeRoomID(t *testing.T) {
+ w := New(t.TempDir())
+ w.EnsureObjectStates(1, []string{"fishing_spot"})
+ st := w.GetObjState(1, "fishing_spot", 0)
+ if st.HomeRoomID != 1 {
+ t.Fatalf("setup: HomeRoomID=%d, want 1", st.HomeRoomID)
+ }
+ w.RekeyRooms(map[int]int{1: 3})
+ moved := w.GetObjState(3, "fishing_spot", 0)
+ if moved == nil {
+ t.Fatal("object state lost after rekey")
+ }
+ if moved.HomeRoomID != 3 {
+ t.Fatalf("HomeRoomID after rekey = %d, want 3", moved.HomeRoomID)
+ }
+}