b451f83174
Captures the pre-Godot-port state of the codebase. This is the rollback anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md). All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
108 lines
4.1 KiB
C#
108 lines
4.1 KiB
C#
using Theriapolis.Core.Data;
|
|
using Xunit;
|
|
|
|
namespace Theriapolis.Tests.Dungeons;
|
|
|
|
/// <summary>
|
|
/// Phase 7 M0 — content-load tests for the room-template + dungeon-layout
|
|
/// schema. These run on the actual <c>Content/Data/room_templates/</c>
|
|
/// + <c>Content/Data/dungeon_layouts/</c> directories so a broken
|
|
/// authoring edit fails the build.
|
|
/// </summary>
|
|
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<string>(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<string>(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");
|
|
}
|
|
}
|