Files
gen1recomp/tests/game3_union_plaza_map_test.lua
2026-09-25 10:28:21 -04:00

200 lines
7.7 KiB
Lua

#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local function eq(a, b, msg)
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
end
local Plaza = require("src.core.game3.link.union_plaza_map")
print("[test] contract")
eq(Plaza.MAP_ID, "FR_UNION_ROOM_PLAZA", "map id")
eq(Plaza.CAP, 40, "cap")
eq(#Plaza.CELLS, 41, "41 cells on the grid")
eq(#Plaza.ROLES, 25, "role grid is 25 rows")
local badRow = nil
for y, row in ipairs(Plaza.ROLES) do
if #row ~= 25 then badRow = y end
for i = 1, #row do
if not Plaza.SOURCE[row:sub(i, i)] then badRow = y end
end
end
check(badRow == nil, "every role row is 25 wide with a known role: " .. tostring(badRow))
print("[test] slot table")
local seen, unique = {}, 0
for slot = 1, Plaza.CAP do
local x, y, facing = Plaza.cellFor(slot)
check(x ~= nil and y ~= nil and facing ~= nil, "slot " .. slot .. " has a cell")
local k = tostring(x) .. "," .. tostring(y)
if not seen[k] then unique = unique + 1 end
seen[k] = true
eq(Plaza.slotAt(x, y), slot, "slotAt inverts slot " .. slot)
end
eq(unique, 40, "40 unique cells")
check(Plaza.cellFor(0) == nil and Plaza.cellFor(41) == nil, "slots outside 1..40 have no cell")
check(Plaza.slotAt(12, 24) == nil and Plaza.slotAt(0, 0) == nil, "slotAt is nil off the cell table")
local ex, ey = Plaza.entry()
local function dist(slot)
local x, y = Plaza.cellFor(slot)
return math.abs(x - ex) + math.abs(y - (ey - 1))
end
local sorted = true
for slot = 2, Plaza.CAP do
if dist(slot) < dist(slot - 1) then sorted = false end
end
check(sorted, "fill order moves outward from the entrance")
local x1, y1 = Plaza.cellFor(1)
eq(x1, 12, "slot 1 sits on the entrance column")
eq(y1, 17, "slot 1 is the first cell up from the door")
local sx, sy = Plaza.cellFor(40)
check(sy == 5, "slot 40 is on the far row")
print("[test] geometry")
eq(ex, 12, "entry x")
eq(ey, 24, "entry y")
eq(Plaza.roleAt(ex, ey), "X", "entry is the exit mat")
local attX, attY = 3, 2
local blocked = {}
local function key(x, y) return y * 64 + x end
for _, c in ipairs(Plaza.CELLS) do blocked[key(c.x, c.y)] = true end
blocked[key(attX, attY)] = true
local function walkable(x, y)
local r = Plaza.roleAt(x, y)
return r ~= nil and not Plaza.BLOCKED[r] and not blocked[key(x, y)]
end
local reach = { [key(ex, ey)] = true }
local queue = { { ex, ey } }
local head = 1
while queue[head] do
local p = queue[head]
head = head + 1
for _, d in ipairs({ { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }) do
local nx, ny = p[1] + d[1], p[2] + d[2]
if walkable(nx, ny) and not reach[key(nx, ny)] then
reach[key(nx, ny)] = true
queue[#queue + 1] = { nx, ny }
end
end
end
local allSides, onBad = true, {}
for _, c in ipairs(Plaza.CELLS) do
for _, d in ipairs({ { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }) do
if not reach[key(c.x + d[1], c.y + d[2])] then
allSides = false
print((" cell %d,%d side %d,%d unreachable"):format(c.x, c.y, d[1], d[2]))
end
end
local r = Plaza.roleAt(c.x, c.y)
if Plaza.BLOCKED[r] or c.y <= 3 or c.y >= 23 or (c.x == ex and c.y >= 19) then
onBad[#onBad + 1] = c.x .. "," .. c.y
end
end
check(allSides, "every cell is reachable from all 4 sides with the room full")
eq(#onBad, 0, "no cell on a wall, the counter rows or the exit lane")
check(reach[key(2, 2)] == true, "the trading board spot below the counter is reachable")
check(reach[key(3, 3)] == true, "the attendant is reachable from the front")
print("[test] a cache that cannot build the plaza is a hard error")
eq(Plaza.shouldUse, nil, "no switch back to the cart room")
local function fails(game, want, msg)
local ok, err = pcall(Plaza.ensure, game)
check(not ok and tostring(err):find(want, 1, true) ~= nil, msg .. ": " .. tostring(err))
end
local realMap = package.loaded["src.core.game3.map"]
package.loaded["src.core.game3.map"] = { ensureMidLayout = function() end }
fails({ data = {} }, "no map table", "no map table")
fails({ data = { maps = {} } }, "FR_UNION_ROOM is missing from the cache", "no FR_UNION_ROOM")
fails({ data = { maps = { FR_UNION_ROOM = { warps = {} } } } }, "FR_UNION_ROOM has no layout",
"FR_UNION_ROOM without a layout")
local tiny = { width = 4, height = 4, cellAt = function() return { mid = 1, coll = 0, elev = 0 } end }
fails({ data = { maps = { FR_UNION_ROOM = { warps = {}, midLayout = tiny } } } }, "layout has no cell",
"a truncated FR_UNION_ROOM layout")
local Adapters = require("src.core.game3.scripting.adapters")
local broken = Adapters.host(nil, { data = { maps = { FR_UNION_ROOM = { warps = {} } } } }, nil)
local realVersions = package.loaded["src.import.gba.versions"]
package.loaded["src.import.gba.versions"] = {
frMapFor = function() return "FR_UNION_ROOM" end,
seviiMapFor = function() return nil end,
}
local okWarp, warpErr = pcall(broken.warp, 1, 1, 0xFF, 7, 11, function() end, "warpspinenter")
package.loaded["src.import.gba.versions"] = realVersions
package.loaded["src.core.game3.map"] = realMap
check(not okWarp and tostring(warpErr):find("FR_UNION_ROOM has no layout", 1, true) ~= nil,
"the Union Room warp errors instead of landing in the cart room: " .. tostring(warpErr))
print("[test] built from the imported cache")
local Cache = require("tests.game3_cache")
local root = Cache.mount("meta.json", { native = true })
if not root then
print("[skip] plaza cache build: " .. tostring(Cache.reason))
else
local Dataset = require("src.core.game3.dataset")
local maps = Dataset.buildMaps()
local src = maps.FR_UNION_ROOM
check(src ~= nil, "FR_UNION_ROOM is in the cache")
Dataset.attachMidLayouts({ FR_UNION_ROOM = src }, Cache.cache())
local game = { data = { maps = maps } }
local def = Plaza.ensure(game)
check(def ~= nil and maps[Plaza.MAP_ID] == def, "ensure registers the plaza def")
eq(Plaza.ensure(game), def, "ensure is idempotent")
if def and src and src.midLayout then
local L = def.midLayout
eq(L.width, 25, "layout width")
eq(L.height, 25, "layout height")
eq(def.width, 25, "def width")
eq(def.height, 25, "def height")
eq(def.pair, src.pair, "same tileset pair as the cart room")
local srcMids = {}
local SL = src.midLayout
for y = 0, (SL.trueHeight or SL.height) - 1 do
for x = 0, (SL.trueWidth or SL.width) - 1 do
srcMids[SL:midAt(x, y)] = true
end
end
local foreign, collMismatch = 0, 0
for y = 0, 24 do
for x = 0, 24 do
if not srcMids[L:midAt(x, y)] then foreign = foreign + 1 end
local c = L:collAt(x, y)
local r = Plaza.roleAt(x, y)
if (c ~= 0) ~= (Plaza.BLOCKED[r] == true or r == "X") then
collMismatch = collMismatch + 1
end
end
end
eq(foreign, 0, "every metatile comes from the Union Room layout")
eq(collMismatch, 0, "blocked roles match the cart collision")
for slot = 1, Plaza.CAP do
local x, y = Plaza.cellFor(slot)
if L:collAt(x, y) ~= 0 then check(false, "slot " .. slot .. " cell is floor") end
end
eq(#def.warps, 1, "one warp")
local w, sw = def.warps[1], src.warps[1]
eq(w.x, 12, "warp x")
eq(w.y, 24, "warp y")
eq(w.mapGroup, sw.mapGroup, "warp group matches FR_UNION_ROOM")
eq(w.mapNum, sw.mapNum, "warp num matches FR_UNION_ROOM")
eq(w.destWarp, sw.destWarp, "warp dest matches FR_UNION_ROOM")
eq(w.destMap, sw.destMap, "warp destMap matches FR_UNION_ROOM")
eq(L:collAt(12, 24), SL:collAt(7, 11), "exit tile keeps the cart warp collision")
eq(def.music, src.music, "music copied")
end
end
if failed > 0 then
print(("[FAIL] %d check(s) failed"):format(failed))
os.exit(1)
end
print("[PASS] union plaza map")