Add scripted trainer battle foundation and native move AI
Checks / web (push) Canceled after 0s

This commit is contained in:
Sébastien QUEROL committed 2026-09-24 09:33:33 +02:00
1 parent 6a74f59e46
commit 3e92936cb9
20 files changed
+1417 -32

No files matched your search

+120
View File
@@ -0,0 +1,120 @@
import { describe, expect, it } from "vitest";
import { existsSync, readFileSync } from "node:fs";
import { TrainerData } from "../src/game/rom/trainers";
import { RomReader } from "../src/game/rom/reader";
import { CrystalAssets } from "../src/game/rom/assets";
import { NUM_TRAINER_CLASSES } from "../src/game/rom/profile";
const romPath =
process.env.CRYSTAL_ROM_PATH ??
"Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc";
describe.skipIf(!existsSync(romPath))("ROM trainer decoder", () => {
const bytes = existsSync(romPath)
? new Uint8Array(readFileSync(romPath))
: new Uint8Array();
const data = new TrainerData(new RomReader(bytes));
it("reads Joey and Mikey, including class DVs and actual payout", () => {
const joey = data.trainer(22, 1),
mikey = data.trainer(22, 2);
expect(joey).toMatchObject({
name: "JOEY",
className: "YOUNGSTER",
dvs: 0x9888,
reward: 64,
partyType: 0,
});
expect(joey.party).toEqual([{ species: 19, level: 4, item: 0 }]);
expect(mikey.party.map((mon) => [mon.species, mon.level])).toEqual([
[16, 2],
[19, 4],
]);
expect(data.createParty(joey)[0]).toMatchObject({
moves: [33, 39, 0, 0],
dvs: 0x9888,
statExp: [0, 0, 0, 0, 0],
});
});
it("reads Falkner's explicit moves and the rival starter variants", () => {
const falkner = data.trainer(1, 1);
expect(falkner).toMatchObject({
name: "FALKNER",
partyType: 1,
dvs: 0x9a77,
reward: 900,
});
const party = data.createParty(falkner, { ot: "CHRIS", otId: 123 });
expect(party.map((mon) => mon.moves)).toEqual([
[33, 189, 0, 0],
[33, 189, 16, 0],
]);
expect(party[0].pp.slice(0, 2)).toEqual([35, 10]);
expect(
party.every((mon) => mon.hp === mon.stats[0] && mon.otId === 123),
).toBe(true);
expect([1, 2, 3].map((id) => data.trainer(9, id).party[0].species)).toEqual(
[152, 155, 158],
);
expect(data.trainer(9, 1).dvs).toBe(0xdddd);
});
it("reads held items with and without explicit moves", () => {
expect(data.trainer(59, 1).partyType).toBe(2);
const gaven = data.trainer(27, 7);
expect(gaven.partyType).toBe(3);
expect(gaven.party[1].item).toBe(82);
expect(data.createParty(gaven)[1].moves).toEqual(gaven.party[1].moves);
});
it.skipIf(!existsSync(".reference/pokecrystal/data/trainers/parties.asm"))(
"sweeps every party listed in the pinned source",
() => {
const source = readFileSync(
".reference/pokecrystal/data/trainers/parties.asm",
"utf8",
);
const groups = source.split(/^\w+Group:\s*$/m).slice(1);
expect(groups).toHaveLength(NUM_TRAINER_CLASSES);
const formats = new Set<number>();
let total = 0;
groups.forEach((group, index) => {
const count = [...group.matchAll(/db -1 ; end/g)].length;
for (let id = 1; id <= count; id++) {
const trainer = data.trainer(index + 1, id);
formats.add(trainer.partyType);
total++;
if (index + 1 === 12 && id === 2) {
expect(() => data.createParty(trainer)).toThrow(/Mystery Gift/);
continue;
}
const party = data.createParty(trainer);
expect(party.length).toBeGreaterThan(0);
expect(
party.every((mon) => mon.hp > 0 && mon.hp === mon.stats[0]),
).toBe(true);
}
expect(() => data.trainer(index + 1, count + 1)).toThrow();
});
expect(formats).toEqual(new Set([0, 1, 2, 3]));
expect(total).toBeGreaterThan(500);
},
);
it("reads map trainer headers and vision range", () => {
const assets = new CrystalAssets(bytes);
const route = assets.map("26:1");
const trainers = route.objects.filter((object) => object.kind === 2);
expect(trainers.length).toBeGreaterThan(0);
for (const object of trainers) {
expect(object.sightRange).toBeGreaterThan(0);
const header = data.header(object.script);
expect(header.seenText).toBeGreaterThan(0);
expect(header.beatenText).toBeGreaterThan(0);
expect(
data.trainer(header.classId, header.id).party.length,
).toBeGreaterThan(0);
}
});
it("rejects invalid class and trainer IDs", () => {
for (const classId of [0, -1, 68, 1.5])
expect(() => data.trainer(classId, 1)).toThrow();
for (const id of [0, -1, 256, 1.5, 2])
expect(() => data.trainer(1, id)).toThrow();
});
});