Files
bryanthaboi ae6cac89e1 G2 support
2026-08-11 11:53:30 -04:00

182 lines
6.8 KiB
Lua

-- Navigation probe: can the bot WALK from A to B?
--
-- POKEPORT_IDENTITY=gold-dev POKEPORT_GAME=gold POKEPORT_SPEED=200 \
-- POKEPORT_GOLD_RESUME=05 \
-- POKEPORT_GOLD_PROBE="CHERRYGROVE_CITY>ILEX_FOREST,VIOLET_CITY>AZALEA_TOWN" \
-- POKEPORT_DRIVER=tests/drivers/gold_travel_probe.lua love .
--
-- Every teleport the route bot logs is a map the planner could not reach, and
-- each one used to cost a ten-minute run to reproduce. This puts the player at
-- the start map with the checkpoint's badges and HMs, asks Bot:travelTo for the
-- destination, and reports hops and frames -- so a planner change is a
-- one-minute experiment instead of a full replay.
--
-- The placement itself is A.teleport, the same harness shortcut the bot falls
-- back on; it is the SETUP here, never the measurement. What is measured is
-- only the walk that follows.
local Bot = dofile("tests/drivers/gold/bot.lua")
local A = Bot.adapter
local DEFAULT = "CHERRYGROVE_CITY>ILEX_FOREST"
return function(game)
local bot = Bot.new(game)
for _ = 1, 3000 do
if A.ready(game) then break end
bot:wait(1)
end
if not A.ready(game) then
print("[probe] the world never came up")
return
end
local resume = os.getenv("POKEPORT_GOLD_RESUME")
if resume then
local ok, err = A.loadCheckpoint(game, resume)
if not ok then
print(("[probe] cannot resume %s: %s"):format(resume, tostring(err)))
return
end
for _ = 1, 3000 do
if A.ready(game) then break end
bot:wait(1)
end
end
-- Single-map walk probe: "ROUTE_32@18,6>6,79" places the player and asks for
-- one local walk. Travel failures usually bottom out in one map's local
-- pathfinding, and isolating that is the difference between a 60k-frame
-- reproduction and a 400-frame one.
local walkSpec = os.getenv("POKEPORT_GOLD_WALK")
if walkSpec then
for spec in walkSpec:gmatch("[^;]+") do
local map, sx, sy, gx, gy =
spec:match("^%s*([%w_]+)@(%-?%d+),(%-?%d+)>(%-?%d+),(%-?%d+)%s*$")
if not map then
print(("[probe] cannot parse walk %q"):format(spec))
else
if not A.teleport(game, map, tonumber(sx), tonumber(sy)) then
print(("[probe] cannot place the player on %s"):format(map))
else
bot:wait(30)
bot:clearDialogue()
bot:progress()
local start = bot:frames()
local ok, res = pcall(function()
return bot:walkTo(tonumber(gx), tonumber(gy))
end)
local px, py = A.pos(game)
print(("[probe] walk %s: %s in %d frames, ended at (%s,%s) on %s")
:format(spec, (ok and res) and "ok" or "FAIL",
bot:frames() - start, tostring(px), tostring(py),
tostring(A.mapId(game))))
-- Who is standing where, now. An NPC blocks a step exactly like a
-- wall but appears nowhere in the extracted map, so a corridor that
-- looks two cells wide on paper can be one cell wide in play -- which
-- is the difference between "the planner is wrong" and "there is no
-- way round".
local map = A.map(game)
if map and px then
local near = {}
for cy = math.max(0, py - 6), math.min(map.heightCells - 1, py + 6) do
for cx = 0, map.widthCells - 1 do
if A.npcAt(game, cx, cy) then
near[#near + 1] = ("(%d,%d)"):format(cx, cy)
end
end
end
print(("[probe] npcs within 6 rows: %s")
:format(#near > 0 and table.concat(near, " ") or "none"))
end
bot:progress()
end
end
end
return
end
local pairsSpec = os.getenv("POKEPORT_GOLD_PROBE") or DEFAULT
local budget = tonumber(os.getenv("POKEPORT_GOLD_PROBE_BUDGET")) or 60000
local results = {}
for spec in pairsSpec:gmatch("[^,]+") do
-- "A>B" or "A>B#N", where N pins which REGION of B counts as arriving.
-- Split maps are the whole reason travelTo grew a region argument
-- (TEAM_ROCKET_BASE_B3F), so the probe has to be able to ask for one or it
-- cannot test the thing that matters.
local from, to, region = spec:match("^%s*([%w_]+)%s*>%s*([%w_]+)%s*#(%d+)%s*$")
if not from then
from, to = spec:match("^%s*([%w_]+)%s*>%s*([%w_]+)%s*$")
end
region = tonumber(region)
if not from then
print(("[probe] cannot parse %q"):format(spec))
else
-- Place the player at `from` (setup, not navigation), then walk.
local defs = bot:mapDefs()
local def = defs[from]
local landing = def and (def.warps or {})[1]
local placed
if landing then
placed = A.teleport(game, from, landing.x, landing.y)
else
placed = A.teleport(game, from, 0, 0)
end
if not placed then
print(("[probe] cannot place the player on %s"):format(from))
results[#results + 1] = { spec = spec, ok = false, why = "no placement" }
else
bot:wait(30)
bot:clearDialogue()
bot:progress()
local start = bot:frames()
local hops = 0
local origSay = bot.say
-- Count hops without threading a counter through the core.
bot.say = function(self, ...)
local line = table.concat({ ... }, " ")
if type((...)) == "string" and (...):match("^hop ") then
hops = hops + 1
end
return origSay(self, ...)
end
local ok, res = pcall(function() return bot:travelTo(to, region) end)
bot.say = origSay
local spent = bot:frames() - start
if not ok then
results[#results + 1] = { spec = spec, ok = false, frames = spent,
hops = hops,
why = (type(res) == "table" and res.why)
or tostring(res) }
else
results[#results + 1] = { spec = spec, ok = res, frames = spent,
hops = hops,
why = res and "" or "no route",
landed = tostring(A.mapId(game)) }
end
bot:progress()
if spent > budget then
print("[probe] budget spent, stopping")
break
end
end
end
end
print("")
print("================ travel probe ================")
local passed = 0
for _, r in ipairs(results) do
if r.ok then passed = passed + 1 end
print(("%-4s %-46s %7s frames %2s hops %s")
:format(r.ok and "ok" or "FAIL", r.spec, tostring(r.frames or "-"),
tostring(r.hops or "-"),
r.ok and "" or ("%s (on %s)"):format(tostring(r.why),
tostring(r.landed))))
end
print(("%d/%d reachable by walking"):format(passed, #results))
print("==============================================")
end