Files
Crystal/tests/azalea.test.ts
T
kawaandClaude Opus 5.5 f4ef244fc7 Open Route 32 to Route 34, the Ruins of Alph and Azalea Town (#32)
Enable Route 32 and its Pokémon Center, the Ruins of Alph with the four
chambers, item and word rooms, inner chamber and research center, Union
Cave, Route 33, Azalea Town with Kurt's house, the kiln and Bugsy's gym,
the Slowpoke Well, Ilex Forest and Route 34.

Port what their scripts need: saved WRAM bytes by label (readmem,
writemem, loadmem: wFarfetchdPosition, wKurtApricornQuantity),
VAR_UNOWNCOUNT and VAR_KURT_APRICORNS, SelectApricornForKurt and
verbosegiveitemvar, Unown letters (GetUnownLetter, per-letter pictures,
CheckUnownLetter's redraw, UpdateUnownDex, wFirstUnownSeen), the chamber
puzzles (_UnownPuzzle), DisplayUnownWords, HoOhChamber and
OmanyteChamber.

Minigames and specials that draw a whole screen in tiles now share a
tile screen effect with ROM graphics, palettes and sprites, answered
with buttons; the standard font and textbox frame load for them.

The playthrough continues through Kurt's Slowpoke Well, Bugsy, the rival
and the Farfetch'd to the Route 34 gate; its helpers move to
tests/playthrough-driver.ts. New tests solve the Kabuto puzzle through
the world, catch Unown and make Kurt's balls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 15:35:27 +02:00

129 lines
5.2 KiB
TypeScript

import { 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 { settle } from "./world-driver";
const romPath =
process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
describe.skipIf(!existsSync(romPath))("Azalea Town with the local ROM", () => {
const bytes = existsSync(romPath) ? new Uint8Array(readFileSync(romPath)) : new Uint8Array();
const KURTS_HOUSE = "8:4";
const EVENT_CLEARED_SLOWPOKE_WELL = 0x2b,
EVENT_KURT_GAVE_YOU_LURE_BALL = 0x35,
EVENT_GAVE_KURT_RED_APRICORN = 0x258,
ENGINE_KURT_MAKING_BALLS = 80;
const RED_APRICORN = 0x55,
BLU_APRICORN = 0x56,
LEVEL_BALL = 0x9f;
const world = (day: number) => {
let seed = 9;
const game = new GameWorld(
new CrystalAssets(bytes),
() => new Date(`2026-09-${day}T12:00:00`),
() => {
seed = (seed * 1103515245 + 12345) >>> 0;
return (seed >>> 16) & 255;
},
);
game.state.party.push(
generatePartyMon(game.assets.pokemon, {
species: 155, level: 20, item: 0, otId: game.playerId, ot: "CHRIS", dvs: 0x8888, caughtData: 0,
}),
);
return game;
};
/** Answer menus and quantities in order, collecting the pages shown. */
const answer = (game: GameWorld, answers: number[]) => {
const pages: string[] = [];
for (let tick = 0; tick < 5000; tick++) {
const dialogue = game.dialogue;
if (dialogue) {
const page = dialogue.pages[dialogue.index];
if (page && pages.at(-1) !== page) pages.push(page.replace(/\n/g, " "));
if (dialogue.index === dialogue.pages.length - 1 && dialogue.choice) {
const choice = dialogue.choice;
if (choice.kind === "martquantity") {
pages.push(choice.prompt.replace(/\n/g, " "));
const wanted = answers.shift()!;
while (choice.selected < wanted) game.moveCursor("up");
game.choose(choice.selected);
} else game.choose(answers.shift() ?? 0);
} else game.advanceDialogue();
continue;
}
if (!game.busy) break;
game.tick();
}
expect(game.notice, game.diagnostic).toBe("");
return pages.join(" ");
};
it("makes Level Balls from Red Apricorns by the next day", () => {
const game = world(22);
game.state.events.add(EVENT_CLEARED_SLOWPOKE_WELL);
game.state.events.add(EVENT_KURT_GAVE_YOU_LURE_BALL);
game.state.bag.items = [
{ item: BLU_APRICORN, quantity: 1 },
{ item: RED_APRICORN, quantity: 4 },
];
game.restore({ ...game.save(), map: KURTS_HOUSE, x: 3, y: 3, direction: "up" });
settle(game);
game.interact();
// The list follows ApricornBalls: RED before BLU. Three of four.
let pages = answer(game, [0, 3]);
expect(pages).toContain("Which APRICORN");
expect(pages).toContain("How many");
expect(game.state.events.has(EVENT_GAVE_KURT_RED_APRICORN)).toBe(true);
expect(game.state.flags.has(ENGINE_KURT_MAKING_BALLS)).toBe(true);
expect(game.state.memory.wKurtApricornQuantity).toBe(3);
expect(game.state.bag.items).toEqual([
{ item: BLU_APRICORN, quantity: 1 },
{ item: RED_APRICORN, quantity: 1 },
]);
// The next day, ENGINE_KURT_MAKING_BALLS is cleared and the balls are
// ready.
const next = world(23);
next.restore(JSON.parse(JSON.stringify(game.save())));
settle(next);
expect(next.state.flags.has(ENGINE_KURT_MAKING_BALLS)).toBe(false);
// EVENT_TEMPORARY_UNTIL_MAP_RELOAD_1 clears when the house is entered
// again (ResetMapBufferEventFlags).
next.warpTo(KURTS_HOUSE, 3, 3);
settle(next);
next.move("up"); // Kurt: this only turns the player
next.interact();
pages = answer(next, [1]);
expect(next.state.bag.balls).toContainEqual({ item: LEVEL_BALL, quantity: 3 });
expect(next.state.events.has(EVENT_GAVE_KURT_RED_APRICORN)).toBe(false);
});
it("returns to the apricorn list when the quantity is cancelled", () => {
const game = world(22);
game.state.events.add(EVENT_CLEARED_SLOWPOKE_WELL);
game.state.events.add(EVENT_KURT_GAVE_YOU_LURE_BALL);
game.state.bag.items = [{ item: RED_APRICORN, quantity: 2 }];
game.restore({ ...game.save(), map: KURTS_HOUSE, x: 3, y: 3, direction: "up" });
settle(game);
game.interact();
// RED, cancel the quantity, then CANCEL the list.
for (let tick = 0; tick < 200 && game.dialogue?.choice?.kind !== "menu"; tick++)
if (game.dialogue && !game.dialogue.choice) game.advanceDialogue();
else game.tick();
game.choose(0);
expect(game.dialogue?.choice?.kind).toBe("martquantity");
game.choose(-1);
for (let tick = 0; tick < 200 && game.dialogue?.choice?.kind !== "menu"; tick++)
if (game.dialogue && !game.dialogue.choice) game.advanceDialogue();
else game.tick();
expect(game.dialogue?.choice?.kind).toBe("menu");
game.choose(1);
const pages = answer(game, []);
expect(pages).toContain("letdown");
expect(game.state.bag.items).toEqual([{ item: RED_APRICORN, quantity: 2 }]);
});
});