diff options
| author | historia <[not public]> | 2026-07-07 05:50:37 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-07-07 05:50:37 -0400 |
| commit | 3f30fa3eec9c2e2acf9a984ccefb9529a25e688a (patch) | |
| tree | b6c146ec7f3dc2ea21dbf8ee7612c3889d93ca1a /internal/world/insert_remove_test.go | |
| parent | a51f7a53aa2c487ebf94ba0363038574ae8bb590 (diff) | |
| download | thehouseoficarus-3f30fa3eec9c2e2acf9a984ccefb9529a25e688a.tar.gz | |
feat: add insert/remove room to context menu in admin web GUI to 'push' or 'pull' map rooms
Diffstat (limited to 'internal/world/insert_remove_test.go')
| -rw-r--r-- | internal/world/insert_remove_test.go | 186 |
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) + } +} |
