ok the auto driver works but needs work

This commit is contained in:
bryanthaboi
2026-07-21 13:19:53 -04:00
parent 9468ffccdf
commit 60b3cebdce
6 changed files with 419 additions and 36 deletions
+8 -1
View File
@@ -12,8 +12,15 @@ function FixedStep:init(callback)
self.callback = callback
end
-- The anti-spiral clamp doubles as a steps-per-frame ceiling (0.25s = 15
-- steps), which silently throttled the high fast-forward levels: at 100X
-- a 60fps frame wants ~100 steps. Game:update raises this to fit the
-- current speed target; a stall still cannot snowball past one frame's
-- intended budget.
FixedStep.maxAccum = MAX_ACCUM
function FixedStep:update(dt)
self.accum = math.min(self.accum + dt, MAX_ACCUM)
self.accum = math.min(self.accum + dt, self.maxAccum or MAX_ACCUM)
while self.accum >= self.STEP do
self.accum = self.accum - self.STEP
self.callback(self.STEP)
+5 -1
View File
@@ -170,7 +170,11 @@ function Game:update(dt)
-- deferred A and edge pulses land in Input's press queue for this step.
TouchInput:update(dt)
-- Fast-forward scales only the logic clock (see src/core/GameSpeed.lua).
FixedStep:update(dt * self:logicSpeed())
-- Give the accumulator room for one full frame at the current speed,
-- or the anti-spiral clamp quietly caps every level above ~15X.
local speed = self:logicSpeed()
FixedStep.maxAccum = math.max(0.25, speed * FixedStep.STEP * 1.5)
FixedStep:update(dt * speed)
-- Audio runs off real time at a fixed 60Hz regardless of game speed or
-- display refresh, so fades and chip synthesis keep their intended tempo
-- whether we are at 1X, 10X, or running with vsync disabled.
+1 -1
View File
@@ -18,7 +18,7 @@ local GameSpeed = {}
-- attempt is long enough that the iteration loop, not the engine, is the
-- bottleneck. Vsync caps how much a real frame can do, so past 10X the
-- multiplier is increasingly a ceiling rather than a rate.
GameSpeed.LEVELS = { 1, 2, 4, 10, 20, 30, 50, 75 }
GameSpeed.LEVELS = { 1, 2, 4, 10, 20, 30, 50, 75, 100,500 }
GameSpeed.DEFAULT = 1
function GameSpeed.levelLabel(v)
+40
View File
@@ -29,6 +29,46 @@ local function hashSet(list, into)
return into
end
-- Passability of a cell of an UNLOADED map def -- the connected neighbor
-- during an edge crossing. pokered's collision check reads the neighbor
-- strip's tile bytes, so a step off the map edge onto a solid tile of
-- the connected map bumps exactly like an in-map wall; the port needs
-- the same read without building the whole Map. Same math as cellTile
-- on the raw blocks, honoring the surf rule (water/shore passable only
-- while surfing, same fallbacks as Map.new).
function Map.defPassable(def, tilesetDef, cx, cy, surfing)
if not (def and tilesetDef and tilesetDef.blocks and tilesetDef.walkable) then
return true -- no data to judge with: keep the old permissive behavior
end
local tx, ty = cx * 2, cy * 2 + 1
local bx, by = math.floor(tx / 4), math.floor(ty / 4)
local id
if bx < 0 or by < 0 or bx >= def.width or by >= def.height then
id = def.borderBlock
else
id = def.blocks[by * def.width + bx + 1]
end
local block = tilesetDef.blocks[(id or 0) + 1]
if not block then return false end
local tile = block[(ty % 4) * 4 + (tx % 4) + 1]
for _, t in ipairs(tilesetDef.walkable) do
if t == tile then return true end
end
if surfing then
for _, t in ipairs(tilesetDef.waterTiles or WATER_TILES) do
if t == tile then return true end
end
local shore = tilesetDef.shoreTiles
if shore == nil and not NO_SHORE_TILESETS[def.tileset] then
shore = SHORE_TILES
end
for _, t in ipairs(shore or {}) do
if t == tile then return true end
end
end
return false
end
function Map.new(def, tilesetDef)
local self = setmetatable({}, Map)
self.def = def
+12 -3
View File
@@ -883,8 +883,7 @@ function OverworldState:checkEdgeExit(dir)
local conn = self.map:connection(COMPASS[dir])
if conn then
self:crossConnection(dir, conn)
return true
return self:crossConnection(dir, conn)
end
return false
end
@@ -898,7 +897,7 @@ function OverworldState:crossConnection(dir, conn)
local dest = Game.data.maps[conn.map]
if not dest then
Logger.warn("connection to unknown map %s", tostring(conn.map))
return
return false
end
local p = self.player
local x, y = p.cellX, p.cellY
@@ -914,6 +913,15 @@ function OverworldState:crossConnection(dir, conn)
end
x = math.max(0, math.min(destW - 1, x))
y = math.max(0, math.min(destH - 1, y))
-- pokered's collision check reads the NEIGHBOR strip's tile bytes, so
-- stepping off the edge onto a solid tile of the connected map bumps
-- exactly like an in-map wall. Without this read, Pallet's south
-- shore (land at x2-3) walked straight onto ROUTE_21 (3,0) -- a
-- collision tile -- stranding the player on a cell no walk can leave.
if not Map.defPassable(dest, Game.data.tilesets[dest.tileset], x, y,
p.surfing) then
return false
end
self:setMap(conn.map, x, y, p.facing, { seamless = true })
-- place the player one cell before the seam (their old world spot,
-- which the neighbor strip renders identically) and start the step
@@ -931,6 +939,7 @@ function OverworldState:crossConnection(dir, conn)
p.stepFramesCur = Game.save.onBike
and (FieldDefaults.world(Game.data, "bikeStepFrames") or 8)
or (FieldDefaults.world(Game.data, "stepFrames") or 16)
return true
end
-- -------------------------------------------------------------------------
+353 -30
View File
@@ -1104,6 +1104,9 @@ local mountSurfToward
-- travelTo); declared here for the same reason as cutToward.
local arriveByWarp
-- pickupBlockingBall (defined below, next to reachableSet) is a GLOBAL,
-- like cutToward: the main chunk is at LuaJIT's 200-local ceiling.
-- Cross to another region of the same map through a gate. Assigned below
-- (needs walkOntoWarp); declared here because ops.goto_ calls it.
local reEnterThroughGate
@@ -1141,10 +1144,14 @@ end
function ops.goto_(s, _where, isLast)
local o = ow()
local map = o.map
-- one surf mount per goto: walking ashore dismounts, so an unreachable
-- target beyond a pond can otherwise mount -> cross -> dismount -> fail
-- -> mount again forever (Celadon's pond, target (39,8))
local surfMounts = 0
-- surf mounts per goto, BUDGETED: walking ashore dismounts, so an
-- unreachable target beyond a pond can mount -> cross -> dismount ->
-- fail -> mount forever (Celadon's pond, target (39,8)) -- but a hard
-- cap of ONE stranded the bot on Route 20's mid-island strip, where a
-- legitimate crossing is surf -> beach -> surf again. Three mounts
-- covers every real island-hop; a still-unreachable target then fails
-- the goto as before.
local surfMounts, SURF_BUDGET = 0, 3
-- ...and a small cut budget per goto: a pocket behind two trees opens
-- over two passes, but a target that stays unreachable must not
-- chain-cut every ornamental bush on the map (Saffron/Celadon cut-spam)
@@ -1171,6 +1178,8 @@ function ops.goto_(s, _where, isLast)
local tx = math.max(0, math.min(s.x, map.widthCells - 1))
local ty = math.max(0, math.min(s.y, map.heightCells - 1))
local refusedEdge = knownWalls(map.id) or {}
-- one border-cell retarget per goto (see the fenced-lane rung below)
local edgeRetargeted = false
for edgeTry = 1, 2 do
for _ = 1, 300 do
if ow().map.id ~= startMap then return true end
@@ -1206,11 +1215,43 @@ function ops.goto_(s, _where, isLast)
end
-- ...or the border cell is on WATER (Pallet's south edge):
-- mount SURF at the shore and re-plan over the sea.
if surfMounts == 0 and mountSurfToward
if surfMounts < SURF_BUDGET and mountSurfToward
and mountSurfToward(tx, ty) then
surfMounts = surfMounts + 1
goto edgeStep
end
-- ...or THIS border column is fenced off while another of
-- the same edge is open. Viridian's south border is three
-- fenced lanes and only x20-21 connects to the city; the
-- planner's seam cell (15,35) is statically walkable-paired
-- but dynamically sealed, so the exit spun cutting
-- ornamental bushes and surfing the pond. Retarget ONCE to
-- the nearest border cell that is BOTH reachable right now
-- and has a walkable far side.
if not edgeRetargeted then
local compass = (dir == "up" and "north")
or (dir == "down" and "south")
or (dir == "left" and "west") or "east"
local conn = map.def.connections and map.def.connections[compass]
if conn and seamCells and reachableSet then
local seen, WW = reachableSet()
local bestAlt, bestD
for _, c in ipairs(seamCells(map.def, compass, conn)) do
if seen[c[2] * WW + c[1]] and (c[1] ~= tx or c[2] ~= ty) then
local d2 = math.abs(c[1] - tx) + math.abs(c[2] - ty)
if not bestD or d2 < bestD then bestAlt, bestD = c, d2 end
end
end
if bestAlt then
say(("edge exit %s on %s: border cell (%d,%d) is walled "
.. "off; crossing at (%d,%d) instead"):format(dir,
tostring(map.id), tx, ty, bestAlt[1], bestAlt[2]))
tx, ty = bestAlt[1], bestAlt[2]
edgeRetargeted = true
goto edgeStep
end
end
end
end
break -- already there, or genuinely no path
end
@@ -1250,7 +1291,7 @@ function ops.goto_(s, _where, isLast)
-- The push stalled. Once per goto: if this edge has a WATER crossing
-- and the party can surf, mount and retry the whole exit over the
-- sea column instead of bumping the shore (Cinnabar -> Route 21).
if edgeTry == 1 and surfMounts == 0 and not ow().player.surfing
if edgeTry == 1 and surfMounts < SURF_BUDGET and not ow().player.surfing
and slotKnowing("SURF") then
local wc = borderWaterCell(map, dir, tx, ty)
if wc and mountSurfToward and mountSurfToward(wc[1], wc[2]) then
@@ -1353,12 +1394,21 @@ function ops.goto_(s, _where, isLast)
for _ = 1, 8 do
if ow().map.id ~= from then return true end
local m = ow().map
-- only a warp ON the map border needs the held direction; an
-- interior door fires on its own and has no "outward" to push
local outward = (s.x >= m.widthCells - 1 and "right")
or (s.x <= 0 and "left")
or (s.y >= m.heightCells - 1 and "down")
or (s.y <= 0 and "up")
-- A warp needing a held press: mirror the engine's
-- Warp.extraCheck -- facing the map edge, OR a warp-carpet
-- door tile in front (Route 16's gate mats: plain path
-- tiles whose warp fires only pressing INTO the building).
-- A true interior door fires on its own; no direction
-- passes extraCheck there and we stop pushing.
local WarpMod = require("src.world.Warp")
local carpets = G.data.field and G.data.field.warpCarpets
local outward
for _, dd in ipairs(DIRS) do
if WarpMod.extraCheck(m, carpets, s.x, s.y, dd[3]) then
outward = dd[3]
break
end
end
if not outward then break end
pushed = true
faceDir(outward)
@@ -1464,6 +1514,15 @@ function ops.goto_(s, _where, isLast)
blocked = 0
goto keepGoing
end
-- ...or an item ball is the wall (Pokémon Tower 6F's
-- RARE_CANDY sits in the floor's only crossing corridor).
-- Tried BEFORE arriveByWarp: taking the ball keeps the walk on
-- this floor, while arriveByWarp warps clear off it hunting
-- the far end.
if pickupBlockingBall and pickupBlockingBall(s.x, s.y) then
blocked = 0
goto keepGoing
end
-- ...or the target is in a part of this map no walk connects.
--
-- Cave floors are not one connected room. MT_MOON_B1F is four
@@ -1533,7 +1592,7 @@ function ops.goto_(s, _where, isLast)
-- an NPC-blocked target in a pond town does not trigger a
-- pointless swim, and ONCE per goto -- coming ashore dismounts,
-- so a still-unreachable target re-mounts forever otherwise.
if surfMounts == 0 and mountSurfToward
if surfMounts < SURF_BUDGET and mountSurfToward
and mountSurfToward(s.x, s.y) then
surfMounts = surfMounts + 1
blocked = 0
@@ -2439,6 +2498,8 @@ local function walkOntoWarp(wx, wy)
local toggledSwitch = false
-- one Victory Road switch solve per call (see solveVictoryRoadSwitches)
local solvedVR = false
-- one blocking-item-ball pickup per call (see pickupBlockingBall)
local tookBall = false
for _ = 1, 300 do
if ow().map.id ~= from then return true end
if inBattle() then fightBattle()
@@ -2456,11 +2517,28 @@ local function walkOntoWarp(wx, wy)
-- "leave by this edge" idiom -- which is Red shoving downward into
-- the wall below the door instead of walking out of it.
local m = ow().map
local outward = (wy >= m.heightCells - 1 and "down")
or (wy <= 0 and "up")
or (wx <= 0 and "left")
or (wx >= m.widthCells - 1 and "right")
or "down"
-- Which press can actually FIRE this warp? Mirror the engine:
-- Warp.extraCheck says whether pressing `dir` from this cell can
-- warp -- facing the map edge, OR a warp-carpet door tile in
-- front. Route 16's gate mats are the latter: plain path tiles
-- whose warp fires only pressing INTO the building ($4B door to
-- the LEFT), and the old edge-based guess pushed "down" forever
-- ("the step onto (24,10) keeps getting shoved back").
local WarpMod = require("src.world.Warp")
local carpets = G.data.field and G.data.field.warpCarpets
local outward
for _, d in ipairs(DIRS) do
if WarpMod.extraCheck(m, carpets, wx, wy, d[3]) then
outward = d[3]
break
end
end
outward = outward
or (wy >= m.heightCells - 1 and "down")
or (wy <= 0 and "up")
or (wx <= 0 and "left")
or (wx >= m.widthCells - 1 and "right")
or "down"
faceDir(outward)
walk(outward)
-- If pushing outward does nothing, the warp we are standing on is
@@ -2543,6 +2621,12 @@ local function walkOntoWarp(wx, wy)
solvedVR = true
solveVictoryRoadSwitches()
bfsNil = 0
-- ...or an item ball on the only corridor (Pokémon Tower 6F's
-- RARE_CANDY): take it and re-path.
elseif not tookBall and pickupBlockingBall
and pickupBlockingBall(wx, wy) then
tookBall = true
bfsNil = 0
else
bfsNil = (bfsNil or 0) + 1
if bfsNil >= 8 then
@@ -2638,7 +2722,10 @@ end
-- pairing is exact and a seam with no walkable pair can never be crossed
-- however hard we push at it. This is what keeps the search from planning
-- Vermilion -> Saffron -> Cerulean through a walled city.
local function seamCells(def, dir, conn)
-- GLOBAL (not local): ops.goto_'s edge-exit retarget below needs it and
-- is defined earlier in the file; the main chunk is also at LuaJIT's
-- 200-local ceiling.
function seamCells(def, dir, conn)
local dest = G.data.maps and G.data.maps[conn.map]
if not dest then return {} end
local aW, aH = def.width * 2, def.height * 2
@@ -2770,14 +2857,16 @@ local function regionGraph()
local dw = maps[src].warps and maps[src].warps[w.destWarp]
local dr = dw and cellRegionOf(src, dw.x, dw.y)
if sr and dr then
local c = (id:find("^SEAFOAM") or src:find("^SEAFOAM")) and 43 or 3
add(regionNode(id, sr), regionNode(src, dr),
{ warp = { x = w.x, y = w.y }, cost = 3, dynamic = true })
{ warp = { x = w.x, y = w.y }, cost = c, dynamic = true })
end
end
elseif type(d) == "string" and maps[d] then
local dw = maps[d].warps and maps[d].warps[w.destWarp]
local dr = dw and cellRegionOf(d, dw.x, dw.y)
if sr and dr then
if sr and dr and not (d:find("^SEAFOAM") and not id:find("^SEAFOAM")) then
-- entering Seafoam pruned, same reason as the map graph
local c = (id:find("ELEVATOR") or d:find("ELEVATOR")) and 50 or 1
add(regionNode(id, sr), regionNode(d, dr),
{ warp = { x = w.x, y = w.y }, cost = c })
@@ -2802,8 +2891,13 @@ local function regionGraph()
local k = tostring(sr) .. ">" .. tostring(dr)
if sr and dr and not seen[k] then
seen[k] = true
add(regionNode(id, sr), regionNode(conn.map, dr),
{ dir = COMPASS_DIR[dir], cells = cells, cost = 1 })
-- ROUTE_20 <-> CINNABAR: pruned, same reason as the map
-- graph (the east and west seas never connect)
if not ((id == "ROUTE_20" and conn.map == "CINNABAR_ISLAND")
or (id == "CINNABAR_ISLAND" and conn.map == "ROUTE_20")) then
add(regionNode(id, sr), regionNode(conn.map, dr),
{ dir = COMPASS_DIR[dir], cells = cells, cost = 1 })
end
end
end
end
@@ -2945,7 +3039,17 @@ local function mapGraph()
-- rather than deleted so a floor only the elevator can reach (Silph's
-- top) is still on the graph.
local elev = (id:find("ELEVATOR") or d:find("ELEVATOR")) and 50 or 1
g[id][#g[id] + 1] = { to = d, warp = w, cost = elev }
-- ENTERING Seafoam is pruned outright: 1F's two door pockets
-- only connect through the BASEMENT floors, which hop execution
-- cannot thread, so every plan through the islands ping-ponged
-- the Route 20 doors -- pricing alone lost to a wall of banned
-- alternatives after hours of wandering. The route never uses
-- Seafoam (Cinnabar is reached by sea from Pallet). Exits (the
-- LAST_MAP door edges) are kept so a bot stranded inside can
-- always leave.
if not (d:find("^SEAFOAM") and not id:find("^SEAFOAM")) then
g[id][#g[id] + 1] = { to = d, warp = w, cost = elev }
end
into[d] = into[d] or {}
into[d][id] = true
end
@@ -2958,8 +3062,23 @@ local function mapGraph()
-- gate script can open one later, and a wrongly deleted edge is
-- an unreachable map, while a wrongly costly one is a detour.
local cells = seamCells(def, dir, c)
g[id][#g[id] + 1] = { to = c.map, dir = COMPASS_DIR[dir],
cells = cells, cost = #cells > 0 and 1 or 8 }
-- ROUTE_20's east and west seas NEVER connect on the map: a
-- walled channel splits them at Seafoam, and the only crossing
-- is through the islands' basement, which hop execution cannot
-- thread. The map graph sees one ROUTE_20 node, so from the
-- east side the CINNABAR seam looked one hop away, and one
-- travelTo(PALLET_TOWN) spent HOURS banning its way across
-- Kanto until this impossible seam was the cheapest edge left,
-- then camped it. PRUNED, not priced -- this is a hard physical
-- fact, not a maybe-a-tree-opens-it seam, and both sides stay
-- reachable without it (CINNABAR via ROUTE_21, ROUTE_20 via
-- ROUTE_19). The executable Cinnabar path is Pallet ->
-- ROUTE_21 south, the HOF run's own.
if not ((id == "ROUTE_20" and c.map == "CINNABAR_ISLAND")
or (id == "CINNABAR_ISLAND" and c.map == "ROUTE_20")) then
g[id][#g[id] + 1] = { to = c.map, dir = COMPASS_DIR[dir],
cells = cells, cost = #cells > 0 and 1 or 8 }
end
end
end
end
@@ -2972,7 +3091,8 @@ local function mapGraph()
for _, w in ipairs(def.warps or {}) do
if w.destMap == "LAST_MAP" then
for src in pairs(into[id] or {}) do
g[id][#g[id] + 1] = { to = src, warp = w, dynamic = true, cost = 3 }
local c = (id:find("^SEAFOAM") or src:find("^SEAFOAM")) and 43 or 3
g[id][#g[id] + 1] = { to = src, warp = w, dynamic = true, cost = c }
end
end
end
@@ -3436,12 +3556,74 @@ function reachableSet()
and not pairBlockedStep(m, c[1], c[2], nx, ny, p.surfing) then
seen[id] = true
q[#q + 1] = { nx, ny }
elseif nx >= 0 and ny >= 0 and nx < W and ny < H
and not passableCell(m, nx, ny) then
-- ledge hops, so this set matches what bfsNextKey can walk
-- (see cutToward for the ROUTE_9 case this fixed)
local lx, ly = ledgeLanding(m, c[1], c[2], d[3], d[1], d[2])
if lx then
local lid = ly * W + lx
if not seen[lid] and not blocked[lid] then
seen[lid] = true
q[#q + 1] = { lx, ly }
end
end
end
end
end
return seen, W
end
-- Pick up an item ball that is walling off the walk to (tx, ty).
--
-- An item ball is an NPC and therefore a BFS wall -- and Pokémon Tower
-- 6F's RARE_CANDY at (6,8) stands in the ONLY corridor between the
-- floor's east and west halves. Vanilla players grab it in passing (the
-- ball despawns, opening the way) and the route's own pickup steps do
-- the same -- but a resume desync that skips those steps leaves the
-- floor severed for good: "goto (6,14) unreachable on POKEMON_TOWER_6F"
-- with a fresh save's balls all standing. So: when the target is
-- outside the reachable set and a ball sits on its frontier, stand
-- beside it, take it, and let the caller re-plan. Same one-per-call
-- contract as cutToward.
pickingBall = false -- global: the main chunk is at the 200-local ceiling
function pickupBlockingBall(tx, ty)
if pickingBall then return false end
local m = ow().map
local seen, W = reachableSet()
if seen[ty * W + tx] then return false end -- not a blocking-ball problem
for _, npc in ipairs(ow().npcs) do
if tostring(npc.def and npc.def.sprite or "") == "SPRITE_POKE_BALL" then
local bx, by = npc.cellX, npc.cellY
for _, d in ipairs(DIRS) do
local ax, ay = bx + d[1], by + d[2]
if ax >= 0 and ay >= 0 and ax < W and ay < m.heightCells
and seen[ay * W + ax] then
pickingBall = true
local took = false
ops.goto_({ x = ax, y = ay })
local p = ow().player
if ow().map.id == m.id and p.cellX == ax and p.cellY == ay then
faceDir((by < ay and "up") or (by > ay and "down")
or (bx < ax and "left") or "right")
press("a")
U.wait(20)
mashUntilIdle()
took = not ow():npcAtCell(bx, by)
end
pickingBall = false
if took then
say(("picked up the item ball at (%d,%d) that was walling off "
.. "(%d,%d)"):format(bx, by, tx, ty))
return true
end
end
end
end
end
return false
end
-- Reach another region of THIS map by going through a gate and out its far
-- door.
--
@@ -3945,6 +4127,8 @@ end
-- CASCADEBADGE (src/ui/PartyMenu.lua:342).
function ops.fieldMove(s, where)
local move = tostring(s.move or ""):upper()
-- the tree cell CUT will face, for the post-use verification below
local cutCellX, cutCellY
local slot = slotKnowing(move)
if not slot then
-- DIG and FLY are TRANSPORT, and a transport step we cannot perform is
@@ -3981,6 +4165,21 @@ function ops.fieldMove(s, where)
say(("fieldMove %s: nobody in the party knows it"):format(move))
return false
end
-- The knower must also be CONSCIOUS: the party submenu hides field
-- moves on a fainted mon (vanilla -- partyKnows requires hp > 0), so
-- the menu walk below finds only STATS/SWITCH and fails. Route 23's
-- mandatory surf strip is where this bit: WARTORTLE arrived dead from
-- the Viridian Gym attrition, "no SURF entry" x3, the water was never
-- crossed, and the whole league skip-cascaded with all eight badges
-- in the bag. Heal, fail this attempt loudly, and let the caller's
-- retry run the step again with a live user.
local knower = (G.save.party or {})[slot]
if knower and (knower.hp or 0) <= 0 then
say(("fieldMove %s: its only user has fainted -- visiting a centre "
.. "before retrying"):format(move))
visitPokeCenter(where)
return false
end
-- CUT acts on the tile the player is FACING (OverworldState:tryCut), and
-- a `goto` leaves us facing whatever direction we last stepped. Without
-- turning to the tree first the move fires into empty ground, the game
@@ -3991,6 +4190,19 @@ function ops.fieldMove(s, where)
local m = ow().map
local function cuttable(cx, cy)
if not m:inBounds(cx, cy) or m:isWalkableCell(cx, cy) then return false end
-- The same FACE gate as the engine (tryCut): a tree BLOCK is 2x2
-- cells and only the tree-tile half is cuttable -- facing the
-- fence/sign half answers "Nothing to CUT". Without this the scan
-- below picked whichever unwalkable half it met first: Route 9's
-- tree at (5,8) has the block's sign half at (4,9), DOWN is
-- scanned before RIGHT, and the run cut "the tree" twenty times
-- from (4,8) without ever opening the way east.
local ts = m.def and m.def.tileset
local tile = m:cellTile(cx, cy)
if not ((ts == "OVERWORLD" and tile == 0x3d)
or (ts == "GYM" and tile == 0x50)) then
return false
end
local block = m:blockAt(math.floor(cx / 2), math.floor(cy / 2))
for _, sw in ipairs(G.data.field.cutTreeSwaps or {}) do
if sw.before == block then return true end
@@ -4001,7 +4213,11 @@ function ops.fieldMove(s, where)
local facing
for _, d in ipairs({ { 0, 1, "down" }, { 0, -1, "up" },
{ -1, 0, "left" }, { 1, 0, "right" } }) do
if cuttable(p.cellX + d[1], p.cellY + d[2]) then facing = d[3] break end
if cuttable(p.cellX + d[1], p.cellY + d[2]) then
facing = d[3]
cutCellX, cutCellY = p.cellX + d[1], p.cellY + d[2]
break
end
end
if not facing then
note("fieldMove: no tree adjacent", where)
@@ -4102,6 +4318,19 @@ function ops.fieldMove(s, where)
press("a")
U.wait(12)
mashUntilIdle()
-- CUT is verifiable: the faced tree cell must be walkable afterwards.
-- "used CUT" used to print regardless, so an engine refusal ("Nothing
-- to CUT") looked like success and the caller re-cut the same tree
-- for its whole budget.
if move == "CUT" and cutCellX then
local now = ow().map
if now:inBounds(cutCellX, cutCellY)
and not now:isWalkableCell(cutCellX, cutCellY) then
say(("fieldMove CUT: the tree at (%d,%d) is still standing -- the "
.. "cut did not land"):format(cutCellX, cutCellY))
return false
end
end
say(("used %s on %s"):format(move, tostring(where)))
-- DIG and FLY are the route's long-distance travel, and neither lands
@@ -4182,6 +4411,22 @@ function cutToward(tx, ty)
and not blockedBy[id] and m:isWalkableCell(nx, ny) then
seen[id] = true
queue[#queue + 1] = { nx, ny }
elseif nx >= 0 and ny >= 0 and nx < W and ny < H
and not m:isWalkableCell(nx, ny) then
-- ledge hops reach cells this flood otherwise misses -- and
-- bfsNextKey WALKS them, so leaving them out makes the two
-- disagree: from ROUTE_9 (46,9) the regrown west tree's face is
-- only ledge-reachable, cutToward found "no tree", and
-- travelTo(CINNABAR_ISLAND) died -- taking Blaine, the 7-badge
-- Viridian Gym and the whole league gate with it.
local lx, ly = ledgeLanding(m, c[1], c[2], d[3], d[1], d[2])
if lx then
local lid = ly * W + lx
if not seen[lid] and not blockedBy[lid] then
seen[lid] = true
queue[#queue + 1] = { lx, ly }
end
end
end
end
end
@@ -4863,14 +5108,21 @@ function mountSurfToward(tx, ty)
-- water tilesets only (water_tilesets.asm); a cave with no water would
-- scan every cell for nothing
local seen, W = reachableSet()
-- nearest reachable land cell with water beside it
-- The shore to mount from is the reachable land-beside-water cell
-- nearest the TARGET, with the player's own distance only breaking
-- ties. Nearest-to-player was a trap on Route 20's mid-island strip:
-- a rock wall splits the strip's shores, and the bot re-mounted on
-- the west shore every time, surfed back over ground it had already
-- crossed, dismounted in the same spot and burned the whole mount
-- budget without ever entering the water that leads east.
local best, bestD
for cellId in pairs(seen) do
local cx, cy = cellId % W, math.floor(cellId / W)
for _, d in ipairs(DIRS) do
local wx, wy = cx + d[1], cy + d[2]
if m:inBounds(wx, wy) and m:isWaterCell(wx, wy) then
local dist = math.abs(cx - p.cellX) + math.abs(cy - p.cellY)
local dist = (math.abs(cx - tx) + math.abs(cy - ty)) * 4
+ math.abs(cx - p.cellX) + math.abs(cy - p.cellY)
if not bestD or dist < bestD then best, bestD = { cx, cy }, dist end
break
end
@@ -6024,6 +6276,26 @@ local function runRoute(startIndex)
-- already at the heal point, so this is what the death gets attributed to
local lastRanMap
local diedOn = {} -- segment index -> deaths this attempt
-- Badge go-back (see the league-gate check in the loop): badges whose
-- gym segment this attempt has already rewound to, so a gym that
-- cannot be won does not loop the rewind forever.
local badgeRescued = {}
local LEAGUE_GATE_MAPS = {
ROUTE_22_GATE = true, ROUTE_23 = true, VICTORY_ROAD_1F = true,
VICTORY_ROAD_2F = true, VICTORY_ROAD_3F = true,
INDIGO_PLATEAU = true, INDIGO_PLATEAU_LOBBY = true,
}
-- route order; badge names are inventory keys, gyms are segment maps
local BADGE_GYM_SEGMENTS = {
{ "BOULDERBADGE", "PEWTER_GYM" },
{ "CASCADEBADGE", "CERULEAN_GYM" },
{ "THUNDERBADGE", "VERMILION_GYM" },
{ "RAINBOWBADGE", "CELADON_GYM" },
{ "SOULBADGE", "FUCHSIA_GYM" },
{ "MARSHBADGE", "SAFFRON_GYM" },
{ "VOLCANOBADGE", "CINNABAR_GYM" },
{ "EARTHBADGE", "VIRIDIAN_GYM" },
}
-- segment index -> deaths in a row where training gained no levels.
-- Reported, not acted on: no death count writes off a segment any more.
-- A long barren streak means the grind cannot reach a training ground
@@ -6291,6 +6563,54 @@ local function runRoute(startIndex)
end
end
-- Rolling checkpoint every 10 segments, no external toucher needed.
-- A run once reached segment 182 with all eight badges and then
-- "completed" by skipping the sealed league -- and NOTHING was on
-- disk: no toucher was running, no abandon fired, and route
-- completion never saved. Hours of progress, gone at exit. Never
-- again: the driver banks its own progress as it goes.
if i % 10 == 0 then
saveCheckpoint(i, "rolling (every 10 segments)")
end
-- GO BACK FOR A MISSING BADGE before the league gate can wall us.
--
-- Route 22/23's guards badge-check in order and shove the player
-- back (vanilla), and the route only ever moves FORWARD -- so a run
-- that lost a gym segment to a travel failure (Blaine's whole
-- island skipped over a failed sea trip, once) used to reach the
-- gate with seven badges and thrash against the shove until the
-- attempt died. The badges are ordinary inventory; when a league
-- segment comes up short, rewind the index to the missing badge's
-- own gym segment and let the ordinary mismatch->travelTo machinery
-- carry us there. Earliest missing badge first (Cinnabar's segment
-- sits before Viridian's, and Viridian Gym itself only unlocks at
-- seven badges). Once per badge per attempt, so a gym that
-- genuinely cannot be won does not loop the rewind forever.
if LEAGUE_GATE_MAPS[seg.map] then
local missing
for _, bg in ipairs(BADGE_GYM_SEGMENTS) do
if not (G.save.inventory or {})[bg[1]] and not badgeRescued[bg[1]] then
missing = bg
break
end
end
if missing then
local back
for j = i - 1, 1, -1 do
if ROUTE[j].map == missing[2] then back = j break end
end
if back then
badgeRescued[missing[1]] = true
say(("the league gate is ahead but %s is missing -- rewinding "
.. "to segment %d/%d (%s) to earn it")
:format(missing[1], back, #ROUTE, missing[2]))
i = back - 1
goto nextSegment
end
end
end
-- Standing in an elevator? Ride out to the floor we need, before the
-- map-wait can react to a bounce.
--
@@ -6694,6 +7014,9 @@ local function runRoute(startIndex)
say("---- route complete ----")
say(("segments: %d failures: %d"):format(#ROUTE, failures))
-- bank the final state whatever happened: a "complete" run that
-- skip-cascaded its tail used to exit with nothing on disk
saveCheckpoint(#ROUTE, "route complete")
return true
end