Files
Crystal/tests/atlas.test.ts
T
kawaandClaude Opus 5.5 0d8ff442ce Load the atlas and Pokédex test data only when the suite runs (#66)
describe.skipIf still runs the describe body to collect tests, so the
ROM and PRET reads at describe level failed the synthetic CI job without
a ROM (runs 123–133). The reads now happen in beforeAll, as in the other
ROM suites.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 09:53:10 +02:00

139 lines
5.9 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_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: false, flypoint: { visited: false } });
// Kanto stays closed, so 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: false });
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();
});
});