test: cover tests.gameLoadRaisesAClearErrorForASaveFromAFutureVersion
This commit is contained in:
1 parent
8250eacded
commit
f47cfd379f
1 file changed
+173
+173
@@ -11720,6 +11720,179 @@ end
|
|||||||
|
|
||||||
function tests.gameLoadRaisesAClearErrorOnATruncatedSaveFileInsteadOfCrashingObscurely()
|
function tests.gameLoadRaisesAClearErrorOnATruncatedSaveFileInsteadOfCrashingObscurely()
|
||||||
local game = Game.new(1)
|
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
|
end
|
||||||
|
|
||||||
local order = {
|
local order = {
|
||||||
|
|||||||
Reference in new issue
Block a user