diff --git a/tests/run.lua b/tests/run.lua index 07021bb..bb5aee7 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -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