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 | |
| 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')
| -rw-r--r-- | internal/world/grid.go | 26 | ||||
| -rw-r--r-- | internal/world/grid_test.go | 181 | ||||
| -rw-r--r-- | internal/world/insert_remove.go | 152 | ||||
| -rw-r--r-- | internal/world/insert_remove_test.go | 186 |
4 files changed, 540 insertions, 5 deletions
diff --git a/internal/world/grid.go b/internal/world/grid.go index 2ea897a..7be8eed 100644 --- a/internal/world/grid.go +++ b/internal/world/grid.go @@ -87,12 +87,28 @@ func BuildGrid(seed int, load func(int) (*Room, bool), include func(int) bool, e continue } - g.Coord[target] = want - g.RoomAt[want] = target - g.Dist[target] = g.Dist[rid] + 1 - queue = append(queue, target) - } + g.Coord[target] = want + g.RoomAt[want] = target + g.Dist[target] = g.Dist[rid] + 1 + queue = append(queue, target) + } } return g } + +// BuildGridConflicts lays out the rooms reachable from seed exactly like +// BuildGrid but returns every twist/overlap conflict encountered instead of +// silently skipping them. Callers pass a load closure that may return *edited +// copies* of rooms to model a hypothetical edit (e.g. an insert or a remove) +// without writing anything to disk; a non-empty result means the modeled world +// cannot be embedded on the grid. include and edgeInclude default to "place +// and follow everything" (geometry is independent of gating), matching the +// startup validator's validateRoomGrid. +func BuildGridConflicts(seed int, load func(int) (*Room, bool)) []GridConflict { + var conflicts []GridConflict + BuildGrid(seed, load, nil, nil, func(gc GridConflict) { + conflicts = append(conflicts, gc) + }) + return conflicts +} diff --git a/internal/world/grid_test.go b/internal/world/grid_test.go new file mode 100644 index 0000000..04d749c --- /dev/null +++ b/internal/world/grid_test.go @@ -0,0 +1,181 @@ +package world + +import ( + "os" + "path/filepath" + "strconv" + "testing" +) + +func writeGridTestRoom(t *testing.T, dir string, id int, body string) { + t.Helper() + rooms := filepath.Join(dir, "rooms") + if err := os.MkdirAll(rooms, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(rooms, strconv.Itoa(id)+".yaml"), []byte(body), 0o644); err != nil { + t.Fatal(err) + } +} + +func loadGridRoom(w *World) func(int) (*Room, bool) { + return func(id int) (*Room, bool) { + r, err := w.LoadRoom(id) + if err != nil { + return nil, false + } + return r, true + } +} + +func hasConflictKind(conflicts []GridConflict, kind string) bool { + for _, c := range conflicts { + if c.Kind == kind { + return true + } + } + return false +} + +func TestBuildGridConflictsClean(t *testing.T) { + dir := t.TempDir() + writeGridTestRoom(t, dir, 1, "exits:\n south: 3\n east: 2\n") + writeGridTestRoom(t, dir, 2, "exits:\n south: 4\n") + writeGridTestRoom(t, dir, 3, "exits:\n east: 4\n") + writeGridTestRoom(t, dir, 4, "name: corner\n") + + w := New(dir) + if conflicts := BuildGridConflicts(1, loadGridRoom(w)); len(conflicts) != 0 { + t.Errorf("expected no conflicts, got: %+v", conflicts) + } +} + +func TestBuildGridConflictsOverlap(t *testing.T) { + dir := t.TempDir() + writeGridTestRoom(t, dir, 1, "exits:\n south: 3\n east: 2\n") + writeGridTestRoom(t, dir, 2, "exits:\n south: 5\n") + writeGridTestRoom(t, dir, 3, "exits:\n east: 4\n") + writeGridTestRoom(t, dir, 4, "name: four\n") + writeGridTestRoom(t, dir, 5, "name: five\n") + + w := New(dir) + conflicts := BuildGridConflicts(1, loadGridRoom(w)) + if !hasConflictKind(conflicts, "overlap") { + t.Errorf("expected an overlap conflict, got: %+v", conflicts) + } +} + +func TestBuildGridConflictsTwist(t *testing.T) { + dir := t.TempDir() + writeGridTestRoom(t, dir, 1, "exits:\n south: 4\n east: 2\n") + writeGridTestRoom(t, dir, 2, "exits:\n east: 3\n") + writeGridTestRoom(t, dir, 4, "exits:\n east: 3\n") + writeGridTestRoom(t, dir, 3, "name: three\n") + + w := New(dir) + conflicts := BuildGridConflicts(1, loadGridRoom(w)) + if !hasConflictKind(conflicts, "twist") { + t.Errorf("expected a twist conflict, got: %+v", conflicts) + } +} + +func TestBuildGridConflictsDiagonalClean(t *testing.T) { + dir := t.TempDir() + writeGridTestRoom(t, dir, 1, "exits:\n northeast: 2\n") + writeGridTestRoom(t, dir, 2, "name: two\n exits:\n southwest: 1\n") + + w := New(dir) + if conflicts := BuildGridConflicts(1, loadGridRoom(w)); len(conflicts) != 0 { + t.Errorf("expected no conflicts for clean diagonal, got: %+v", conflicts) + } +} + +// TestBuildGridConflictsHypotheticalEdit models the insert-remove use case: +// the load closure returns edited copies of rooms reflecting a proposed edit, +// and BuildGridConflicts reports whether the resulting world is still clean. +// Here we model "insert a new room 5 between 1 and 2 on the east axis" and +// confirm the (clean) post-edit world has no conflicts. +func TestBuildGridConflictsHypotheticalInsert(t *testing.T) { + dir := t.TempDir() + writeGridTestRoom(t, dir, 1, "name: one\nexits:\n east: 2\n") + writeGridTestRoom(t, dir, 2, "name: two\nexits:\n west: 1\n") + w := New(dir) + + newRoom := &Room{Name: "new", Exits: map[ExitDir]ExitDef{ + East: {Room: 2}, West: {Room: 1}, + }} + load := func(id int) (*Room, bool) { + if id == 5 { + return newRoom, true + } + r, err := w.LoadRoom(id) + if err != nil { + return nil, false + } + copy := *r + if r.Exits != nil { + copy.Exits = make(map[ExitDir]ExitDef, len(r.Exits)) + for k, v := range r.Exits { + if id == 1 && k == East { + copy.Exits[k] = ExitDef{Room: 5, Condition: v.Condition, BlockedMessage: v.BlockedMessage, SetFlags: v.SetFlags, SetPlayerFlags: v.SetPlayerFlags, Hidden: v.Hidden, AlwaysBlocked: v.AlwaysBlocked} + continue + } + if id == 2 && k == West { + copy.Exits[k] = ExitDef{Room: 5, Condition: v.Condition, BlockedMessage: v.BlockedMessage, SetFlags: v.SetFlags, SetPlayerFlags: v.SetPlayerFlags, Hidden: v.Hidden, AlwaysBlocked: v.AlwaysBlocked} + continue + } + copy.Exits[k] = v + } + } + return ©, true + } + if conflicts := BuildGridConflicts(1, load); len(conflicts) != 0 { + t.Errorf("clean insert should produce no conflicts, got: %+v", conflicts) + } +} + +// TestBuildGridConflictsHypotheticalRemoveOverlap models "remove room 2 and +// pull the beyond-rooms back toward room 1". Layout: +// +// 1 east→2 east→3 south→6 (6 at (2,1,0)) +// 1 south→4 east→5 (5 at (1,1,0)) +// +// After removing 2 and rewiring 1.Exits[East]=3, 3.Exits[West]=1, the BFS +// re-places 3 at (1,0,0) and 6 at (1,1,0) — colliding with 5 which stays at +// (1,1,0). The helper must report an overlap without anything being written. +func TestBuildGridConflictsHypotheticalRemoveOverlap(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) + + load := func(id int) (*Room, bool) { + if id == 2 { + return nil, false // room removed + } + r, err := w.LoadRoom(id) + if err != nil { + return nil, false + } + rc := *r + rc.Exits = make(map[ExitDir]ExitDef, len(r.Exits)) + for k, v := range r.Exits { + rc.Exits[k] = v + } + if id == 1 { + rc.Exits[East] = ExitDef{Room: 3} + } + if id == 3 { + rc.Exits[West] = ExitDef{Room: 1} + } + return &rc, true + } + conflicts := BuildGridConflicts(1, load) + if !hasConflictKind(conflicts, "overlap") { + t.Errorf("expected an overlap from pulling 6 onto 5's cell, got: %+v", conflicts) + } +} diff --git a/internal/world/insert_remove.go b/internal/world/insert_remove.go new file mode 100644 index 0000000..0811587 --- /dev/null +++ b/internal/world/insert_remove.go @@ -0,0 +1,152 @@ +package world + +// copyExits returns a shallow copy of an exit map so a hypothetical room copy +// can be mutated without touching the original. A fresh map is returned even +// when src is nil so callers can safely assign into it. +func copyExits(src map[ExitDir]ExitDef) map[ExitDir]ExitDef { + dst := make(map[ExitDir]ExitDef, len(src)) + for k, v := range src { + dst[k] = v + } + return dst +} + +// RewirePreserving returns an ExitDef pointing at newTarget that keeps every +// non-Room field of src (conditions, blocked messages, flags, hidden/always- +// blocked). It is the shared "repoint an exit without losing its properties" +// operation used by insert and remove (both in-game and admin). +func RewirePreserving(src ExitDef, newTarget int) ExitDef { + return ExitDef{ + Room: newTarget, + Condition: src.Condition, + BlockedMessage: src.BlockedMessage, + SetFlags: src.SetFlags, + SetPlayerFlags: src.SetPlayerFlags, + Hidden: src.Hidden, + AlwaysBlocked: src.AlwaysBlocked, + } +} + +// InsertGridConflicts returns the grid conflicts that would result from +// inserting a new room (identified by newID, which may be a sentinel such as +// -1 since no file is written) between roomID and the room its dir exit +// currently leads to (targetID). The new room inherits roomID's outward dir +// exit properties on the near side and, when the far room's opposite exit +// points back at roomID, the far room's reciprocal exit is repointed at the +// new room preserving its own properties — exactly mirroring the writes +// roomInsert performs. load is the real room loader (g.World.LoadRoom or +// s.world.LoadRoom). A non-empty result means the insert would make the map +// unembeddable (overlap or twist) and must be rejected. +func InsertGridConflicts(roomID int, dir ExitDir, targetID, newID int, load func(int) (*Room, bool)) []GridConflict { + oppositeDir := OppositeExit[dir] + + srcRoom, _ := load(roomID) + var srcDirExit ExitDef + if srcRoom != nil { + srcDirExit = srcRoom.Exits[dir] + } + + targetRoom, targetOK := load(targetID) + hasReciprocal := targetOK && targetRoom.Exits[oppositeDir].Room == roomID + var targetOppExit ExitDef + if hasReciprocal { + targetOppExit = targetRoom.Exits[oppositeDir] + } + + newRoom := &Room{Exits: map[ExitDir]ExitDef{ + dir: {Room: targetID}, + oppositeDir: {Room: roomID}, + }} + + return BuildGridConflicts(roomID, func(id int) (*Room, bool) { + if id == newID { + return newRoom, true + } + r, ok := load(id) + if !ok { + return nil, false + } + if id == roomID { + rc := *r + rc.Exits = copyExits(r.Exits) + rc.Exits[dir] = RewirePreserving(srcDirExit, newID) + return &rc, true + } + if id == targetID && hasReciprocal { + rc := *r + rc.Exits = copyExits(r.Exits) + rc.Exits[oppositeDir] = RewirePreserving(targetOppExit, newID) + return &rc, true + } + return r, true + }) +} + +// RemoveGridConflicts returns the grid conflicts that would result from +// removing roomID's dir exit's target (the "inserted" room B) and pulling the +// far room C (B's dir exit's target, when present) back to roomID — the exact +// inverse of InsertGridConflicts. roomID's dir exit is repointed at C +// preserving its current properties; C's opposite exit is repointed at roomID +// preserving its current properties. When B has no forward exit (a dead end), +// roomID's dir exit is simply removed (no pull). load is the real room loader. +// A non-empty result means the pull would make the map unembeddable. +// +// The structural guards (B actually leads back to roomID, B has a forward +// exit, B has no other exits, no extra inbound edges, C's opposite points back +// at B) are the caller's responsibility; this helper only models the +// geometric consequence of the rewiring. +func RemoveGridConflicts(roomID int, dir ExitDir, load func(int) (*Room, bool)) []GridConflict { + oppositeDir := OppositeExit[dir] + + srcRoom, ok := load(roomID) + if !ok { + return nil + } + bExit, has := srcRoom.Exits[dir] + if !has { + return nil + } + bID := bExit.Room + bRoom, ok := load(bID) + if !ok { + return nil + } + cExit, hasFar := bRoom.Exits[dir] + var cID int + if hasFar { + cID = cExit.Room + } + var cOppExit ExitDef + if hasFar { + if cRoom, ok := load(cID); ok { + cOppExit = cRoom.Exits[oppositeDir] + } + } + + return BuildGridConflicts(roomID, func(id int) (*Room, bool) { + if id == bID { + return nil, false + } + r, ok := load(id) + if !ok { + return nil, false + } + if id == roomID { + rc := *r + rc.Exits = copyExits(r.Exits) + if hasFar { + rc.Exits[dir] = RewirePreserving(bExit, cID) + } else { + delete(rc.Exits, dir) + } + return &rc, true + } + if hasFar && id == cID { + rc := *r + rc.Exits = copyExits(r.Exits) + rc.Exits[oppositeDir] = RewirePreserving(cOppExit, roomID) + return &rc, true + } + return r, true + }) +} 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) + } +} |
