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 { 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 => ({ 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, seeded by each test from // its own title (FNV-1a): a test's cases are the same whichever tests ran // before it in the worker, and in every browser. let seed = 0; test.beforeEach(({}, testInfo) => { seed = 0x811c9dc5; for (const character of testInfo.title) seed = Math.imul(seed ^ character.charCodeAt(0), 0x01000193) >>> 0; }); const next = (limit = 256) => { seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0; return (seed >>> 8) % limit; }; const pick = (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 => { 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(); 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(); 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(); // 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, ]); }); });