From 0129c399f568ee393287a518dd5f568f8fb21603 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 15:21:10 +0200 Subject: [PATCH] test: cover tests.commandsApplyHelpMentionsTheOptionalTargetNumberOnE... --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index 7892f31..164fc30 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -13969,6 +13969,179 @@ function tests.commandsApplyExploreDrivesGameAutoExploreAndReturnsItsResult() end function tests.commandsApplyHelpMentionsExplore() + local game = stagedGame(1, arena(21, 11), 5, 5) + local _, message = Commands.apply(game, "help") + ok(contains(message, "explore"), "help mentions explore") +end + +function tests.commandsApplyHelpMentionsTheOptionalTargetNumberOnEveryTargetedVerb() + local game = stagedGame(1, arena(21, 11), 5, 5) + local _, message = Commands.apply(game, "help") + ok(contains(message, "throw [target]"), "help spells out throw's optional target number") + ok(contains(message, "fire [target]"), "help spells out fire's optional target number") + ok(contains(message, "capture [target]"), "help spells out capture's optional target number") + ok(contains(message, "cast [target]"), "help spells out cast's optional target number") + ok(contains(message, "useskill [target]"), "help spells out useskill's optional target number") +end + +function tests.gameRestUntilHealedRefusesWithoutSpendingATurnWhenAlreadyAtFullHealth() + local game = stagedGame(1, arena(11, 9), 5, 5) + local turnBefore = game.turn + local consumesTurn, message = game:restUntilHealed() + eq(consumesTurn, false, "resting at full health spends no turn") + eq(message, "you are already at full health", "the refusal explains why") + eq(game.turn, turnBefore, "the turn counter never advances") +end + +function tests.gameRestUntilHealedRefusesWithoutSpendingATurnWhileAHostileIsAlreadyInSight() + local level = arena(21, 11) + local game = stagedGame(1, level, 5, 5) + game.player.hp = game.player.maxHp - 5 + local orc = Monsters.create(Monsters.byKey.orc, 1, 7, 5, 1) + orc.awareness = Stealth.DORMANT + level.entities = { orc } + game:computeVisible() + local turnBefore = game.turn + local consumesTurn, message = game:restUntilHealed() + eq(consumesTurn, false, "a hostile already visible refuses to even start") + eq(message, "a monster is already in sight", "the refusal names the reason") + eq(game.turn, turnBefore, "the turn counter never advances") +end + +function tests.gameRestUntilHealedHealsTurnByTurnUntilFullThenStops() + local game = stagedGame(1, arena(11, 9), 5, 5) + game.player.hp = game.player.maxHp - 6 + Status.apply(game.player, Status.REGEN, 20, 2) + local turnBefore = game.turn + local consumesTurn, message = game:restUntilHealed() + eq(consumesTurn, true, "a rest that actually heals spends turns") + eq(message, "you rest until your wounds close", "a completed rest reports it finished") + eq(game.player.hp, game.player.maxHp, "hp is capped exactly at maxHp, never over it") + eq(game.turn, turnBefore + 3, "exactly ceil(6/2) turns of the regen tick were needed") +end + +-- Deliberately the same "nothing left to explore" shape as autoExplore's own +-- steps==0 refusal, but rest's equivalent needs a *second* unchanged turn +-- before it gives up rather than judging the first one alone - see +-- Game:restUntilHealed's own comment for why: a real player's typed "rest" +-- always runs through Game:manualStep, which already primes one turn of +-- ticking before this function's loop ever gets to take its own first +-- hpBefore snapshot, so that first reading alone cannot be trusted. +function tests.gameRestUntilHealedStopsAfterTwoUnchangedTurnsWhenNothingIsHealingThePlayer() + local game = stagedGame(1, arena(11, 9), 5, 5) + game.player.hp = game.player.maxHp - 5 + local turnBefore = game.turn + local consumesTurn, message = game:restUntilHealed() + eq(consumesTurn, true, "the turns spent checking still count as spent") + eq(message, "you stop resting - nothing here is healing you", "the interrupt explains itself") + eq(game.turn, turnBefore + 2, "exactly two turns were spent before giving up") + eq(game.player.hp, game.player.maxHp - 5, "hp is unchanged - nothing was healing it in the first place") +end + +function tests.gameRestUntilHealedStopsTheInstantThePlayerTakesDamage() + local game = stagedGame(1, arena(11, 9), 5, 5) + game.player.hp = game.player.maxHp - 5 + Status.apply(game.player, Status.POISON, 5, 4) + local turnBefore = game.turn + local hpBefore = game.player.hp + local consumesTurn, message = game:restUntilHealed() + eq(consumesTurn, true, "the interrupted turn itself still spends") + eq(message, "you stop resting - you have taken damage", "the interrupt explains itself") + eq(game.turn, turnBefore + 1, "exactly the one turn that took the damage was spent") + eq(game.player.hp, hpBefore - 4, "the poison tick's own damage lands normally before the interrupt fires") +end + +-- Same row placement trick as autoExplore's own sight-interrupt test, but +-- mirrored: here the *player* stands still (resting) and the orc is the one +-- that has to close the distance on its own, so it starts HUNTING (already +-- aware, not DORMANT) rather than relying on stealth.lua's wake roll - the +-- point of this test is Game:canSeePlayer's own chase range (10) reaching +-- further than the player's own vision radius (8), not the wake mechanic. +-- Status.REGEN is applied so the run has real healing to interrupt (see +-- Game:restUntilHealed's own comment - two turns are needed before "nothing +-- is healing you" could even fire, and this scenario resolves before that). +function tests.gameRestUntilHealedStopsTheMomentAHostileComesIntoSightWithoutEngagingIt() + local level = arena(30, 11) + local game = stagedGame(2, level, 20, 6) + game.player.hp = game.player.maxHp - 20 + Status.apply(game.player, Status.REGEN, 50, 1) + local orc = Monsters.create(Monsters.byKey.orc, 1, 11, 6, 1) + orc.awareness = Stealth.HUNTING + level.entities = { orc } + game:computeVisible() + ok(not game.visible[level.map:index(orc.x, orc.y)], "the orc starts out of sight") + local turnBefore = game.turn + local hpBefore = game.player.hp + local orcHpBefore = orc.hp + local consumesTurn, message = game:restUntilHealed() + eq(consumesTurn, true, "real turns were spent healing before the interrupt fired") + eq(message, "you stop resting - a monster is in sight", "the interrupt names a sighted hostile") + eq(game.turn, turnBefore + 2, "exactly the steps needed to bring the orc within vision range 8 were taken") + ok(game.player.hp > hpBefore, "the turns spent before the interrupt actually healed the player") + eq(orc.hp, orcHpBefore, "the orc is only spotted, never engaged") + ok(game.visible[level.map:index(orc.x, orc.y)], "the orc really is the thing that got seen") + eq(orc.awareness, Stealth.HUNTING, "the orc's own awareness is unaffected by simply being spotted") +end + +function tests.gameManualStepDrivenRestSpendsExactlyAsManyTurnsAsCallingGameRestUntilHealedDirectly() + local direct = stagedGame(9, arena(11, 9), 5, 5) + direct.player.hp = direct.player.maxHp - 6 + Status.apply(direct.player, Status.REGEN, 20, 2) + direct:restUntilHealed() + local viaManualStep = stagedGame(9, arena(11, 9), 5, 5) + viaManualStep.player.hp = viaManualStep.player.maxHp - 6 + Status.apply(viaManualStep.player, Status.REGEN, 20, 2) + viaManualStep:manualStep("rest") + eq(viaManualStep.turn, direct.turn, + "routing rest through Game:manualStep must not end the turn a second time on top of what " .. + "restUntilHealed already closed itself, and must credit the turn manualStep primes before " .. + "Commands.apply ever runs to the same run rather than losing it") + eq(viaManualStep.player.hp, direct.player.hp, "both paths heal the player by the same total amount") +end + +function tests.commandsApplyRestDrivesGameRestUntilHealedAndReturnsItsResult() + local game = stagedGame(1, arena(11, 9), 5, 5) + local consumesTurn, message = Commands.apply(game, "rest") + eq(consumesTurn, false, "a player already at full health spends no turn") + eq(message, "you are already at full health", "the command surfaces Game:restUntilHealed's own message verbatim") +end + +function tests.commandsApplyHelpMentionsRest() + local game = stagedGame(1, arena(21, 11), 5, 5) + local _, message = Commands.apply(game, "help") + ok(contains(message, "rest"), "help mentions rest") +end + +-- Splits an otherwise-open arena into two floor regions with a solid wall +-- column at x == gapX between them - used only to build a destination that is +-- provably unreachable from the player's own side, for Game:travelTo's "no +-- route exists" refusal below. +local function splitArena(w, h, gapX) + local map = Map.new(w, h) + for y = 2, h - 1 do + for x = 2, gapX - 1 do map:set(x, y, Map.FLOOR) end + for x = gapX + 1, w - 1 do map:set(x, y, Map.FLOOR) end + end + return Level.new({ + depth = 1, generator = "arena", map = map, + entities = {}, floorItems = {}, traps = {}, lights = {}, dark = false, explored = {}, + upX = 2, upY = 2, downX = w - 1, downY = h - 1, + }) +end + +function tests.gameTravelToWalksTheShortestPathAndArrivesAtTheDestination() + local level = arena(21, 11) + local game = stagedGame(1, level, 3, 6) + local expectedSteps = #game:playerPathTo(level.downX, level.downY) - 1 + local turnBefore = game.turn + local consumesTurn, message = game:travelTo(level.downX, level.downY) + eq(consumesTurn, true, "a real trip spends turns") + eq(message, "you arrive", "arriving reports so") + eq(game.player.x, level.downX, "the player actually reaches the destination x") + eq(game.player.y, level.downY, "the player actually reaches the destination y") + eq(game.turn, turnBefore + expectedSteps, "exactly the shortest path's own number of steps were spent") +end + end local order = {