Open Kanto: the S.S. Aqua, the Kanto gyms, the Magnet Train, Mt. Silver and Red (Closes #33)
Checks / web (push) Failing after 18s
Private ROM checks / private-rom (push) Failing after 25m40s

All 146 Kanto maps, Route 22, Route 28 and Silver Cave are open. The
Magnet Train, Snorlax and the Poké Flute channel, the Diploma, the Trainer
House, Bill's grandpa, Oak's Pokédex rating in Pallet and Goldenrod's
postgame move tutor are ported; big objects cover 2x2 tiles. The Magnet
Train and the Diploma are compared with the cartridge frame by frame.

The playthrough continues from the Hall of Fame through the S.S. Aqua,
the eight Kanto badges, Oak opening Mt. Silver and Red's defeat and
credits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kawaandClaude Opus 5.5 committed 2026-10-17 15:53:57 +02:00
1 parent f05fd8ebee
commit d3be732118
22 files changed
+1877 -31

No files matched your search

+78 -3
View File
@@ -145,7 +145,12 @@ export class Playthrough {
item: world.liveState.bag.balls[ball].item,
slot: ball,
});
else if (engine.battleMode === C.WILD_BATTLE && mon.hp * 2 < mon.maxHp)
else if (
engine.battleMode === C.WILD_BATTLE &&
mon.hp * 2 < mon.maxHp &&
// A careful player trapped by Bind or Wrap fights back.
!(this.careful && log.slice(-4).includes("Can't escape!"))
)
world.battleAction({ action: "run" });
else if (
engine.battleMode !== C.WILD_BATTLE &&
@@ -524,8 +529,10 @@ export class Playthrough {
this.walk(warp.x, warp.y);
const exit = WARP_EXITS[world.assets.collision(world.map, warp.x, warp.y)];
if (world.mapId === from && exit && world.move(exit)) this.play();
if (world.mapId !== from && this.route(target, maps, depth - 1, goal))
return true;
// A warp to another place on the same map lands at its partner.
const arrival = warp.targetMap === from ? world.map.warps[warp.targetWarp - 1] : undefined;
const moved = arrival ? world.x === arrival.x && world.y === arrival.y : world.mapId !== from;
if (moved && this.route(target, maps, depth - 1, goal)) return true;
world.restore(here);
}
return false;
@@ -710,6 +717,74 @@ export class Playthrough {
}
this.play();
};
/** Face a tile (a hidden item) from a side reachable on foot or by
* Surf and press A. From land a step towards it only turns the player
* (a surfer would move onto it), so land sides come first. */
inspect = (x: number, y: number) => {
const world = this.world,
land = (dx: number, dy: number) =>
world.assets.permission(world.assets.collision(world.map, x + dx, y + dy)) === 0;
const sides = (
[
["up", 0, 1],
["down", 0, -1],
["left", 1, 0],
["right", -1, 0],
] as const
)
.slice()
.sort(([, ax, ay], [, bx, by]) => Number(!land(ax, ay)) - Number(!land(bx, by)));
for (const [face, dx, dy] of sides) {
try {
findPath(world, x + dx, y + dy, { water: true, ledges: this.ledges });
} catch {
continue;
}
this.sail(x + dx, y + dy);
if (world.save().direction !== face) world.move(face);
world.interact();
this.play();
return;
}
throw new Error(`No side to inspect ${x},${y}`);
};
/** Ride up a downhill slope (ENGINE_DOWNHILL rolls an idle bike down,
* so stopping between steps loses ground): the planned direction is held
* on every tick to (x, y), then up until the map changes. */
rideUp = (x: number, y: number) => {
const world = this.world,
map = world.mapId;
for (let tick = 0; tick < 20_000 && world.mapId === map; tick++) {
if (world.dialogue || world.battle) {
this.play();
continue;
}
const path = world.x === x && world.y === y ? [] : findPath(world, x, y);
world.tick(path[0] ?? "up");
}
expect(world.mapId, `riding up ${map}`).not.toBe(map);
this.play();
};
/** The Pokégear's radio card from the start menu: turn the knob to
* `frequency`, then close the Pokégear and the menu; the station keeps
* playing (ExitPokegearRadio_HandleMusic). */
tuneRadio = (frequency: string) => {
const world = this.world;
expect(world.openStartMenu()).toBe(true);
this.choose("POKéGEAR");
for (let press = 0; press < 3 && world.pokegear?.card !== "radio"; press++) world.choose("right");
expect(world.pokegear?.card).toBe("radio");
for (let press = 0; press < 80 && world.pokegear?.radio?.frequency !== frequency; press++) {
world.bufferScreenButton(press < 60 ? "up" : "down");
world.tick();
}
expect(world.pokegear?.radio?.frequency).toBe(frequency);
for (let tick = 0; tick < 30; tick++) world.tick();
world.bufferScreenButton("b");
for (let tick = 0; tick < 30 && world.pokegear; tick++) world.tick();
if (world.dialogue?.choice?.kind === "menu") world.choose(-1);
for (let tick = 0; tick < 30 && world.busy; tick++) world.tick();
};
/** Push Strength boulder `id` one tile in `direction` from its far
* side: the first bump only turns the player, a boulder Strength is not
* yet active for asks first (AskStrengthScript, then YES), and the