using Theriapolis.Core.Data; using Xunit; namespace Theriapolis.Tests.Dungeons; /// /// Phase 7 M0 — content-load tests for the room-template + dungeon-layout /// schema. These run on the actual Content/Data/room_templates/ /// + Content/Data/dungeon_layouts/ directories so a broken /// authoring edit fails the build. /// public sealed class RoomTemplateValidationTests { private static ContentLoader Loader() => new(TestHelpers.DataDirectory); [Fact] public void RoomTemplates_LoadAndValidate() { // M0 vertical-slice: 5 imperium + 3 mine + 2 cave = 10 templates. // Test asserts ≥ 5 to allow content authoring growth without // modifying this test on every drop. var rooms = Loader().LoadRoomTemplates(); Assert.True(rooms.Length >= 10, $"expected ≥10 room templates after Phase 7 M0 vertical slice, got {rooms.Length}"); // Every template must declare at least one role and be one of the // five known dungeon types. var validTypes = new HashSet(StringComparer.OrdinalIgnoreCase) { "imperium", "mine", "cult", "cave", "overgrown" }; foreach (var r in rooms) { Assert.True(validTypes.Contains(r.Type), $"room '{r.Id}' has invalid type '{r.Type}'"); Assert.NotEmpty(r.RolesEligible); } } [Fact] public void EveryRoomTemplate_HasGridMatchingFootprint() { var rooms = Loader().LoadRoomTemplates(); foreach (var r in rooms) { Assert.Equal(r.FootprintHTiles, r.Grid.Length); for (int y = 0; y < r.Grid.Length; y++) Assert.Equal(r.FootprintWTiles, r.Grid[y].Length); } } [Fact] public void EveryRoomTemplate_HasIntactPerimeter() { var rooms = Loader().LoadRoomTemplates(); foreach (var r in rooms) { int w = r.FootprintWTiles, h = r.FootprintHTiles; for (int x = 0; x < w; x++) { Assert.True(IsPerimeterChar(r.Grid[0][x]), $"room '{r.Id}' top perimeter ({x},0) is '{r.Grid[0][x]}'"); Assert.True(IsPerimeterChar(r.Grid[h - 1][x]), $"room '{r.Id}' bottom perimeter ({x},{h - 1}) is '{r.Grid[h - 1][x]}'"); } for (int y = 0; y < h; y++) { Assert.True(IsPerimeterChar(r.Grid[y][0]), $"room '{r.Id}' left perimeter (0,{y}) is '{r.Grid[y][0]}'"); Assert.True(IsPerimeterChar(r.Grid[y][w - 1]), $"room '{r.Id}' right perimeter ({w - 1},{y}) is '{r.Grid[y][w - 1]}'"); } } } private static bool IsPerimeterChar(char c) => c == '#' || c == 'D' || c == 'S'; [Fact] public void DungeonLayouts_LoadAndValidate() { var loader = Loader(); var rooms = loader.LoadRoomTemplates(); var loot = loader.LoadLootTables(loader.LoadItems()); var layouts = loader.LoadDungeonLayouts(rooms, loot); // M0 vertical-slice: imperium_medium + mine_small = 2 layouts. Assert.True(layouts.Length >= 2, $"expected ≥2 dungeon layouts after Phase 7 M0, got {layouts.Length}"); // Every layout must declare a coherent room-count band. foreach (var l in layouts) { Assert.True(l.RoomCountMin >= 1, $"layout '{l.Id}' room_count_min < 1"); Assert.True(l.RoomCountMax >= l.RoomCountMin, $"layout '{l.Id}' room_count_max < min"); } } [Fact] public void EveryLayout_LootTableReferences_Resolve() { var loader = Loader(); var loot = loader.LoadLootTables(loader.LoadItems()); var layouts = loader.LoadDungeonLayouts(loader.LoadRoomTemplates(), loot); var ids = new HashSet(loot.Select(t => t.Id), StringComparer.OrdinalIgnoreCase); foreach (var l in layouts) foreach (var (band, table) in l.LootTablePerBand) Assert.True(ids.Contains(table), $"layout '{l.Id}' loot_table_per_band['{band}'] = '{table}' not in loot_tables.json"); } }