test: cover tests.mazeGeneratorIsDeterministicDiffersAcrossSeedsAndLe...
This commit is contained in:
1 parent
6d392eed88
commit
25c412bcd3
1 file changed
+188
+188
@@ -15,6 +15,7 @@ local Entity = require("entity")
|
|||||||
local Combat = require("combat")
|
local Combat = require("combat")
|
||||||
local Items = require("items")
|
local Items = require("items")
|
||||||
local Affixes = require("affixes")
|
local Affixes = require("affixes")
|
||||||
|
local Shop = require("shop")
|
||||||
local Identify = require("identify")
|
local Identify = require("identify")
|
||||||
local Effects = require("effects")
|
local Effects = require("effects")
|
||||||
local Status = require("status")
|
local Status = require("status")
|
||||||
@@ -26,8 +27,78 @@ local Gen = require("gen.init")
|
|||||||
local Connect = require("gen.connect")
|
local Connect = require("gen.connect")
|
||||||
local Level = require("level")
|
local Level = require("level")
|
||||||
local Dungeon = require("dungeon")
|
local Dungeon = require("dungeon")
|
||||||
|
local Builder = require("builder")
|
||||||
local Serialize = require("serialize")
|
local Serialize = require("serialize")
|
||||||
local Game = require("game")
|
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 passed, failed = 0, 0
|
||||||
local failures = {}
|
local failures = {}
|
||||||
@@ -251,6 +322,123 @@ function tests.generators()
|
|||||||
end
|
end
|
||||||
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()
|
function tests.connectivityRepair()
|
||||||
local map = Map.new(15, 7)
|
local map = Map.new(15, 7)
|
||||||
for y = 2, 4 do for x = 2, 4 do map:set(x, y, Map.FLOOR) end end
|
for y = 2, 4 do for x = 2, 4 do map:set(x, y, Map.FLOOR) end end
|
||||||
|
|||||||
Reference in new issue
Block a user