From 0db5c1683df2d863d49f4ce158a307e385f0885d Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 16:31:36 +0200 Subject: [PATCH] test: cover tests.pickFieldSurvivesSerialization --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index 0e59bbc..87e8c6f 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -7914,6 +7914,179 @@ end function tests.gameMonsterTunnelsThroughAWallToReachAndAttackThePlayer() local level = arena(21, 11) + for y = 2, 10 do level.map:set(10, y, Tiles.WALL) end + local game = stagedGame(1, level, 8, 6) + local burrower = Entity.new({ + id = 1, x = 12, y = 6, glyph = "b", name = "rock burrower", kind = "monster", + ai = "tunnel", hp = 20, maxHp = 20, attack = 5, defense = 0, speed = 100, + }) + game.level.entities = { burrower } + local hpBefore = game.player.hp + local sawRubble = false + for _ = 1, 10 do + game.rng = Rng.new(1) + Ai.act(game, burrower) + if game.level.map:get(10, 6) ~= Tiles.WALL then sawRubble = true end + if game.player.hp < hpBefore then break end + end + ok(sawRubble, "the burrower actually carves through the partition wall") + ok(game.player.hp < hpBefore, "the burrower eventually reaches and melees the player through its own tunnel") +end + +function tests.pickFieldSurvivesSerialization() + local game = Game.new(3) + game.player.pick = Items.pick("saved pick", 7) + local path = os.tmpname() + game:save(path) + local restored = Game.load(path) + os.remove(path) + eq(restored.player.pick.name, "saved pick", "the equipped pick's name survives a save/load round trip") + eq(restored.player.pick.uses, 7, "the equipped pick's remaining durability survives a save/load round trip") +end + +function tests.builderNeverPlacesAPickBeforeDepthTwo() + for seed = 1, 20 do + local level = Builder.build(1, Rng.new(seed), {}) + for _, ground in ipairs(level.floorItems) do + ok(ground.item.kind ~= Items.PICK, "a pick never turns up on the entrance floor") + end + end +end + +function tests.builderOccasionallyPlacesAPickFromDepthTwo() + local level = Builder.build(2, Rng.new(6), {}) + local found + for _, ground in ipairs(level.floorItems) do + if ground.item.kind == Items.PICK then found = ground.item end + end + ok(found ~= nil, "this seed's depth-2 floor places a miner's pick") + ok(Shop.priceOf(found) > 0, "the placed pick still carries a positive catalog price") +end + +function tests.sanityCurrentDefaultsToMaxForAFreshEntity() + eq(Sanity.current({}), Sanity.MAX, "an entity with no sanity field reads as fully composed") +end + +function tests.sanityTierOfClassifiesEachBandAtItsBoundaries() + eq(Sanity.tierOf({ sanity = 100 }), Sanity.COMPOSED, "a full meter is composed") + eq(Sanity.tierOf({ sanity = 61 }), Sanity.COMPOSED, "just above the shaken ceiling is still composed") + eq(Sanity.tierOf({ sanity = 60 }), Sanity.SHAKEN, "the shaken ceiling itself reads as shaken") + eq(Sanity.tierOf({ sanity = 31 }), Sanity.SHAKEN, "just above the terrified ceiling is still shaken") + eq(Sanity.tierOf({ sanity = 30 }), Sanity.TERRIFIED, "the terrified ceiling itself reads as terrified") + eq(Sanity.tierOf({ sanity = 1 }), Sanity.TERRIFIED, "one point above zero is still terrified, never broken") + eq(Sanity.tierOf({ sanity = 0 }), Sanity.BROKEN, "zero itself is broken") +end + +function tests.sanityAdjustClampsToTheZeroAndMaxBounds() + local entity = { sanity = 95 } + eq(Sanity.adjust(entity, 30), Sanity.MAX, "adjusting past the ceiling clamps to max") + eq(Sanity.adjust(entity, -1000), 0, "adjusting far past the floor clamps to zero") + eq(Sanity.adjust(entity, 5), 5, "an ordinary adjustment lands exactly where expected") +end + +function tests.sanityAttackAndDefensePenaltiesEscalateByTier() + eq(Sanity.attackPenalty({ sanity = 100 }), 0, "a composed mind carries no attack penalty") + eq(Sanity.attackPenalty({ sanity = 60 }), -1, "shaken costs a point of attack") + eq(Sanity.attackPenalty({ sanity = 30 }), -3, "terrified costs three points of attack") + eq(Sanity.attackPenalty({ sanity = 0 }), -5, "broken costs five points of attack") + eq(Sanity.defensePenalty({ sanity = 100 }), 0, "a composed mind carries no defense penalty") + eq(Sanity.defensePenalty({ sanity = 60 }), -1, "shaken costs a point of defense") + eq(Sanity.defensePenalty({ sanity = 30 }), -2, "terrified costs two points of defense") + eq(Sanity.defensePenalty({ sanity = 0 }), -4, "broken costs four points of defense") +end + +function tests.entityEffectiveAttackAndDefenseFoldInTheSanityPenalty() + local entity = Entity.new({ x = 1, y = 1, glyph = "@", name = "you", kind = "player", hp = 20, attack = 6, defense = 4 }) + eq(entity:effectiveAttack(), 6, "a fresh entity with no sanity field carries no penalty") + entity.sanity = 0 + eq(entity:effectiveAttack(), 1, "a broken mind's attack penalty is live in effectiveAttack") + eq(entity:effectiveDefense(), 0, "a broken mind's defense penalty is live in effectiveDefense") +end + +function tests.sanityDarknessDreadFiresOnlyOnADarkFloorWithASpentTorch() + local dark = stagedGame(1, arenaDark(21, 11), 3, 6) + dark.player.light.fuel = 0 + eq(Sanity.darknessDread(dark), Sanity.DARKNESS_DREAD, "a dark floor with a spent torch dreads the mind") + dark.player.light.fuel = 50 + eq(Sanity.darknessDread(dark), 0, "a lit torch on a dark floor holds the dread off entirely") + local lit = stagedGame(1, arena(21, 11), 3, 6) + lit.player.light.fuel = 0 + eq(Sanity.darknessDread(lit), 0, "an ordinary floor never dreads the mind regardless of torch fuel") +end + +function tests.sanityCorpseDreadCountsOnlyCorpsesAdjacentToThePlayer() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.corpses = { { x = 2, y = 5 } } + eq(Sanity.corpseDread(game), Sanity.CORPSE_DREAD_PER, "one adjacent corpse contributes its fixed share") + game.level.corpses[#game.level.corpses + 1] = { x = 10, y = 6 } + eq(Sanity.corpseDread(game), Sanity.CORPSE_DREAD_PER, "a corpse well outside arm's reach adds nothing") +end + +function tests.sanityCorpseDreadCapsAtItsFixedCeiling() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.level.corpses = { + { x = 2, y = 5 }, { x = 2, y = 6 }, { x = 2, y = 7 }, { x = 3, y = 5 }, { x = 3, y = 7 }, + } + eq(Sanity.corpseDread(game), Sanity.CORPSE_DREAD_CAP, "five adjacent corpses still cap at the fixed ceiling") +end + +function tests.sanityUndeadDreadCountsOnlyVisibleUndeadAndCapsAtItsCeiling() + local game = stagedGame(1, arena(21, 11), 3, 6) + local zombie = Monsters.create(Monsters.byKey.zombie, 1, 4, 6, 1) + game.level.entities = { zombie } + eq(Sanity.undeadDread(game), Sanity.UNDEAD_DREAD_PER, "one visible undead contributes its fixed share") + game.visible = {} + eq(Sanity.undeadDread(game), 0, "an undead outside the player's sight adds nothing") + game:computeVisible() + local orc = Monsters.create(Monsters.byKey.orc, 2, 5, 6, 1) + game.level.entities[#game.level.entities + 1] = orc + eq(Sanity.undeadDread(game), Sanity.UNDEAD_DREAD_PER, "a visible living monster never counts as undead dread") + for i = 3, 6 do + game.level.entities[#game.level.entities + 1] = Monsters.create(Monsters.byKey.zombie, i, 6, 6, 1) + end + eq(Sanity.undeadDread(game), Sanity.UNDEAD_DREAD_CAP, "a crowd of visible undead still caps at the fixed ceiling") +end + +function tests.sanityCurseDreadFiresOnlyForARevealedCurse() + local player = { ring = Curses.afflict(Items.ring("ring of power", 0, 0), Curses.pool.ring[1]) } + eq(Sanity.curseDread(player), 0, "a curse still hidden behind an ordinary appearance dreads nothing") + Curses.reveal(player.ring) + eq(Sanity.curseDread(player), Sanity.CURSE_DREAD, "a revealed curse gnaws at the mind") +end + +function tests.sanityWeatherDreadFiresOnlyForInfestedWeather() + eq(Sanity.weatherDread(Atmosphere.INFESTED), Sanity.WEATHER_DREAD, "an infested floor dreads the mind") + eq(Sanity.weatherDread(Atmosphere.FOGGY), 0, "a merely foggy floor is not itself dreadful") + eq(Sanity.weatherDread(nil), 0, "a clear floor carries no weather dread") +end + +function tests.sanityTotalDreadSumsEverySourceAtOnce() + local dark = stagedGame(1, arenaDark(21, 11), 3, 6) + dark.player.light.fuel = 0 + dark.level.weather = Atmosphere.INFESTED + dark.level.corpses = { { x = 2, y = 5 } } + eq(Sanity.totalDread(dark), Sanity.DARKNESS_DREAD + Sanity.WEATHER_DREAD + Sanity.CORPSE_DREAD_PER, + "every currently-active dread source is summed at once") +end + +function tests.sanityTickAppliesDreadAndLogsOnlyOnATierCrossing() + local game = stagedGame(1, arenaDark(21, 11), 3, 6) + game.player.light.fuel = 0 + game.player.sanity = 63 + eq(Sanity.tick(game), nil, "a dread turn that stays within its tier reports no line") + eq(game.player.sanity, 61, "the dread was still deducted even without a tier crossing") + eq(Sanity.tick(game), "your nerves begin to fray", "crossing down into shaken is announced") + eq(game.player.sanity, 59, "the meter keeps draining turn over turn") +end + +function tests.sanityTickAnnouncesReachingTerrifiedAndBroken() + local game = stagedGame(1, arenaDark(21, 11), 3, 6) + game.player.light.fuel = 0 + game.player.sanity = 32 + eq(Sanity.tick(game), "dread claws at the edges of your sanity", "crossing down into terrified is announced") + game.player.sanity = 2 + eq(Sanity.tick(game), "your mind gives way to raw terror", "crossing down into broken is announced") + eq(game.player.sanity, 0, "broken is a floor, not a negative value") end local order = {