Files
Crystal/tests/revisions.test.ts
T
kawaandClaude Opus 5.5 3c480a28f0
Checks / web (push) Failing after 19s
Private ROM checks / private-rom (push) Failing after 25m36s
Read the English 1.0 and Australian cartridges besides 1.1 (#84)
PRET's crystal and crystal_au builds reproduce both retail cartridges,
and every label the profile pins keeps its address in all three;
check:reference now verifies that when the rebuilds are present. The
revision comes from the header. Revision 1.0's Battle Tower draws only
the first 21 trainers and picks its texts' gender from the name's sixth
character, checked against each cartridge. A native backup imports
across the three, which share every map, event and object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:12:00 +02:00

55 lines
2.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { existsSync, readFileSync } from "node:fs";
import { CrystalAssets } from "../src/game/rom/assets";
import { romRevision } from "../src/game/rom/revision";
import { CRYSTAL_ROMS, validateRom } from "../src/rom/validate";
import { emptyBattleTowerState, loadOpponent } from "../src/game/scripts/battle-tower";
import type { SpecialContext } from "../src/game/scripts/special-context";
/** The user's 1.1 cartridge and PRET's rebuilds of the other English ones
* (`make crystal crystal_au` in .reference/pokecrystal), each skipped when
* absent. */
const cartridges = [
{ revision: "1.1", path: process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc", rom: CRYSTAL_ROMS[0] },
{ revision: "1.0", path: ".reference/pokecrystal/pokecrystal.gbc", rom: CRYSTAL_ROMS[1] },
{ revision: "au", path: ".reference/pokecrystal/pokecrystal_au.gbc", rom: CRYSTAL_ROMS[2] },
] as const;
for (const { revision, path, rom } of cartridges)
describe.skipIf(!existsSync(path))(`the ${revision} cartridge`, () => {
const bytes = existsSync(path) ? new Uint8Array(readFileSync(path)) : new Uint8Array();
it("is accepted under its name and recognised from its header", async () => {
// CRYSTAL_ROM_PATH may point the 1.1 entry at another cartridge.
const assets = new CrystalAssets(bytes);
const own = cartridges.find((c) => c.revision === romRevision(assets.reader))!;
expect(await validateRom(new File([bytes], "crystal.gbc"))).toMatchObject({ sha1: own.rom.sha1, name: own.rom.name });
if (revision !== "1.1" || !process.env.CRYSTAL_ROM_PATH) expect(own.revision).toBe(revision);
// One changed byte is a modified ROM.
const changed = bytes.slice();
changed[0x7fff] ^= 1;
await expect(validateRom(new File([changed], "crystal.gbc"))).rejects.toThrow("not a supported English");
});
it("draws Battle Tower trainers from all 70, or from the first 21 on 1.0", () => {
const assets = new CrystalAssets(bytes);
let seed = 84;
const context = {
reader: assets.reader,
data: assets.pokemon,
random: () => {
seed = (seed * 1103515245 + 12345) >>> 0;
return (seed >>> 16) & 255;
},
text: () => ({ player: "KAWA", rival: "SILVER", mom: "MOM", strings: {}, weekday: "SUNDAY" }),
} as unknown as SpecialContext;
const drawn = new Set<number>();
for (let draw = 0; draw < 2000; draw++) {
const bt = { ...emptyBattleTowerState(), levelGroup: 1 + (draw % 10) };
drawn.add(loadOpponent(context, bt).trainer);
}
const limit = romRevision(assets.reader) === "1.0" ? 21 : 70;
expect(Math.max(...drawn)).toBe(limit - 1);
expect(drawn.size).toBe(limit);
});
});