aboutsummaryrefslogtreecommitdiff
path: root/internal/world/insert_remove_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/world/insert_remove_test.go')
-rw-r--r--internal/world/insert_remove_test.go186
1 files changed, 186 insertions, 0 deletions
diff --git a/internal/world/insert_remove_test.go b/internal/world/insert_remove_test.go
new file mode 100644
index 0000000..3be3222
--- /dev/null
+++ b/internal/world/insert_remove_test.go
@@ -0,0 +1,186 @@
+package world
+
+import "testing"
+
+// insertSentinel is the hypothetical new-room ID passed to InsertGridConflicts
+// during tests. It must be a positive value not used by any real room in the
+// fixture (BuildGrid skips exits whose target <= 0).
+const insertSentinel = 9999
+
+func TestInsertGridConflictsCleanNSEW(t *testing.T) {
+ dir := t.TempDir()
+ writeGridTestRoom(t, dir, 1, "exits:\n east: 2\n")
+ writeGridTestRoom(t, dir, 2, "exits:\n west: 1\n")
+ w := New(dir)
+ if c := InsertGridConflicts(1, East, 2, insertSentinel, loadGridRoom(w)); len(c) != 0 {
+ t.Errorf("clean east insert: expected no conflicts, got %+v", c)
+ }
+}
+
+func TestInsertGridConflictsCleanDiagonal(t *testing.T) {
+ dir := t.TempDir()
+ writeGridTestRoom(t, dir, 1, "exits:\n northeast: 2\n")
+ writeGridTestRoom(t, dir, 2, "exits:\n southwest: 1\n")
+ w := New(dir)
+ if c := InsertGridConflicts(1, Northeast, 2, insertSentinel, loadGridRoom(w)); len(c) != 0 {
+ t.Errorf("clean NE insert: expected no conflicts, got %+v", c)
+ }
+}
+
+func TestInsertGridConflictsCleanUpDown(t *testing.T) {
+ dir := t.TempDir()
+ writeGridTestRoom(t, dir, 1, "exits:\n up: 2\n")
+ writeGridTestRoom(t, dir, 2, "exits:\n down: 1\n")
+ w := New(dir)
+ if c := InsertGridConflicts(1, Up, 2, insertSentinel, loadGridRoom(w)); len(c) != 0 {
+ t.Errorf("clean up insert: expected no conflicts, got %+v", c)
+ }
+}
+
+// TestInsertGridConflictsOverlap: inserting on 1→east→2 would push room 3
+// (beyond 2 to the east) from (2,0,0) to (3,0,0); a separate room 4 already at
+// (3,0,0) via 1→north→5→east→4 collides with the pushed 3.
+//
+// 1 east→2 east→3 (3 at (2,0,0), pushed to (3,0,0))
+// 1 north→5 east→4 (4 at (1,-1,0)? no: 5 at (0,-1,0), 4 at (1,-1,0))
+//
+// To land 4 at (3,0,0) we use 1→east is the insert axis, so place the blocker
+// via a longer chain that resolves to (3,0,0): 1→north→5→east→6→east→7 lands
+// 7 at (2,-1,0), not (3,0,0). Instead use a clean ring: 1→east→2 is the axis,
+// and put 4 at (2,0,0) (3's current spot) — no, 3 is there.
+//
+// Simplest reliable overlap: 1→east→2→east→3 and 1→south→6→east→7→north→3 is
+// a twist (3 reachable two ways). For an *overlap* on push, the pushed 3 must
+// collide with a room NOT in the beyond-set. Place room 8 at (3,0,0) via a
+// path that does not pass through 2: 1→north→5→north→8? that is (0,-2,0).
+//
+// Correct geometry for an overlap: 1 east→2 east→3 (3 at (2,0,0)). Insert on
+// 1→east pushes 2→(2,0,0)? No — insert pushes the *beyond* set (rooms beyond
+// the inserted room), i.e. 2 and everything reachable from 2 without going
+// back through 1. Inserting between 1 and 2 pushes 2 (and 3) east by one:
+// 2→(2,0,0), 3→(3,0,0). So we need a room already at (3,0,0) reachable from 1
+// without using 1→east. 1→south→4→east→5→east→6 lands 6 at (2,1,0). Not it.
+// 1→south→4→east→5→north→6 lands 6 at (1,0,0)? 4 at (0,1,0), 5 at (1,1,0),
+// 6 at (1,0,0) — that's where 2 currently is → overlap with 2 after push? 2
+// pushes to (2,0,0), 6 stays at (1,0,0): no collision. We need (3,0,0).
+// 1→south→4→east→5→north→6→east→7: 4(0,1,0) 5(1,1,0) 6(1,0,0) 7(2,0,0) =
+// collides with 3's *current* (2,0,0) → that's a pre-existing overlap (invalid
+// world), not what we want.
+//
+// Use a longer chain to (3,0,0): 1→south→4(0,1,0)→east→5(1,1,0)→east→6(2,1,0)
+// →north→7(2,0,0) collides with 3. So reach (3,0,0): 1→s→4→e→5→e→6→e→7(3,1,0)
+// →n→8(3,0,0). Then pushing 3 to (3,0,0) collides with 8. 8 is not in the
+// beyond-set (reachable from 1 via south, not via east/2), so it's an overlap.
+func TestInsertGridConflictsOverlap(t *testing.T) {
+ dir := t.TempDir()
+ writeGridTestRoom(t, dir, 1, "exits:\n east: 2\n south: 4\n")
+ writeGridTestRoom(t, dir, 2, "exits:\n east: 3\n west: 1\n")
+ writeGridTestRoom(t, dir, 3, "exits:\n west: 2\n")
+ writeGridTestRoom(t, dir, 4, "exits:\n east: 5\n north: 1\n")
+ writeGridTestRoom(t, dir, 5, "exits:\n east: 6\n west: 4\n")
+ writeGridTestRoom(t, dir, 6, "exits:\n east: 7\n west: 5\n")
+ writeGridTestRoom(t, dir, 7, "exits:\n north: 8\n west: 6\n")
+ writeGridTestRoom(t, dir, 8, "exits:\n south: 7\n")
+ w := New(dir)
+
+ // Sanity: the pre-edit world must be clean.
+ if c := BuildGridConflicts(1, loadGridRoom(w)); len(c) != 0 {
+ t.Fatalf("precondition: world should be clean, got %+v", c)
+ }
+ c := InsertGridConflicts(1, East, 2, insertSentinel, loadGridRoom(w))
+ if !hasConflictKind(c, "overlap") {
+ t.Errorf("expected overlap from pushing 3 onto 8's cell, got %+v", c)
+ }
+}
+
+// TestInsertGridConflictsTwist: room 3 beyond 2 is also reachable from 1 via a
+// separate path, so after the push 3 would have two candidate positions.
+// 1 east→2 east→3 (3 at (2,0,0), pushed to (3,0,0))
+// 1 south→4 east→5 north→3 (3 at (1,0,0))
+// 3 is reachable two ways already → the pre-edit world is itself a twist. The
+// insert helper must surface a twist conflict.
+func TestInsertGridConflictsTwist(t *testing.T) {
+ dir := t.TempDir()
+ writeGridTestRoom(t, dir, 1, "exits:\n east: 2\n south: 4\n")
+ writeGridTestRoom(t, dir, 2, "exits:\n east: 3\n west: 1\n")
+ writeGridTestRoom(t, dir, 3, "exits:\n west: 2\n south: 5\n")
+ writeGridTestRoom(t, dir, 4, "exits:\n east: 5\n north: 1\n")
+ writeGridTestRoom(t, dir, 5, "exits:\n north: 3\n west: 4\n")
+ w := New(dir)
+
+ c := InsertGridConflicts(1, East, 2, insertSentinel, loadGridRoom(w))
+ if !hasConflictKind(c, "twist") {
+ t.Errorf("expected a twist conflict, got %+v", c)
+ }
+}
+
+// TestInsertGridConflictsOneWayFarReciprocal: when the far room's opposite
+// exit does NOT point back at the source, insert performs a one-way insertion
+// (only the near side is rewired). The helper must model that and still allow a
+// geometrically clean insert.
+func TestInsertGridConflictsOneWayFarReciprocal(t *testing.T) {
+ dir := t.TempDir()
+ writeGridTestRoom(t, dir, 1, "exits:\n east: 2\n")
+ writeGridTestRoom(t, dir, 2, "exits:\n") // no west exit back to 1
+ w := New(dir)
+ if c := InsertGridConflicts(1, East, 2, insertSentinel, loadGridRoom(w)); len(c) != 0 {
+ t.Errorf("one-way insert should be clean, got %+v", c)
+ }
+}
+
+func TestRemoveGridConflictsCleanPull(t *testing.T) {
+ dir := t.TempDir()
+ // 1 east→2 east→3, with reciprocals. Removing 2 pulls 3 to (1,0,0). Clean.
+ writeGridTestRoom(t, dir, 1, "exits:\n east: 2\n")
+ writeGridTestRoom(t, dir, 2, "exits:\n east: 3\n west: 1\n")
+ writeGridTestRoom(t, dir, 3, "exits:\n west: 2\n")
+ w := New(dir)
+ if c := RemoveGridConflicts(1, East, loadGridRoom(w)); len(c) != 0 {
+ t.Errorf("clean pull should produce no conflicts, got %+v", c)
+ }
+}
+
+func TestRemoveGridConflictsDiagonalPull(t *testing.T) {
+ dir := t.TempDir()
+ writeGridTestRoom(t, dir, 1, "exits:\n northeast: 2\n")
+ writeGridTestRoom(t, dir, 2, "exits:\n northeast: 3\n southwest: 1\n")
+ writeGridTestRoom(t, dir, 3, "exits:\n southwest: 2\n")
+ w := New(dir)
+ if c := RemoveGridConflicts(1, Northeast, loadGridRoom(w)); len(c) != 0 {
+ t.Errorf("clean diagonal pull should produce no conflicts, got %+v", c)
+ }
+}
+
+// TestRemoveGridConflictsOverlap mirrors TestBuildGridConflictsHypotheticalRemoveOverlap:
+// removing 2 pulls 6 onto 5's cell.
+func TestRemoveGridConflictsOverlap(t *testing.T) {
+ dir := t.TempDir()
+ writeGridTestRoom(t, dir, 1, "exits:\n east: 2\n south: 4\n")
+ writeGridTestRoom(t, dir, 2, "exits:\n east: 3\n west: 1\n")
+ writeGridTestRoom(t, dir, 3, "exits:\n south: 6\n west: 2\n")
+ writeGridTestRoom(t, dir, 4, "exits:\n east: 5\n north: 1\n")
+ writeGridTestRoom(t, dir, 5, "exits:\n west: 4\n")
+ writeGridTestRoom(t, dir, 6, "exits:\n north: 3\n")
+ w := New(dir)
+
+ if c := BuildGridConflicts(1, loadGridRoom(w)); len(c) != 0 {
+ t.Fatalf("precondition: world should be clean, got %+v", c)
+ }
+ c := RemoveGridConflicts(1, East, loadGridRoom(w))
+ if !hasConflictKind(c, "overlap") {
+ t.Errorf("expected overlap from pulling 6 onto 5's cell, got %+v", c)
+ }
+}
+
+// TestRemoveGridConflictsDeadEnd: when the room being removed has no forward
+// exit, the helper models a plain deletion (A's dir exit removed). In a clean
+// world this cannot introduce a conflict.
+func TestRemoveGridConflictsDeadEnd(t *testing.T) {
+ dir := t.TempDir()
+ writeGridTestRoom(t, dir, 1, "exits:\n east: 2\n")
+ writeGridTestRoom(t, dir, 2, "exits:\n west: 1\n")
+ w := New(dir)
+ if c := RemoveGridConflicts(1, East, loadGridRoom(w)); len(c) != 0 {
+ t.Errorf("dead-end removal should produce no conflicts, got %+v", c)
+ }
+}