From 1a617008d4d6afad6e1dbb72a8d2caca891649ce Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 17:37:50 +0200 Subject: [PATCH] test: cover tests.sanityTickRecoversAndAnnouncesClimbingBackOutOfBroken --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index 87e8c6f..1491f82 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -8089,6 +8089,179 @@ function tests.sanityTickAnnouncesReachingTerrifiedAndBroken() eq(game.player.sanity, 0, "broken is a floor, not a negative value") end +function tests.sanityTickRecoversAndAnnouncesClimbingBackOutOfBroken() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.player.sanity = 0 + eq(Sanity.tick(game), "your nerves start to steady", + "recovering out of broken is announced as steadying, never as fresh dread") + eq(game.player.sanity, 1, "a calm turn only ever recovers its small fixed amount") +end + +function tests.sanityTickAnnouncesFullyRegainingComposure() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.player.sanity = 60 + eq(Sanity.tick(game), "your mind is your own again", "climbing back above the shaken ceiling is announced") +end + +function tests.sanityTickNeverClimbsPastTheMaxOrReportsAPhantomCrossing() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.player.sanity = 100 + eq(Sanity.tick(game), nil, "an already-composed mind with nothing to recover reports no line") + eq(game.player.sanity, 100, "recovery never climbs the meter past its own maximum") +end + +function tests.sanityRollPanicNeverFiresAboveBroken() + local entity = { sanity = 1 } + for seed = 1, 50 do + ok(not Sanity.rollPanic(Rng.new(seed), entity), "terrified never risks losing the turn to panic") + end +end + +function tests.sanityRollPanicRollsAgainstItsFixedChanceOnceBroken() + local entity = { sanity = 0 } + local sawPanic, sawComposure = false, false + for seed = 1, 50 do + if Sanity.rollPanic(Rng.new(seed), entity) then sawPanic = true else sawComposure = true end + end + ok(sawPanic, "broken panics on at least some rolls across a handful of seeds") + ok(sawComposure, "broken does not panic on every single roll") +end + +function tests.sanityRollScreamNeverFiresAboveTerrified() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.player.sanity = 31 + for seed = 1, 50 do + game.rng = Rng.new(seed) + ok(not Sanity.rollScream(game.rng, game), "merely shaken never risks an involuntary scream") + end +end + +function tests.sanityRollScreamAlertsEveryDormantMonsterWithinItsRadius() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.player.sanity = 0 + local zombie = Monsters.create(Monsters.byKey.zombie, 1, 5, 6, 1) + zombie.awareness = Stealth.DORMANT + game.level.entities = { zombie } + local triggered = false + for seed = 1, 80 do + game.rng = Rng.new(seed) + if Sanity.rollScream(game.rng, game) then triggered = true break end + end + ok(triggered, "a broken mind eventually screams across a handful of seeds") + eq(zombie.awareness, Stealth.HUNTING, "the scream startles a nearby dormant monster fully awake") + eq(zombie.lastKnownX, game.player.x, "the alerted monster now knows exactly where the scream came from") +end + +function tests.sanityFieldSurvivesSerialization() + local game = Game.new(3) + game.player.sanity = 37 + local path = os.tmpname() + game:save(path) + local restored = Game.load(path) + os.remove(path) + eq(restored.player.sanity, 37, "the player's sanity survives a save/load round trip") +end + +function tests.gameEndPlayerTurnTicksSanityAndTheLogReportsATierCrossing() + local game = stagedGame(1, arenaDark(21, 11), 3, 6) + game.player.light.fuel = 0 + game.player.sanity = 63 + game:endPlayerTurn() + eq(game.player.sanity, 61, "ending a turn on a dark, torchless floor drains the fixed darkness dread") + game:endPlayerTurn() + ok(contains(game.log:last(), "fray"), "crossing into shaken is reported in the message log") + eq(game.player:effectiveAttack(), game.player.attack - 1, "the fresh shaken penalty is already live in effectiveAttack") +end + +function tests.gameAutoStepSkipsThePlayerTurnWhenPanicFires() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.player.sanity = 0 + local px, py = game.player.x, game.player.y + local sawPanic = false + for seed = 1, 80 do + game.rng = Rng.new(seed) + game.player.x, game.player.y = px, py + game.player.sanity = 0 + -- endPlayerTurn's own Sanity.tick line (recovery or otherwise) always + -- lands after any panic line within the same turn, so the panic line + -- itself has to be found among the turn's recent lines rather than + -- assumed to be the very last one. + game:autoStep() + if contains(table.concat(game.log:recent(3), " "), "terror seizes you") then sawPanic = true break end + end + ok(sawPanic, "a broken mind eventually loses a whole turn to panic across a handful of seeds") + eq(game.player.x, px, "a panicked turn never moves the player") + eq(game.player.y, py, "a panicked turn never moves the player") +end + +function tests.gameUseItemPotionOfClarityRestoresSanity() + local game = stagedGame(1, arena(21, 11), 3, 6) + game.player.sanity = 40 + game.player.inventory = { Items.potionOf("clarity", 30) } + ok(game:useItem(1), "drinking the potion of clarity succeeds") + eq(game.player.sanity, 70, "clarity restores its fixed power to the drinker's sanity") + ok(contains(game.log:last(), "steadies"), "the log reports the mind steadying") +end + +function tests.renderPanelShowsSanityOnlyOnceItDropsBelowComposed() + local game = stagedGame(1, arena(21, 11), 3, 6) + ok(not contains(Render.panel(game), "sanity"), "a fully composed player shows no sanity line at all") + game.player.sanity = 30 + ok(contains(Render.panel(game), "sanity 30/100 (terrified)"), "a shaken mind is reported in the status panel") +end + +-- Disease used to be invisible outside the transient log lines Disease.tick +-- emits (the eruption, a damage tick, an escalation) - once one of those +-- scrolled off the log, a player mid-incubation or mid-plague had no +-- persistent way to check their own infection at all, unlike every timed +-- status statusTags already folds into this same line. +function tests.renderPanelShowsTheDiseaseStageOnceThePlayerIsInfectedAndNothingBeforeInfection() + local game = stagedGame(1, arena(21, 11), 3, 6) + ok(not contains(Render.panel(game), "parasites"), "an uninfected player's status line mentions no disease") + Disease.infect(game.player, Disease.PARASITES) + ok(contains(Render.panel(game), "gut parasites(incubating)"), + "an incubating infection shows up in the status panel even before it erupts") + game.player.disease.stage = 2 + ok(contains(Render.panel(game), "gut parasites(stage 2)"), + "an erupted infection reports its current stage in the status panel") +end + +function tests.builderOccasionallyRollsAPotionOfClarityAsLoot() + local found + for seed = 1, 200 do + local level = Builder.build(2, Rng.new(seed), {}) + for _, ground in ipairs(level.floorItems) do + if ground.item.kind == Items.POTION and ground.item.effect == "clarity" then found = ground.item end + end + if found then break end + end + ok(found ~= nil, "a potion of clarity eventually turns up in the depth-2 loot table across a handful of seeds") + ok(Shop.priceOf(found) > 0, "a found clarity potion still carries a positive catalog price") +end + +function tests.builderOccasionallyRollsAPotionOfConfusionAsLoot() + local found + for seed = 1, 200 do + local level = Builder.build(2, Rng.new(seed), {}) + for _, ground in ipairs(level.floorItems) do + if ground.item.kind == Items.POTION and ground.item.effect == "confuse" then found = ground.item end + end + if found then break end + end + ok(found ~= nil, "a potion of confusion eventually turns up in the depth-2 loot table across a handful of seeds") + ok(Shop.priceOf(found) > 0, "a found confusion potion still carries a positive catalog price") +end + +function tests.builderOccasionallyRollsAPotionOfBlindnessAsLoot() + local found + for seed = 1, 200 do + local level = Builder.build(2, Rng.new(seed), {}) + for _, ground in ipairs(level.floorItems) do + if ground.item.kind == Items.POTION and ground.item.effect == "blind" then found = ground.item end + end + if found then break end +end + local order = { "rngDeterminism", "mapDeterminism", "tilesRegistry", "fovKnownScene", "losSightLine", "astarPathing", "combatResolution", "itemsAndEquipment", "affixes", "statusEffects",