test: cover tests.pullingAnAlreadyThrownLeverAgainDoesNotInflateTheVa...

This commit is contained in:
dualfroz committed 2026-09-10 18:54:24 +02:00
1 parent 6e216940ac
commit 8f7fd7151e
1 file changed
+173
+173
View File
@@ -6357,6 +6357,179 @@ function tests.gamePullLeverOnATwoLeverVaultWaitsForTheSecondPull()
ok(contains(game.log:last(), "clunks into place"), "a partial pull is logged as a clunk, not an opening")
game:pullLever(leverB)
eq(game.level.map:get(10, 6), Tiles.DOOR_OPEN, "the door opens the instant the last lever is thrown")
eq(game.stats.vaultsOpened, 1, "the vault tally advances exactly once, on the pull that completes the puzzle")
end
function tests.pullingAnAlreadyThrownLeverAgainDoesNotInflateTheVaultTally()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.level.map:set(6, 6, Tiles.LEVER)
game.level.map:set(10, 6, Tiles.VAULT_DOOR)
local lever = { x = 6, y = 6, id = "v5", pulled = false }
game.level.vaultLevers = { lever }
game.level.vaultDoors = { { x = 10, y = 6, id = "v5", opened = false } }
game:pullLever(lever)
eq(game.stats.vaultsOpened, 1, "the first pull opens the vault")
local pulledAgain = game:pullLever(lever)
eq(pulledAgain, false, "re-pulling an already-thrown lever reports failure")
eq(game.stats.vaultsOpened, 1, "re-pulling an already-thrown lever does not inflate the vault tally")
end
function tests.movePlayerAutoPullsALeverItStepsOnto()
local game = stagedGame(1, arena(21, 11), 5, 5)
game.level.map:set(6, 5, Tiles.LEVER)
local lever = { x = 6, y = 5, id = "v6", pulled = false }
game.level.vaultLevers = { lever }
game.level.vaultDoors = { { x = 15, y = 5, id = "v6", opened = false } }
game:movePlayer(1, 0)
eq(game.player.x, 6, "the player actually steps onto the lever's cell")
eq(lever.pulled, true, "stepping onto a lever automatically throws it")
end
function tests.vaultDoorBlocksMovementUntilItsLeversOpenIt()
local game = stagedGame(1, arena(21, 11), 9, 5)
game.level.map:set(10, 5, Tiles.VAULT_DOOR)
local moved = game:movePlayer(1, 0)
eq(moved, false, "an unopened vault door blocks the step onto it")
eq(game.player.x, 9, "the player does not pass through a sealed vault door")
game.level.map:set(10, 5, Tiles.DOOR_OPEN)
local movedAfter = game:movePlayer(1, 0)
eq(movedAfter, true, "once opened, the door's tile behaves like any plain open door")
eq(game.player.x, 10, "the player steps onto the formerly sealed cell")
end
function tests.builderOccasionallyPlacesAVaultFromDepthFive()
-- Seed 41, not 1: adding the banshee species (minDepth 5) to Monsters.species
-- shifts Monsters.forDepth's eligible count for every depth-5+ monster spawn
-- roll, which desyncs Rng.new(1)'s stream from the very first monster onward -
-- seed 1 no longer rolls a vault onto this floor at all. Recomputed by
-- brute-force search (seeds 1-50) for one that still places exactly one
-- vault door with its treasure, same as every other assertion below expects.
local level = Builder.build(5, Rng.new(41), {})
eq(#level.vaultDoors, 1, "this seed's depth-5 floor places exactly one vault door")
ok(#level.vaultLevers >= 2, "the vault's puzzle has at least two levers")
local door = level.vaultDoors[1]
eq(level.map:get(door.x, door.y), Tiles.VAULT_DOOR, "the vault door's own tile matches its sealed glyph")
for _, lever in ipairs(level.vaultLevers) do
eq(lever.id, door.id, "every placed lever shares the door's own vault id")
eq(level.map:get(lever.x, lever.y), Tiles.LEVER, "each lever's tile matches its unpulled glyph")
end
local found
for _, ground in ipairs(level.floorItems) do
if ground.item.name and ground.item.name:find("vault", 1, true) then found = ground.item end
end
ok(found ~= nil, "the vault's guaranteed treasure is placed somewhere on the floor")
end
function tests.vaultsStayOffFloorsAboveDepthFive()
for seed = 1, 4 do
for depth = 1, 4 do
local level = Builder.build(depth, Rng.new(seed), {})
eq(#level.vaultDoors, 0, "no vault door appears above depth five")
eq(#level.vaultLevers, 0, "no vault lever appears above depth five")
end
end
end
-- Every vault the builder actually places must be genuinely solvable: each
-- of its levers sits somewhere already reachable from the entrance without
-- crossing a locked door or the vault door itself (see builder.lua's
-- fallback to a plain closed door whenever that cannot be guaranteed).
function tests.builderOnlyEverPlacesASolvableVaultPuzzle()
local checked = 0
for seed = 1, 200 do
local level = Builder.build(5, Rng.new(seed), {})
if #level.vaultDoors == 1 then
checked = checked + 1
for _, lever in ipairs(level.vaultLevers) do
local path = Astar.find(level.map, level.upX, level.upY, lever.x, lever.y, function(x, y)
if x == lever.x and y == lever.y then return false end
return not level.map:isPassable(x, y, false)
end)
ok(path ~= nil, "seed " .. seed .. ": every vault lever is reachable without a key or the vault door")
end
end
end
ok(checked > 0, "at least one of the sampled seeds actually placed a vault to check")
end
function tests.vaultFieldsSurviveSerialization()
local level = arena(21, 11)
level.map:set(6, 6, Tiles.LEVER_PULLED)
level.map:set(10, 6, Tiles.DOOR_OPEN)
level.vaultLevers = { { x = 6, y = 6, id = "v7", pulled = true } }
level.vaultDoors = { { x = 10, y = 6, id = "v7", opened = true } }
local restored = Level.fromData(level:toData())
eq(#restored.vaultLevers, 1, "the vault lever list survives a round trip")
eq(restored.vaultLevers[1].pulled, true, "a lever's pulled state survives a round trip")
eq(#restored.vaultDoors, 1, "the vault door list survives a round trip")
eq(restored.vaultDoors[1].opened, true, "a door's opened state survives a round trip")
end
function tests.gameVaultsOpenedStatSurvivesSaveLoad()
local game = Game.new(1)
local lx, ly = game.player.x + 1, game.player.y
game.level.map:set(lx, ly, Tiles.LEVER)
local lever = { x = lx, y = ly, id = "v8", pulled = false }
game.level.vaultLevers = { lever }
game.level.vaultDoors = { { x = game.player.x + 2, y = game.player.y, id = "v8", opened = false } }
game:pullLever(lever)
eq(game.stats.vaultsOpened, 1, "pulling the lever opens the vault")
local path = os.tmpname()
game:save(path)
local restored = Game.load(path)
os.remove(path)
eq(restored.stats.vaultsOpened, 1, "the vault-opened tally survives a save/load round trip")
eq(#restored.level.vaultLevers, 1, "the level's vault lever list survives a save/load round trip")
eq(restored.level.vaultLevers[1].pulled, true, "a pulled lever's state survives a save/load round trip")
end
function tests.metaRenownForScalesWithVaultsOpened()
local base = Meta.renownFor({ deepestDepth = 3, bossKills = 0, questsCompleted = 0, vaultsOpened = 0, won = false })
local withVault = Meta.renownFor({ deepestDepth = 3, bossKills = 0, questsCompleted = 0, vaultsOpened = 2, won = false })
eq(withVault, base + 20, "each opened vault adds a fixed renown bonus")
end
function tests.metaRecordRunUnlocksVaultBreakerOnceAVaultOpens()
local meta = Meta.new()
local stats = {
deepestDepth = 1, kills = 0, bossKills = 0, questsCompleted = 0,
artifactsFound = 0, vaultsOpened = 1, won = false,
}
local unlocked = Meta.recordRun(meta, stats)
eq(unlocked[1], "Vault Breaker", "opening a vault for the first time unlocks Vault Breaker")
eq(meta.achievements.vault_breaker, true, "vault_breaker is marked unlocked")
local again = Meta.recordRun(meta, stats)
eq(#again, 0, "an already-unlocked Vault Breaker is not re-announced on a later run")
end
-- Runewords (runewords.lua): rune stones socket into a weapon or armor's
-- rune chain in order; an exact match against the catalog grants a fixed
-- attack/defense bonus read live by combat.lua, exactly like a skill bonus.
function tests.runewordsSocketAppendsToTheChainInOrderAndCapsAtMaxRunes()
local sword = Items.weapon("sword", 4)
ok(Runewords.socket(sword, Runewords.rune("sol")), "socketing a rune into a bare weapon succeeds")
ok(Runewords.socket(sword, Runewords.rune("ith")), "a second rune joins the same chain")
eq(table.concat(sword.runes, "-"), "sol-ith", "the chain records runes in the order they were socketed")
ok(Runewords.socket(sword, Runewords.rune("tal")), "a third rune still fits under the cap")
local success, reason = Runewords.socket(sword, Runewords.rune("ral"))
eq(success, false, "a fourth rune overflows the chain")
eq(reason, "the rune chain is full", "socket explains why a full chain refuses another rune")
end
function tests.runewordsSocketRefusesAnythingThatIsNotWeaponOrArmor()
local potion = Items.potion(10)
local success, reason = Runewords.socket(potion, Runewords.rune("sol"))
eq(success, false, "a potion cannot hold a rune chain")
eq(reason, "cannot hold runes", "socket explains why a non-gear item refuses a rune")
end
function tests.runewordsActiveWordMatchesOnlyAnExactChain()
local sword = Items.weapon("sword", 4)
eq(Runewords.activeWord(sword), nil, "a bare weapon forms no runeword")
Runewords.socket(sword, Runewords.rune("sol"))
eq(Runewords.activeWord(sword), nil, "a single rune alone matches no cataloged word")
Runewords.socket(sword, Runewords.rune("ith"))
end
local order = {