Files
Crystal/tests/atlas.test.ts
T
kawaandClaude Opus 5.5 d3be732118
Checks / web (push) Failing after 18s
Private ROM checks / private-rom (push) Failing after 25m40s
Open Kanto: the S.S. Aqua, the Kanto gyms, the Magnet Train, Mt. Silver and Red (Closes #33)
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>
2026-10-17 15:53:57 +02:00

143 lines
6.1 KiB
TypeScript

import { beforeAll, describe, expect, it } from "vitest";
import { existsSync, readFileSync } from "node:fs";
import { CrystalAssets } from "../src/game/rom/assets";
import { GameWorld } from "../src/game/world/world";
import { generatePartyMon } from "../src/game/party/pokemon";
import { flypoints, landmarkPosition, regionAtlas } from "../src/game/world/atlas";
import { defaultTextContext } from "../src/game/scripts/text";
import { OW } from "../src/game/world/constants";
const romPath =
process.env.CRYSTAL_ROM_PATH ??
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
const pret = ".reference/pokecrystal";
const NEW_BARK_TOWN = "24:4",
LANDMARK_NEW_BARK_TOWN = 1,
LANDMARK_ROUTE_29 = 2,
LANDMARK_CHERRYGROVE_CITY = 3,
LANDMARK_VIOLET_CITY = 6,
LANDMARK_AZALEA_TOWN = 12,
LANDMARK_BLACKTHORN_CITY = 0x29,
LANDMARK_SILVER_CAVE = 0x2e,
LANDMARK_PALLET_TOWN = 47;
describe.skipIf(!existsSync(romPath) || !existsSync(`${pret}/data/maps/landmarks.asm`))(
"the atlas data against the pinned PRET source",
() => {
let assets!: CrystalAssets;
beforeAll(() => {
assets = new CrystalAssets(new Uint8Array(readFileSync(romPath)));
});
const read = (path: string) => readFileSync(`${pret}/${path}`, "utf8");
// const_def counts from 0, or from the value given.
const constants = (path: string, prefix: string) => {
const values = new Map<string, number>();
let next = 0;
for (const [, start, name] of read(path).matchAll(/^\s+(?:const_def(?: (\d+))?|const (\w+))/gm)) {
if (name === undefined) next = Number(start ?? 0);
else values.set(name, next++);
}
return new Map([...values].filter(([name]) => name.startsWith(prefix)));
};
it("places every landmark where data/maps/landmarks.asm does", () => {
const rows = [...read("data/maps/landmarks.asm").matchAll(/^\s+landmark\s+(-?\d+),\s*(-?\d+),/gm)];
expect(rows).toHaveLength(OW.LANDMARK_FAST_SHIP + 1);
rows.forEach(([, x, y], landmark) =>
expect(landmarkPosition(assets.reader, landmark), String(landmark)).toEqual({
x: Number(x),
y: Number(y),
}),
);
});
it("reads the flypoints of data/maps/flypoints.asm", () => {
const landmarks = constants("constants/landmark_constants.asm", "LANDMARK_");
const spawns = constants("constants/map_data_constants.asm", "SPAWN_");
const rows = [...read("data/maps/flypoints.asm").matchAll(/^\s+db (LANDMARK_\w+),\s*(SPAWN_\w+)/gm)].map(
([, landmark, spawn]) => ({ landmark: landmarks.get(landmark), spawn: spawns.get(spawn) }),
);
expect(rows).toHaveLength(OW.NUM_FLYPOINTS);
expect(flypoints(assets.reader)).toEqual(rows);
});
},
);
describe.skipIf(!existsSync(romPath))("the region atlas", () => {
let assets!: CrystalAssets;
beforeAll(() => {
assets = new CrystalAssets(new Uint8Array(readFileSync(romPath)));
});
function fixture(visited: number[]) {
const world = new GameWorld(assets, () => new Date("2026-09-25T12:00:00"), () => 0x42);
world.state.party = [
generatePartyMon(assets.pokemon, {
species: 155, level: 8, item: 0, otId: world.playerId,
ot: "GOLD", dvs: 0x9abc, caughtData: 0,
}),
];
world.restore({ ...world.save(), map: NEW_BARK_TOWN, x: 13, y: 7, direction: "down", objects: [] });
for (const spawn of visited) world.state.flags.add(assets.spawnEngineFlag(spawn)!);
return world;
}
const spawnOf = (landmark: number) =>
flypoints(assets.reader).find((point) => point.landmark === landmark)!.spawn;
it("shows Johto with the player, the visited flypoints and the open areas", () => {
const world = fixture([spawnOf(LANDMARK_NEW_BARK_TOWN), spawnOf(LANDMARK_CHERRYGROVE_CITY)]);
const atlas = world.atlas();
expect(atlas).toMatchObject({ region: "johto", regions: ["johto"], player: LANDMARK_NEW_BARK_TOWN });
expect(atlas.landmarks.map((landmark) => landmark.landmark)).toEqual(
Array.from({ length: OW.JOHTO_LANDMARK_LAST }, (_, i) => i + 1),
);
const at = (landmark: number) => atlas.landmarks[landmark - 1];
expect(at(LANDMARK_NEW_BARK_TOWN)).toMatchObject({
name: "NEW BARK TOWN",
open: true,
flypoint: { visited: true },
});
expect(at(LANDMARK_CHERRYGROVE_CITY).flypoint?.visited).toBe(true);
expect(at(LANDMARK_VIOLET_CITY)).toMatchObject({ open: true, flypoint: { visited: false } });
expect(at(LANDMARK_ROUTE_29)).toMatchObject({ open: true });
expect(at(LANDMARK_ROUTE_29).flypoint).toBeUndefined();
expect(at(LANDMARK_AZALEA_TOWN)).toMatchObject({ open: true, flypoint: { visited: false } });
expect(at(LANDMARK_BLACKTHORN_CITY)).toMatchObject({ open: true, flypoint: { visited: false } });
expect(at(LANDMARK_SILVER_CAVE)).toMatchObject({ open: true, flypoint: { visited: false } });
// Before Indigo Plateau or a step into Kanto, asking for it shows Johto.
expect(world.atlas("kanto").region).toBe("johto");
});
it("opens Kanto once Indigo Plateau is visited, without the player", () => {
const world = fixture([OW.SPAWN_INDIGO]);
expect(world.atlas().regions).toEqual(["johto", "kanto"]);
const kanto = world.atlas("kanto");
expect(kanto.region).toBe("kanto");
expect(kanto.player).toBeUndefined();
expect(kanto.landmarks[0]).toMatchObject({ landmark: LANDMARK_PALLET_TOWN, open: true });
expect(kanto.landmarks).toHaveLength(OW.KANTO_LANDMARK_LAST - OW.KANTO_LANDMARK + 1);
});
it("starts on Kanto when the player stands there", () => {
const atlas = regionAtlas({
reader: assets.reader,
text: () => defaultTextContext("GOLD"),
landmark: LANDMARK_PALLET_TOWN,
visited: () => false,
open: new Set(),
});
expect(atlas).toMatchObject({ region: "kanto", regions: ["johto", "kanto"], player: LANDMARK_PALLET_TOWN });
});
it("leaves the Fast Ship's landmark outside both regions", () => {
const ship = regionAtlas({
reader: assets.reader,
text: () => defaultTextContext("GOLD"),
landmark: OW.LANDMARK_FAST_SHIP,
visited: () => false,
open: new Set(),
});
expect(ship.region).toBe("johto");
expect(ship.player).toBeUndefined();
});
});