Breed at the Day-Care, hatch Eggs, draw MAIL stationery and browse the Hall of Fame (Closes #29)
- Day-Care and breeding: deposits, withdrawal pricing and growth, compatibility, egg species, DVs and moves, DayCareStep, egg collection and the Odd Egg, compared with the cartridge's routines. - CountStep: walking happiness, DoEggStep, DayCareStep and DoBikeStep in the original's order; HatchEggs with the ROM's hatch animation, compared frame by frame with the original. - MAIL: all ten stationery types and the Portrait MAIL picture, compared with the cartridge's tilemaps, graphics and palettes. - Printing: Bill's PC boxes, Pokedex PRNT, MAIL and the Photo Studio take the original's absent-printer path. - Hall of Fame records (saved apart from the League flag, ready for the League to call) and the Hall of Fame PC, compared with the cartridge's _HallOfFamePC on 255 pages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7d03f5c5ca
commit
e49bc6de9a
49 files changed
+6028
-88
No files matched your search
@@ -0,0 +1,816 @@
|
||||
import { test, expect, type Page } from "@playwright/test";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { CrystalAssets } from "../src/game/rom/assets";
|
||||
import {
|
||||
breedLevelGrowth,
|
||||
breedingCompatibility,
|
||||
dayCareStep,
|
||||
emptyDayCare,
|
||||
initBreeding,
|
||||
oddEgg,
|
||||
retrievedMon,
|
||||
} from "../src/game/party/day-care";
|
||||
import { ENGINE_FLAGS, JOHTO } from "../src/game/rom/profile";
|
||||
import type { BoxMon } from "../src/game/party/storage";
|
||||
import {
|
||||
romPath,
|
||||
symbolsPath,
|
||||
loadSymbols,
|
||||
openReference,
|
||||
} from "./reference-fixture";
|
||||
|
||||
/** The original Day-Care and breeding routines, executed in the reference
|
||||
* emulator on random inputs, against the native ones. Only Random is
|
||||
* replaced (a stub that replays fixed bytes, like reference-smart-ai), so
|
||||
* every other instruction, table and bank of the cartridge runs. */
|
||||
|
||||
const DITTO = 132;
|
||||
const BOX_STRUCT = 32,
|
||||
PARTY_STRUCT = 48,
|
||||
NAME = 11;
|
||||
|
||||
/** What one run of a routine in the emulator is asked and returns. */
|
||||
interface Job {
|
||||
routine: { bank: number; address: number };
|
||||
random: { address: number; bytesPerCall: 1 | 2 };
|
||||
stream: number[];
|
||||
hROMBank: number;
|
||||
hRandomAdd: number;
|
||||
hRandomSub: number;
|
||||
/** Registers b, d and e after the call are saved at $c0f1 to $c0f3. */
|
||||
registers?: boolean;
|
||||
/** Memory to set before each case (once, shared). */
|
||||
globals: [number, number][];
|
||||
cases: { writes: [number, number][]; reads: [number, number][] }[];
|
||||
}
|
||||
interface Run {
|
||||
returned: boolean;
|
||||
draws: number;
|
||||
registers: number[];
|
||||
reads: number[][];
|
||||
}
|
||||
|
||||
async function runReference(page: Page, job: Job): Promise<Run[]> {
|
||||
return page.evaluate(async (job) => {
|
||||
const { BinjgbEmulator } = await import("/src/emulator/binjgb.ts");
|
||||
const file = (
|
||||
document.querySelector("#reference-fixture") as HTMLInputElement
|
||||
).files![0];
|
||||
const rom = new Uint8Array(await file.arrayBuffer());
|
||||
// Random becomes JP $c080, to a stub that reads the stream at $c100
|
||||
// with the counter at $c0e0: hRandomSub (the returned byte) and
|
||||
// hRandomAdd come from one byte, or from two when asked.
|
||||
rom.set([0xc3, 0x80, 0xc0], job.random.address);
|
||||
const reference = new BinjgbEmulator();
|
||||
await reference.load(rom, document.createElement("canvas"));
|
||||
const write = (address: number, value: number) =>
|
||||
reference.writeMemory(address, value & 0xff);
|
||||
const stub = [
|
||||
0xe5, // push hl
|
||||
0xd5, // push de
|
||||
0xfa,
|
||||
0xe0,
|
||||
0xc0, // ld a, [$c0e0]
|
||||
0x5f, // ld e, a
|
||||
0x16,
|
||||
0x00, // ld d, 0
|
||||
0x21,
|
||||
0x00,
|
||||
0xc1, // ld hl, $c100
|
||||
0x19, // add hl, de
|
||||
0xc6,
|
||||
job.random.bytesPerCall, // add a, n
|
||||
0xea,
|
||||
0xe0,
|
||||
0xc0, // ld [$c0e0], a
|
||||
0x2a, // ld a, [hli]
|
||||
0xe0,
|
||||
job.hRandomSub & 0xff, // ldh [hRandomSub], a
|
||||
...(job.random.bytesPerCall === 2
|
||||
? [0x7e, 0xe0, job.hRandomAdd & 0xff, 0xf0, job.hRandomSub & 0xff]
|
||||
: [0xe0, job.hRandomAdd & 0xff]),
|
||||
0xd1, // pop de
|
||||
0xe1, // pop hl
|
||||
0xc9, // ret
|
||||
];
|
||||
stub.forEach((value, index) => write(0xc080 + index, value));
|
||||
const { bank, address } = job.routine;
|
||||
// DI; SP = $c7fe; select the bank; CALL routine; [save b, d, e];
|
||||
// sentinel; loop.
|
||||
const program = [
|
||||
0xf3,
|
||||
0x31,
|
||||
0xfe,
|
||||
0xc7,
|
||||
0x3e,
|
||||
bank,
|
||||
0xe0,
|
||||
job.hROMBank & 0xff,
|
||||
0xea,
|
||||
0x00,
|
||||
0x20,
|
||||
0xcd,
|
||||
address & 0xff,
|
||||
address >> 8,
|
||||
...(job.registers
|
||||
? [
|
||||
0x78, 0xea, 0xf1, 0xc0, 0x7a, 0xea, 0xf2, 0xc0, 0x7b, 0xea, 0xf3,
|
||||
0xc0,
|
||||
]
|
||||
: []),
|
||||
0x3e,
|
||||
0x42,
|
||||
0xea,
|
||||
0xf0,
|
||||
0xc0,
|
||||
0x18,
|
||||
0xfe,
|
||||
];
|
||||
write(0xff70, 1);
|
||||
for (const [address, value] of job.globals) write(address, value);
|
||||
const runs: Run[] = [];
|
||||
for (const input of job.cases) {
|
||||
write(0xc0e0, 0);
|
||||
write(0xc0f0, 0);
|
||||
job.stream.forEach((value, index) => write(0xc100 + index, value));
|
||||
for (const [address, value] of input.writes) write(address, value);
|
||||
program.forEach((value, index) => write(0xc000 + index, value));
|
||||
reference.setProgramCounter(0xc000);
|
||||
// CalcLevel through level 100 needs more than 20 frames.
|
||||
for (
|
||||
let frame = 0;
|
||||
frame < 120 && reference.readMemory(0xc0f0) !== 0x42;
|
||||
frame++
|
||||
)
|
||||
reference.advanceFrames(1);
|
||||
runs.push({
|
||||
returned: reference.readMemory(0xc0f0) === 0x42,
|
||||
draws: reference.readMemory(0xc0e0),
|
||||
registers: [0xc0f1, 0xc0f2, 0xc0f3].map((address) =>
|
||||
reference.readMemory(address),
|
||||
),
|
||||
reads: input.reads.map(([address, length]) =>
|
||||
Array.from({ length }, (_, i) => reference.readMemory(address + i)),
|
||||
),
|
||||
});
|
||||
}
|
||||
reference.dispose();
|
||||
return runs;
|
||||
}, job);
|
||||
}
|
||||
|
||||
test.describe("Day-Care routines against the cartridge", () => {
|
||||
test.skip(
|
||||
!existsSync(romPath) || !existsSync(symbolsPath),
|
||||
"Needs the local ROM and the private PRET rebuild.",
|
||||
);
|
||||
test.setTimeout(240_000);
|
||||
|
||||
const symbols = existsSync(symbolsPath) ? loadSymbols() : undefined;
|
||||
const assets = existsSync(romPath)
|
||||
? new CrystalAssets(new Uint8Array(readFileSync(romPath)))
|
||||
: undefined;
|
||||
const data = () => assets!.pokemon;
|
||||
const at = (name: string) => symbols!.at(name);
|
||||
const routine = (name: string) => symbols!.routine(name);
|
||||
const job = (
|
||||
name: string,
|
||||
cases: Job["cases"],
|
||||
stream: number[],
|
||||
extra: Partial<Job> = {},
|
||||
): Job => ({
|
||||
routine: routine(name),
|
||||
random: { address: routine("Random").address, bytesPerCall: 1 },
|
||||
stream,
|
||||
hROMBank: at("hROMBank"),
|
||||
hRandomAdd: at("hRandomAdd"),
|
||||
hRandomSub: at("hRandomSub"),
|
||||
globals: [],
|
||||
cases,
|
||||
...extra,
|
||||
});
|
||||
|
||||
// A small deterministic generator for the cases.
|
||||
let seed = 0x5eed;
|
||||
const next = (limit = 256) => {
|
||||
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
|
||||
return (seed >>> 8) % limit;
|
||||
};
|
||||
const pick = <T>(items: readonly T[]) => items[next(items.length)];
|
||||
|
||||
const word = (value: number): [number, number] => [
|
||||
(value >> 8) & 0xff,
|
||||
value & 0xff,
|
||||
];
|
||||
const set = (address: number, bytes: readonly number[]): [number, number][] =>
|
||||
bytes.map((value, index) => [address + index, value]);
|
||||
/** A box_struct as it lies in memory. */
|
||||
const boxBytes = (mon: BoxMon) => [
|
||||
mon.species,
|
||||
mon.item,
|
||||
...mon.moves,
|
||||
...word(mon.otId),
|
||||
(mon.exp >> 16) & 0xff,
|
||||
...word(mon.exp),
|
||||
...mon.statExp.flatMap(word),
|
||||
...word(mon.dvs),
|
||||
...mon.pp,
|
||||
mon.happiness,
|
||||
mon.pokerus,
|
||||
...word(mon.caughtData),
|
||||
mon.level,
|
||||
];
|
||||
/** The charmap's uppercase letters, ending at the terminator. */
|
||||
const name = (text: string, length = NAME) =>
|
||||
[...text.split("").map((letter) => 0x80 + letter.charCodeAt(0) - 65), 0x50]
|
||||
.concat(Array(length).fill(0x50))
|
||||
.slice(0, length);
|
||||
const decode = (bytes: number[]) => {
|
||||
let text = "";
|
||||
for (const byte of bytes) {
|
||||
if (byte === 0x50) break;
|
||||
text +=
|
||||
byte >= 0x80 && byte < 0x9a
|
||||
? String.fromCharCode(65 + byte - 0x80)
|
||||
: "?";
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
const allSpecies = Array.from({ length: 251 }, (_, i) => i + 1);
|
||||
const breedable = () =>
|
||||
allSpecies.filter((s) => data().baseData(s).eggGroups !== 0xff);
|
||||
const groupsOf = (species: number) => {
|
||||
const groups = data().baseData(species).eggGroups;
|
||||
return [groups & 15, groups >> 4];
|
||||
};
|
||||
/** A random pair that shares an egg group (or has a Ditto or a repeat). */
|
||||
const pairOfSpecies = (): [number, number] => {
|
||||
const first = pick(breedable());
|
||||
const choice = next(8);
|
||||
if (choice === 0) return [first, first];
|
||||
if (choice === 1) return [first, DITTO];
|
||||
if (choice === 2) return [DITTO, first];
|
||||
const partners = breedable().filter((candidate) =>
|
||||
groupsOf(candidate).some((group) => groupsOf(first).includes(group)),
|
||||
);
|
||||
return [first, pick(partners)];
|
||||
};
|
||||
const tmhm = Array.from({ length: 57 }, (_, i) => i).flatMap((i) => {
|
||||
const move = data().reader.byte(
|
||||
JOHTO.EggMovePointers - JOHTO.EggMovePointers + 0x1167a + i,
|
||||
);
|
||||
return move ? [move] : [];
|
||||
});
|
||||
/** Moves that may be inherited, to make the inheritance rules matter. */
|
||||
const movePool = (baby: number) => [
|
||||
...data().eggMoves(baby),
|
||||
...data()
|
||||
.learnset(baby)
|
||||
.map((entry) => entry.move),
|
||||
...tmhm,
|
||||
...Array.from({ length: 6 }, () => 1 + next(251)),
|
||||
];
|
||||
const randomMoves = (pool: number[]): BoxMon["moves"] => {
|
||||
const count = next(5);
|
||||
const moves = Array.from({ length: count }, () => pick(pool));
|
||||
const unique = moves.filter((move, i) => moves.indexOf(move) === i);
|
||||
return [...unique, 0, 0, 0, 0].slice(0, 4) as BoxMon["moves"];
|
||||
};
|
||||
const stored = (
|
||||
species: number,
|
||||
moves: BoxMon["moves"],
|
||||
extra: Partial<BoxMon> = {},
|
||||
): BoxMon => {
|
||||
const growth = data().baseData(species).growthRate;
|
||||
const level = 2 + next(60);
|
||||
return {
|
||||
species,
|
||||
item: 0,
|
||||
moves,
|
||||
otId: next(65536),
|
||||
ot: "KAWA",
|
||||
nickname: "MON",
|
||||
exp: data().expAtLevel(growth, level) + next(300),
|
||||
statExp: [0, 1, 2, 3, 4].map(() => next(65536)) as BoxMon["statExp"],
|
||||
dvs: next(65536),
|
||||
pp: moves.map((move) =>
|
||||
move ? (next(4) << 6) | (1 + next(40)) : 0,
|
||||
) as BoxMon["pp"],
|
||||
happiness: next(256),
|
||||
pokerus: next(256),
|
||||
caughtData: next(65536),
|
||||
level,
|
||||
...extra,
|
||||
};
|
||||
};
|
||||
|
||||
test("CheckBreedmonCompatibility", async ({ page }) => {
|
||||
const wBreedMon1 = at("wBreedMon1"),
|
||||
wBreedMon2 = at("wBreedMon2"),
|
||||
result = at("wBreedingCompatibility");
|
||||
const pairs = Array.from({ length: 1500 }, () => {
|
||||
const [first, second] =
|
||||
next(5) === 0
|
||||
? ([pick(allSpecies), pick(allSpecies)] as [number, number])
|
||||
: pairOfSpecies();
|
||||
const mon1 = stored(first, [0, 0, 0, 0]),
|
||||
mon2 = stored(second, [0, 0, 0, 0]);
|
||||
// Equal IDs, Defense DVs and Special DVs make the special bands matter.
|
||||
if (next(3) === 0) mon2.otId = mon1.otId;
|
||||
if (next(4) === 0) mon2.dvs = (mon2.dvs & 0xf0f8) | (mon1.dvs & 0x0f07);
|
||||
if (next(8) === 0) mon2.dvs = (mon2.dvs & 0x0fff) | (mon1.dvs & 0xf000);
|
||||
return { mon1, mon2 };
|
||||
});
|
||||
await openReference(page);
|
||||
const runs = await runReference(
|
||||
page,
|
||||
job(
|
||||
"CheckBreedmonCompatibility",
|
||||
pairs.map(({ mon1, mon2 }) => ({
|
||||
writes: [
|
||||
...set(wBreedMon1, boxBytes(mon1)),
|
||||
...set(wBreedMon2, boxBytes(mon2)),
|
||||
],
|
||||
reads: [[result, 1]],
|
||||
})),
|
||||
[],
|
||||
),
|
||||
);
|
||||
const seen = new Set<number>();
|
||||
pairs.forEach(({ mon1, mon2 }, index) => {
|
||||
expect(runs[index].returned, `pair ${index} returned`).toBe(true);
|
||||
const native = breedingCompatibility(data(), mon1, mon2);
|
||||
expect(native, `pair ${index}: ${mon1.species}/${mon2.species}`).toBe(
|
||||
runs[index].reads[0][0],
|
||||
);
|
||||
seen.add(native);
|
||||
});
|
||||
// 0, 255, 254, 177, 128 and 51 all occur.
|
||||
expect([...seen].sort((a, b) => a - b)).toEqual([
|
||||
0, 51, 128, 177, 254, 255,
|
||||
]);
|
||||
});
|
||||
|
||||
test("GetBreedMon1LevelGrowth", async ({ page }) => {
|
||||
const wBreedMon1 = at("wBreedMon1");
|
||||
const cases = Array.from({ length: 600 }, () => {
|
||||
const species = pick(allSpecies);
|
||||
const mon = stored(species, [0, 0, 0, 0]);
|
||||
const growth = data().baseData(species).growthRate;
|
||||
const extra =
|
||||
next(4) === 0 ? 0 : next(3) === 0 ? 1 << 20 : next(100) * 2000;
|
||||
mon.exp = Math.min(
|
||||
0xffffff,
|
||||
data().expAtLevel(growth, 1 + next(100)) + extra - next(3),
|
||||
);
|
||||
mon.exp = Math.max(0, mon.exp);
|
||||
return mon;
|
||||
});
|
||||
await openReference(page);
|
||||
const runs = await runReference(
|
||||
page,
|
||||
job(
|
||||
"GetBreedMon1LevelGrowth",
|
||||
cases.map((mon) => ({
|
||||
writes: set(wBreedMon1, boxBytes(mon)),
|
||||
reads: [],
|
||||
})),
|
||||
[],
|
||||
{ registers: true },
|
||||
),
|
||||
);
|
||||
cases.forEach((mon, index) => {
|
||||
expect(
|
||||
runs[index].returned,
|
||||
`case ${index}: ${JSON.stringify(mon)}`,
|
||||
).toBe(true);
|
||||
const { oldLevel, level, growth } = breedLevelGrowth(data(), mon);
|
||||
expect({ oldLevel, growth, level }, `case ${index}`).toEqual({
|
||||
oldLevel: runs[index].registers[0],
|
||||
growth: runs[index].registers[1],
|
||||
level: runs[index].registers[2],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("DayCare_InitBreeding: the egg, its species, DVs and inherited moves", async ({
|
||||
page,
|
||||
}) => {
|
||||
const sym = {
|
||||
man: at("wDayCareMan"),
|
||||
lady: at("wDayCareLady"),
|
||||
mon1: at("wBreedMon1"),
|
||||
mon2: at("wBreedMon2"),
|
||||
steps: at("wStepsToEgg"),
|
||||
mother: at("wBreedMotherOrNonDitto"),
|
||||
egg: at("wEggMon"),
|
||||
eggNickname: at("wEggMonNickname"),
|
||||
eggOT: at("wEggMonOT"),
|
||||
playerId: at("wPlayerID"),
|
||||
playerName: at("wPlayerName"),
|
||||
};
|
||||
const player = { id: 0, name: "KAWA" };
|
||||
const cases = Array.from({ length: 900 }, (_, index) => {
|
||||
let [first, second] = pairOfSpecies();
|
||||
// Nidoran♀ and the baby forms, with the partners that matter.
|
||||
if (index % 9 === 0) [first, second] = [29, pick([32, 33, 34, DITTO])];
|
||||
if (index % 9 === 1) [first, second] = [pick([32, 33, 34, DITTO]), 29];
|
||||
if (index % 9 === 2)
|
||||
[first, second] = [pick([25, 26, 82, 129, 130]), DITTO];
|
||||
const baby = data().preEvolution(
|
||||
data().preEvolution(next(2) ? first : second),
|
||||
);
|
||||
const pool = movePool(baby);
|
||||
const mon1 = stored(first, randomMoves(pool)),
|
||||
mon2 = stored(second, randomMoves(pool));
|
||||
if (next(3) === 0) mon2.otId = mon1.otId;
|
||||
if (next(10) === 0) mon2.dvs = (mon2.dvs & 0xf0f8) | (mon1.dvs & 0x0f07);
|
||||
const stream = Array.from({ length: 80 }, () => next(256));
|
||||
return {
|
||||
mon1,
|
||||
mon2,
|
||||
stream,
|
||||
id: next(65536),
|
||||
prior: { steps: next(256), mother: next(2), man: next(2) << 5 },
|
||||
};
|
||||
});
|
||||
await openReference(page);
|
||||
// One job per stream: the stream is part of the job. Batched by case.
|
||||
for (let start = 0; start < cases.length; start += 60) {
|
||||
const batch = cases.slice(start, start + 60);
|
||||
const runs: Run[] = [];
|
||||
for (const input of batch) {
|
||||
const [run] = await runReference(
|
||||
page,
|
||||
job(
|
||||
"DayCare_InitBreeding",
|
||||
[
|
||||
{
|
||||
writes: [
|
||||
...set(sym.man, [1 | input.prior.man]),
|
||||
...set(sym.lady, [1]),
|
||||
...set(sym.mon1, boxBytes(input.mon1)),
|
||||
...set(sym.mon2, boxBytes(input.mon2)),
|
||||
...set(sym.steps, [input.prior.steps]),
|
||||
...set(sym.mother, [input.prior.mother]),
|
||||
...set(sym.playerId, word(input.id)),
|
||||
...set(sym.playerName, name(player.name)),
|
||||
// Leftovers the routine must overwrite.
|
||||
...set(sym.egg, Array(BOX_STRUCT).fill(0xaa)),
|
||||
],
|
||||
reads: [
|
||||
[sym.man, 1],
|
||||
[sym.steps, 1],
|
||||
[sym.mother, 1],
|
||||
[sym.egg, BOX_STRUCT],
|
||||
[sym.eggNickname, NAME],
|
||||
[sym.eggOT, NAME],
|
||||
],
|
||||
},
|
||||
],
|
||||
input.stream,
|
||||
),
|
||||
);
|
||||
runs.push(run);
|
||||
}
|
||||
batch.forEach((input, offset) => {
|
||||
const label = `case ${start + offset}: ${input.mon1.species}/${input.mon2.species}`;
|
||||
const run = runs[offset];
|
||||
expect(run.returned, label).toBe(true);
|
||||
const used = [...input.stream];
|
||||
let draws = 0;
|
||||
const host = {
|
||||
flags: new Set([
|
||||
ENGINE_FLAGS.dayCareManHasMon,
|
||||
ENGINE_FLAGS.dayCareLadyHasMon,
|
||||
]),
|
||||
dayCare: {
|
||||
...emptyDayCare(),
|
||||
mon1: structuredClone(input.mon1),
|
||||
mon2: structuredClone(input.mon2),
|
||||
stepsToEgg: input.prior.steps,
|
||||
motherOrNonDitto: input.prior.mother,
|
||||
compatible: Boolean(input.prior.man),
|
||||
},
|
||||
};
|
||||
initBreeding(
|
||||
data(),
|
||||
host,
|
||||
{ id: input.id, name: player.name },
|
||||
() => used[draws++],
|
||||
);
|
||||
const [man, steps, mother, egg, eggNickname, eggOT] = run.reads;
|
||||
expect(draws, `${label}: draws`).toBe(run.draws);
|
||||
expect(Boolean(man[0] & 0x20), `${label}: compatible`).toBe(
|
||||
host.dayCare.compatible,
|
||||
);
|
||||
expect(steps[0], `${label}: steps`).toBe(host.dayCare.stepsToEgg);
|
||||
expect(mother[0], `${label}: mother`).toBe(
|
||||
host.dayCare.motherOrNonDitto,
|
||||
);
|
||||
if (host.dayCare.egg) {
|
||||
expect(egg, `${label}: egg`).toEqual(boxBytes(host.dayCare.egg));
|
||||
expect(decode(eggNickname), label).toBe(host.dayCare.egg.nickname);
|
||||
expect(decode(eggOT), label).toBe(host.dayCare.egg.ot);
|
||||
} else {
|
||||
// Incompatible: the routine returned before touching the egg.
|
||||
expect(egg, `${label}: egg untouched`).toEqual(
|
||||
Array(BOX_STRUCT).fill(0xaa),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("DayCareStep: experience, the step count and the egg chance", async ({
|
||||
page,
|
||||
}) => {
|
||||
const sym = {
|
||||
man: at("wDayCareMan"),
|
||||
lady: at("wDayCareLady"),
|
||||
mon1: at("wBreedMon1"),
|
||||
mon2: at("wBreedMon2"),
|
||||
steps: at("wStepsToEgg"),
|
||||
};
|
||||
const cases = Array.from({ length: 1200 }, () => {
|
||||
const [first, second] = pairOfSpecies();
|
||||
const mon1 = stored(first, [0, 0, 0, 0]),
|
||||
mon2 = stored(second, [0, 0, 0, 0]);
|
||||
if (next(3) === 0) mon2.otId = mon1.otId;
|
||||
if (next(8) === 0) mon2.dvs = (mon2.dvs & 0xf0f8) | (mon1.dvs & 0x0f07);
|
||||
// Experience on a byte boundary, levels at the cap.
|
||||
for (const mon of [mon1, mon2]) {
|
||||
if (next(3) === 0) mon.exp = (mon.exp & 0xffff00) | 0xff;
|
||||
if (next(6) === 0) mon.exp = (mon.exp & 0xff0000) | 0xffff;
|
||||
if (next(8) === 0) mon.exp = (0x4f << 16) | 0xffff;
|
||||
if (next(8) === 0) mon.level = 100;
|
||||
if (next(10) === 0) mon.level = 99;
|
||||
}
|
||||
const hasMon1 = next(5) !== 0,
|
||||
hasMon2 = next(5) !== 0;
|
||||
return {
|
||||
mon1,
|
||||
mon2,
|
||||
hasMon1,
|
||||
hasMon2,
|
||||
compatible: next(4) !== 0,
|
||||
steps: next(3) === 0 ? 1 : next(256),
|
||||
stream: Array.from({ length: 4 }, () => next(256)),
|
||||
};
|
||||
});
|
||||
await openReference(page);
|
||||
for (const [index, input] of cases.entries()) {
|
||||
const [run] = await runReference(
|
||||
page,
|
||||
job(
|
||||
"DayCareStep",
|
||||
[
|
||||
{
|
||||
writes: [
|
||||
...set(sym.man, [
|
||||
(input.hasMon1 ? 1 : 0) | (input.compatible ? 0x20 : 0),
|
||||
]),
|
||||
...set(sym.lady, [input.hasMon2 ? 1 : 0]),
|
||||
...set(sym.mon1, boxBytes(input.mon1)),
|
||||
...set(sym.mon2, boxBytes(input.mon2)),
|
||||
...set(sym.steps, [input.steps]),
|
||||
],
|
||||
reads: [
|
||||
[sym.man, 1],
|
||||
[sym.steps, 1],
|
||||
[sym.mon1 + 8, 3],
|
||||
[sym.mon2 + 8, 3],
|
||||
],
|
||||
},
|
||||
],
|
||||
input.stream,
|
||||
),
|
||||
);
|
||||
expect(run.returned, `case ${index}`).toBe(true);
|
||||
const used = [...input.stream];
|
||||
let draws = 0;
|
||||
const flags = new Set<number>();
|
||||
if (input.hasMon1) flags.add(ENGINE_FLAGS.dayCareManHasMon);
|
||||
if (input.hasMon2) flags.add(ENGINE_FLAGS.dayCareLadyHasMon);
|
||||
const dayCare = {
|
||||
...emptyDayCare(),
|
||||
mon1: structuredClone(input.mon1),
|
||||
mon2: structuredClone(input.mon2),
|
||||
stepsToEgg: input.steps,
|
||||
compatible: input.compatible,
|
||||
};
|
||||
dayCareStep(data(), { flags, dayCare }, () => used[draws++]);
|
||||
const [man, steps, exp1, exp2] = run.reads;
|
||||
const label = `case ${index}: ${JSON.stringify({ ...input, stream: input.stream })}`;
|
||||
expect(exp1, label).toEqual([
|
||||
(dayCare.mon1.exp >> 16) & 0xff,
|
||||
(dayCare.mon1.exp >> 8) & 0xff,
|
||||
dayCare.mon1.exp & 0xff,
|
||||
]);
|
||||
expect(exp2, label).toEqual([
|
||||
(dayCare.mon2.exp >> 16) & 0xff,
|
||||
(dayCare.mon2.exp >> 8) & 0xff,
|
||||
dayCare.mon2.exp & 0xff,
|
||||
]);
|
||||
expect(steps[0], label).toBe(dayCare.stepsToEgg);
|
||||
expect(
|
||||
{ compatible: Boolean(man[0] & 0x20), egg: Boolean(man[0] & 0x40) },
|
||||
label,
|
||||
).toEqual({
|
||||
compatible: dayCare.compatible,
|
||||
egg: flags.has(ENGINE_FLAGS.dayCareManHasEgg),
|
||||
});
|
||||
expect(draws, label).toBe(run.draws);
|
||||
}
|
||||
});
|
||||
|
||||
test("RetrieveBreedmon: stats, learned moves, PP, HP and experience", async ({
|
||||
page,
|
||||
}) => {
|
||||
const sym = {
|
||||
count: at("wPartyCount"),
|
||||
species: at("wPartySpecies"),
|
||||
party: at("wPartyMon1"),
|
||||
nicknames: at("wPartyMonNicknames"),
|
||||
ots: at("wPartyMonOTs"),
|
||||
mon1: at("wBreedMon1"),
|
||||
nickname1: at("wBreedMon1Nickname"),
|
||||
ot1: at("wBreedMon1OT"),
|
||||
parameter: at("wPokemonWithdrawDepositParameter"),
|
||||
level: at("wCurPartyLevel"),
|
||||
previous: at("wPrevPartyLevel"),
|
||||
curSpecies: at("wCurPartySpecies"),
|
||||
skip: at("wSkipMovesBeforeLevelUp"),
|
||||
};
|
||||
const cases = Array.from({ length: 700 }, () => {
|
||||
const species = pick(allSpecies);
|
||||
const learnLevels = data()
|
||||
.learnset(species)
|
||||
.map((entry) => entry.level);
|
||||
const moves = randomMoves(movePool(species));
|
||||
const mon = stored(species, moves);
|
||||
// Level up across learnset entries about half the time.
|
||||
const oldLevel = next(2)
|
||||
? 1 + next(40)
|
||||
: Math.max(1, (pick(learnLevels) ?? 10) - next(3));
|
||||
const newLevel = Math.min(100, oldLevel + (next(4) === 0 ? 0 : next(30)));
|
||||
mon.level = oldLevel;
|
||||
mon.exp =
|
||||
data().expAtLevel(data().baseData(species).growthRate, newLevel) +
|
||||
next(500);
|
||||
return { mon, oldLevel, newLevel, count: next(6) };
|
||||
});
|
||||
await openReference(page);
|
||||
for (const [index, input] of cases.entries()) {
|
||||
const slot = input.count;
|
||||
const [run] = await runReference(
|
||||
page,
|
||||
job(
|
||||
"RetrieveBreedmon",
|
||||
[
|
||||
{
|
||||
writes: [
|
||||
...set(sym.count, [slot]),
|
||||
...set(sym.mon1, boxBytes(input.mon)),
|
||||
...set(sym.nickname1, name("NICKNAME")),
|
||||
...set(sym.ot1, name("OTNAME")),
|
||||
...set(sym.parameter, [0]),
|
||||
...set(sym.level, [input.newLevel]),
|
||||
...set(sym.previous, [input.oldLevel]),
|
||||
...set(sym.curSpecies, [input.mon.species]),
|
||||
// Leftovers in the slot, as an earlier occupant leaves.
|
||||
...set(
|
||||
sym.party + slot * PARTY_STRUCT,
|
||||
Array(PARTY_STRUCT).fill(0x5a),
|
||||
),
|
||||
],
|
||||
reads: [
|
||||
[sym.count, 1],
|
||||
[sym.species + slot, 2],
|
||||
[sym.party + slot * PARTY_STRUCT, PARTY_STRUCT],
|
||||
[sym.nicknames + slot * NAME, NAME],
|
||||
[sym.ots + slot * NAME, NAME],
|
||||
],
|
||||
},
|
||||
],
|
||||
[],
|
||||
),
|
||||
);
|
||||
const label = `case ${index}: ${JSON.stringify(input)}`;
|
||||
expect(run.returned, label).toBe(true);
|
||||
const mon = retrievedMon(
|
||||
data(),
|
||||
{ ...input.mon, nickname: "NICKNAME", ot: "OTNAME" },
|
||||
input.oldLevel,
|
||||
input.newLevel,
|
||||
);
|
||||
const [count, species, struct, nickname, ot] = run.reads;
|
||||
expect(count[0], label).toBe(slot + 1);
|
||||
expect(species, label).toEqual([input.mon.species, 0xff]);
|
||||
expect(decode(nickname), label).toBe("NICKNAME");
|
||||
expect(decode(ot), label).toBe("OTNAME");
|
||||
const word16 = (value: number) => word(value);
|
||||
const expected = [
|
||||
...boxBytes(mon),
|
||||
mon.status,
|
||||
0,
|
||||
...word16(mon.hp),
|
||||
...mon.stats.flatMap(word16),
|
||||
];
|
||||
// The unused byte after the status is whatever the slot held; the
|
||||
// routine does not clear it (HealPartyMon writes it as 0).
|
||||
expect(struct, label).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
test("_GiveOddEgg: the egg chosen by the random word and its party entry", async ({
|
||||
page,
|
||||
}) => {
|
||||
const sym = {
|
||||
count: at("wPartyCount"),
|
||||
species: at("wPartySpecies"),
|
||||
party: at("wPartyMon1"),
|
||||
nicknames: at("wPartyMonNicknames"),
|
||||
ots: at("wPartyMonOTs"),
|
||||
};
|
||||
// Each OddEggProbabilities entry, and the words either side of it.
|
||||
const reader = data().reader;
|
||||
const thresholds = Array.from({ length: 14 }, (_, i) => {
|
||||
const entry = JOHTO.OddEggProbabilities + i * 2;
|
||||
return reader.byte(entry) | (reader.byte(entry + 1) << 8);
|
||||
});
|
||||
const words = [
|
||||
...thresholds.flatMap((value) => [value - 1, value, value + 1]),
|
||||
0,
|
||||
0xffff,
|
||||
...Array.from({ length: 300 }, () => next(65536)),
|
||||
]
|
||||
.filter((value) => value >= 0 && value <= 0xffff)
|
||||
.map((value) => [value >> 8, value & 0xff]);
|
||||
await openReference(page);
|
||||
const seenSpecies = new Set<number>();
|
||||
// The map is reached with a nonempty party. AddMobileMonToParty
|
||||
// assumes that: its decrement loop underflows for an empty party.
|
||||
for (const [index, [sub, add]] of words.entries()) {
|
||||
const [run] = await runReference(
|
||||
page,
|
||||
job(
|
||||
"_GiveOddEgg",
|
||||
[
|
||||
{
|
||||
writes: set(sym.count, [1 + (index % 4)]),
|
||||
reads: [
|
||||
[sym.count, 1],
|
||||
[sym.species, 6],
|
||||
[sym.party + (1 + (index % 4)) * PARTY_STRUCT, PARTY_STRUCT],
|
||||
[sym.nicknames + (1 + (index % 4)) * NAME, NAME],
|
||||
[sym.ots + (1 + (index % 4)) * NAME, NAME],
|
||||
],
|
||||
},
|
||||
],
|
||||
[sub, add],
|
||||
{ random: { address: routine("Random").address, bytesPerCall: 2 } },
|
||||
),
|
||||
);
|
||||
const label = `word ${sub.toString(16)}${add.toString(16)}`;
|
||||
expect(run.returned, label).toBe(true);
|
||||
expect(run.draws, label).toBe(2);
|
||||
const slot = 1 + (index % 4);
|
||||
const [count, species, struct, nickname, ot] = run.reads;
|
||||
const used = [sub, add];
|
||||
let draws = 0;
|
||||
const egg = oddEgg(data(), () => used[draws++]);
|
||||
seenSpecies.add(egg.species);
|
||||
expect(draws, label).toBe(2);
|
||||
expect(count[0], label).toBe(slot + 1);
|
||||
expect(species[slot], label).toBe(0xfd);
|
||||
const expected = [
|
||||
egg.species,
|
||||
egg.item,
|
||||
...egg.moves,
|
||||
...word(egg.otId),
|
||||
(egg.exp >> 16) & 0xff,
|
||||
...word(egg.exp),
|
||||
...egg.statExp.flatMap(word),
|
||||
...word(egg.dvs),
|
||||
...egg.pp,
|
||||
egg.happiness,
|
||||
egg.pokerus,
|
||||
...word(egg.caughtData),
|
||||
egg.level,
|
||||
egg.status,
|
||||
0,
|
||||
...word(egg.hp),
|
||||
...egg.stats.flatMap(word),
|
||||
];
|
||||
expect(struct, label).toEqual(expected);
|
||||
expect(decode(nickname), label).toBe(egg.nickname);
|
||||
expect(decode(ot), label).toBe(egg.ot);
|
||||
}
|
||||
// Pichu, Cleffa, Igglybuff, Smoochum, Magby, Elekid and Tyrogue.
|
||||
expect([...seenSpecies].sort((a, b) => a - b)).toEqual([
|
||||
172, 173, 174, 236, 238, 239, 240,
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user