aboutsummaryrefslogtreecommitdiff
path: root/internal/validate/checks.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/validate/checks.go')
-rw-r--r--internal/validate/checks.go113
1 files changed, 112 insertions, 1 deletions
diff --git a/internal/validate/checks.go b/internal/validate/checks.go
index f1d6b5e..b2e5449 100644
--- a/internal/validate/checks.go
+++ b/internal/validate/checks.go
@@ -6,6 +6,7 @@ import (
"strings"
"thehouseoficarus/internal/behavior"
+ "thehouseoficarus/internal/color"
"thehouseoficarus/internal/world"
)
@@ -34,6 +35,13 @@ func validateRooms(s Source) []Issue {
Message: fmt.Sprintf("Room %d: has no name", id),
})
}
+ if room.Color != "" && color.Parse(room.Color).Empty() {
+ issues = append(issues, Issue{
+ Level: "WARN",
+ Type: "reference",
+ Message: fmt.Sprintf("Room %d: invalid color %q", id, room.Color),
+ })
+ }
for dir, exit := range room.Exits {
switch dir {
@@ -654,7 +662,7 @@ func validateRoomWiring(s Source) []Issue {
roomIndex := s.World.RoomIndex()
sourceSet := make(map[int]bool)
- for _, src := range s.CheckSources {
+ for _, src := range s.RootRooms {
sourceSet[src] = true
}
@@ -705,6 +713,109 @@ func validateRoomWiring(s Source) []Issue {
return issues
}
+// gridDeltas maps the horizontal exits to their grid movement. Up/Down are
+// intentionally excluded: they connect separate horizontal planes rather than
+// moving within one.
+var gridDeltas = map[world.ExitDir][2]int{
+ world.North: {0, -1},
+ world.South: {0, 1},
+ world.East: {1, 0},
+ world.West: {-1, 0},
+}
+
+// validateRoomGrid lays each reachable horizontal plane on a 2D grid starting
+// from the configured root rooms and reports when the exit layout cannot be
+// embedded without conflict:
+// - overlap: two distinct rooms land on the same grid cell.
+// - twist: one room is forced onto two different grid cells.
+//
+// Up/Down exits don't move on the grid; each leads to a new plane that is laid
+// out independently (fresh origin), so one root validates every reachable
+// floor. Exit conditions are ignored (geometry is independent of gating), and
+// exits to nonexistent rooms are skipped (covered by referential checks).
+func validateRoomGrid(s Source) []Issue {
+ var issues []Issue
+ roomIndex := s.World.RoomIndex()
+
+ placed := make(map[int]bool)
+ var seeds []int
+ for _, r := range s.RootRooms {
+ if roomIndex[r] {
+ seeds = append(seeds, r)
+ }
+ }
+
+ for len(seeds) > 0 {
+ origin := seeds[0]
+ seeds = seeds[1:]
+ if placed[origin] {
+ continue
+ }
+
+ coordOf := map[int][2]int{origin: {0, 0}}
+ roomAt := map[[2]int]int{{0, 0}: origin}
+ placed[origin] = true
+ queue := []int{origin}
+
+ for len(queue) > 0 {
+ rid := queue[0]
+ queue = queue[1:]
+ room, err := s.World.LoadRoom(rid)
+ if err != nil {
+ continue
+ }
+ c := coordOf[rid]
+ for _, dir := range world.ExitOrder {
+ exit, ok := room.Exits[dir]
+ if !ok || exit.Room <= 0 || !roomIndex[exit.Room] {
+ continue
+ }
+ target := exit.Room
+
+ if dir == world.Up || dir == world.Down {
+ if !placed[target] {
+ seeds = append(seeds, target)
+ }
+ continue
+ }
+
+ d := gridDeltas[dir]
+ want := [2]int{c[0] + d[0], c[1] + d[1]}
+
+ if existing, ok := coordOf[target]; ok {
+ if existing != want {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "integrity",
+ Message: fmt.Sprintf(
+ "Grid twist: room %d (via %d %s) maps to grid (%d,%d) but was already placed at (%d,%d) [plane origin %d]",
+ target, rid, dir, want[0], want[1], existing[0], existing[1], origin),
+ })
+ }
+ continue
+ }
+ if occupier, ok := roomAt[want]; ok && occupier != target {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "integrity",
+ Message: fmt.Sprintf(
+ "Grid overlap: room %d (via %d %s) wants grid (%d,%d), already used by room %d [plane origin %d]",
+ target, rid, dir, want[0], want[1], occupier, origin),
+ })
+ continue
+ }
+
+ coordOf[target] = want
+ roomAt[want] = target
+ placed[target] = true
+ queue = append(queue, target)
+ }
+ }
+ }
+
+ return issues
+}
+
func validateTechs(s Source) []Issue {
var issues []Issue
seen := make(map[string]bool)