Files
Crystal/tests/reference-revisions.spec.ts
kawaandClaude Opus 5.5 d3e3faff72
Checks / web (push) Failing after 19s
Private ROM checks / private-rom (push) Failing after 27m7s
Measure, weigh and name the European releases' own way (#84)
The French, German, Italian and Spanish releases store Magikarp's
length in millimetres (and print a tenth of them), start the Magikarp
house's record at their own length and holder, keep Pokédex heights in
decimetres and weights in hectograms (printed in metres and kilograms),
read the Heavy Ball's weight byte without converting it, and place the
enemy's nickname before EnemyText (French, Italian) or the trainer's
name before the class (French, Italian, Spanish).

map-revision --symbols writes each release's symbol file, so the
reference specs run against a release; specs that pin English data skip
there.

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

95 lines
5.0 KiB
TypeScript

import { test, expect } from "@playwright/test";
import { existsSync } from "node:fs";
import { romPath, symbolsPath, loadSymbols, openReference, revisionRoms } from "./reference-fixture";
test.skip(!existsSync(romPath) || !existsSync(symbolsPath), "Needs private ROM/reference.");
// The English cartridges share 1.1's symbols; not run with another's.
test.skip(Boolean(process.env.CRYSTAL_SYMBOLS_PATH), "Compares the English cartridges.");
/** BattleTowerText on each English cartridge, with PrintTextboxTextAt
* patched to keep the text pointer: for every BattleTowerTrainers entry,
* several hRandomAdd values and each of the three texts, the pointer matches
* battleTowerText's. Revision 1.0 indexes BTTrainerClassGenders with the
* name's sixth character instead of the class. */
for (const [revision, rom] of [["1.1", romPath], ["1.0", revisionRoms["1.0"]], ["au", revisionRoms.au]] as const)
test(`BattleTowerText matches the ${revision} cartridge`, async ({ page }) => {
test.skip(!existsSync(rom), `Needs PRET's ${revision} rebuild.`);
test.setTimeout(120_000);
const { at, routine } = loadSymbols();
const a = {
romBank: at("hROMBank"),
program: at("wOverworldMapBlocks"),
stackTop: at("wStackTop"),
randomAdd: at("hRandomAdd"),
name: routine("wBT_OTName"),
textIndex: routine("wBT_TrainerTextIndex"),
};
const routines = { text: routine("BattleTowerText"), print: routine("PrintTextboxTextAt") };
await openReference(page, rom);
const result = await page.evaluate(
async ({ a, routines, revision }) => {
const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts");
const { CrystalAssets } = await import("/src/game/rom/assets.ts");
const { romRevision } = await import("/src/game/rom/revision.ts");
const { JOHTO } = await import("/src/game/rom/profile.ts");
const { battleTowerText, emptyBattleTowerState } = await import("/src/game/scripts/battle-tower.ts");
const rom = new Uint8Array(
await (document.querySelector("#reference-fixture") as HTMLInputElement).files![0].arrayBuffer(),
);
const assets = new CrystalAssets(rom);
const r = assets.reader;
// PrintTextboxTextAt: keep HL at program + $f0 and return.
const kept = a.program + 0xf0;
[0x7d, 0xea, kept & 255, kept >> 8, 0x7c, 0xea, (kept + 1) & 255, (kept + 1) >> 8, 0xc9].forEach(
(value, i) => (rom[routines.print.address + i] = value),
);
const reference = new BinjgbEmulator();
await reference.load(rom, document.createElement("canvas"));
reference.setMuted(true);
const read = (p: number) => reference.readMemory(p);
const write = (p: number, v: number) => reference.writeMemory(p, v & 255);
reference.advanceFrames(300);
const sentinel = a.program + 0xf8;
const failures: unknown[] = [];
let compared = 0;
for (let trainer = 0; trainer < 70; trainer++)
for (const roll of [0x00, 0x0e, 0x13, 0x1f, 0x9b, 0xff]) {
const bt = emptyBattleTowerState();
const entry = JOHTO.BattleTowerTrainers + trainer * 11;
bt.opponent = { trainer, name: "", trainerClass: r.byte(entry + 10), party: [] };
for (const which of [1, 2, 3] as const) {
write(0xff70, a.name.bank);
for (let i = 0; i < 11; i++) write(a.name.address + i, r.byte(entry + i));
write(0xff70, 1);
write(a.randomAdd, roll);
write(sentinel, 0);
const program = [
0xf3, 0x31, a.stackTop & 255, a.stackTop >> 8,
0x3e, routines.text.bank, 0xe0, a.romBank & 255, 0xea, 0, 0x20,
0x0e, which,
0xcd, routines.text.address & 255, routines.text.address >> 8,
0x3e, 0x42, 0xea, sentinel & 255, sentinel >> 8,
0x18, 0xfe,
];
program.forEach((v, i) => write(a.program + i, v));
reference.setProgramCounter(a.program);
for (let frame = 0; frame < 10 && read(sentinel) !== 0x42; frame++) reference.advanceFrames(1);
const original = read(kept) | (read(kept + 1) << 8);
// A flat offset in BattleTowerText's bank, as the CPU sees it.
const flat = battleTowerText({ reader: r, random: () => roll } as never, bt, which);
const native = r.bank(flat) === routines.text.bank ? (flat & 0x3fff) | 0x4000 : -flat;
compared++;
if (read(sentinel) !== 0x42 || original !== native)
failures.push({ trainer, roll, which, original: original.toString(16), native: native.toString(16) });
}
}
reference.dispose();
return { revision: romRevision(r), compared, failures: failures.slice(0, 8), failed: failures.length };
},
{ a, routines, revision },
);
expect(result.revision).toBe(revision);
expect(result.failures, JSON.stringify(result)).toEqual([]);
expect(result.compared).toBe(70 * 6 * 3);
});