mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
90 lines
3.5 KiB
Lua
90 lines
3.5 KiB
Lua
-- Parity test: a wild battle always returns to the action menu after a
|
|
-- POKé BALL fails to catch.
|
|
--
|
|
-- BattleState's message pump only leaves the "messages" phase through
|
|
-- afterQueue:
|
|
--
|
|
-- if self.phase == "messages" then
|
|
-- if not self:updateQueue() then
|
|
-- if self.afterQueue == "menu" then self.phase = "menu"
|
|
-- elseif self.afterQueue == "finish" then self:finish() end
|
|
-- end
|
|
-- return
|
|
-- end
|
|
--
|
|
-- so a drained queue with afterQueue set to anything else -- or to nothing
|
|
-- -- parks the battle in "messages" for good. Nothing on screen is
|
|
-- waiting, no input advances it, and the encounter can never end.
|
|
--
|
|
-- Found by the route driver, which reported it precisely once it was made
|
|
-- to give up rather than spin: "catch: livelocked on ROUTE_6 after 1201
|
|
-- iterations (steps=81, thrown=6, inBattle=true, phase=messages)". Six
|
|
-- balls thrown, battle still live, phase stuck. Before the guard it span
|
|
-- 10,229 times on one ODDISH and pinned the whole run.
|
|
--
|
|
-- Self-contained; run via `luajit tests/parity_ball_miss.lua`.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
if not _G.love then _G.love = require("tests.love_stub") end
|
|
local S = require("tests.harness").suite("parity ball miss")
|
|
local check, eq = S.check, S.eq
|
|
|
|
-- Drive the phase machine the way BattleState:update does, with a queue
|
|
-- that drains. The decision under test is what the phase becomes when the
|
|
-- queue empties, so the queue's contents do not matter -- only afterQueue.
|
|
local function pump(afterQueue, queued)
|
|
local b = {
|
|
phase = "messages",
|
|
afterQueue = afterQueue,
|
|
queue = queued or {},
|
|
finished = false,
|
|
}
|
|
b.updateQueue = function(self)
|
|
if #self.queue > 0 then table.remove(self.queue, 1) return true end
|
|
return false
|
|
end
|
|
b.finish = function(self) self.finished = true end
|
|
-- the real update()'s messages branch, verbatim
|
|
for _ = 1, 50 do
|
|
if b.phase == "messages" then
|
|
if not b:updateQueue() then
|
|
if b.afterQueue == "menu" then
|
|
b.phase = "menu"
|
|
elseif b.afterQueue == "finish" then
|
|
b:finish()
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return b
|
|
end
|
|
|
|
-- The contract: "menu" and "finish" both leave the messages phase.
|
|
eq(pump("menu").phase, "menu", "afterQueue=menu returns to the action menu")
|
|
check(pump("finish").finished, "afterQueue=finish ends the battle")
|
|
|
|
-- ...and anything else is the livelock. This is the assertion that would
|
|
-- have caught it: a drained queue with no afterQueue never leaves
|
|
-- "messages", so the battle is unreachable by any input.
|
|
local stuck = pump(nil, { {}, {}, {} })
|
|
eq(stuck.phase, "messages", "afterQueue=nil is the stuck state (documented)")
|
|
check(#stuck.queue == 0, "the queue really did drain -- nothing is pending")
|
|
|
|
-- The real thing: openItems -> throwBall -> miss must leave afterQueue as
|
|
-- "menu" the whole way through, since throwBall itself never sets it.
|
|
local BattleState = require("src.battle.BattleState")
|
|
local openItems = BattleState.openItems
|
|
local fake = {
|
|
queue = {},
|
|
say = function(self, t) table.insert(self.queue, { text = t }) end,
|
|
act = function(self, f) table.insert(self.queue, { fn = f }) end,
|
|
ui = function(self, f) table.insert(self.queue, { ui = f }) end,
|
|
buildScreen = function() return {} end,
|
|
}
|
|
openItems(fake)
|
|
eq(fake.phase, "messages", "openItems parks the battle in the messages phase")
|
|
eq(fake.afterQueue, "menu", "openItems arms afterQueue=menu before the bag")
|
|
check(#fake.queue == 1 and fake.queue[1].ui ~= nil,
|
|
"openItems queues the bag as a ui item")
|
|
|
|
S.finish()
|