diff --git a/tests/run.lua b/tests/run.lua index d5fd8bf..f3e02bd 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -11201,6 +11201,179 @@ function tests.blocksPushFailsAgainstAWallOrAnotherBlockOrAnEntity() 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") + 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 local order = {