mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 00:10:56 +02:00
225 lines
7.8 KiB
Lua
225 lines
7.8 KiB
Lua
-- Does the Gold bot route describe a world that actually exists?
|
|
--
|
|
-- luajit tests/gold_route_validate_test.lua
|
|
--
|
|
-- tests/drivers/gold/route.lua is hand-authored from the asm-walk documents,
|
|
-- which were themselves written by reading a pokegold checkout. Two independent
|
|
-- transcriptions sit between the cart and this route, and every coordinate in it
|
|
-- is a chance for one of them to have slipped. Finding that out four hours into
|
|
-- a run -- when the bot is stood in a doorway that is one cell left of where the
|
|
-- route says -- is the failure mode this test exists to prevent. Everything it
|
|
-- checks costs milliseconds and is checked against the EXTRACTED CACHE, i.e.
|
|
-- against the same data the running bot will path over.
|
|
--
|
|
-- Checked per row: the op is known and carries the fields that op needs; the map
|
|
-- constant exists; a `warp` cell really is a warp and really leads to `to`; an
|
|
-- `edge` direction really is a connection to `to`; a `talk`/`battle` target
|
|
-- really has an object, bg event or warp on it; and every `expect` names a real
|
|
-- EVENT_*/ENGINE_* flag rather than a plausible-looking typo.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local S = require("tests.harness").suite("gold route validation")
|
|
local check, eq = S.check, S.eq
|
|
|
|
local route = dofile("tests/drivers/gold/route.lua")
|
|
local names = dofile("tests/drivers/gold/flag_names.lua")
|
|
|
|
check(#route > 0, "the route has rows at all")
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Shape checks : no cache needed
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
local REQUIRED = {
|
|
travel = {},
|
|
walk = { "x", "y" },
|
|
warp = { "x", "y", "to" },
|
|
edge = { "dir", "to" },
|
|
talk = { "x", "y" },
|
|
battle = {}, -- x/y optional: a `talk` battle names its NPC,
|
|
-- a sight-line one names the cell to walk onto
|
|
grind = { "level" },
|
|
heal = {},
|
|
teach = { "move" },
|
|
field = { "move", "x", "y" },
|
|
settle = {},
|
|
catch = { "species" },
|
|
manual = { "why" },
|
|
check = { "expect" },
|
|
-- `wander` walks about on one map waiting for something to be DELIVERED (a
|
|
-- phone call), so it names no coordinate and its oracle may be a flag being
|
|
-- cleared rather than set.
|
|
wander = {},
|
|
-- `push` names the boulder's cell and which way to shove it; `press` is a
|
|
-- raw direction list for an ice floor, where no cell can be aimed at.
|
|
push = { "x", "y", "dir" },
|
|
press = { "dirs" },
|
|
-- `buy` names a Mart's map and what to take off the shelf; the price and the
|
|
-- money check are the engine's.
|
|
buy = { "item" },
|
|
-- `elevator` reads the panel at (x, y), picks `floor` off the scrolling
|
|
-- menu, and leaves through the door at (doorX, doorY); `to` is the floor
|
|
-- the door must land on.
|
|
elevator = { "x", "y", "floor", "doorX", "doorY", "to" },
|
|
}
|
|
|
|
local seenIds = {}
|
|
local badShape = 0
|
|
for i, row in ipairs(route) do
|
|
local where = ("row %d (%s)"):format(i, tostring(row.id))
|
|
if not row.id then
|
|
badShape = badShape + 1
|
|
print(" " .. where .. ": missing id")
|
|
elseif seenIds[row.id] then
|
|
badShape = badShape + 1
|
|
print(" " .. where .. ": duplicate id")
|
|
end
|
|
seenIds[row.id or i] = true
|
|
|
|
local spec = REQUIRED[row.op]
|
|
if not spec then
|
|
badShape = badShape + 1
|
|
print((" %s: unknown op %q"):format(where, tostring(row.op)))
|
|
else
|
|
for _, field in ipairs(spec) do
|
|
if row[field] == nil then
|
|
badShape = badShape + 1
|
|
print((" %s: op %s needs %s"):format(where, row.op, field))
|
|
end
|
|
end
|
|
end
|
|
if not row.map then
|
|
badShape = badShape + 1
|
|
print(" " .. where .. ": missing map")
|
|
end
|
|
for _, field in ipairs({ "expect", "expectClear" }) do
|
|
local name = row[field]
|
|
if name and not (names.events[name] or names.engine[name]) then
|
|
badShape = badShape + 1
|
|
print((" %s: %s %q is not a known EVENT_*/ENGINE_* name")
|
|
:format(where, field, tostring(name)))
|
|
end
|
|
end
|
|
end
|
|
eq(badShape, 0, "every row is well-formed and every expect names a real flag")
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Geometry checks : need a Gold cache
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
local cache = os.getenv("GOLD_CACHE")
|
|
if not cache then
|
|
cache = (os.getenv("HOME") or "")
|
|
.. "/Library/Application Support/LOVE/gold-dev/gold"
|
|
end
|
|
local mapsPath = cache .. "/data/generated/maps.lua"
|
|
local mf = io.open(mapsPath, "r")
|
|
if not mf then
|
|
check(true, "gold cache absent : shape checked, geometry SKIPPED")
|
|
S.finish()
|
|
return
|
|
end
|
|
mf:close()
|
|
|
|
local maps = assert(loadfile(mapsPath))()
|
|
|
|
-- Extracted connections are keyed by compass word; the route writes some rows
|
|
-- with the walk direction instead, because that is how the asm-walk phrases
|
|
-- them ("walk north out of the map"). Accept both spellings rather than make
|
|
-- the route remember which map used which.
|
|
local DIR_ALIAS = {
|
|
up = "north", down = "south", left = "west", right = "east",
|
|
north = "north", south = "south", west = "west", east = "east",
|
|
}
|
|
|
|
local function connectionOf(def, dir)
|
|
local conns = def.connections or {}
|
|
local want = DIR_ALIAS[dir] or dir
|
|
for key, conn in pairs(conns) do
|
|
local norm = DIR_ALIAS[key] or key
|
|
if norm == want then return conn end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function warpAt(def, x, y)
|
|
for i, w in ipairs(def.warps or {}) do
|
|
if w.x == x and w.y == y then return w, i end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Anything a `talk` can legitimately aim at: an NPC/item-ball object, a sign or
|
|
-- hidden item, or -- for the handful of rows that talk to something standing on
|
|
-- a door -- a warp.
|
|
local function targetAt(def, x, y)
|
|
for _, o in ipairs(def.objects or {}) do
|
|
if o.x == x and o.y == y then return "object" end
|
|
end
|
|
for _, b in ipairs(def.bgEvents or {}) do
|
|
if b.x == x and b.y == y then return "bg" end
|
|
end
|
|
if warpAt(def, x, y) then return "warp" end
|
|
return nil
|
|
end
|
|
|
|
local problems = 0
|
|
local function bad(row, msg)
|
|
problems = problems + 1
|
|
print((" %s [%s %s]: %s")
|
|
:format(tostring(row.id), row.op, tostring(row.map), msg))
|
|
end
|
|
|
|
for _, row in ipairs(route) do
|
|
local def = maps[row.map]
|
|
if not def then
|
|
bad(row, "no such map in the cache")
|
|
else
|
|
-- Every explicit cell must be inside the map. Cells are 2x the block
|
|
-- dimensions (src/world/gen2/Map.lua).
|
|
if row.x and row.y then
|
|
local w, h = def.width * 2, def.height * 2
|
|
if row.x < 0 or row.y < 0 or row.x >= w or row.y >= h then
|
|
bad(row, ("(%d,%d) is outside the %dx%d cell grid")
|
|
:format(row.x, row.y, w, h))
|
|
end
|
|
end
|
|
|
|
if row.op == "warp" then
|
|
local warp = warpAt(def, row.x, row.y)
|
|
if not warp then
|
|
bad(row, ("no warp at (%d,%d)"):format(row.x, row.y))
|
|
elseif warp.destMap ~= row.to then
|
|
bad(row, ("warp at (%d,%d) leads to %s, route says %s")
|
|
:format(row.x, row.y, tostring(warp.destMap), tostring(row.to)))
|
|
end
|
|
|
|
elseif row.op == "edge" then
|
|
local conn = connectionOf(def, row.dir)
|
|
if not conn then
|
|
bad(row, ("no %s connection"):format(tostring(row.dir)))
|
|
elseif conn.mapId ~= row.to then
|
|
bad(row, ("%s connection leads to %s, route says %s")
|
|
:format(tostring(row.dir), tostring(conn.mapId), tostring(row.to)))
|
|
end
|
|
|
|
elseif row.op == "talk" or (row.op == "battle" and row.talk) then
|
|
if not targetAt(def, row.x, row.y) then
|
|
bad(row, ("nothing to talk to at (%d,%d)"):format(row.x, row.y))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
eq(problems, 0, "every route coordinate matches the extracted cache")
|
|
|
|
-- A route that reached the last badge but never named the map it happens on is
|
|
-- a route that quietly stopped short.
|
|
local sawOlivineGym = false
|
|
for _, row in ipairs(route) do
|
|
if row.map == "OLIVINE_GYM" then sawOlivineGym = true end
|
|
end
|
|
check(sawOlivineGym, "the route reaches OLIVINE_GYM")
|
|
|
|
S.finish()
|