diff options
| author | historia <[not public]> | 2026-09-19 01:55:34 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-19 01:55:34 -0400 |
| commit | cc33072eced3d5460512c09c42ec4531c807d7af (patch) | |
| tree | b1b451fa87fa389cb8343a3645372f3b43f5523e /internal/world/fix_regressions_test.go | |
| parent | 2c38386484cdb507c4687caceb37e0b963e33594 (diff) | |
| download | thehouseoficarus-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.go | 153 |
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) + } +} |
