test: cover tests.blocksUpdateGatesOpensOnlyWhileAPlatedBlockHoldsItA...

This commit is contained in:
dualfroz committed 2026-09-12 13:46:09 +02:00
1 parent aaa221f6bf
commit a1cacb21bb
1 file changed
+173
+173
View File
@@ -11201,6 +11201,179 @@ function tests.blocksPushFailsAgainstAWallOrAnotherBlockOrAnEntity()
eq(Blocks.push(level, block, 1, 0), false, "a shove into a wall fails outright") eq(Blocks.push(level, block, 1, 0), false, "a shove into a wall fails outright")
eq(block.x, 3, "a failed shove leaves the block exactly where it was") eq(block.x, 3, "a failed shove leaves the block exactly where it was")
level.map:set(4, 3, Tiles.FLOOR)
level.blocks = { block, { x = 4, y = 3 } }
eq(Blocks.push(level, block, 1, 0), false, "a shove into another block fails outright")
level.blocks = { block }
level.entities = { Entity.new({ x = 4, y = 3, glyph = "r", name = "rat", kind = "monster", hp = 5, attack = 1, defense = 0 }) }
eq(Blocks.push(level, block, 1, 0), false, "a shove into a living entity fails outright")
end
function tests.blocksUpdateGatesOpensOnlyWhileAPlatedBlockHoldsItAndReseals()
local level = arena(9, 9)
level.map:set(6, 3, Tiles.GATE_CLOSED)
level.gates = { { x = 6, y = 3, open = false } }
level.plates = { { x = 4, y = 3, gateX = 6, gateY = 3 } }
local block = { x = 3, y = 3 }
level.blocks = { block }
Blocks.updateGates(level)
eq(level.map:get(6, 3), Tiles.GATE_CLOSED, "the gate stays sealed with no block on its plate")
eq(level.gates[1].open, false, "the gate's own open field matches its sealed tile")
Blocks.push(level, block, 1, 0)
Blocks.updateGates(level)
eq(level.map:get(6, 3), Tiles.GATE_OPEN, "the gate swings open the instant its plate takes the block's weight")
eq(level.gates[1].open, true, "the gate's own open field matches its opened tile")
Blocks.push(level, block, 1, 0)
Blocks.updateGates(level)
eq(level.map:get(6, 3), Tiles.GATE_CLOSED, "the gate reseals the instant the block is shoved off its plate")
eq(level.gates[1].open, false, "the gate's own open field matches its resealed tile")
end
function tests.movePlayerShovesABlockAheadAndTheGateItHoldsOpenReactsImmediately()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.level.map:set(6, 6, Tiles.GATE_CLOSED)
game.level.gates = { { x = 6, y = 6, open = false } }
game.level.plates = { { x = 5, y = 6, gateX = 6, gateY = 6 } }
game.level.blocks = { { x = 4, y = 6 } }
game:movePlayer(1, 0)
eq(game.player.x, 4, "the player steps into the cell the block just vacated")
eq(game.level.blocks[1].x, 5, "the shove relocates the block by exactly one cell")
eq(game.level.blocks[1].y, 6, "the shove does not drift off the push axis")
eq(game.level.map:get(6, 6), Tiles.GATE_OPEN, "the linked gate opens the instant the block lands on its plate")
ok(contains(game.log:last(), "shove"), "shoving the block is logged")
end
function tests.movePlayerCannotShoveABlockIntoAWallAndDoesNotMoveEither()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.level.map:set(5, 6, Tiles.WALL)
game.level.blocks = { { x = 4, y = 6 } }
local moved = game:movePlayer(1, 0)
eq(moved, false, "the player's own step fails along with the blocked shove")
eq(game.player.x, 3, "the player does not move when the shove it would trigger fails")
eq(game.level.blocks[1].x, 4, "the block stays put when its shove fails")
end
function tests.movePlayerCannotShoveOneBlockIntoAnother()
local game = stagedGame(1, arena(21, 11), 3, 6)
game.level.blocks = { { x = 4, y = 6 }, { x = 5, y = 6 } }
local moved = game:movePlayer(1, 0)
eq(moved, false, "shoving a block into another block fails the whole step")
eq(game.player.x, 3, "the player does not move when the chained shove fails")
end
function tests.movePlayerSwapsPlacesWithItsOwnCompanionInsteadOfAttackingIt()
local game = stagedGame(1, arena(21, 11), 5, 5)
local hound = Companions.summon(game, "hound")
local hpBefore = hound.hp
game:movePlayer(1, 0)
eq(game.player.x, 6, "the player steps onto the cell its companion just vacated")
eq(game.player.y, 5, "the player's row is unchanged by the swap")
eq(hound.x, 5, "the companion is displaced onto the player's old cell")
eq(hound.y, 5, "the companion's row is unchanged by the swap")
eq(hound.hp, hpBefore, "swapping past a companion never damages it")
ok(contains(game.log:last(), "swap"), "swapping past an ally is logged")
end
function tests.movePlayerSwapsPlacesWithAHiredHirelingToo()
local game = stagedGame(1, arena(21, 11), 5, 5)
local sellsword = Mercenaries.hire(game, "sellsword")
local hpBefore = sellsword.hp
game:movePlayer(1, 0)
eq(sellsword.x, 5, "a hireling is displaced by a swap exactly like a companion, never attacked")
eq(sellsword.hp, hpBefore, "swapping past a hireling never damages it")
end
function tests.adjacentMonsterSkipsTheBondedCompanionAndFindsARealHostileInstead()
local game = stagedGame(1, arena(21, 11), 5, 5)
Companions.summon(game, "hound")
local rat = Monsters.create(Monsters.byKey.rat, 50, 5, 6, 1)
game.level.entities[#game.level.entities + 1] = rat
eq(game:adjacentMonster(), rat, "the autopilot's melee target skips its own companion and finds the hostile rat")
end
function tests.adjacentMonsterReturnsNilWhenOnlyAnAllyStandsNextToThePlayer()
local game = stagedGame(1, arena(21, 11), 5, 5)
Companions.summon(game, "hound")
eq(game:adjacentMonster(), nil, "a bonded companion alone never counts as a melee target")
end
function tests.adjacentMonsterAlsoSkipsAThrallStandingNextToThePlayer()
local game = stagedGame(1, arena(21, 11), 5, 5)
local thrall = Entity.new({ id = 99, x = 6, y = 5, glyph = "z", name = "raised thrall", kind = "thrall",
hp = 5, attack = 2, defense = 0 })
game.level.entities[#game.level.entities + 1] = thrall
eq(game:adjacentMonster(), nil, "a raised thrall standing next to the player is never a melee target either")
end
function tests.choosePlayerActionNeverAttacksItsOwnBondedCompanionEvenWithNothingElseToDo()
local game = stagedGame(1, arena(21, 11), 5, 5)
local hound = Companions.summon(game, "hound")
local hpBefore = hound.hp
game:choosePlayerAction()
eq(hound.hp, hpBefore, "the autopilot never bumps its own companion into a melee attack")
end
function tests.aStarNeverRoutesAMonsterThroughABlockedCell()
local game = stagedGame(1, arena(21, 11), 3, 6)
for y = 2, 10 do game.level.map:set(11, y, Tiles.WALL) end
game.level.map:set(11, 6, Tiles.FLOOR)
game.level.blocks = { { x = 11, y = 6 } }
local orc = Monsters.create(Monsters.byKey.orc, 1, 13, 6, 1)
game.level.entities = { orc }
Ai.act(game, orc)
ok(not (orc.x == 11 and orc.y == 6), "a chasing monster never steps onto a cell a block occupies")
eq(orc.x, 13, "with the single gap blocked, the chaser has no route and holds its ground")
eq(orc.y, 6, "the chaser's y is likewise unchanged with no route available")
end
function tests.playerPathToNeverRoutesThroughABlockedCell()
local game = stagedGame(1, arena(21, 11), 3, 6)
for y = 2, 10 do game.level.map:set(11, y, Tiles.WALL) end
game.level.map:set(11, 6, Tiles.FLOOR)
game.level.blocks = { { x = 11, y = 6 } }
local path = game:playerPathTo(15, 6)
eq(path, nil, "no path is found when the only corridor cell is occupied by a block")
end
function tests.gatesAndBlocksStayOffFloorsAboveDepthTwo()
for seed = 1, 4 do
for depth = 1, 2 do
local level = Builder.build(depth, Rng.new(seed), {})
eq(#level.gates, 0, "no gate appears above depth three")
eq(#level.blocks, 0, "no pushable block appears above depth three")
eq(#level.plates, 0, "no pressure plate appears above depth three")
end
end
end
-- Depth 5 cycles to the rooms generator (see gen/init.lua's Gen.order),
-- the one that actually carves the doorway pinch points this puzzle needs;
-- depths 3-4 cycle to cellular/drunkard caves that essentially never have
-- one, so the gate's own depth >= 3 gate is exercised here rather than at
-- depth three itself, exactly like vaults.lua's own test picks depth five.
function tests.builderOccasionallyPlacesAGateBlockPuzzleOnceADoorwayIsAvailable()
local found = false
for seed = 1, 200 do
local level = Builder.build(5, Rng.new(seed), {})
if #level.gates == 1 then
found = true
eq(#level.plates, 1, "a placed gate always comes with exactly one linked plate")
eq(#level.blocks, 1, "a placed gate always comes with exactly one pushable block")
local gate, plate, block = level.gates[1], level.plates[1], level.blocks[1]
eq(plate.gateX, gate.x, "the plate's gate link matches the placed gate's x")
eq(plate.gateY, gate.y, "the plate's gate link matches the placed gate's y")
eq(level.map:get(gate.x, gate.y), Tiles.GATE_CLOSED, "the gate starts sealed on its own tile")
local dx, dy = plate.x - block.x, plate.y - block.y
ok((dx == 0 and math.abs(dy) == 1) or (dy == 0 and math.abs(dx) == 1),
"the block always starts exactly one cardinal step from its plate")
local treasureFound
for _, ground in ipairs(level.floorItems) do
if ground.item.name and ground.item.name:find("gate%-forged", 1) then treasureFound = ground.item end
end
ok(treasureFound ~= nil, "the puzzle's guaranteed treasure is placed somewhere on the floor")
end end
local order = { local order = {