From 851e57ec97fa4a7492cb62911f873b871b167bf4 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 14:46:50 +0200 Subject: [PATCH] test: cover tests.gameManualStepEndsTheTurnOnlyWhenTheCommandActually... --- tests/run.lua | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/tests/run.lua b/tests/run.lua index fc3d5f2..7892f31 100644 --- a/tests/run.lua +++ b/tests/run.lua @@ -13796,6 +13796,179 @@ end -- Game:manualStep - the turn-cycle integration around commands.lua. +function tests.gameManualStepEndsTheTurnOnlyWhenTheCommandActuallySucceeds() + local game = stagedGame(1, arena(21, 11), 5, 5) + local turnBefore = game.turn + local consumesTurn = game:manualStep("north") + eq(consumesTurn, true, "a valid move spends the turn") + eq(game.turn, turnBefore + 1, "Game:manualStep advances the turn counter on a successful command") +end + +function tests.gameManualStepLeavesTheTurnCounterUntouchedOnARejectedCommand() + local game = stagedGame(1, arena(21, 11), 5, 5) + local turnBefore = game.turn + local consumesTurn, message = game:manualStep("nonsense") + eq(consumesTurn, false, "an unrecognized command reports failure") + ok(message ~= nil, "the rejection carries a reason") + eq(game.turn, turnBefore, "the turn counter never advances on a rejected command") +end + +function tests.gameManualStepIsIdempotentAcrossRepeatedRejectedCommandsWithinOneUnspentTurn() + local game = stagedGame(1, arena(21, 11), 5, 5) + Status.apply(game.player, Status.POISON, 5, 5) + game:manualStep("nonsense") + local hpAfterFirst = game.player.hp + local durationAfterFirst = game.player.statuses[1].duration + game:manualStep("gibberish") + eq(game.player.hp, hpAfterFirst, + "a second rejected command within the same unspent turn does not re-tick poison damage") + eq(game.player.statuses[1].duration, durationAfterFirst, + "...nor does it re-decrement the poison's own remaining duration") +end + +function tests.gameManualStepStunnedPlayerIgnoresTheTypedCommandButStillSpendsTheTurn() + local game = stagedGame(1, arena(21, 11), 5, 5) + -- Duration 2, not 1: Status.tick (run inside beginTurn, before the skip + -- check) decrements duration first, so a duration-1 stun would already + -- have expired by the time Status.skipsTurn looks for it this same turn. + Status.apply(game.player, Status.STUN, 2, 0) + local turnBefore = game.turn + local px, py = game.player.x, game.player.y + local consumesTurn = game:manualStep("north") + eq(consumesTurn, true, "a stunned turn is spent regardless of the typed command") + eq(game.player.x, px, "the player never actually moves while stunned") + eq(game.player.y, py, "the player never actually moves while stunned") + eq(game.turn, turnBefore + 1, "the stunned turn still advances the turn counter") +end + +function tests.gameManualStepHaltsWithoutSpendingATurnOnceTheRunIsAlreadyWon() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.won = true + local turnBefore = game.turn + local consumesTurn = game:manualStep("north") + eq(consumesTurn, false, "a manual command after victory spends no turn") + eq(game.turn, turnBefore, "the turn counter never advances once the run is already won") +end + +-- Game:autoExplore / commands.lua's "explore" - autoexplore, repeating +-- Game:exploreStep turn after turn inside a single call instead of forcing +-- the player to retype "explore" once per step, and stopping itself the +-- instant something worth their attention shows up. + +function tests.gameAutoExploreWalksTowardUnexploredTerrainUntilTheWholeReachableAreaIsSeen() + local level = arena(25, 15) + local game = stagedGame(1, level, 3, 3) + local turnBefore = game.turn + local consumesTurn, message = game:autoExplore() + eq(consumesTurn, true, "a run with real ground left to cover spends turns") + eq(message, "you have finished exploring", "a completed run reports it finished") + ok(game.turn > turnBefore, "autoExplore actually advances the turn counter") + local map = level.map + local allSeen = true + for y = 2, 14 do + for x = 2, 24 do + if not level.explored[map:index(x, y)] then allSeen = false end + end + end + ok(allSeen, "every walkable cell in the reachable room ends up explored") +end + +function tests.gameAutoExploreReportsNothingLeftWithoutSpendingATurnWhenAlreadyFullyExplored() + local game = stagedGame(1, arena(11, 9), 5, 5) + local turnBefore = game.turn + local consumesTurn, message = game:autoExplore() + eq(consumesTurn, false, "a room already fully visible from the start spends no turn") + eq(message, "there is nothing left to explore", "the refusal explains why") + eq(game.turn, turnBefore, "the turn counter never advances") +end + +function tests.gameAutoExploreHaltsWithoutSpendingATurnOnceTheRunIsAlreadyWon() + local game = stagedGame(1, arena(21, 11), 5, 5) + game.won = true + local turnBefore = game.turn + local consumesTurn, message = game:autoExplore() + eq(consumesTurn, false, "an autoexplore attempt after victory spends no turn") + eq(message, "there is nothing left to do", "the refusal matches Game:manualStep's own halted message") + eq(game.turn, turnBefore, "the turn counter never advances once the run is already won") +end + +function tests.gameAutoExploreStopsTheInstantThePlayerTakesDamage() + local game = stagedGame(1, arena(25, 15), 3, 3) + Status.apply(game.player, Status.POISON, 5, 4) + local turnBefore = game.turn + local hpBefore = game.player.hp + local consumesTurn, message = game:autoExplore() + eq(consumesTurn, true, "the interrupted turn itself still spends") + eq(message, "you stop exploring - 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 + +function tests.gameAutoExploreRefusesToStartWhileAHostileIsAlreadyInSight() + local level = arena(21, 11) + local game = stagedGame(1, level, 5, 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:autoExplore() + 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 + +-- The room is wide open (not a corridor) on purpose: Game:exploreStep's own +-- BFS treats an entity's tile as unreachable (see game.lua), so a monster +-- sitting in a one-wide corridor would simply wall off the far end and +-- "nothing left to explore" would fire before the player ever got close +-- enough to see it - a real, separately-confirmed property of exploreStep, +-- not the thing this test means to exercise. A big room lets the BFS route +-- around the orc's single tile instead, so sight is what actually stops +-- this run. Both the room's height and the orc's row sit well within the +-- player's initial vision radius (8), so the whole approach plays out along +-- a single row and Stealth.WAKE_RADIUS (6, smaller than that same vision +-- radius) guarantees the orc is seen before it is ever close enough to roll +-- a wake of its own (see stealth.lua's canSpot) - the interrupt below is +-- proven to fire on sight alone, never on contact, without relying on RNG. +function tests.gameAutoExploreStopsTheMomentAHostileComesIntoSightWithoutEngagingIt() + local level = arena(30, 11) + local game = stagedGame(2, level, 3, 6) + local orc = Monsters.create(Monsters.byKey.orc, 1, 25, 6, 1) + orc.awareness = Stealth.DORMANT + level.entities = { orc } + local turnBefore = game.turn + local hpBefore = game.player.hp + local orcHpBefore = orc.hp + local consumesTurn, message = game:autoExplore() + eq(consumesTurn, true, "real ground was covered before the interrupt fired") + eq(message, "you stop exploring - a monster is in sight", "the interrupt names a sighted hostile") + eq(game.turn, turnBefore + 14, "exactly the steps needed to bring the orc within vision range 8 were taken") + eq(game.player.hp, hpBefore, "no damage was taken - explore stops on sight, not on contact") + 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.x, 25, "the still-dormant orc never took a single step of its own") + eq(orc.awareness, Stealth.DORMANT, "and never woke up at all") +end + +function tests.gameManualStepDrivenExploreSpendsExactlyAsManyTurnsAsCallingGameAutoExploreDirectly() + local direct = stagedGame(9, arena(25, 15), 3, 3) + direct:autoExplore() + local viaManualStep = stagedGame(9, arena(25, 15), 3, 3) + viaManualStep:manualStep("explore") + eq(viaManualStep.turn, direct.turn, + "routing explore through Game:manualStep must not end the turn a second time on top of what " .. + "autoExplore already closed itself") +end + +function tests.commandsApplyExploreDrivesGameAutoExploreAndReturnsItsResult() + local game = stagedGame(1, arena(11, 9), 5, 5) + local consumesTurn, message = Commands.apply(game, "explore") + eq(consumesTurn, false, "a room already fully visible from the start spends no turn") + eq(message, "there is nothing left to explore", "the command surfaces Game:autoExplore's own message verbatim") +end + +function tests.commandsApplyHelpMentionsExplore() end local order = {