test: cover tests.biomesForDepthCyclesThroughTheCatalogInOrder

This commit is contained in:
dualfroz committed 2026-09-10 20:11:22 +02:00
1 parent 2c6fae4f44
commit 865b04ef93
1 file changed
+173
+173
View File
@@ -6703,6 +6703,179 @@ end
-- Biomes: a fixed, seed-independent theme per depth (contrast with
-- Atmosphere's own dice-rolled weather above), read by the builder's biome
-- pass to occasionally reinforce a floor with one more native monster or
-- bloom a small pocket of its own hazard kind.
function tests.biomesForDepthCyclesThroughTheCatalogInOrder()
for i, biome in ipairs(Biomes.CATALOG) do
eq(Biomes.forDepth(i), biome, "depth " .. i .. " maps to the catalog's own entry in order")
end
eq(Biomes.forDepth(#Biomes.CATALOG + 1), Biomes.CATALOG[1],
"depth wraps back to the first biome once the catalog is exhausted")
eq(Biomes.forDepth(3 * #Biomes.CATALOG), Biomes.CATALOG[#Biomes.CATALOG],
"depth keeps cycling any number of catalog lengths deep")
end
function tests.biomesDescribeNamesTheBiomeAndItsAmbientText()
eq(Biomes.describe(Biomes.byKey.warrens), "the mossy warrens: damp stone and skittering claws",
"describe joins a biome's own name and ambient line")
end
function tests.biomesCatalogEveryEntryNamesARealFactionAndHazardKind()
local realFactions = {}
for _, faction in pairs(Factions) do
if type(faction) == "string" then realFactions[faction] = true end
end
for _, biome in ipairs(Biomes.CATALOG) do
ok(realFactions[biome.favoredFaction], biome.key .. " names a real faction")
ok(biome.hazardKind == Hazards.FIRE or biome.hazardKind == Hazards.GAS,
biome.key .. " names a real hazard kind")
end
end
function tests.biomesNativesAmongFiltersToTheFavoredFactionAndExcludesRangedSpecs()
local crypts = Biomes.byKey.crypts
local deepRoster = Monsters.forDepth(6)
local natives = Biomes.nativesAmong(crypts, deepRoster)
ok(#natives > 0, "crypts finds at least one native among a deep enough roster")
for _, spec in ipairs(natives) do
eq(spec.faction, Factions.UNDEAD, "every native belongs to the biome's own faction")
ok(spec.ai ~= "ranged", "no native reinforcement is ever a ranged specialist")
end
local shallowRoster = Monsters.forDepth(1)
eq(#Biomes.nativesAmong(crypts, shallowRoster), 0,
"no undead have surfaced yet this shallow, so crypts finds no native at depth one")
end
function tests.builderLevelsCarryTheBiomeMatchingTheirDepthRegardlessOfSeed()
for depth = 1, 8 do
local expected = Biomes.forDepth(depth).key
for seed = 1, 3 do
local level = Builder.build(depth, Rng.new(seed), {})
eq(level.biome, expected, "depth " .. depth .. " always carries its own fixed biome no matter the seed")
end
end
end
function tests.builderOccasionallyReinforcesAFloorWithADormantNativeMonster()
local level = Builder.build(1, Rng.new(7), {})
ok(#level.entities > 7, "this seed's entrance floor carries one more monster than the ordinary roster ever places")
local reinforcement = level.entities[#level.entities]
eq(reinforcement.faction, Factions.VERMIN, "the extra monster belongs to the entrance floor's own biome")
eq(reinforcement.awareness, Stealth.DORMANT, "the reinforcement starts dormant like every other wild monster")
end
function tests.builderOccasionallyBloomsAHazardMatchingTheFloorSBiome()
local warrens = Builder.build(1, Rng.new(7), {})
eq(warrens.hazards[1].kind, Hazards.GAS, "a gas-favoring biome only ever blooms gas at generation")
local warcamp = Builder.build(4, Rng.new(11), {})
eq(warcamp.hazards[1].kind, Hazards.FIRE, "a fire-favoring biome only ever blooms fire at generation")
end
function tests.biomeFieldSurvivesSerialization()
local level = arena(21, 11)
level.biome = "crypts"
local restored = Level.fromData(level:toData())
eq(restored.biome, "crypts", "a floor's biome survives a round trip")
end
-- Stealth: monster alertness (asleep/hunting/searching), sneak attacks.
local function wallSplitArena()
local map = Map.new(21, 11)
for y = 2, 10 do for x = 2, 20 do map:set(x, y, Map.FLOOR) end end
for y = 2, 10 do map:set(11, y, Map.WALL) end
return Level.new({
depth = 1, generator = "arena", map = map,
entities = {}, floorItems = {}, traps = {}, lights = {}, dark = false, explored = {},
upX = 2, upY = 2, downX = 20, downY = 10,
})
end
function tests.stealthHandBuiltMonsterDefaultsToAlertWhenAwarenessIsUnset()
local orc = Monsters.create(Monsters.byKey.orc, 1, 5, 5, 1)
eq(orc.awareness, nil, "a monster built directly carries no awareness field at all")
ok(not Stealth.isUnaware(orc), "an unset awareness field is treated as already alert, not asleep")
eq(Stealth.sneakMultiplier(orc), 1, "no sneak bonus applies against an already-alert target")
end
function tests.builderPlacesWildDepthOneMonstersAsleepByDefault()
local game = Game.new(9)
ok(#game.level.entities > 0, "the opening floor spawns at least one wild monster")
for _, entity in ipairs(game.level.entities) do
eq(entity.awareness, Stealth.DORMANT, "every builder-placed wild monster on the opening floor starts asleep")
end
end
function tests.stealthBossesStartFullyAlertRatherThanDormant()
local warlord = Bosses.create(Bosses.byKey.warlord, 1, 5, 5)
ok(not Stealth.isUnaware(warlord), "a named boss is never asleep, unlike a wild monster")
eq(warlord.awareness, Stealth.HUNTING, "a boss starts directly in the hunting state")
end
function tests.stealthSpawnedMinionsStartHuntingRatherThanDormant()
local game = stagedGame(1, arena(21, 11), 3, 6)
local minion = game:spawnMinion("zombie", 10, 6)
eq(minion.awareness, Stealth.HUNTING, "a minion summoned or split mid-fight is already aware, never asleep")
end
function tests.stealthDormantMonsterWakesImmediatelyWhenThePlayerStandsNextToIt()
local game = stagedGame(1, arena(21, 11), 3, 6)
local orc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
orc.awareness = Stealth.DORMANT
game.level.entities = { orc }
local hpBefore = game.player.hp
game:takeMonsterTurn(orc)
eq(orc.awareness, Stealth.HUNTING, "a monster right next to the player always notices it")
ok(game.player.hp < hpBefore, "the freshly woken monster still gets to act on the very same turn")
end
function tests.stealthDormantMonsterBeyondItsWakeRadiusNeverStirs()
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, 10 do
game:takeMonsterTurn(orc)
end
eq(orc.awareness, Stealth.DORMANT, "a monster far outside its wake radius stays asleep no matter how many turns pass")
eq(orc.x, 19, "an asleep monster that never wakes does not move")
eq(orc.y, 6, "an asleep monster that never wakes does not move either")
end
function tests.stealthDormantMonsterWithinRangeEventuallyWakesOnASuccessfulRoll()
local game = stagedGame(1, arena(21, 11), 3, 6)
local orc = Monsters.create(Monsters.byKey.orc, 1, 6, 6, 1)
orc.awareness = Stealth.DORMANT
game.level.entities = { orc }
local woke = false
for _ = 1, 20 do
game:takeMonsterTurn(orc)
if orc.awareness ~= Stealth.DORMANT then woke = true; break end
end
ok(woke, "a monster well within its wake radius eventually notices the player across enough turns")
end
function tests.stealthMeleeAttackOnADormantMonsterDealsATripledSneakBonus()
local sneakGame = stagedGame(1, arena(21, 11), 3, 6)
local sneakOrc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
sneakOrc.awareness = Stealth.DORMANT
sneakGame.level.entities = { sneakOrc }
local before = sneakOrc.hp
sneakGame:playerAttack(sneakOrc)
local sneakDamage = before - sneakOrc.hp
eq(sneakOrc.awareness, Stealth.HUNTING, "landing the blow wakes the target immediately")
ok(contains(table.concat(sneakGame.log:recent(6), "\n"), "unaware"), "the log calls out the sneak attack")
local plainGame = stagedGame(1, arena(21, 11), 3, 6)
local plainOrc = Monsters.create(Monsters.byKey.orc, 1, 4, 6, 1)
plainGame.level.entities = { plainOrc }
local plainBefore = plainOrc.hp
plainGame:playerAttack(plainOrc)
local plainDamage = plainBefore - plainOrc.hp
eq(sneakDamage, plainDamage * 3, "a sneak attack deals exactly triple the damage of the same hit thrown while already noticed")
end
function tests.stealthHuntingMonsterLosesTrackAndSwitchesToSearching()
end
local order = {