test: cover tests.gameTravelToRefusesWithoutSpendingATurnWhenAlreadyT...
This commit is contained in:
1 parent
0129c399f5
commit
9627ab970a
1 file changed
+173
+173
@@ -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 = {
|
||||
|
||||
Reference in new issue
Block a user