test: cover tests.mazeGeneratorIsDeterministicDiffersAcrossSeedsAndLe...

This commit is contained in:
dualfroz committed 2026-09-08 17:03:25 +02:00
1 parent 6d392eed88
commit 25c412bcd3
1 file changed
+188
+188
View File
@@ -15,6 +15,7 @@ local Entity = require("entity")
local Combat = require("combat")
local Items = require("items")
local Affixes = require("affixes")
local Shop = require("shop")
local Identify = require("identify")
local Effects = require("effects")
local Status = require("status")
@@ -26,8 +27,78 @@ local Gen = require("gen.init")
local Connect = require("gen.connect")
local Level = require("level")
local Dungeon = require("dungeon")
local Builder = require("builder")
local Serialize = require("serialize")
local Game = require("game")
local Progression = require("progression")
local Spells = require("spells")
local Abilities = require("abilities")
local Hazards = require("hazards")
local Shrines = require("shrines")
local Classes = require("classes")
local Skills = require("skills")
local Crafting = require("crafting")
local Companions = require("companions")
local Artifacts = require("artifacts")
local Bosses = require("bosses")
local Elites = require("elites")
local Quests = require("quests")
local Meta = require("meta")
local Alchemy = require("alchemy")
local Resistances = require("resistances")
local Factions = require("factions")
local Encumbrance = require("encumbrance")
local Render = require("render")
local Terrain = require("terrain")
local Vaults = require("vaults")
local Runewords = require("runewords")
local Atmosphere = require("atmosphere")
local Stealth = require("stealth")
local Compendium = require("compendium")
local Nodes = require("nodes")
local Sets = require("sets")
local Mimics = require("mimics")
local Curses = require("curses")
local Momentum = require("momentum")
local Conductivity = require("conductivity")
local Corpses = require("corpses")
local Digging = require("digging")
local Sanity = require("sanity")
local Hunger = require("hunger")
local Events = require("events")
local Graves = require("graves")
local Proficiency = require("proficiency")
local Ascension = require("ascension")
local Durability = require("durability")
local Disarm = require("disarm")
local Chests = require("chests")
local Reanimation = require("reanimation")
local Secrets = require("secrets")
local Mercenaries = require("mercenaries")
local Morgue = require("morgue")
local Leaderboard = require("leaderboard")
local Replay = require("replay")
local Challenges = require("challenges")
local Analytics = require("analytics")
local Titles = require("titles")
local Export = require("export")
local Streaks = require("streaks")
local Nemesis = require("nemesis")
local Biomes = require("biomes")
local Scavenging = require("scavenging")
local Infighting = require("infighting")
local Blocks = require("blocks")
local SaveGame = require("savegame")
local Polymorph = require("polymorph")
local Prefab = require("gen.prefab")
local Noise = require("noise")
local Disease = require("disease")
local Captives = require("captives")
local Horde = require("horde")
local Branches = require("branches")
local Phases = require("phases")
local Commands = require("commands")
local Examine = require("examine")
local passed, failed = 0, 0
local failures = {}
@@ -251,6 +322,123 @@ function tests.generators()
end
end
function tests.mazeGeneratorIsDeterministicDiffersAcrossSeedsAndLeavesEveryFloorCellReachable()
local m1, reach1 = Gen.build("maze", 60, 24, Rng.new(2024))
local m2 = Gen.build("maze", 60, 24, Rng.new(2024))
eq(table.concat(m1.tiles), table.concat(m2.tiles), "maze is deterministic")
local other = Gen.build("maze", 60, 24, Rng.new(2025))
ok(table.concat(m1.tiles) ~= table.concat(other.tiles), "maze differs across seeds")
local walkable = 0
for y = 1, m1.height do for x = 1, m1.width do if m1:isWalkable(x, y) then walkable = walkable + 1 end end end
ok(walkable > 0, "maze carves open floor")
eq(#reach1, walkable, "maze leaves every floor cell reachable without gen.connect stitching anything extra")
end
function tests.mazeGeneratorIsRegisteredButExcludedFromTheDepthCycle()
ok(Gen.has("maze"), "maze is a selectable generator")
local found = false
for _, name in ipairs(Gen.order) do
if name == "maze" then found = true end
end
ok(not found, "maze is deliberately absent from the auto-cycled depth order")
end
function tests.mazeGeneratorProducesOneWideCorridorsRatherThanOpenRooms()
-- Braiding deliberately opens a handful of the diagonal cells a pure
-- backtracker never touches (see mazeGeneratorBraidingOpensSomeDeadEnds...
-- below), so this checks the unbraided core algorithm's own shape rather
-- than the default, braided output.
local map = Gen.generators.maze(60, 24, Rng.new(7), { braidNumerator = 0, braidDenominator = 1 })
local openTwoByTwoBlocks = 0
for y = 2, map.height - 2 do
for x = 2, map.width - 2 do
if map:isWalkable(x, y) and map:isWalkable(x + 1, y)
and map:isWalkable(x, y + 1) and map:isWalkable(x + 1, y + 1) then
openTwoByTwoBlocks = openTwoByTwoBlocks + 1
end
end
end
eq(openTwoByTwoBlocks, 0, "a maze never opens a 2x2 block of floor the way a room-based generator does")
end
function tests.mazeGeneratorBraidingOpensSomeDeadEndsWithoutTouchingConnectivity()
local braided = Gen.generators.maze(60, 24, Rng.new(3), { braidNumerator = 1, braidDenominator = 1 })
local unbraided = Gen.generators.maze(60, 24, Rng.new(3), { braidNumerator = 0, braidDenominator = 1 })
local function walkableCount(map)
local total = 0
for y = 1, map.height do for x = 1, map.width do if map:isWalkable(x, y) then total = total + 1 end end end
return total
end
ok(walkableCount(braided) > walkableCount(unbraided), "raising the braid odds opens strictly more floor")
local reachable = Connect.ensure(unbraided, Rng.new(3))
eq(#reachable, walkableCount(unbraided), "an unbraided perfect maze is already fully connected on its own")
end
function tests.prefabVaultSizeMatchesItsWidestRowAndItsRowCount()
local vault = { rows = { "####", "#..#", "####" } }
local w, h = Prefab.size(vault)
eq(w, 4, "size reports the width of a template row")
eq(h, 3, "size reports the number of template rows")
end
function tests.prefabVaultFitsAcceptsAnAnchorWithAWallGapToEveryBorderAndRejectsOneWithout()
local map = Map.new(20, 15)
local vault = { rows = { "###", "#.#", "###" } }
ok(Prefab.fits(map, vault, 2, 2), "an anchor two cells in on every side leaves a wall gap to the border")
ok(not Prefab.fits(map, vault, 1, 2), "an anchor flush against the west border is rejected")
ok(not Prefab.fits(map, vault, 2, 1), "an anchor flush against the north border is rejected")
local w, h = Prefab.size(vault)
ok(not Prefab.fits(map, vault, map.width - w + 2, 2), "an anchor that spills past the east border is rejected")
ok(not Prefab.fits(map, vault, 2, map.height - h + 2), "an anchor that spills past the south border is rejected")
end
function tests.prefabVaultStampWritesEveryGlyphAndCollectsTreasureMonsterAndLootMarkersInMapOrder()
local map = Map.new(20, 15)
local vault = {
name = "testVault",
rows = {
"#####",
"#*M+#",
"#.L.#",
"#####",
},
}
local result = Prefab.stamp(map, vault, 3, 4)
eq(result.name, "testVault", "the stamp result remembers the template's own name")
eq(map:get(3, 4), Tiles.WALL, "a '#' glyph becomes a wall tile")
eq(map:get(4, 5), Tiles.FLOOR, "a '*' treasure marker still becomes walkable floor")
eq(map:get(5, 5), Tiles.FLOOR, "an 'M' monster marker still becomes walkable floor")
eq(map:get(6, 5), Tiles.DOOR_CLOSED, "a '+' glyph becomes a closed door")
eq(map:get(5, 6), Tiles.FLOOR, "an 'L' loot marker still becomes walkable floor")
eq(#result.treasure, 1, "exactly one treasure anchor is collected")
eq(result.treasure[1].x, 4, "the treasure anchor's x is offset by the stamp's own anchor")
eq(result.treasure[1].y, 5, "the treasure anchor's y is offset by the stamp's own anchor")
eq(#result.monsters, 1, "exactly one monster anchor is collected")
eq(result.monsters[1].x, 5, "the monster anchor's x is offset by the stamp's own anchor")
eq(#result.loot, 1, "exactly one loot anchor is collected")
eq(result.loot[1].x, 5, "the loot anchor's x is offset by the stamp's own anchor")
eq(result.loot[1].y, 6, "the loot anchor's y is offset by the stamp's own anchor")
end
function tests.prefabVaultStampIsDeterministicForAFixedAnchor()
local vault = Prefab.vaults[1]
local m1, m2 = Map.new(20, 15), Map.new(20, 15)
Prefab.stamp(m1, vault, 3, 3)
Prefab.stamp(m2, vault, 3, 3)
eq(table.concat(m1.tiles), table.concat(m2.tiles), "stamping the same vault at the same anchor twice is byte-identical")
end
function tests.builderCanBeExplicitlySteeredToAMazeFloorAndStillYieldsAPlayableLevel()
local level = Builder.build(3, Rng.new(11), { generator = "maze" })
eq(level.generator, "maze", "the level remembers it was built with the maze generator")
eq(level.map:get(level.upX, level.upY), Tiles.STAIRS_UP, "a maze floor still gets its up stairs")
eq(level.map:get(level.downX, level.downY), Tiles.STAIRS_DOWN, "a maze floor still gets its down stairs")
local path = Astar.find(level.map, level.upX, level.upY, level.downX, level.downY, function(x, y)
return not level.map:isPassable(x, y, true)
end)
ok(path ~= nil, "the down stairs are reachable from the up stairs on a maze floor")
end
function tests.connectivityRepair()
local map = Map.new(15, 7)
for y = 2, 4 do for x = 2, 4 do map:set(x, y, Map.FLOOR) end end