test: cover tests.pullingAnAlreadyThrownLeverAgainDoesNotInflateTheVa...
This commit is contained in:
1 parent
6e216940ac
commit
8f7fd7151e
1 file changed
+173
+173
@@ -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 = {
|
||||
|
||||
Reference in new issue
Block a user