Open Tin Tower and Whirl Islands: Suicune, Ho-Oh and Lugia (#32)

Tin Tower's floors and roof and the Whirl Islands are open. The Clear Bell
gets the player past the sages to Suicune in Tin Tower; after the League,
the Rainbow Wing leads up to Ho-Oh, and the Silver Wing to Lugia.

New specials: MonCheck and BeastsCheck (CheckOwnMonAnywhere, with its
five-letter OT comparison and no Day-Care search).

The playthrough driver plans ledge jumps for the later stages, waterfall
descents, warp panels and Surf between warps; the run gains two stages,
Routes 45 and 46 back to Ecruteak and Tin Tower's Suicune.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
kawaandClaude Opus 5.5 committed 2026-10-13 14:47:45 +02:00
1 parent 9c3579937c
commit 3fbc6798eb
10 files changed
+369 -21

No files matched your search

+35 -11
View File
@@ -36,6 +36,9 @@ export class Playthrough {
readonly log: string[] = [];
/** A wild species to throw Poké Balls at instead of fighting. */
wanted = 0;
/** Plan ledge jumps (Tin Tower, Route 45). Off for the earlier stages,
* whose seeded runs were recorded walking around ledges. */
ledges = false;
seed = 0x2a;
private stage = 0;
readonly now = () => new Date("2026-09-22T12:00:00");
@@ -227,10 +230,11 @@ export class Playthrough {
if (world.x === x && world.y === y) return;
// A surfer crosses water until it steps ashore.
const water = world.state.playerState === PLAYER_SURF;
for (const direction of findPath(world, x, y, { water })) {
for (const direction of findPath(world, x, y, { water, ledges: this.ledges })) {
const end = landing(world, world.x, world.y, direction, {
water,
target: { x, y },
ledges: this.ledges,
});
if (!world.move(direction)) {
for (let tick = 0; tick < 64; tick++) world.tick();
@@ -255,7 +259,11 @@ export class Playthrough {
this.play();
expect(world.mapId, `sailing to ${x},${y}`).toBe(map);
if (world.x === x && world.y === y) return;
for (const direction of findPath(world, x, y, { water: true, whirlpools })) {
for (const direction of findPath(world, x, y, {
water: true,
whirlpools,
ledges: this.ledges,
})) {
const [dx, dy] = STEP[direction];
const from = [world.x, world.y];
const ahead = world.assets.collision(world.map, from[0] + dx, from[1] + dy);
@@ -298,8 +306,10 @@ export class Playthrough {
? [dx < 0 ? 0 : world.map.width - 1, i]
: [i, dy < 0 ? 0 : world.map.height - 1],
).flatMap(([x, y]) => {
// A warp on the edge (a gate's doorway) is not the connection.
if (world.map.warps.some((warp) => warp.x === x && warp.y === y)) return [];
try {
return [{ x, y, length: findPath(world, x, y, { water }).length }];
return [{ x, y, length: findPath(world, x, y, { water, ledges: this.ledges }).length }];
} catch {
return [];
}
@@ -426,7 +436,7 @@ export class Playthrough {
const world = this.world;
for (const warp of world.map.warps.filter((warp) => warp.targetMap === target)) {
try {
findPath(world, warp.x, warp.y);
findPath(world, warp.x, warp.y, { ledges: this.ledges });
} catch {
continue;
}
@@ -452,7 +462,7 @@ export class Playthrough {
const reachable = () => {
if (!goal) return true;
try {
findPath(world, goal.x, goal.y);
findPath(world, goal.x, goal.y, { ledges: this.ledges });
return true;
} catch {
return false;
@@ -464,7 +474,7 @@ export class Playthrough {
for (const warp of world.map.warps) {
if (!maps.includes(warp.targetMap) && warp.targetMap !== target) continue;
try {
findPath(world, warp.x, warp.y);
findPath(world, warp.x, warp.y, { ledges: this.ledges });
} catch {
continue;
}
@@ -480,23 +490,37 @@ export class Playthrough {
};
/** The fewest warps (stairs, holes, doors) among `maps` to `target`:
* breadth first over the places each warp lands, so a maze of floors
* whose holes drop into cut-off corners is solved without looping. */
travel = (target: string, maps: string[]) => {
* whose holes drop into cut-off corners is solved without looping.
* `water` lets a player with Surf and Whirlpool sail to the warps. */
travel = (target: string, maps: string[], { water = false } = {}) => {
const world = this.world;
const queue = [world.save()];
const seen = new Set([`${world.mapId} ${world.x},${world.y}`]);
for (let i = 0; i < queue.length && i < 200; i++) {
for (let i = 0; i < queue.length && i < 1000; i++) {
world.restore(queue[i]);
const from = world.mapId;
for (const warp of world.map.warps) {
if (!maps.includes(warp.targetMap) && warp.targetMap !== target) continue;
world.restore(queue[i]);
try {
findPath(world, warp.x, warp.y);
findPath(world, warp.x, warp.y, { water, whirlpools: water, ledges: this.ledges });
} catch {
continue;
}
this.walk(warp.x, warp.y);
// A surfer reaches a cave's stairs over water, then steps on.
if (water) {
try {
this.sail(warp.x, warp.y, { whirlpools: true });
} catch {
if (world.mapId === from) continue;
}
} else
try {
this.walk(warp.x, warp.y);
} catch {
// A warp panel on the way can land elsewhere on the same map.
if (world.mapId === from) continue;
}
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) continue;