test: cover tests.gameTravelToRefusesWithoutSpendingATurnWhenAlreadyT...

This commit is contained in:
dualfroz committed 2026-09-13 18:19:35 +02:00
1 parent 0129c399f5
commit 9627ab970a
1 file changed
+173
+173
View File
@@ -14142,6 +14142,179 @@ function tests.gameTravelToWalksTheShortestPathAndArrivesAtTheDestination()
eq(game.turn, turnBefore + expectedSteps, "exactly the shortest path's own number of steps were spent")
end
function tests.gameTravelToRefusesWithoutSpendingATurnWhenAlreadyThere()
local level = arena(11, 9)
local game = stagedGame(1, level, level.downX, level.downY)
local turnBefore = game.turn
local consumesTurn, message = game:travelTo(level.downX, level.downY)
eq(consumesTurn, false, "already standing on the destination spends no turn")
eq(message, "you are already there", "the refusal explains why")
eq(game.turn, turnBefore, "the turn counter never advances")
end
function tests.gameTravelToRefusesWithoutSpendingATurnWhenNoRouteExists()
local level = splitArena(21, 9, 11)
local game = stagedGame(1, level, 3, 5)
local turnBefore = game.turn
local consumesTurn, message = game:travelTo(level.downX, level.downY)
eq(consumesTurn, false, "an unreachable destination refuses to even start")
eq(message, "you cannot find a path there", "the refusal names the reason")
eq(game.turn, turnBefore, "the turn counter never advances")
end
function tests.gameTravelToRefusesWithoutSpendingATurnWhileAHostileIsAlreadyInSight()
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:travelTo(level.downX, level.downY)
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.gameTravelToHaltsWithoutSpendingATurnOnceTheRunIsAlreadyWon()
local level = arena(21, 11)
local game = stagedGame(1, level, 5, 5)
game.won = true
local turnBefore = game.turn
local consumesTurn, message = game:travelTo(level.downX, level.downY)
eq(consumesTurn, false, "a travel 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.gameTravelToStopsTheInstantThePlayerTakesDamage()
local level = arena(25, 15)
local game = stagedGame(1, level, 3, 3)
Status.apply(game.player, Status.POISON, 5, 4)
local turnBefore = game.turn
local hpBefore = game.player.hp
local consumesTurn, message = game:travelTo(level.downX, level.downY)
eq(consumesTurn, true, "the interrupted turn itself still spends")
eq(message, "you stop traveling - 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 Game:autoExplore's own sight-interrupt test
-- (see its comment for the exact reasoning: both the room's height and the
-- orc's row sit well within the player's initial vision radius (8), and
-- Stealth.WAKE_RADIUS (6) is smaller than that, so the orc is always seen
-- before it could ever wake on its own). The destination (28, 6) sits well
-- past the orc's own cell (25, 6) on the same row, but the interrupt fires
-- purely from Game:anyHostileVisible at the top of the loop - before that
-- turn's own move (and any path recomputation to route around the now-
-- occupied cell) is ever attempted - so the two extra columns behind the orc
-- never come into play; empirically confirmed to land on the exact same
-- turn count as the analogous autoExplore test.
function tests.gameTravelToStopsTheMomentAHostileComesIntoSightWithoutEngagingIt()
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:travelTo(28, 6)
eq(consumesTurn, true, "real ground was covered before the interrupt fired")
eq(message, "you stop traveling - 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 - travel 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.gameManualStepDrivenTravelSpendsExactlyAsManyTurnsAsCallingGameTravelToDirectly()
local level1 = arena(21, 11)
local direct = stagedGame(9, level1, 3, 6)
direct:travelTo(level1.downX, level1.downY)
local level2 = arena(21, 11)
local viaManualStep = stagedGame(9, level2, 3, 6)
viaManualStep:manualStep("travel down")
eq(viaManualStep.turn, direct.turn,
"routing travel through Game:manualStep must not end the turn a second time on top of what " ..
"travelTo already closed itself")
eq(viaManualStep.player.x, direct.player.x, "both paths reach the same destination x")
eq(viaManualStep.player.y, direct.player.y, "both paths reach the same destination y")
end
function tests.commandsApplyTravelDownDrivesGameTravelToTowardTheDownStairs()
local level = arena(21, 11)
local game = stagedGame(1, level, level.downX, level.downY)
local consumesTurn, message = Commands.apply(game, "travel down")
eq(consumesTurn, false, "already standing on the down stairs spends no turn")
eq(message, "you are already there", "the command surfaces Game:travelTo's own message verbatim")
end
function tests.commandsApplyTravelUpDrivesGameTravelToTowardTheUpStairs()
local level = arena(21, 11)
local game = stagedGame(1, level, level.upX, level.upY)
local consumesTurn, message = Commands.apply(game, "travel up")
eq(consumesTurn, false, "already standing on the up stairs spends no turn")
eq(message, "you are already there", "the command surfaces Game:travelTo's own message verbatim")
end
function tests.commandsApplyTravelWithoutAnArgumentFailsWithoutSpendingATurn()
local game = stagedGame(1, arena(21, 11), 5, 5)
local consumesTurn, message = Commands.apply(game, "travel")
eq(consumesTurn, false, "an unrecognized destination spends no turn")
eq(message, "travel where? down or up", "the failure explains the two valid destinations")
end
function tests.commandsApplyHelpMentionsTravel()
local game = stagedGame(1, arena(21, 11), 5, 5)
local _, message = Commands.apply(game, "help")
ok(contains(message, "travel down/up"), "help mentions travel down/up")
end
-- Every test above exercises the engine in-process, by requiring src/ modules
-- directly - never the actual `lua warren.lua ...` entry point a real player
-- (or the --play REPL) invokes. That leaves argument parsing, the playLoop
-- stdin/stdout loop, and the CLI-level wiring between flags and Game/Meta
-- entirely unverified by automation (three prior sessions flagged this and
-- checked it by hand instead). This section closes that gap: it spawns the
-- real interpreter against the real warren.lua as a subprocess, the same way
-- a player or a shell script would, and asserts on what actually comes back.
local warrenEntry = scriptDir .. "/../warren.lua"
local luaExe = arg[-1] or "lua"
local function shellQuote(s)
return "'" .. tostring(s):gsub("'", "'\\''") .. "'"
end
-- Runs `lua warren.lua <argv...>`, optionally piping stdinText in (for
-- --play), and returns (combinedOutput, exitCode). stderr is folded into
-- stdout so a crash's traceback shows up in the captured text instead of
-- vanishing into the terminal.
local function runWarrenCli(argv, stdinText)
local parts = { shellQuote(luaExe), shellQuote(warrenEntry) }
for _, a in ipairs(argv) do parts[#parts + 1] = shellQuote(a) end
local inPath
if stdinText then
inPath = os.tmpname()
local input = io.open(inPath, "w")
input:write(stdinText)
input:close()
parts[#parts + 1] = "<"
parts[#parts + 1] = shellQuote(inPath)
end
parts[#parts + 1] = "2>&1"
local proc = io.popen(table.concat(parts, " "), "r")
local output = proc:read("a")
local success, exitType, code = proc:close()
if inPath then os.remove(inPath) end
local exitCode = success and 0 or (exitType == "exit" and code or -1)
return output, exitCode
end
function tests.cliHeadlessAutoRunPrintsTheStatusBarAndExitsZero()
end
local order = {