From f47cfd379f92ddb3f5bd6eb9bfad9170467fe118 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 15:55:05 +0200 Subject: [PATCH] test: cover tests.gameLoadRaisesAClearErrorForASaveFromAFutureVersion --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index 6542389..0139363 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -11720,6 +11720,179 @@ end function tests.gameLoadRaisesAClearErrorOnATruncatedSaveFileInsteadOfCrashingObscurely() local game = Game.new(1) + local path = os.tmpname() + game:save(path) + local file = assert(io.open(path, "r")) + local text = file:read("a") + file:close() + local out = assert(io.open(path, "w")) + out:write(text:sub(1, 20)) + out:close() + local success, err = pcall(Game.load, path) + os.remove(path) + eq(success, false, "loading a truncated save fails rather than silently misloading") + ok(contains(err or "", "corrupt"), "the failure names corruption instead of an obscure nil-call error") +end + +function tests.gameLoadRaisesAClearErrorForASaveFromAFutureVersion() + local payload = Serialize.encode({ marker = 1 }) + local envelope = { + magic = "WARRENSAVE", version = 999, + checksum = SaveGame.fnv1a32(payload), payload = payload, + } + local path = os.tmpname() + local file = assert(io.open(path, "w")) + file:write(Serialize.encode(envelope)) + file:close() + local success, err = pcall(Game.load, path) + os.remove(path) + eq(success, false, "loading a save from a newer format fails rather than misinterpreting it") + ok(contains(err or "", "future version"), "the failure names the version mismatch") +end + +-- A dead-end room reachable from the player's own room only by turning a +-- corner, so a straight sightline between the two ends always crosses solid +-- rock while a four-directional flood (or a walker) can still reach it. +local function noiseCornerArena() + local map = Map.new(15, 9) + for x = 2, 5 do map:set(x, 3, Map.FLOOR) end + for y = 3, 5 do map:set(5, y, Map.FLOOR) end + return Level.new({ + depth = 1, generator = "arena", map = map, + entities = {}, floorItems = {}, traps = {}, lights = {}, dark = false, explored = {}, + upX = 2, upY = 3, downX = 5, downY = 5, + }) +end + +function tests.noisePropagateDecaysByExactlyOneLoudnessPerStepThroughOpenFloor() + local level = arena(21, 11) + local field = Noise.propagate(level.map, 5, 5, 4) + eq(Noise.intensityAt(field, level.map, 5, 5), 4, "the origin itself keeps the full starting loudness") + eq(Noise.intensityAt(field, level.map, 6, 5), 3, "one step away has decayed by exactly one") + eq(Noise.intensityAt(field, level.map, 7, 5), 2, "two steps away has decayed by exactly two") + eq(Noise.intensityAt(field, level.map, 8, 5), 1, "three steps away has decayed by exactly three") + eq(Noise.intensityAt(field, level.map, 9, 5), 0, "four steps away has decayed to nothing") + eq(Noise.intensityAt(field, level.map, 10, 5), 0, "five steps away never even carried a trailing zero forward") +end + +function tests.noisePropagateNeverCrossesASolidWallEvenWhenTheOtherSideIsCloseByEuclideanDistance() + local level = wallSplitArena() + local field = Noise.propagate(level.map, 5, 6, 20) + ok(Noise.intensityAt(field, level.map, 10, 6) > 0, "loudness reaches all the way up to the dividing wall") + eq(Noise.intensityAt(field, level.map, 12, 6), 0, "nothing at all gets through to the far side of the wall") +end + +function tests.noisePropagateBendsAroundACornerToReachACellWithNoStraightLineAtAll() + local level = noiseCornerArena() + ok(not Los.clear(level.map, 2, 3, 5, 5), "no straight sightline exists between the two ends of the bend") + local field = Noise.propagate(level.map, 2, 3, 10) + ok(Noise.intensityAt(field, level.map, 5, 5) > 0, "the flood still reaches the far end by turning the corner") + eq(Noise.intensityAt(field, level.map, 5, 5), 10 - 5, "the intensity there reflects the corridor's actual walked distance, five steps") +end + +function tests.noisePropagateFromAWallOriginStillReachesItsWalkableNeighbours() + local level = arena(21, 11) + level.map:set(10, 6, Map.WALL) + local field = Noise.propagate(level.map, 10, 6, 5) + eq(Noise.intensityAt(field, level.map, 10, 6), 5, "the unwalkable origin itself still records the starting loudness") + eq(Noise.intensityAt(field, level.map, 11, 6), 4, "a walkable neighbour of the struck wall still hears it one step quieter") + eq(Noise.intensityAt(field, level.map, 9, 6), 4, "every walkable side of the wall hears it, not just one") +end + +function tests.noiseIntensityAtIsZeroOutOfBoundsAndWhereverTheFloodNeverReached() + local level = arena(21, 11) + local field = Noise.propagate(level.map, 5, 5, 3) + eq(Noise.intensityAt(field, level.map, -3, 5), 0, "a cell outside the map bounds reads as silent") + eq(Noise.intensityAt(field, level.map, 5, 5000), 0, "a wildly out of range cell reads as silent rather than erroring") + eq(Noise.intensityAt(field, level.map, 19, 9), 0, "a cell the flood never reached reads as silent") +end + +function tests.noisePropagateWithZeroOrNegativeLoudnessReachesNothing() + local level = arena(21, 11) + eq(next(Noise.propagate(level.map, 5, 5, 0)), nil, "zero loudness produces an entirely empty field") + eq(next(Noise.propagate(level.map, 5, 5, -3)), nil, "negative loudness produces an entirely empty field too") +end + +function tests.gameEmitNoiseEventuallyStirsADormantMonsterWithinRangeIntoSearchingAtTheNoisesOrigin() + local game = stagedGame(1, arena(21, 11), 3, 6) + local orc = Monsters.create(Monsters.byKey.orc, 1, 7, 6, 1) + orc.awareness = Stealth.DORMANT + game.level.entities = { orc } + local stirred = false + for _ = 1, 200 do + game:emitNoise(3, 6, Noise.MELEE) + if orc.awareness ~= Stealth.DORMANT then stirred = true; break end + end + ok(stirred, "a dormant monster within the flood's reach eventually stirs across enough noisy bursts") + eq(orc.awareness, Stealth.SEARCHING, "hearing a noise only starts a search, unlike Stealth.alert's outright hunting") + eq(orc.lastKnownX, 3, "the search heads for the noise's own origin") + eq(orc.lastKnownY, 6, "the search heads for the noise's own origin") +end + +function tests.gameEmitNoiseNeverStirsADormantMonsterTheFloodCannotReach() + local game = stagedGame(1, arena(21, 11), 3, 6) + local orc = Monsters.create(Monsters.byKey.orc, 1, 19, 6, 1) + orc.awareness = Stealth.DORMANT + game.level.entities = { orc } + for _ = 1, 200 do + game:emitNoise(3, 6, Noise.MELEE) + end + eq(orc.awareness, Stealth.DORMANT, "a monster far outside even the loudest reach used here never stirs") +end + +function tests.gameEmitNoiseNeverAffectsAMonsterThatIsAlreadyAware() + local game = stagedGame(1, arena(21, 11), 3, 6) + local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1) + orc.awareness = Stealth.HUNTING + orc.lastKnownX, orc.lastKnownY = 99, 99 + game.level.entities = { orc } + game:emitNoise(3, 6, Noise.MELEE) + eq(orc.awareness, Stealth.HUNTING, "an already-aware monster is untouched by a noise burst") + eq(orc.lastKnownX, 99, "its existing last-known position is left exactly as it was") +end + +function tests.gamePlayerAttackCanStirADormantBystanderThatIsNotEvenTheTarget() + local game = stagedGame(1, arena(21, 11), 3, 6) + local target = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1) + local bystander = Monsters.create(Monsters.byKey.orc, 2, 8, 6, 1) + bystander.awareness = Stealth.DORMANT + game.level.entities = { target, bystander } + local stirred = false + for _ = 1, 200 do + target.hp = target.maxHp + game:playerAttack(target) + if bystander.awareness ~= Stealth.DORMANT then stirred = true; break end + end + ok(stirred, "a sustained melee fight eventually stirs a wild bystander who was never the target") + eq(bystander.awareness, Stealth.SEARCHING, "the bystander only starts searching, having merely heard the clash") +end + +function tests.gameMonsterMeleeCanStirADormantBystanderTooNotJustPlayerActions() + local game = stagedGame(1, arena(21, 11), 3, 6) + local attacker = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1) + local bystander = Monsters.create(Monsters.byKey.orc, 2, 8, 6, 1) + bystander.awareness = Stealth.DORMANT + game.level.entities = { attacker, bystander } + local stirred = false + for _ = 1, 200 do + game.player.hp = game.player.maxHp + game:monsterMelee(attacker) + if bystander.awareness ~= Stealth.DORMANT then stirred = true; break end + end + ok(stirred, "a monster hitting the player is exactly as loud as the player hitting back") +end + +function tests.gameCastSpellOnlyEmitsNoiseOnASuccessfulCastNeverOnAFailedOne() + local game = stagedGame(5, arena(21, 11), 3, 6) + local rat = Monsters.create(Monsters.byKey.rat, 1, 6, 6, 1) + local bystander = Monsters.create(Monsters.byKey.orc, 2, 9, 6, 1) + bystander.awareness = Stealth.DORMANT + game.level.entities = { rat, bystander } + game.player.mana = 0 + for _ = 1, 200 do + game:castSpell("missile", rat) + end + eq(bystander.awareness, Stealth.DORMANT, "two hundred failed casts for lack of mana never make a sound") end local order = {