From 43cb574191d940913d0ce04483047068ed911fd1 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 28 Aug 2026 22:40:00 +0200 Subject: [PATCH] test: add doors, effects and autoplay integration tests --- tests/run.lua | 112 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 109 insertions(+), 3 deletions(-) diff --git a/tests/run.lua b/tests/run.lua index 5b3a3da..07021bb 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -413,12 +413,118 @@ function tests.identification() eq(Identify.label(a, healing), "potion of healing", "identifying reveals the true name") end +function tests.effects() + local game = Game.new(31) + game.player.hp = 10 + Effects.apply("heal", game, game.player, { power = 8 }) + eq(game.player.hp, 18, "heal effect restores hp") + Effects.apply("poison", game, game.player, { power = 3 }) + eq(Status.has(game.player, Status.POISON), true, "poison effect afflicts the target") + + Effects.apply("mapping", game, game.player, {}) + local map = game.level.map + local exploredAll = true + for idx = 1, map.width * map.height do + if not game.level.explored[idx] then exploredAll = false end + end + ok(exploredAll, "magic mapping reveals the whole level") + + local unknown = Items.potionOf("strength") + game.player.inventory = { unknown } + Effects.apply("identify", game, game.player, {}) + eq(Identify.isKnown(game.identify, unknown), true, "identify reveals an inventory item type") + + local ox, oy = game.player.x, game.player.y + Effects.apply("teleport", game, game.player, {}) + ok(game.player.x ~= ox or game.player.y ~= oy, "teleport relocates the player") + eq(game.level.map:isWalkable(game.player.x, game.player.y), true, "teleport lands on walkable ground") + + local blastGame = stagedGame(1, arena(21, 11), 6, 6) + local orc = Monsters.create(Monsters.byKey.orc, 1, 7, 6, 1) + orc.hp, orc.maxHp = 20, 20 + blastGame.level.entities = { orc } + Effects.apply("fireball", blastGame, blastGame.player, {}) + eq(orc.hp, 12, "fireball burns nearby monsters") + eq(Status.has(orc, Status.BURNING), true, "fireball sets monsters alight") +end + +function tests.messageLog() + local log = MessageLog.new(3) + for i = 1, 5 do log:add("line " .. i) end + eq(#log.lines, 3, "the log keeps only its capacity") + eq(log:last(), "line 5", "the newest line is retained") + local recent = log:recent(2) + eq(recent[1], "line 4", "recent returns the tail in order") + eq(recent[2], "line 5", "recent ends on the newest line") +end + +function tests.weaponAffixOnHit() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.player.weapon = Affixes.decorate(Items.weapon("dagger", 2), Affixes.weapon[3]) + local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1) + orc.hp, orc.maxHp = 30, 30 + game.level.entities = { orc } + game:playerAttack(orc) + eq(Status.has(orc, Status.BURNING), true, "a flaming weapon ignites what it hits") +end + +function tests.saveLoadRoundTrip() + local game = Game.new(2024) + for _ = 1, 80 do + if not game.player:isAlive() then break end + game:autoStep() + end + local path = os.tmpname() + game:save(path) + local restored = Game.load(path) + os.remove(path) + eq(Serialize.encode(restored:toData()), Serialize.encode(game:toData()), "state is byte-identical after round trip") + eq(restored.player.hp, game.player.hp, "player hp survives round trip") + eq(restored.depth, game.depth, "depth survives round trip") + eq(restored.turn, game.turn, "turn survives round trip") + eq(table.concat(restored.level.map.tiles), table.concat(game.level.map.tiles), "current map survives round trip") + eq(#restored.level.entities, #game.level.entities, "monster count survives round trip") + eq(restored:render(), game:render(), "render is identical after round trip") +end + +function tests.deterministicPlaythrough() + local a = Game.new(1337) + local b = Game.new(1337) + for _ = 1, 120 do + if a.player:isAlive() then a:autoStep() end + if b.player:isAlive() then b:autoStep() end + end + eq(a:render(), b:render(), "same seed yields the same playthrough") + eq(a.player.hp, b.player.hp, "same seed yields the same player hp") + eq(a.depth, b.depth, "same seed yields the same depth") + local c = Game.new(4242) + for _ = 1, 120 do if c.player:isAlive() then c:autoStep() end end + ok(a:render() ~= c:render(), "a different seed diverges") +end + +function tests.autoplayDescends() + local function playout(seed) + local game = Game.new(seed) + for _ = 1, 600 do + if not game.player:isAlive() then break end + game:autoStep() + end + return game + end + local run = playout(2024) + ok(run.depth >= 3, "the autopilot descends several levels") + local again = playout(2024) + eq(again.depth, run.depth, "the autopilot reaches the same depth on the same seed") + eq(again.turn, run.turn, "the autopilot takes the same number of turns") +end + local order = { "rngDeterminism", "mapDeterminism", "tilesRegistry", "fovKnownScene", "losSightLine", "astarPathing", "combatResolution", "itemsAndEquipment", "affixes", "statusEffects", - "scheduler", "generators", "connectivityRepair", "dungeonPersistence", "monsterAiChase", - "monsterAiRanged", "monsterAiFlee", "monsterAiDeterministic", "doorsAndKeys", "traps", - "lightAndDark", "identification", + "scheduler", "generators", "connectivityRepair", "dungeonPersistence", "monsterAiChase", "monsterAiRanged", + "monsterAiFlee", "monsterAiDeterministic", "doorsAndKeys", "traps", "lightAndDark", + "identification", "effects", "messageLog", "weaponAffixOnHit", "saveLoadRoundTrip", + "deterministicPlaythrough", "autoplayDescends", } for _, name in ipairs(order) do