diff options
| author | historia <[not public]> | 2026-06-27 16:50:41 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-27 16:50:41 -0400 |
| commit | 988b006a5bb9edb6465653566e7bdf8175347b8c (patch) | |
| tree | 3a1fceff2ee80c11b5dafcc49c13ababe021e880 /internal/validate | |
| parent | 1cab9ca20e24742f770a676f84e4ed9f1636f297 (diff) | |
| download | thehouseoficarus-988b006a5bb9edb6465653566e7bdf8175347b8c.tar.gz | |
feat: one-way map links, blocked paths on map, map grid startup validation, user colors for maps
Diffstat (limited to 'internal/validate')
| -rw-r--r-- | internal/validate/checks.go | 113 | ||||
| -rw-r--r-- | internal/validate/grid_test.go | 105 | ||||
| -rw-r--r-- | internal/validate/validate.go | 3 |
3 files changed, 219 insertions, 2 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) diff --git a/internal/validate/grid_test.go b/internal/validate/grid_test.go new file mode 100644 index 0000000..9a8ef02 --- /dev/null +++ b/internal/validate/grid_test.go @@ -0,0 +1,105 @@ +package validate + +import ( + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + "thehouseoficarus/internal/world" +) + +func writeGridRoom(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 runGridCheck(t *testing.T, rooms map[int]string, roots ...int) []Issue { + t.Helper() + dir := t.TempDir() + for id, body := range rooms { + writeGridRoom(t, dir, id, body) + } + return validateRoomGrid(Source{World: world.New(dir), RootRooms: roots}) +} + +func containsMsg(issues []Issue, substr string) bool { + for _, iss := range issues { + if strings.Contains(iss.Message, substr) { + return true + } + } + return false +} + +func TestGridCleanLayout(t *testing.T) { + // A consistent 2x2 block: 4 is reached the same way from 2 and from 3. + rooms := map[int]string{ + 1: "exits:\n south: 3\n east: 2\n", + 2: "exits:\n south: 4\n", + 3: "exits:\n east: 4\n", + 4: "name: corner\n", + } + if issues := runGridCheck(t, rooms, 1); len(issues) != 0 { + t.Errorf("expected no grid issues, got: %+v", issues) + } +} + +func TestGridOverlap(t *testing.T) { + // rooms 4 and 5 both resolve to grid (1,1). + rooms := map[int]string{ + 1: "exits:\n south: 3\n east: 2\n", + 2: "exits:\n south: 5\n", + 3: "exits:\n east: 4\n", + 4: "name: four\n", + 5: "name: five\n", + } + issues := runGridCheck(t, rooms, 1) + if !containsMsg(issues, "Grid overlap") { + t.Errorf("expected a grid overlap, got: %+v", issues) + } + for _, iss := range issues { + if iss.Level != "ERROR" { + t.Errorf("grid issues should be ERROR, got %q", iss.Level) + } + } +} + +func TestGridTwist(t *testing.T) { + // room 3 is forced onto two different cells. + rooms := map[int]string{ + 1: "exits:\n south: 4\n east: 2\n", + 2: "exits:\n east: 3\n", + 4: "exits:\n east: 3\n", + 3: "name: three\n", + } + issues := runGridCheck(t, rooms, 1) + if !containsMsg(issues, "Grid twist") { + t.Errorf("expected a grid twist, got: %+v", issues) + } +} + +func TestGridMultiPlaneViaUpDown(t *testing.T) { + // Plane A is just room 1. Going up seeds plane B (origin 10), which has an + // overlap. This proves up/down crosses planes and frames are independent + // (room 1 and room 10 both sit at (0,0) without conflicting). + rooms := map[int]string{ + 1: "exits:\n up: 10\n", + 10: "exits:\n south: 12\n east: 11\n", + 11: "exits:\n south: 14\n", + 12: "exits:\n east: 13\n", + 13: "name: thirteen\n", + 14: "name: fourteen\n", + } + issues := runGridCheck(t, rooms, 1) + if !containsMsg(issues, "plane origin 10") { + t.Errorf("expected an overlap in plane origin 10, got: %+v", issues) + } +} diff --git a/internal/validate/validate.go b/internal/validate/validate.go index 36969ed..56de6f1 100644 --- a/internal/validate/validate.go +++ b/internal/validate/validate.go @@ -51,7 +51,7 @@ type Source struct { Courses []CourseView Techs []TechView TechIDs map[string]bool - CheckSources []int + RootRooms []int IgnoreUnreachable []int } @@ -80,6 +80,7 @@ func Run(s Source) []Issue { issues = append(issues, validateRoomEnterSteps(s)...) issues = append(issues, validateTechs(s)...) issues = append(issues, validateRoomWiring(s)...) + issues = append(issues, validateRoomGrid(s)...) return issues } |
