Files
gen1recomp/tests/gen2_prize_counter_test.lua
T
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

187 lines
7.5 KiB
Lua

-- The Game Corner's prize counters are MAP SCRIPT, not a screen.
--
-- luajit tests/gen2_prize_counter_test.lua (ROM-free; the cache half SKIPs
-- without a gold cache)
--
-- GoldenrodGameCornerTMVendorScript and GoldenrodGameCornerPrizeMonVendorScript
-- (pokegold maps/GoldenrodGameCorner.asm), and their two Celadon twins, are
-- `opentext` / `checkitem` / `loadmenu` / `verticalmenu` / `checkcoins` /
-- `giveitem` / `givepoke` / `takecoins` and nothing else -- every one of which
-- src/script/gen2/Vm.lua already runs. So talking to a vendor runs the
-- extracted bytecode, and no engine screen is reached: an id in
-- src/ui/Screens.lua for one would only ever have been resolved by a mod
-- replacing a screen the game never pushes.
--
-- What this pins is the reachable path (the real script, driven to a real
-- transaction with no unknown opcodes left over) and the registry (no id for
-- something the VM owns).
package.path = "./?.lua;./?/init.lua;" .. package.path
local S = require("tests.harness").suite("gen2 prize counter")
local check, eq = S.check, S.eq
love = require("tests.love_stub")
local Screens = require("src.ui.Screens")
local Vm = require("src.script.gen2.Vm")
local Events = require("src.world.gen2.Events")
-- ---- the registry ---------------------------------------------------------
do
local registered = {}
for _, id in ipairs(Screens.GEN2_IDS) do registered[id] = true end
eq(registered.Gen2PrizeMenu, nil,
"no Gen2PrizeMenu id: the prize counters are script")
eq(registered.Gen2IntroStub, nil,
"and no Gen2IntroStub: Gen2GoldSilverIntro is the intro")
check(registered.Gen2SlotMachine,
"the slot machine next door IS an engine screen and keeps its id")
check(registered.Gen2CardFlip, "so does card flip")
check(registered.Gen2GoldSilverIntro, "and so does the real intro")
-- Every remaining id has to resolve, or the list is worse than the two rows
-- taken out of it.
local missing = {}
for _, id in ipairs(Screens.GEN2_IDS) do
local ok = pcall(Screens.get, {}, id)
if not ok then missing[#missing + 1] = id end
end
eq(#missing, 0, "every id still on the list resolves to a module")
Screens.invalidate()
end
-- ---- the counter, driven ---------------------------------------------------
--
-- The transaction arm of a TM counter, in the cart's own order: the coin check
-- first, then the question, then the PACK, and only then the coins actually
-- come off. Built as a command list rather than lifted out of the cache so
-- this half runs with no gold cache at all; the cache half below asserts that
-- the extracted script is the same shape.
do
local HAVE_MORE, HAVE_LESS = 0, 2
local TM_THUNDER, PRICE = 0xd4, 5500
local function vendorScript()
return {
generation = 2,
["s:tm"] = {
{ op = "checkcoins", args = { PRICE % 256, math.floor(PRICE / 256) } },
{ op = "ifequal", value = HAVE_LESS, script = "s:broke" },
{ op = "yesorno" },
{ op = "iffalse", script = "s:cancel" },
{ op = "giveitem", item = TM_THUNDER, quantity = 1 },
{ op = "iffalse", script = "s:noroom" },
{ op = "takecoins", args = { PRICE % 256, math.floor(PRICE / 256) } },
{ op = "end" },
},
["s:broke"] = { { op = "end" } },
["s:cancel"] = { { op = "end" } },
["s:noroom"] = { { op = "end" } },
}
end
local function run(opts)
local state = { coins = opts.coins, given = nil, arm = nil }
local vm = Vm.new(vendorScript(), {}, Events.new(), {
showText = function(_, onDone) if onDone then onDone() end end,
yesorno = function(onChoose) onChoose(opts.yes ~= false) end,
getCoins = function() return state.coins end,
setCoins = function(n) state.coins = n end,
giveItem = function(item, qty)
if opts.fullPack then return false end
state.given = { item = item, qty = qty }
return true
end,
})
vm:start("s:tm")
for _ = 1, 40 do vm:update() end
state.running = vm:running()
state.unknown = vm.unknownOps
return state
end
local bought = run({ coins = 6000 })
eq(bought.coins, 6000 - PRICE, "the counter takes the price in coins")
eq(bought.given and bought.given.item, TM_THUNDER, "and hands over the TM")
check(not bought.running, "with the script run to its end")
eq(next(bought.unknown), nil, "and no opcode fell through the VM")
local broke = run({ coins = 100 })
eq(broke.coins, 100, "HAVE_LESS takes the not-enough-coins arm")
eq(broke.given, nil, "and nothing is handed over")
local said = run({ coins = 6000, yes = false })
eq(said.coins, 6000, "answering NO cancels before the coins move")
eq(said.given, nil, "and before the item does")
-- `giveitem / iffalse NoRoomForPrize`: a full PACK is discovered AFTER the
-- question, which is the item counter's own order and the reason the coins
-- are still there.
local full = run({ coins = 6000, fullPack = true })
eq(full.coins, 6000, "a full PACK costs nothing")
eq(full.given, nil, "and gives nothing")
end
-- ---- the extracted scripts -------------------------------------------------
do
local cache = os.getenv("GOLD_CACHE")
if not cache then
local home = os.getenv("HOME") or ""
cache = home .. "/Library/Application Support/LOVE/gold-dev/gold"
end
local mapChunk = loadfile(cache .. "/data/generated/maps.lua")
local scriptChunk = loadfile(cache .. "/data/generated/scripts.lua")
if not (mapChunk and scriptChunk) then
check(true, "no gold cache: the extracted vendor scripts (SKIP)")
else
local maps, scripts = mapChunk(), scriptChunk()
local room = maps.GOLDENROD_GAME_CORNER
check(room ~= nil, "the cache carries GOLDENROD_GAME_CORNER")
-- The two vendors, found the way a player finds them: an object whose own
-- script opens a menu and checks the coin case.
local vendors = {}
for _, obj in ipairs((room or {}).objects or {}) do
local cmds = obj.scriptKey and scripts[obj.scriptKey]
if type(cmds) == "table" then
local ops = {}
for _, cmd in ipairs(cmds) do ops[cmd.op] = true end
if ops.loadmenu and ops.verticalmenu and ops.checkitem then
vendors[#vendors + 1] = { key = obj.scriptKey, cmds = cmds }
end
end
end
eq(#vendors, 2, "two prize counters stand in the room")
-- Follow every arm the counters jump to, so the transaction opcodes are
-- asserted where they actually live rather than only in the entry script.
local reached, queue = {}, {}
for _, vendor in ipairs(vendors) do queue[#queue + 1] = vendor.key end
local ops = {}
while #queue > 0 do
local key = table.remove(queue)
if not reached[key] then
reached[key] = true
for _, cmd in ipairs(scripts[key] or {}) do
ops[cmd.op] = (ops[cmd.op] or 0) + 1
local target = cmd.script or cmd.target
if type(target) == "string" and scripts[target] then
queue[#queue + 1] = target
end
end
end
end
for _, op in ipairs({ "loadmenu", "verticalmenu", "checkcoins", "takecoins",
"giveitem", "givepoke", "yesorno", "checkitem" }) do
check((ops[op] or 0) > 0,
"the extracted counters use " .. op .. ", which the VM runs")
end
-- And nothing in them reached the extractor's own failure rows, which is
-- what an opcode the VM would have to hand to a screen would look like.
eq(ops.unknown, nil, "with no unknown command in any arm")
eq(ops.truncated, nil, "and nothing truncated")
end
end
S.finish()