import { beforeEach, describe, expect, it, onTestFailed } from "vitest"; import { existsSync, readFileSync } from "node:fs"; import { CrystalAssets } from "../src/game/rom/assets"; import { C } from "../src/game/battle/constants"; import { tmhmItem } from "../src/game/rom/pokemon"; import { Playthrough } from "./playthrough-driver"; import { findPath } from "./world-driver"; const romPath = process.env.CRYSTAL_ROM_PATH ?? "Pokemon - Crystal Version (UE) (V1.1) [C][!].gbc"; const NEW_BARK = "24:4", ELMS_LAB = "24:5", ROUTE_29 = "24:3", CHERRYGROVE = "26:3", ROUTE_30 = "26:1", MR_POKEMONS_HOUSE = "26:10", CHERRYGROVE_CENTER = "26:5", ROUTE_31 = "26:2", ROUTE_31_GATE = "26:11", VIOLET = "10:5", VIOLET_CENTER = "10:10", VIOLET_GYM = "10:7", SPROUT_1F = "3:1", SPROUT_2F = "3:2", SPROUT_3F = "3:3", KYLES_HOUSE = "10:11"; const DARK_CAVE = "3:78", EARLS_ACADEMY = "10:8"; const ROUTE_32 = "10:1", ROUTE_32_CENTER = "10:13", UNION_CAVE_1F = "3:37", ROUTE_33 = "8:6", AZALEA = "8:7", AZALEA_CENTER = "8:1", KURTS_HOUSE = "8:4", AZALEA_GYM = "8:5", SLOWPOKE_WELL_B1F = "3:40", ILEX_GATE = "11:22", ILEX_FOREST = "3:52", ROUTE_34_GATE = "11:23", ROUTE_34 = "11:1", GOLDENROD = "11:2", GOLDENROD_CENTER = "11:20", GOLDENROD_GYM = "11:3", BIKE_SHOP = "11:4", FLOWER_SHOP = "11:8", RADIO_TOWER_1F = "3:17"; const ROUTE_35 = "10:2", ROUTE_35_NATIONAL_PARK_GATE = "10:15", NATIONAL_PARK = "3:15", ROUTE_36_NATIONAL_PARK_GATE = "10:17", ROUTE_35_GATE = "10:14", ROUTE_36 = "10:3", ROUTE_37 = "10:4", ECRUTEAK = "4:9", ECRUTEAK_CENTER = "4:3", ECRUTEAK_MART = "4:6", ECRUTEAK_GYM = "4:7", DANCE_THEATER = "4:5", BURNED_TOWER_1F = "3:13", BURNED_TOWER_B1F = "3:14"; const MIRACLE_SEED = 0x75, LURE_BALL = 0xa0, HM01 = 0xf3, CUT = 15, BICYCLE = 7; const EVENT_CLEARED_SLOWPOKE_WELL = 0x2b, EVENT_BEAT_BUGSY = 0x4be, EVENT_RIVAL_AZALEA_TOWN = 0x6bf, EVENT_HERDED_FARFETCHD = 0x29, ENGINE_HIVEBADGE = 0x1c, ENGINE_PLAINBADGE = 0x1d, ENGINE_RADIO_CARD = 0, EVENT_BEAT_WHITNEY = 0x4bf; const PHONE_BUG_CATCHER_WADE = 16; const FLASH = 148, HM05 = 0xf7; const TOGEPI = 175, BELLSPROUT = 69, ONIX = 95; const EVENT_TALKED_TO_MOM_AFTER_MYSTERY_EGG_QUEST = 0x40, EVENT_GOT_HM05_FLASH = 0x14, EVENT_BEAT_FALKNER = 0x4bd, EVENT_GOT_TM31_MUD_SLAP = 0x8, EVENT_GOT_TOGEPI_EGG_FROM_ELMS_AIDE = 0x2d, ENGINE_ZEPHYRBADGE = 0x1b; const SQUIRTBOTTLE = 0xaf; const EVENT_MET_FLORIA = 0xb9, EVENT_GOT_SQUIRTBOTTLE = 0x5c, EVENT_FOUGHT_SUDOWOODO = 0x2a, EVENT_GOT_HM03_SURF = 0x12, EVENT_RIVAL_BURNED_TOWER = 0x6c5, EVENT_HOLE_IN_BURNED_TOWER = 0x332; const EVENT_RELEASED_THE_BEASTS = 0x7b, EVENT_BURNED_TOWER_MORTY = 0x764, EVENT_ECRUTEAK_GYM_GRAMPS = 0x7a8, EVENT_ECRUTEAK_CITY_GRAMPS = 0x7a9, EVENT_BEAT_SAGE_JEFFREY = 0x415, EVENT_BEAT_SAGE_PING = 0x416, EVENT_BEAT_MEDIUM_MARTHA = 0x58b, EVENT_BEAT_MEDIUM_GRACE = 0x58c, EVENT_BEAT_MORTY = 0x4c0, EVENT_GOT_TM30_SHADOW_BALL = 0xc, ENGINE_FOGBADGE = 30, RAIKOU = 0xf3, ENTEI = 0xf4, ROUTE_42 = "2:5"; const ROUTE_38_GATE = "1:9", ROUTE_38 = "1:12", ROUTE_39 = "1:13", OLIVINE = "1:14", OLIVINE_CENTER = "1:1", OLIVINE_GYM = "1:2", OLIVINE_CAFE = "1:7", GOOD_ROD_HOUSE = "1:6", OLIVINE_MART = "1:8", LIGHTHOUSE = ["3:42", "3:43", "3:44", "3:45", "3:46", "3:47"], ROUTE_40 = "22:1", ROUTE_41 = "22:2", CIANWOOD = "22:3", CIANWOOD_GYM = "22:5", CIANWOOD_CENTER = "22:6", CIANWOOD_PHARMACY = "22:7"; const GOOD_ROD = 0x3b, SECRETPOTION = 0x43, HM02 = 0xf4, HM03 = 0xf5, HM04 = 0xf6, KRABBY = 98, SURF = 0x39, STRENGTH = 0x46, EVENT_GOT_HM04_STRENGTH = 0x13, EVENT_GOT_GOOD_ROD = 0x18, EVENT_RIVAL_OLIVINE_CITY = 0x6c3, EVENT_JASMINE_EXPLAINED_AMPHYS_SICKNESS = 0x37, EVENT_FOUGHT_EUSINE = 0x333, EVENT_GOT_SECRETPOTION_FROM_PHARMACY = 0x23, EVENT_BEAT_CHUCK = 0x4c2, EVENT_GOT_TM01_DYNAMICPUNCH = 0xa, EVENT_GOT_HM02_FLY = 0x11, EVENT_JASMINE_RETURNED_TO_GYM = 0x20, EVENT_BEAT_JASMINE = 0x4c1, EVENT_GOT_TM23_IRON_TAIL = 0xd, ENGINE_MINERALBADGE = 31, ENGINE_STORMBADGE = 32; const ROUTE_42_GATE = "2:4", MAHOGANY = "2:7", MAHOGANY_CENTER = "2:3", MAHOGANY_GYM = "2:2", MAHOGANY_SHOP = "3:48", ROCKET_HIDEOUT = ["3:49", "3:50", "3:51"], ROUTE_43_MAHOGANY_GATE = "9:3", ROUTE_43_GATE = "9:4", ROUTE_43 = "9:5", LAKE_OF_RAGE = "9:6"; const RED_SCALE = 0x42, EVENT_BEAT_POKEMANIAC_SHANE = 0x4e7, EVENT_LAKE_OF_RAGE_RED_GYARADOS = 0x751, EVENT_DECIDED_TO_HELP_LANCE = 0x60, EVENT_UNCOVERED_STAIRCASE_IN_MAHOGANY_MART = 0x2e2, EVENT_LANCE_HEALED_YOU_IN_TEAM_ROCKET_BASE = 0x4c, EVENT_LEARNED_SLOWPOKETAIL = 0x301, EVENT_LEARNED_RATICATE_TAIL = 0x302, EVENT_LEARNED_HAIL_GIOVANNI = 0x2ff, EVENT_RIVAL_TEAM_ROCKET_BASE = 0x6c0, EVENT_BEAT_ROCKET_EXECUTIVEM_4 = 0x574, EVENT_BEAT_ROCKET_EXECUTIVEF_2 = 0x570, EVENT_CLEARED_ROCKET_HIDEOUT = 0x22, EVENT_GOT_HM06_WHIRLPOOL = 0x15, EVENT_BEAT_PRYCE = 0x4c3, EVENT_GOT_TM16_ICY_WIND = 0xe, EVENT_RADIO_TOWER_ROCKET_TAKEOVER = 0x6ce, ENGINE_GLACIERBADGE = 0x21, ENGINE_ROCKETS_IN_RADIO_TOWER = 0x13; const RADIO_TOWER = ["3:17", "3:18", "3:19", "3:20", "3:21"], UNDERGROUND = "3:53", SWITCH_ROOM = "3:54", DEPT_STORE_B1F = "3:55", WAREHOUSE = "3:56", DEPT_STORE_1F = "11:11", ELEVATOR = "11:17", ROUTE_44 = "2:6", ICE_PATH = ["3:61", "3:62", "3:63", "3:64", "3:65"], BLACKTHORN = "5:10", BLACKTHORN_CENTER = "5:6", BLACKTHORN_MART = "5:5", BLACKTHORN_GYM = ["5:1", "5:2"], ROUTE_45 = "5:8", DRAGONS_DEN = ["3:80", "3:81"], DRAGON_SHRINE = "3:82"; const BASEMENT_KEY = 0x85, CARD_KEY = 0x7f, CLEAR_BELL = 0x46, HM06 = 0xf8, DRATINI = 147, EXTREMESPEED = 245, EVENT_BEAT_ROCKET_EXECUTIVEM_3 = 0x573, EVENT_USED_BASEMENT_KEY = 0x49, EVENT_RIVAL_GOLDENROD_UNDERGROUND = 0x6c1, EVENT_RECEIVED_CARD_KEY = 0x4a, EVENT_USED_THE_CARD_KEY_IN_THE_RADIO_TOWER = 0x25, EVENT_CLEARED_RADIO_TOWER = 0x21, EVENT_GOT_CLEAR_BELL = 0x78, EVENT_BOULDER_IN_ICE_PATH_2A = 0x70e, EVENT_BEAT_CLAIR = 0x4c4, EVENT_GOT_TM24_DRAGONBREATH = 0xf, EVENT_GOT_DRATINI = 0xbd, EVENT_ANSWERED_DRAGON_MASTER_QUIZ_WRONG = 0xc1, ENGINE_RISINGBADGE = 0x22; const ROUTE_46 = "5:9", ROUTE_29_ROUTE_46_GATE = "24:13", TIN_TOWER_ENTRANCE = "4:1", WISE_TRIOS_ROOM = "4:2", TIN_TOWER_1F = "3:4", EVENT_RANG_CLEAR_BELL_2 = 0x767, EVENT_FOUGHT_SUICUNE = 0x335; const WISE_TRIOS_ROOM_EXIT = "4:2", MAHOGANY_ROUTE_44 = "2:6", ROUTE_27 = "24:2", TOHJO_FALLS = "3:83", ROUTE_26 = "24:1", VICTORY_ROAD_GATE = "23:13", VICTORY_ROAD = "3:91", ROUTE_23 = "16:1", INDIGO_PLATEAU = "16:2", ELITE_FOUR = ["16:3", "16:4", "16:5", "16:6"], LANCES_ROOM = "16:7"; const HM07 = 0xf9, WATERFALL = 127, IRON_TAIL = 231, TYPHLOSION = 157, EVENT_GOT_HM07_WATERFALL = 0x688, EVENT_RIVAL_VICTORY_ROAD = 0x6c2, EVENT_BEAT_ELITE_4_WILL = 0x5b8, EVENT_BEAT_ELITE_4_KOGA = 0x5b9, EVENT_BEAT_ELITE_4_BRUNO = 0x5ba, EVENT_BEAT_ELITE_4_KAREN = 0x5bb, EVENT_BEAT_CHAMPION_LANCE = 0x5bc, EVENT_BEAT_ELITE_FOUR = 0x44; const FAST_SHIP_1F = "15:3", FAST_SHIP_CABINS_NNW = "15:4", FAST_SHIP_CABINS_SE = "15:6", FAST_SHIP_B1F = "15:7", OLIVINE_PORT = "15:1", OLIVINE_PORT_PASSAGE = "15:8", VERMILION_PORT_PASSAGE = "15:9", VERMILION = "12:3", VERMILION_CENTER = "12:5", VERMILION_GYM = "12:11", ROUTE_6 = "12:1", ROUTE_6_SAFFRON_GATE = "12:12", SAFFRON = "25:2", ROUTE_5 = "25:1", ROUTE_5_SAFFRON_GATE = "25:14", SAFFRON_GYM = "25:4", SAFFRON_MART = "25:5", SAFFRON_CENTER = "25:6", CERULEAN = "7:17", CERULEAN_CENTER = "7:4", CERULEAN_GYM = "7:6", POWER_PLANT = "7:10", ROUTE_9 = "7:13", ROUTE_10_NORTH = "7:14", ROUTE_24 = "7:15", ROUTE_25 = "7:16", ROCK_TUNNEL = ["3:87", "3:88"], ROUTE_10_SOUTH = "18:3", LAVENDER = "18:4", LAVENDER_CENTER = "18:5", LAVENDER_RADIO_TOWER = "18:12", ROUTE_8 = "18:1", ROUTE_8_SAFFRON_GATE = "18:13", DIGLETTS_CAVE = "3:84", ROUTE_2 = "23:1", VIRIDIAN = "23:3", ROUTE_22 = "23:2", VIRIDIAN_GYM = "23:4", VIRIDIAN_CENTER = "23:9", PEWTER = "14:2", PEWTER_GYM = "14:4", PEWTER_CENTER = "14:6", ROUTE_1 = "13:1", PALLET = "13:2", OAKS_LAB = "13:6", CINNABAR_CENTER = "6:1", ROUTE_19_FUCHSIA_GATE = "6:3", SEAFOAM_GYM = "6:4", ROUTE_19 = "6:5", ROUTE_20 = "6:6", ROUTE_21 = "6:7", CINNABAR = "6:8", FUCHSIA_GYM = "17:8", ROUTE_18 = "17:4", FUCHSIA = "17:5", FUCHSIA_CENTER = "17:10", ROUTE_7 = "21:1", ROUTE_17 = "21:3", CELADON = "21:4", CELADON_CENTER = "21:17", CELADON_GYM = "21:21", ROUTE_7_SAFFRON_GATE = "21:25", ROUTE_17_ROUTE_18_GATE = "21:26", ROUTE_28 = "19:1", SILVER_CAVE_OUTSIDE = "19:2", SILVER_CAVE_CENTER = "19:3", SILVER_CAVE = ["3:74", "3:75"], SILVER_CAVE_ROOM_3 = "3:76"; const S_S_TICKET = 0x44, MACHINE_PART = 0x80, ENGINE_EXPN_CARD = 3, ENGINE_BOULDERBADGE = 0x23, ENGINE_CASCADEBADGE = 0x24, ENGINE_THUNDERBADGE = 0x25, ENGINE_RAINBOWBADGE = 0x26, ENGINE_SOULBADGE = 0x27, ENGINE_MARSHBADGE = 0x28, ENGINE_VOLCANOBADGE = 0x29, ENGINE_EARTHBADGE = 0x2a, EARTHQUAKE = 89, EVENT_GOT_SS_TICKET_FROM_ELM = 0x24, EVENT_FAST_SHIP_FIRST_TIME = 0x30, EVENT_RETURNED_MACHINE_PART = 0xc9, EVENT_MET_MANAGER_AT_POWER_PLANT = 0xca, EVENT_RESTORED_POWER_TO_KANTO = 0xcd, EVENT_FOUND_MACHINE_PART_IN_CERULEAN_GYM = 0xfb, EVENT_TALKED_TO_OAK_IN_KANTO = 0xe1, EVENT_BLUE_IN_CINNABAR = 0x775, EVENT_BEAT_BLUE = 0x4cc, EVENT_OPENED_MT_SILVER = 0x74f, EVENT_FOUGHT_SNORLAX = 0x750, EVENT_RED_IN_MT_SILVER = 0x762; /** The acceptance run for PLAN section 2: a new game played from the * introduction to the catching tutorial through the world's own inputs, * without fixtures or story flags set by hand. Every stage ends with a * save serialized to JSON and loaded into a fresh world. The stages share * one world and run in order, so run the file rather than a single stage. */ describe.skipIf(!existsSync(romPath))( "a continuous opening playthrough", () => { const assets = existsSync(romPath) ? new CrystalAssets(new Uint8Array(readFileSync(romPath))) : undefined; const p = new Playthrough(assets!); const { play, walk, cross, enter, exit, reload, lead, writeTestSave, heal, train, stairs, route, travel, sail, toChoice, choose, choosePack, talkTo, buy, } = p; /** From Route 31: through the gate to Violet's Pokémon Center and back. */ const restInViolet = () => { exit(ROUTE_31_GATE, "left"); exit(VIOLET, "left"); heal(VIOLET_CENTER, VIOLET); exit(ROUTE_31_GATE, "right"); exit(ROUTE_31, "right"); }; /** Route 35 and Route 36 meet only through the National Park and * its two gates (Route 36's Sudowoodo still blocks the way from * Violet City). */ const toRoute36 = () => { stairs(ROUTE_35_GATE); stairs(ROUTE_35); stairs(ROUTE_35_NATIONAL_PARK_GATE); stairs(NATIONAL_PARK); stairs(ROUTE_36_NATIONAL_PARK_GATE); stairs(ROUTE_36); }; const toGoldenrod = () => { stairs(ROUTE_36_NATIONAL_PARK_GATE); stairs(NATIONAL_PARK); stairs(ROUTE_35_NATIONAL_PARK_GATE); stairs(ROUTE_35); stairs(ROUTE_35_GATE); stairs(GOLDENROD); }; beforeEach(() => { p.log.length = 0; }); const traceFailures = () => onTestFailed(() => console.log(p.trace())); it("plays the introduction and Mom's scene", () => { p.start(); traceFailures(); p.world.beginIntro(); play([0, "", 1, "", 1, 0, "KAWA"]); expect(p.world.intro).toBe(false); expect(p.world.name).toBe("KAWA"); reload(); walk(7, 0); expect(p.world.mapId).toBe("24:6"); walk(9, 3); expect(p.world.move("down")).toBe(true); play([1, 1, 1, 1, 1]); expect(p.log).toContain("KAWA received\nPOKéGEAR."); reload(); }); it("meets Elm, takes Cyndaquil and the aide's Potion", () => { traceFailures(); walk(6, 7); expect(p.world.move("down")).toBe(true); play(); expect(p.world.mapId).toBe(NEW_BARK); enter(ELMS_LAB, "up"); play([0, 1]); walk(6, 4); p.world.move("up"); p.world.interact(); play([1, 1, ""]); expect(p.world.state.party.map((mon) => mon.species)).toEqual([155]); walk(4, 7); expect(p.world.move("down")).toBe(true); play(); expect(p.world.state.bag.items).toEqual([{ item: 18, quantity: 1 }]); reload(); }); it("walks to Mr. Pokémon's house through Route 29, Cherrygrove and Route 30", () => { traceFailures(); exit(NEW_BARK); cross("left", ROUTE_29); cross("left", CHERRYGROVE); reload(); cross("up", ROUTE_30); enter(MR_POKEMONS_HOUSE, "up"); play(); expect(p.world.state.bag.keyItems).toEqual([0x45]); // MYSTERY_EGG expect(p.world.state.flags.has(11)).toBe(true); // the Pokédex reload(); }); it("takes Elm's call outside and meets the rival in Cherrygrove", () => { traceFailures(); exit(ROUTE_30); expect(p.log.join("\n")).toContain("It's a disaster!"); expect(p.world.state.events.has(67)).toBe(true); // ELM_CALLED_ABOUT_STOLEN_POKEMON reload(); cross("down", CHERRYGROVE); // The rival's scene blocks the way east to Route 29. cross("right", ROUTE_29); const rival = p.log.join("\n"); // PlaceEnemysName: a rival is wRivalName alone, "???" until named. expect(rival).toContain("???\nwants to battle!"); expect(rival).toContain("My name's ???."); expect(p.world.state.scenes[CHERRYGROVE]).toBe(0); reload(); }); it("names the rival for the officer and returns the Egg to Elm", () => { traceFailures(); cross("right", NEW_BARK); enter(ELMS_LAB, "up"); walk(4, 6); expect(p.world.move("up")).toBe(true); play(["SILVER"]); expect(p.world.state.rivalName).toBe("SILVER"); reload(); walk(5, 3); p.world.move("up"); // Elm stands at 5,2: this only turns the player. p.world.interact(); play(); expect(p.world.state.bag.keyItems).toEqual([]); walk(4, 8); expect(p.world.state.bag.balls).toEqual([ { item: C.POKE_BALL, quantity: 5 }, ]); reload(); }); it("watches the catching tutorial on Route 29 and reaches Cherrygrove", () => { traceFailures(); exit(NEW_BARK); cross("left", ROUTE_29); cross("left", CHERRYGROVE); const pages = p.log.join("\n").replace(/\n/g, " "); expect(pages).toContain("show you how to catch POKéMON?"); expect(pages).toContain("That's how you do it"); expect(p.world.state.events.has(66)).toBe(true); // LEARNED_TO_CATCH_POKEMON expect(p.world.state.scenes[ROUTE_29]).toBe(0); reload(); const save = p.world.save(); expect(save.party[0].species).toBe(155); expect(save.pokedex.caught).toContain(155); expect(save.rivalName).toBe("SILVER"); expect(save.intro).toBe(false); }); it("trains and walks Route 30 and Route 31 to Violet City", () => { traceFailures(); const restInCherrygrove = () => { cross("left", CHERRYGROVE); heal(CHERRYGROVE_CENTER, CHERRYGROVE); cross("right", ROUTE_29); }; heal(CHERRYGROVE_CENTER, CHERRYGROVE); cross("right", ROUTE_29); train(10, restInCherrygrove); cross("left", CHERRYGROVE); heal(CHERRYGROVE_CENTER, CHERRYGROVE); cross("up", ROUTE_30); cross("up", ROUTE_31); exit(ROUTE_31_GATE, "left"); // Mom worries when the player has not come home since the Egg; the // call rings on a step after Route 31's callback queued it. expect(p.world.state.events.has(EVENT_TALKED_TO_MOM_AFTER_MYSTERY_EGG_QUEST), p.log.join("|")).toBe(true); exit(VIOLET, "left"); expect(p.world.state.flags.size).toBeGreaterThan(0); reload(); writeTestSave("violet-city"); }, 120_000); it("trains on Route 31 and earns Flash from Elder Li in Sprout Tower", () => { traceFailures(); exit(ROUTE_31_GATE, "right"); exit(ROUTE_31, "right"); train(14, restInViolet); restInViolet(); exit(ROUTE_31_GATE, "left"); exit(VIOLET, "left"); enter(SPROUT_1F, "up"); expect(route(SPROUT_3F, [SPROUT_1F, SPROUT_2F])).toBe(true); walk(10, 3); p.world.move("up"); // Elder Li: this only turns the player p.world.interact(); play(); const pages = p.log.join("\n").replace(/\n/g, " "); expect(pages).toContain("FLASH"); expect(p.world.state.events.has(EVENT_GOT_HM05_FLASH)).toBe(true); reload(); }, 120_000); it("beats Falkner for the ZEPHYRBADGE and TM31, then hears from Elm's aide", () => { traceFailures(); expect(route(SPROUT_1F, [SPROUT_1F, SPROUT_2F, SPROUT_3F], 6, { x: 9, y: 15 })).toBe(true); exit(VIOLET); heal(VIOLET_CENTER, VIOLET); enter(VIOLET_GYM, "up"); walk(5, 2); p.world.move("up"); // Falkner: this only turns the player p.world.interact(); play(); expect(p.world.state.events.has(EVENT_BEAT_FALKNER), p.log.join("|")).toBe(true); expect(p.world.state.flags.has(ENGINE_ZEPHYRBADGE)).toBe(true); expect(p.world.state.events.has(EVENT_GOT_TM31_MUD_SLAP)).toBe(true); expect(p.world.state.bag.tmsHms[30]).toBe(1); // TM31 reload(); exit(VIOLET); // SPECIALCALL_ASSISTANT rings on a step: the aide waits in the // Pokémon Center with the Egg. enter(VIOLET_CENTER, "up"); expect(p.log.join("\n").replace(/\n/g, " ")).toContain("POKéMON CENTER"); // The aide's object flag (EVENT_ELMS_AIDE_IN_VIOLET_POKEMON_CENTER). const aide = p.world.assets.map(VIOLET_CENTER).objects[4]; expect(p.world.state.events.has(aide.eventFlag)).toBe(false); reload(); }, 120_000); it("takes the Egg from Elm's aide", () => { traceFailures(); walk(4, 4); p.world.move("up"); // the aide: this only turns the player p.world.interact(); play([1]); expect(p.world.state.events.has(EVENT_GOT_TOGEPI_EGG_FROM_ELMS_AIDE)).toBe(true); const egg = p.world.state.party.at(-1)!; expect(egg).toMatchObject({ species: TOGEPI, egg: true, nickname: "EGG", hp: 0, happiness: p.world.assets.pokemon.baseData(TOGEPI).eggCycles, }); // An Egg is neither seen nor caught. expect(p.world.state.pokedex.caught.has(TOGEPI)).toBe(false); reload(); }); it("catches a Bellsprout on Route 31 and trades it to Kyle for Onix", () => { traceFailures(); exit(VIOLET); exit(ROUTE_31_GATE, "right"); exit(ROUTE_31, "right"); p.wanted = BELLSPROUT; try { for (let round = 0; round < 400 && !p.world.state.party.some((mon) => mon.species === BELLSPROUT); round++) { if (!p.world.state.bag.balls.length) break; train(lead().level + 1, restInViolet); } } finally { p.wanted = 0; } const bellsprout = p.world.state.party.findIndex((mon) => mon.species === BELLSPROUT); expect(bellsprout, p.log.slice(-20).join("|")).toBeGreaterThan(0); exit(ROUTE_31_GATE, "left"); exit(VIOLET, "left"); enter(KYLES_HOUSE, "up"); const kyle = p.world.objects().find((object) => object.sprite === p.world.assets.map(KYLES_HOUSE).objects[1].sprite && object.id === p.world.assets.map(KYLES_HOUSE).objects[1].id)!; walk(kyle.x - 1, kyle.y); p.world.move("right"); p.world.interact(); play([1, bellsprout]); const onix = p.world.state.party.at(-1)!; expect(onix).toMatchObject({ species: ONIX, nickname: "ROCKY", ot: "KYLE", otId: 48926, dvs: 0x9666 }); expect(p.world.state.party.some((mon) => mon.species === BELLSPROUT && !mon.egg)).toBe(false); expect(p.world.state.tradeFlags).toBe(1 << 1); expect(p.world.state.pokedex.caught.has(ONIX)).toBe(true); reload(); }, 120_000); it("teaches HM05 to a new Bellsprout and lights up Dark Cave with FLASH", () => { traceFailures(); exit(VIOLET); exit(ROUTE_31_GATE, "right"); exit(ROUTE_31, "right"); p.wanted = BELLSPROUT; try { for (let round = 0; round < 400 && !p.world.state.party.some((mon) => mon.species === BELLSPROUT); round++) train(lead().level + 1, restInViolet); } finally { p.wanted = 0; } const slot = p.world.state.party.findIndex((mon) => mon.species === BELLSPROUT); expect(slot).toBeGreaterThan(0); const nickname = p.world.state.party[slot].nickname; // PACK, the TM/HM pocket's HM05, USE, YES, then the Bellsprout. expect(p.world.openStartMenu()).toBe(true); choose("PACK"); choosePack(HM05); choose("USE"); expect(toChoice().kind).toBe("yesno"); p.world.choose(1); const party = toChoice(); expect(party.kind === "menu" && party.details?.[slot]).toBe("ABLE"); p.world.choose(slot); if (p.world.state.party[slot].moves.every(Boolean)) { // A full move list asks which move to forget: the first one. expect(toChoice().kind).toBe("yesno"); p.world.choose(1); toChoice(); p.world.choose(0); } for (let i = 0; i < 10 && p.world.dialogue?.choice?.kind !== "pack"; i++) toChoice(); expect(p.log.join("\n").replace(/\n/g, " ")).toContain("learned FLASH!"); // The menus work on a draft that is kept once they close. for (let i = 0; i < 10 && p.world.busy; i++) { if (p.world.dialogue?.choice) p.world.advanceDialogue(); else p.world.tick(); } play(); expect(p.world.state.party[slot].moves).toContain(FLASH); expect(p.world.state.bag.tmsHms[50 + 4]).toBe(1); // an HM is kept // Dark Cave is dark until FLASH, which the ZEPHYRBADGE allows. enter(DARK_CAVE, "up"); expect(p.world.state.flash).toBe(false); expect(p.world.openStartMenu()).toBe(true); choose("POKéMON"); choose(nickname); choose("FLASH"); play(); expect(p.world.state.flash, p.log.slice(-6).join("|")).toBe(true); reload(); }, 120_000); it("registers Bug Catcher Wade's number and reads Earl's blackboard", () => { traceFailures(); exit(ROUTE_31); talkTo(p.world.map.objects[2].id); play([1]); const pages = p.log.join("\n").replace(/\n/g, " "); expect(pages).toContain("BUG CATCHER WADE"); expect(p.world.state.phone).toContain(PHONE_BUG_CATCHER_WADE); reload(); restInViolet(); exit(ROUTE_31_GATE, "left"); exit(VIOLET, "left"); enter(EARLS_ACADEMY, "up"); walk(3, 1); p.world.move("up"); p.world.interact(); choose("PSN"); toChoice(); expect(p.log.join("\n").replace(/\n/g, " ")).toContain("steadily loses HP"); // B leaves the blackboard. p.world.choose(-1); play(); expect(p.world.busy).toBe(false); reload(); }, 120_000); it("takes Route 32 and Union Cave to Azalea Town", () => { traceFailures(); exit(VIOLET); cross("down", ROUTE_32); // The man at Route 32's north end gives the Miracle Seed after the // ZEPHYRBADGE and the Egg. expect(p.world.state.bag.items.some((slot) => slot.item === MIRACLE_SEED)).toBe(false); p.talkTo(p.world.map.objects[7].id); play(); expect(p.world.state.bag.items.some((slot) => slot.item === MIRACLE_SEED)).toBe(true); stairs(UNION_CAVE_1F); stairs(ROUTE_33); cross("left", AZALEA); reload(); }, 240_000); it("follows Kurt into the Slowpoke Well and drives out Team Rocket", () => { traceFailures(); enter(KURTS_HOUSE, "up"); talkTo(p.world.map.objects[0].id); play(); expect(p.log.join(" ")).toContain("Old KURT is on his\nway!"); exit(AZALEA); stairs(SLOWPOKE_WELL_B1F); // The three grunts, then the one by the boulder, whose defeat ends // the scene in Kurt's house. for (const index of [0, 2, 3, 1]) { const id = p.world.map.objects[index].id; if (!p.world.objects().some((object) => object.id === id)) continue; talkTo(id); play(); } expect(p.world.mapId).toBe(KURTS_HOUSE); expect(p.world.state.events.has(EVENT_CLEARED_SLOWPOKE_WELL)).toBe(true); talkTo(p.world.map.objects[0].id); play(); expect(p.world.state.bag.balls.some((slot) => slot.item === LURE_BALL)).toBe(true); reload(); }, 120_000); it("beats Bugsy for the HIVEBADGE and the rival on the way to Ilex Forest", () => { traceFailures(); exit(AZALEA); heal(AZALEA_CENTER, AZALEA); enter(AZALEA_GYM, "up"); talkTo(p.world.map.objects[0].id); play(); expect(p.world.state.events.has(EVENT_BEAT_BUGSY)).toBe(true); expect(p.world.state.flags.has(ENGINE_HIVEBADGE)).toBe(true); expect(p.world.state.bag.tmsHms[48]).toBe(1); // TM49 FURY CUTTER exit(AZALEA); heal(AZALEA_CENTER, AZALEA); // The rival waits at the town's west end (SCENE_AZALEATOWN_RIVAL_BATTLE). stairs(ILEX_GATE); expect(p.world.state.events.has(EVENT_RIVAL_AZALEA_TOWN)).toBe(true); reload(); }, 120_000); it("herds the Farfetch'd back in Ilex Forest and cuts through to Route 34", () => { traceFailures(); stairs(ILEX_FOREST); const STEP = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] } as const; const bird = p.world.map.objects[0].id; // wFarfetchdPosition 1 → 2 from any side, 2 → 8 and 8 → 9 facing // down, then 9 → 10 facing left. for (const face of ["up", "down", "down", "left"] as const) { const at = p.world.objects().find((object) => object.id === bird)!; walk(at.x - STEP[face][0], at.y - STEP[face][1]); p.world.move(face); p.world.interact(); play(); } expect(p.world.state.memory.wFarfetchdPosition).toBe(10); expect(p.world.state.events.has(EVENT_HERDED_FARFETCHD)).toBe(true); talkTo(p.world.map.objects[2].id); // the charcoal master play(); expect(p.world.state.bag.tmsHms[50]).toBe(1); // HM01 reload(); const bellsprout = p.world.state.party.findIndex((mon) => mon.species === BELLSPROUT); p.teach(HM01, bellsprout); expect(p.world.state.party[bellsprout].moves).toContain(CUT); p.cut(8, 25); expect(p.log).toContain("BELLSPROUT used\nCUT!"); stairs(ROUTE_34_GATE); reload(); writeTestSave("route-34"); }, 120_000); it("reaches Goldenrod City and gets a Bicycle", () => { traceFailures(); stairs(ROUTE_34); cross("up", GOLDENROD); heal(GOLDENROD_CENTER, GOLDENROD); enter(BIKE_SHOP, "up"); talkTo(p.world.map.objects[0].id); // the clerk, across the counter play([1]); expect(p.world.state.bag.keyItems).toContain(BICYCLE); exit(GOLDENROD); reload(); }, 120_000); it("trains on Route 34 and beats Whitney for the PLAINBADGE", () => { traceFailures(); const restInGoldenrod = () => { cross("up", GOLDENROD); heal(GOLDENROD_CENTER, GOLDENROD); cross("down", ROUTE_34); }; cross("down", ROUTE_34); train(30, restInGoldenrod); restInGoldenrod(); cross("up", GOLDENROD); enter(GOLDENROD_GYM, "up"); const whitney = p.world.map.objects[0].id; talkTo(whitney); play(); expect(p.world.state.events.has(EVENT_BEAT_WHITNEY)).toBe(true); // She cries; Bridget explains on the way out, then the badge. exit(GOLDENROD); expect(p.log.join(" ")).toContain("WHITNEY cry."); enter(GOLDENROD_GYM, "up"); talkTo(whitney); play(); expect(p.world.state.flags.has(ENGINE_PLAINBADGE)).toBe(true); expect(p.world.state.bag.tmsHms[44]).toBe(1); // TM45 ATTRACT exit(GOLDENROD); reload(); }, 240_000); it("answers the Radio Tower quiz for the radio card", () => { traceFailures(); enter(RADIO_TOWER_1F, "up"); talkTo(p.world.map.objects[5].id); play([1, 1, 1, 0, 1, 0]); expect(p.world.state.flags.has(ENGINE_RADIO_CARD)).toBe(true); exit(GOLDENROD); reload(); writeTestSave("goldenrod-city"); }, 120_000); it("meets Floria on Route 36 and gets the SquirtBottle from the flower shop", () => { traceFailures(); toRoute36(); // ROUTE36_FLORIA is the eighth object in Route36_MapEvents. talkTo(p.world.map.objects[7].id); play(); expect(p.world.state.events.has(EVENT_MET_FLORIA)).toBe(true); toGoldenrod(); enter(FLOWER_SHOP, "up"); // Floria walked here from Route 36; talk to her first // (EVENT_TALKED_TO_FLORIA_AT_FLOWER_SHOP), then the Teacher gives // the SquirtBottle since the PLAINBADGE is already held. talkTo(p.world.map.objects[1].id); play(); talkTo(p.world.map.objects[0].id); play([1]); // The Squirtbottle is a key item (ItemAttributes' pocket byte). expect(p.world.state.bag.keyItems).toContain(SQUIRTBOTTLE); expect(p.world.state.events.has(EVENT_GOT_SQUIRTBOTTLE)).toBe(true); exit(GOLDENROD); reload(); }, 240_000); it("waters the weird tree on Route 36 and battles Sudowoodo, then crosses Route 37 to Ecruteak City", () => { traceFailures(); toRoute36(); const tree = p.world.map.objects[2].id; // ROUTE36_WEIRD_TREE talkTo(tree); play([1]); expect(p.world.state.events.has(EVENT_FOUGHT_SUDOWOODO)).toBe(true); cross("up", ROUTE_37); cross("up", ECRUTEAK); expect(p.world.mapId).toBe(ECRUTEAK); heal(ECRUTEAK_CENTER, ECRUTEAK); reload(); }, 240_000); it("beats all five Kimono Girls for HM03 Surf from the Dance Theater's gentleman", () => { traceFailures(); enter(DANCE_THEATER, "up"); // The five Kimono Girls are objects 2 through 6 (file order); the // gentleman who gives Surf is object 7. An already-beaten Kimono // Girl's trainer header just shows her after-text, so healing // between fights (five single-Pokémon trainers in a row otherwise // wear the lead down) is safe to repeat. for (let id = 2; id <= 6; id++) { if (lead().hp < lead().stats[0]) { exit(ECRUTEAK); heal(ECRUTEAK_CENTER, ECRUTEAK); enter(DANCE_THEATER, "up"); } talkTo(id); play(); } talkTo(7); play([1]); expect(p.world.state.bag.tmsHms[52]).toBe(1); // HM03 (50 TMs + HM03 - 1) expect(p.world.state.events.has(EVENT_GOT_HM03_SURF)).toBe(true); exit(ECRUTEAK); reload(); }, 240_000); it("meets Eusine and the rival at the Burned Tower, falling to the basement", () => { traceFailures(); // Stock up on Super Potions first: the rival's Croconaw hits the // Fire-type lead for super-effective damage on top of Haunter's // Curse, so a careful player restocks before a hard fight rather // than going in on whatever is left from Route 36. enter(ECRUTEAK_MART, "up"); const money = p.world.state.money; buy(p.world.map.objects[0].id, C.SUPER_POTION, 10); expect(p.world.state.money).toBe(money - 700 * 10); expect(p.world.state.bag.items).toContainEqual({ item: C.SUPER_POTION, quantity: 10 }); exit(ECRUTEAK); enter(BURNED_TOWER_1F, "up"); // BurnedTower1FMeetEusineScene runs automatically on the first idle // tick (sdefer), as Ecruteak's Pokémon Center's Bill scene does. expect(p.log.join(" ")).toContain("EUSINE: My name's"); // The only way to any tile by the rival crosses the coordinate event // at (11, 9); walking there triggers BurnedTowerRivalBattleScript, // which ends with the player falling to the basement, so the usual // walk() helper (which expects to stay on this map unless stepping // onto a listed warp) does not fit here. // A loss whites out (healed) to Ecruteak's spawn point outside the // Pokémon Center with the scene still set, so the player tries again. for (let attempt = 0; attempt < 3; attempt++) { for (const direction of findPath(p.world, 11, 9)) { if (!p.world.move(direction)) break; play(); if (p.world.mapId !== BURNED_TOWER_1F) break; } if (p.world.mapId === BURNED_TOWER_B1F) break; expect(p.world.mapId, "whited out to Ecruteak").toBe(ECRUTEAK); enter(BURNED_TOWER_1F, "up"); } expect(p.world.state.events.has(EVENT_RIVAL_BURNED_TOWER)).toBe(true); expect(p.world.state.events.has(EVENT_HOLE_IN_BURNED_TOWER)).toBe(true); expect(p.world.mapId).toBe(BURNED_TOWER_B1F); reload(); }, 240_000); it("releases the beasts, says goodbye to Eusine and climbs back to Ecruteak", () => { traceFailures(); // The coordinate event at (10, 6) runs ReleaseTheBeasts: the three // beasts flee one by one through InitRoamMons, which starts Raikou // and Entei roaming (Suicune stays put until Cianwood). walk(10, 6); play(); expect(p.world.state.events.has(EVENT_RELEASED_THE_BEASTS)).toBe(true); expect(p.world.state.events.has(EVENT_BURNED_TOWER_MORTY)).toBe(true); expect(p.world.state.events.has(EVENT_ECRUTEAK_GYM_GRAMPS)).toBe(true); expect(p.world.state.events.has(EVENT_ECRUTEAK_CITY_GRAMPS)).toBe(false); expect(p.world.state.roamMons[0]).toMatchObject({ species: RAIKOU, mapId: ROUTE_42 }); expect(p.world.state.roamMons[1]).toMatchObject({ species: ENTEI, mapId: ROUTE_37 }); // Eusine appears once the beasts are gone (BurnedTowerB1F_EUSINE's // own flag, set by `appear` rather than a scene variable) and says // goodbye before leaving through the tower's own exit sound. talkTo(p.world.map.objects[8].id); play(); expect(p.log.join(" ")).toContain("I'm all choked up!"); // Multiple small stairs lead back to 1F now that the ladder at (6, // 14) is in place (BurnedTowerB1FLadderCallback); any of them works. stairs(BURNED_TOWER_1F); exit(ECRUTEAK); reload(); }, 120_000); it("beats Ecruteak Gym's trainers and Morty for the FOGBADGE and TM30", () => { traceFailures(); // Morty's own Gengar leans on Hypnosis and Dream Eater; a careful // player also stocks Awakenings, not just Super Potions, before a // Ghost-type gym. enter(ECRUTEAK_MART, "up"); buy(p.world.map.objects[0].id, C.AWAKENING, 5); expect(p.world.state.bag.items).toContainEqual({ item: C.AWAKENING, quantity: 5 }); exit(ECRUTEAK); // EcruteakGymForcedToLeaveScene (the default scene) no longer plays: // ReleaseTheBeasts already set SCENE_ECRUTEAKGYM_NOOP for this map. enter(ECRUTEAK_GYM, "up"); // The trainers stand past Morty's own invisible-floor maze; findPath // already treats every unlisted pit tile as impassable (the same // warps the player would fall through), so it finds the real floor // on its own, the way a player who knows the safe path would. const [morty, jeffrey, ping, martha, grace] = p.world.map.objects.map( (object) => object.id, ); // Ghost types shrug off Normal moves entirely ("No normal-type // attack can harm them!", straight from Sage Ping's own text), so // EMBER is the lead's only way to damage any of them; a cautious // player heals between fights (restoring its PP, not just HP) so // a long multi-Pokémon trainer never finds EMBER run dry. for (const trainer of [jeffrey, ping, martha, grace]) { exit(ECRUTEAK); heal(ECRUTEAK_CENTER, ECRUTEAK); enter(ECRUTEAK_GYM, "up"); talkTo(trainer); play(); } expect(p.world.state.events.has(EVENT_BEAT_SAGE_JEFFREY)).toBe(true); expect(p.world.state.events.has(EVENT_BEAT_SAGE_PING)).toBe(true); expect(p.world.state.events.has(EVENT_BEAT_MEDIUM_MARTHA)).toBe(true); expect(p.world.state.events.has(EVENT_BEAT_MEDIUM_GRACE)).toBe(true); exit(ECRUTEAK); heal(ECRUTEAK_CENTER, ECRUTEAK); enter(ECRUTEAK_GYM, "up"); talkTo(morty); play([1]); expect(p.world.state.events.has(EVENT_BEAT_MORTY), p.log.join("|")).toBe(true); expect(p.world.state.flags.has(ENGINE_FOGBADGE)).toBe(true); expect(p.log.join(" ")).toContain("FOGBADGE"); // MortyText_FogBadgeSpeech and the TM30 Shadow Ball gift follow in // the same conversation (EVENT_GOT_TM30_SHADOW_BALL was not yet set). expect(p.world.state.events.has(EVENT_GOT_TM30_SHADOW_BALL)).toBe(true); expect(p.world.state.bag.tmsHms[29]).toBe(1); // TM30 expect(p.log.join(" ")).toContain("SHADOW BALL"); exit(ECRUTEAK); reload(); writeTestSave("ecruteak-city"); }, 240_000); it("crosses Routes 38 and 39 to Olivine City, where the rival is waiting", () => { traceFailures(); // Ecruteak's west gate and the gate's own west side are edge warps. exit(ROUTE_38_GATE, "left"); exit(ROUTE_38, "left"); cross("left", ROUTE_39); cross("down", OLIVINE); // OlivineCityRivalSceneTop/Bottom: the way from Route 39 to the // Pokémon Center crosses one of the two coordinate events. heal(OLIVINE_CENTER, OLIVINE); expect(p.log.join(" ")).toContain("GYM LEADER isn't\nhere."); expect(p.world.state.events.has(EVENT_RIVAL_OLIVINE_CITY)).toBe(true); reload(); }, 240_000); it("gets HM04 and the Good Rod, fishes up a Krabby and teaches it Surf", () => { traceFailures(); enter(OLIVINE_CAFE, "up"); talkTo(p.world.map.objects[0].id); // OLIVINECAFE_SAILOR1 play(); expect(p.world.state.events.has(EVENT_GOT_HM04_STRENGTH)).toBe(true); exit(OLIVINE); enter(GOOD_ROD_HOUSE, "up"); talkTo(p.world.map.objects[0].id); play([1]); expect(p.world.state.events.has(EVENT_GOT_GOOD_ROD)).toBe(true); expect(p.world.state.bag.keyItems).toContain(GOOD_ROD); exit(OLIVINE); // Olivine's Mart has Great Balls, not Poké Balls. enter(OLIVINE_MART, "up"); buy(p.world.map.objects[0].id, C.GREAT_BALL, 5); exit(OLIVINE); // Typhlosion cannot learn Surf: fish a Krabby (FISHGROUP_SHORE's // Good Rod table) from the nearest shore tile facing open water. const STEP = { up: [0, -1], down: [0, 1], left: [-1, 0], right: [1, 0] } as const; const map = p.world.map; const shore = (Object.keys(STEP) as (keyof typeof STEP)[]) .flatMap((face) => Array.from({ length: map.width * map.height }, (_, i) => { const x = i % map.width, y = Math.floor(i / map.width); const [dx, dy] = STEP[face]; if (p.world.assets.collision(map, x + dx, y + dy) !== 0x29) return []; try { return [{ x, y, face, length: findPath(p.world, x, y).length }]; } catch { return []; } }).flat(), ) .sort((a, b) => a.length - b.length)[0]; walk(shore.x, shore.y); p.wanted = KRABBY; for (let cast = 0; cast < 40; cast++) { if (p.world.state.party.some((mon) => mon.species === KRABBY)) break; p.world.move(shore.face); expect(p.world.openStartMenu()).toBe(true); choose("PACK"); choosePack(GOOD_ROD); choose("USE"); play(); } p.wanted = 0; const krabby = p.world.state.party.findIndex((mon) => mon.species === KRABBY); expect(krabby).toBeGreaterThan(0); p.teach(HM03, krabby); // Strength replaces Tackle (the first move) on the lead. p.teach(HM04, 0); expect(p.world.state.party[krabby].moves).toContain(SURF); expect(lead().moves).toContain(STRENGTH); reload(); }, 240_000); it("climbs the Lighthouse to Jasmine and her sick Ampharos", () => { traceFailures(); enter(LIGHTHOUSE[0], "up"); // Two stairways and the outside ledges lead up; Jasmine stands by // Amphy in 6F's far corner. expect(route(LIGHTHOUSE[5], LIGHTHOUSE, 12, { x: 8, y: 9 })).toBe(true); talkTo(p.world.map.objects[0].id); // OLIVINELIGHTHOUSE6F_JASMINE play(); expect(p.world.state.events.has(EVENT_JASMINE_EXPLAINED_AMPHYS_SICKNESS)).toBe(true); expect(p.log.join(" ")).toContain("wonderful PHARMACY\nin CIANWOOD"); reload(); }, 240_000); it("drops back down the Lighthouse and surfs Routes 40 and 41 to Cianwood City", () => { traceFailures(); // The holes by the windows drop to the floor below; some land in // corners cut off from the next hole, so search the floors. travel(OLIVINE, LIGHTHOUSE); heal(OLIVINE_CENTER, OLIVINE); cross("left", ROUTE_40); cross("down", ROUTE_41, { water: true }); cross("left", CIANWOOD, { water: true }); expect(p.world.mapId).toBe(CIANWOOD); reload(); }, 240_000); it("meets Suicune and Eusine, gets the SecretPotion and beats Chuck for the STORMBADGE and HM02", () => { traceFailures(); sail(23, 44); // ashore by the Pokémon Center heal(CIANWOOD_CENTER, CIANWOOD); // ReleaseTheBeasts armed SCENE_CIANWOODCITY_SUICUNE_AND_EUSINE; its // coordinate event is on the way north. walk(11, 16); expect(p.world.state.events.has(EVENT_FOUGHT_EUSINE)).toBe(true); expect(p.log.join(" ")).toContain("SUICUNE running on\nthe waves."); heal(CIANWOOD_CENTER, CIANWOOD); enter(CIANWOOD_PHARMACY, "up"); talkTo(p.world.map.objects[0].id); play(); expect(p.world.state.events.has(EVENT_GOT_SECRETPOTION_FROM_PHARMACY)).toBe(true); expect(p.world.state.bag.keyItems).toContain(SECRETPOTION); // Afterwards the pharmacist runs MARTTYPE_PHARMACY. buy(p.world.map.objects[0].id, C.SUPER_POTION, 10); exit(CIANWOOD); // The four black belts stand on the way to the boulders. enter(CIANWOOD_GYM, "up"); walk(3, 8); exit(CIANWOOD); // Chuck's Poliwrath can win with critical hits; a loss whites out to // Cianwood's Pokémon Center and the boulders go back in place. for (let attempt = 0; attempt < 3; attempt++) { if (p.world.state.events.has(EVENT_BEAT_CHUCK)) break; heal(CIANWOOD_CENTER, CIANWOOD); enter(CIANWOOD_GYM, "up"); walk(3, 8); // AskStrengthScript from the left boulder, then push it up, the // right one up and the middle one left into the gap. p.world.move("up"); p.world.interact(); play([1]); p.world.move("up"); play(); walk(5, 8); p.world.move("up"); play(); walk(5, 7); p.world.move("left"); play(); walk(4, 2); p.world.move("up"); p.world.interact(); play([1]); } expect(p.world.state.events.has(EVENT_BEAT_CHUCK)).toBe(true); expect(p.world.state.flags.has(ENGINE_STORMBADGE)).toBe(true); expect(p.world.state.events.has(EVENT_GOT_TM01_DYNAMICPUNCH)).toBe(true); expect(p.world.state.bag.tmsHms[0]).toBe(1); // TM01 exit(CIANWOOD); talkTo(p.world.map.objects[9].id); // CIANWOODCITY_POKEFAN_F play(); expect(p.world.state.events.has(EVENT_GOT_HM02_FLY)).toBe(true); expect(p.world.state.bag.tmsHms[51]).toBe(1); // HM02 reload(); writeTestSave("cianwood-city"); }, 240_000); it("brings the SecretPotion to Amphy and beats Jasmine for the MINERALBADGE and TM23", () => { traceFailures(); // Nobody in the party learns Fly; surf back. cross("right", ROUTE_41, { water: true }); cross("up", ROUTE_40, { water: true }); cross("right", OLIVINE, { water: true }); heal(OLIVINE_CENTER, OLIVINE); enter(LIGHTHOUSE[0], "up"); expect(route(LIGHTHOUSE[5], LIGHTHOUSE, 12, { x: 8, y: 9 })).toBe(true); talkTo(p.world.map.objects[0].id); play([1]); expect(p.world.state.events.has(EVENT_JASMINE_RETURNED_TO_GYM)).toBe(true); expect(p.world.state.bag.keyItems).not.toContain(SECRETPOTION); travel(OLIVINE, LIGHTHOUSE); heal(OLIVINE_CENTER, OLIVINE); enter(OLIVINE_GYM, "up"); talkTo(p.world.map.objects[0].id); play([1]); expect(p.world.state.events.has(EVENT_BEAT_JASMINE)).toBe(true); expect(p.world.state.flags.has(ENGINE_MINERALBADGE)).toBe(true); expect(p.world.state.events.has(EVENT_GOT_TM23_IRON_TAIL)).toBe(true); expect(p.world.state.bag.tmsHms[22]).toBe(1); // TM23 exit(OLIVINE); reload(); writeTestSave("olivine-city"); }, 240_000); it("crosses Route 42 past Mt. Mortar to Mahogany Town", () => { traceFailures(); cross("up", ROUTE_39); cross("right", ROUTE_38); stairs(ROUTE_38_GATE); stairs(ECRUTEAK); stairs(ROUTE_42_GATE); stairs(ROUTE_42); // Route 42's pond splits it around Mt. Mortar; Krabby surfs across. cross("right", MAHOGANY, { water: true }); expect(p.world.state.events.has(EVENT_BEAT_POKEMANIAC_SHANE)).toBe(true); reload(); }, 240_000); it("pays the Rockets' toll on Route 43 and stops the Red Gyarados for Lance", () => { traceFailures(); stairs(ROUTE_43_MAHOGANY_GATE); stairs(ROUTE_43); // Route43GateRocketShakedownScene: ¥1000 to go through. const money = p.world.state.money; stairs(ROUTE_43_GATE); expect(p.log.join(" ")).toContain("The toll is ¥1000"); expect(p.world.state.money).toBe(money - 1000); stairs(ROUTE_43); cross("up", LAKE_OF_RAGE); // The Gyarados waits in the middle of the lake (18, 22). sail(18, 23); p.world.move("up"); p.world.interact(); play(); expect(p.world.state.events.has(EVENT_LAKE_OF_RAGE_RED_GYARADOS)).toBe(true); expect(p.world.state.bag.keyItems).toContain(RED_SCALE); // Lance appears on the shore (21, 28). sail(21, 29); talkTo(p.world.map.objects[0].id); play([1]); expect(p.world.state.events.has(EVENT_DECIDED_TO_HELP_LANCE)).toBe(true); expect(p.log.join(" ")).toContain("from MAHOGANY is\nthe cause."); reload(); }, 240_000); it("follows Lance into the Rocket Hideout and learns both passwords", () => { traceFailures(); cross("down", ROUTE_43); // Route 43 sets the gate's shakedown scene again on every visit. stairs(ROUTE_43_GATE); stairs(ROUTE_43); stairs(ROUTE_43_MAHOGANY_GATE); stairs(MAHOGANY); heal(MAHOGANY_CENTER, MAHOGANY); // MahoganyMart1FLanceUncoversStaircaseScript: Dragonite's Hyper Beam. enter(MAHOGANY_SHOP, "up"); expect(p.world.state.events.has(EVENT_UNCOVERED_STAIRCASE_IN_MAHOGANY_MART)).toBe(true); // B1F's cameras and exploding traps lie on the way down. stairs(ROCKET_HIDEOUT[0]); stairs(ROCKET_HIDEOUT[1]); walk(5, 14); // LanceHealsScript1 expect(p.world.state.events.has(EVENT_LANCE_HEALED_YOU_IN_TEAM_ROCKET_BASE)).toBe(true); // B3F: the grunts tell their passwords when talked to again. stairs(ROCKET_HIDEOUT[2]); expect(p.log.join(" ")).toContain("two passwords to"); const [, , , slowpokeTail, raticateTail] = p.world.map.objects.map((object) => object.id); for (const grunt of [slowpokeTail, slowpokeTail, raticateTail, raticateTail]) { talkTo(grunt); play(); } expect(p.world.state.events.has(EVENT_LEARNED_SLOWPOKETAIL)).toBe(true); expect(p.world.state.events.has(EVENT_LEARNED_RATICATE_TAIL)).toBe(true); // The boss's door is reached from B2F's other stairs; the rival // waits on the way (RocketBaseRival). expect(route(ROCKET_HIDEOUT[2], ROCKET_HIDEOUT, 6, { x: 10, y: 10 })).toBe(true); walk(10, 10); expect(p.world.state.events.has(EVENT_RIVAL_TEAM_ROCKET_BASE)).toBe(true); p.world.move("up"); p.world.interact(); play(); walk(10, 8); // RocketBaseBossLeft expect(p.world.state.events.has(EVENT_BEAT_ROCKET_EXECUTIVEM_4)).toBe(true); talkTo(p.world.map.objects[2].id); // the Murkrow play(); expect(p.world.state.events.has(EVENT_LEARNED_HAIL_GIOVANNI)).toBe(true); reload(); }, 240_000); it("beats the executive with Lance and the Electrodes for HM06", () => { traceFailures(); expect(route(ROCKET_HIDEOUT[1], ROCKET_HIDEOUT, 6, { x: 14, y: 13 })).toBe(true); walk(14, 13); p.world.move("up"); p.world.interact(); play(); // RocketBaseBossFRight's coordinate event is two steps in. p.world.move("up"); play(); p.world.move("up"); play(); expect(p.world.state.events.has(EVENT_BEAT_ROCKET_EXECUTIVEF_2)).toBe(true); // Lance takes the right-hand Electrodes; the player the left three. for (let round = 0; round < 6; round++) { const left = p.world.map.objects .slice(4, 7) .map((object) => object.id) .filter((id) => p.world.objects().some((object) => object.id === id)); if (!left.length) break; talkTo(left[0]); play(); } expect(p.world.state.events.has(EVENT_CLEARED_ROCKET_HIDEOUT)).toBe(true); expect(p.world.state.events.has(EVENT_GOT_HM06_WHIRLPOOL)).toBe(true); expect(p.world.state.bag.tmsHms[55]).toBe(1); // HM06 travel(MAHOGANY_SHOP, ROCKET_HIDEOUT); exit(MAHOGANY); reload(); }, 240_000); it("slides across Pryce's ice for the GLACIERBADGE, and the Rockets take the Radio Tower", () => { traceFailures(); heal(MAHOGANY_CENTER, MAHOGANY); enter(MAHOGANY_GYM, "up"); talkTo(p.world.map.objects[0].id); play([1]); expect(p.world.state.events.has(EVENT_BEAT_PRYCE)).toBe(true); expect(p.world.state.flags.has(ENGINE_GLACIERBADGE)).toBe(true); expect(p.world.state.events.has(EVENT_GOT_TM16_ICY_WIND)).toBe(true); expect(p.world.state.bag.tmsHms[15]).toBe(1); // TM16 // The seventh badge runs RadioTowerRocketsScript. expect(p.world.state.flags.has(ENGINE_ROCKETS_IN_RADIO_TOWER)).toBe(true); expect(p.world.state.events.has(EVENT_RADIO_TOWER_ROCKET_TAKEOVER)).toBe(false); exit(MAHOGANY); reload(); writeTestSave("mahogany-town"); }, 240_000); /** Mahogany to Goldenrod the long way round: Route 42, Ecruteak, * Routes 37 and 36, the National Park and Route 35. */ const mahoganyToGoldenrod = () => { cross("left", ROUTE_42, { water: true }); sail(2, 8); // Route 42's pond, then ashore by the gate stairs(ROUTE_42_GATE); stairs(ECRUTEAK); cross("down", ROUTE_37); cross("down", ROUTE_36); stairs(ROUTE_36_NATIONAL_PARK_GATE); stairs(NATIONAL_PARK); stairs(ROUTE_35_NATIONAL_PARK_GATE); stairs(ROUTE_35); stairs(ROUTE_35_GATE); stairs(GOLDENROD); }; it("walks back to Goldenrod and climbs the Rocket-held Radio Tower to the fake director", () => { traceFailures(); mahoganyToGoldenrod(); heal(GOLDENROD_CENTER, GOLDENROD); enter(RADIO_TOWER[0], "up"); for (const floor of RADIO_TOWER.slice(1)) stairs(floor); walk(0, 3); // FakeDirectorScript expect(p.world.state.events.has(EVENT_BEAT_ROCKET_EXECUTIVEM_3)).toBe(true); expect(p.world.state.bag.keyItems).toContain(BASEMENT_KEY); expect(p.log.join(" ")).toContain("UNDERGROUND\nWAREHOUSE."); travel(GOLDENROD, RADIO_TOWER); reload(); }, 240_000); it("opens the basement door, works the Underground switches and frees the director", () => { traceFailures(); // Fresh PP before the rival waiting in the switch room. heal(GOLDENROD_CENTER, GOLDENROD); stairs(SWITCH_ROOM); stairs(UNDERGROUND); walk(18, 7); p.world.move("up"); p.world.interact(); play(); expect(p.world.state.events.has(EVENT_USED_BASEMENT_KEY)).toBe(true); // The door leads to the Underground's other corner. p.world.move("up"); play(); stairs(SWITCH_ROOM); // From all off, switches 3, 2 and 1 leave doors 3, 5, 6, 8, 9 and 11 // open: a way from the switches down to the warehouse. for (const [x, y] of [[2, 1], [10, 1], [16, 1]]) { walk(x, y + 1); p.world.move("up"); p.world.interact(); play([1]); } expect(p.world.save().memory).toMatchObject({ wUndergroundSwitchPositions: 6 }); expect(p.world.state.events.has(EVENT_RIVAL_GOLDENROD_UNDERGROUND)).toBe(true); stairs(WAREHOUSE); // The warehouse resets the switches when entered. expect(p.world.save().memory).toMatchObject({ wUndergroundSwitchPositions: 0 }); talkTo(p.world.map.objects[3].id); play(); expect(p.world.state.events.has(EVENT_RECEIVED_CARD_KEY)).toBe(true); expect(p.world.state.bag.keyItems).toContain(CARD_KEY); // Out through the department store's basement and elevator. stairs(DEPT_STORE_B1F); stairs(ELEVATOR); walk(3, 1); p.world.move("up"); p.world.interact(); play([1]); // 1F walk(2, 3); p.world.move("down"); play(); expect(p.world.mapId).toBe(DEPT_STORE_1F); exit(GOLDENROD); reload(); }, 240_000); it("opens the 3F shutters with the Card Key and beats the executive for the Clear Bell", () => { traceFailures(); heal(GOLDENROD_CENTER, GOLDENROD); enter(RADIO_TOWER[0], "up"); stairs(RADIO_TOWER[1]); stairs(RADIO_TOWER[2]); walk(14, 3); // CardKeySlotScript faces up p.world.move("up"); p.world.interact(); play(); expect(p.world.state.events.has(EVENT_USED_THE_CARD_KEY_IN_THE_RADIO_TOWER)).toBe(true); walk(17, 0); // the stairs behind the shutters expect(p.world.mapId).toBe(RADIO_TOWER[3]); stairs(RADIO_TOWER[4]); walk(16, 5); // RadioTower5FRocketBossScript expect(p.world.state.events.has(EVENT_CLEARED_RADIO_TOWER)).toBe(true); expect(p.world.state.flags.has(ENGINE_ROCKETS_IN_RADIO_TOWER)).toBe(false); expect(p.world.state.events.has(EVENT_GOT_CLEAR_BELL)).toBe(true); expect(p.world.state.bag.keyItems).toContain(CLEAR_BELL); // The director walks out and disappears; a save keeps him gone. reload(); travel(RADIO_TOWER[0], RADIO_TOWER.slice(1)); exit(GOLDENROD); reload(); }, 240_000); it("crosses Route 44 and Ice Path's boulders to Blackthorn City", () => { traceFailures(); toRoute36(); cross("up", ROUTE_37); cross("up", ECRUTEAK); stairs(ROUTE_42_GATE); stairs(ROUTE_42); cross("right", MAHOGANY, { water: true }); heal(MAHOGANY_CENTER, MAHOGANY); cross("right", ROUTE_44); stairs(ICE_PATH[0]); stairs(ICE_PATH[1]); // B1F: drop boulder 2 down its hole, then fall through another; on // B2F's ice it stops a slide on the way to B3F. p.boulders([{ id: 3, x: 4, y: 7 }], [2]); expect(p.world.state.events.has(EVENT_BOULDER_IN_ICE_PATH_2A)).toBe(false); walk(5, 12); expect(p.world.mapId).toBe(ICE_PATH[2]); walk(9, 11); expect(p.world.mapId).toBe(ICE_PATH[4]); travel(BLACKTHORN, [ICE_PATH[0], ICE_PATH[1], ICE_PATH[3], ICE_PATH[4]]); reload(); }, 240_000); it("trains on Route 45, drops the gym's boulders and beats Clair", () => { traceFailures(); // Clair's Kingdra outclasses a level 53 lead; a player trains first. cross("down", ROUTE_45); train(62, () => { cross("up", BLACKTHORN); heal(BLACKTHORN_CENTER, BLACKTHORN); cross("down", ROUTE_45); }); cross("up", BLACKTHORN); enter(BLACKTHORN_MART, "up"); buy(p.world.map.objects[0].id, C.MAX_POTION, 4); exit(BLACKTHORN); heal(BLACKTHORN_CENTER, BLACKTHORN); enter(BLACKTHORN_GYM[0], "up"); stairs(BLACKTHORN_GYM[1]); // Boulders 1 and 3 fall to 1F and bridge the way to Clair. p.boulders( [ { id: 4, x: 8, y: 3 }, { id: 6, x: 8, y: 7 }, ], [5, 7, 8, 9], ); expect(route(BLACKTHORN_GYM[0], BLACKTHORN_GYM, 4, { x: 5, y: 4 })).toBe(true); talkTo(p.world.map.objects[0].id); play([1]); expect(p.world.state.events.has(EVENT_BEAT_CLAIR)).toBe(true); expect(p.world.state.flags.has(ENGINE_RISINGBADGE)).toBe(false); expect(p.log.join(" ")).toContain("DRAGON'S DEN"); travel(BLACKTHORN, BLACKTHORN_GYM); reload(); }, 360_000); it("passes the Dragon Shrine's test for the RISINGBADGE, TM24 and Dratini", () => { traceFailures(); heal(BLACKTHORN_CENTER, BLACKTHORN); // Krabby learns Whirlpool in place of Leer for Dragon's Den. p.teach(HM06, 4, 1); expect(p.world.state.party[4].moves).toContain(250); const den = p.world.map.warps.find((warp) => warp.targetMap === DRAGONS_DEN[0])!; sail(den.x, den.y + 1); p.world.move("up"); play(); walk(3, 4); // the ladder down to 1F's lower part p.world.move("up"); play(); stairs(DRAGONS_DEN[1]); sail(19, 31, { whirlpools: true }); walk(19, 30); // DragonShrineTakeTestScript: five questions, all answered right. p.world.move("up"); play([0, 0, 1, 0, 1]); expect(p.world.mapId).toBe(DRAGON_SHRINE); expect(p.world.state.flags.has(ENGINE_RISINGBADGE)).toBe(true); expect(p.world.state.events.has(EVENT_ANSWERED_DRAGON_MASTER_QUIZ_WRONG)).toBe(false); // Clair waits by the door with TM24 (DragonsDenB1F_ClairScene). stairs(DRAGONS_DEN[1]); expect(p.world.state.events.has(EVENT_GOT_TM24_DRAGONBREATH)).toBe(true); expect(p.world.state.bag.tmsHms[23]).toBe(1); // TM24 // Back inside, the elder gives Dratini with Extremespeed. p.world.move("up"); play(); talkTo(p.world.map.objects[0].id); play(); expect(p.world.state.events.has(EVENT_GOT_DRATINI)).toBe(true); const dratini = p.world.state.party.at(-1)!; expect(dratini).toMatchObject({ species: DRATINI, level: 15 }); expect(dratini.moves).toEqual([35, 86, 239, EXTREMESPEED]); expect(dratini.pp).toEqual([20, 20, 20, 5]); // Out: the whirlpool is back after the map reload. stairs(DRAGONS_DEN[1]); sail(20, 4, { whirlpools: true }); p.world.move("up"); play(); walk(5, 14); p.world.move("up"); play(); stairs(BLACKTHORN); sail(17, 14); reload(); writeTestSave("blackthorn-city"); }, 240_000); it("takes Routes 45 and 46 down to Route 29 and walks back to Ecruteak", () => { traceFailures(); // From here on the driver plans ledge jumps (Route 45, Tin Tower). p.ledges = true; heal(BLACKTHORN_CENTER, BLACKTHORN); cross("down", ROUTE_45); // Route 45's ledges only lead down; Route 46 joins it on the west. cross("left", ROUTE_46); stairs(ROUTE_29_ROUTE_46_GATE); stairs(ROUTE_29); cross("left", CHERRYGROVE); cross("up", ROUTE_30); cross("up", ROUTE_31); stairs(ROUTE_31_GATE); stairs(VIOLET); cross("left", ROUTE_36); cross("up", ROUTE_37); cross("up", ECRUTEAK); reload(); }, 240_000); it("rings the Clear Bell for the sages and faces Suicune in Tin Tower", () => { traceFailures(); heal(ECRUTEAK_CENTER, ECRUTEAK); enter(TIN_TOWER_ENTRANCE, "up"); // EcruteakTinTowerEntranceSageBlocks: a sage steps in the way; told // about the Clear Bell, he lets the player through. walk(5, 8); p.world.move("up"); play(); if (p.world.save().direction !== "up") p.world.move("up"); p.world.interact(); play(); expect(p.log.join(" ")).toContain("The sound of that\nCLEAR BELL!"); expect(p.world.state.events.has(EVENT_RANG_CLEAR_BELL_2)).toBe(true); walk(5, 4); // the stairs at (5, 3) lead to the building's far side p.world.move("up"); play(); stairs(WISE_TRIOS_ROOM); stairs(ECRUTEAK); // TinTower1FSuicuneBattleScript: Raikou and Entei run; Suicune stays. stairs(TIN_TOWER_1F); expect(p.world.state.events.has(EVENT_FOUGHT_SUICUNE)).toBe(true); expect(p.log.join(" ")).toContain("SUICUNE"); expect(p.log.join(" ")).toContain("rainbow-"); reload(); writeTestSave("tin-tower"); }, 240_000); it("goes back to Ice Path for HM07 and walks home to New Bark Town", () => { traceFailures(); // From here on the run fights as a player facing the League would. p.careful = true; // Tin Tower's grounds lead back through the Wise Trio's room and the // entrance building, whose far side comes down by its stairs. stairs(ECRUTEAK); stairs(WISE_TRIOS_ROOM_EXIT); stairs(TIN_TOWER_ENTRANCE); walk(17, 14); p.world.move("down"); play(); stairs(ECRUTEAK); stairs(ROUTE_42_GATE); stairs(ROUTE_42); cross("right", MAHOGANY, { water: true }); heal(MAHOGANY_CENTER, MAHOGANY); cross("right", MAHOGANY_ROUTE_44); stairs(ICE_PATH[0]); // The HM07 ball at (31, 7), reached from its left over the ice. walk(30, 7); if (p.world.save().direction !== "right") p.world.move("right"); p.world.interact(); play(); expect(p.world.state.events.has(EVENT_GOT_HM07_WATERFALL)).toBe(true); expect(p.world.state.bag.tmsHms[56]).toBe(1); stairs(MAHOGANY_ROUTE_44); cross("left", MAHOGANY); cross("left", ROUTE_42, { water: true }); sail(2, 8); stairs(ROUTE_42_GATE); stairs(ECRUTEAK); cross("down", ROUTE_37); cross("down", ROUTE_36); cross("right", VIOLET); stairs(ROUTE_31_GATE); stairs(ROUTE_31); cross("down", ROUTE_30); cross("down", CHERRYGROVE); cross("right", ROUTE_29); cross("right", NEW_BARK); reload(); }, 240_000); it("surfs Route 27, climbs Tohjo Falls and takes Victory Road to the Plateau", () => { traceFailures(); // Dratini learns Waterfall; Typhlosion takes Iron Tail for Leer. p.teach(HM07, 5, 0); p.teach(tmhmItem(23), 0, 1); expect(p.world.state.party[5].moves[0]).toBe(WATERFALL); expect(p.world.state.party[0].moves[1]).toBe(IRON_TAIL); cross("right", ROUTE_27, { water: true }); travel(TOHJO_FALLS, [], { water: true }); // Up the western waterfall, along the top pool and down the eastern // one to the far shore and Route 27's other half. sail(9, 12); if (p.world.save().direction !== "up") p.world.move("up"); p.world.interact(); play([1]); expect({ x: p.world.x, y: p.world.y }).toEqual({ x: 9, y: 7 }); sail(22, 14); walk(25, 15); if (p.world.mapId === TOHJO_FALLS) { p.world.move("down"); play(); } expect(p.world.mapId).toBe(ROUTE_27); cross("right", ROUTE_26, { water: true }); travel(VICTORY_ROAD_GATE, [], { water: true }); // VictoryRoadGateBadgeCheckScript on the way north. stairs(VICTORY_ROAD); expect(p.log.join(" ")).toContain("BADGES of JOHTO!"); // Victory Road's floors are joined by its own ladders; the rival // waits by the exit. travel(ROUTE_23, [VICTORY_ROAD]); // The rival leaves (EVENT_RIVAL_VICTORY_ROAD hides him) after the battle. expect(p.world.state.events.has(EVENT_RIVAL_VICTORY_ROAD)).toBe(true); expect(p.log.join(" ")).toContain("SILVER\nwants to battle!"); reload(); }, 240_000); it("stocks up at Indigo Plateau and beats Will", () => { traceFailures(); stairs(INDIGO_PLATEAU); talkTo(2); // the nurse, across the counter play(); expect(p.world.state.party.every((mon) => mon.hp === mon.stats[0])).toBe(true); writeTestSave("indigo-plateau"); buy(3, C.FULL_HEAL, 15); buy(3, C.MAX_POTION, Math.floor((p.world.state.money - 2000) / 2500)); stairs(ELITE_FOUR[0]); play(); // the door locks behind the player talkTo(p.world.map.objects[0].id); play(); expect(p.world.state.events.has(EVENT_BEAT_ELITE_4_WILL)).toBe(true); reload(); }, 240_000); it("beats Koga, Bruno and Karen", () => { traceFailures(); const events = [EVENT_BEAT_ELITE_4_KOGA, EVENT_BEAT_ELITE_4_BRUNO, EVENT_BEAT_ELITE_4_KAREN]; ELITE_FOUR.slice(1).forEach((room, index) => { stairs(room); play(); talkTo(p.world.map.objects[0].id); play(); expect(p.world.mapId).toBe(room); expect(p.world.state.events.has(events[index])).toBe(true); }); reload(); }, 240_000); it("beats Lance and enters the Hall of Fame, then wakes in New Bark Town", () => { traceFailures(); stairs(LANCES_ROOM); play(); // Script_ApproachLanceFromRight: the step towards Lance starts it; // after the battle Mary and Oak rush in and Lance leads the way to // the Hall of Fame, where the ceremony and the credits play. walk(5, 6); p.world.move("up"); play(); const log = p.log.join(" "); expect(log).toContain("CHAMPION LANCE\nwas defeated!"); expect(log).toContain("MARY: Let's inter-"); expect(log).toContain("and your partners\nas CHAMPIONS!"); expect(p.world.state.events.has(EVENT_BEAT_CHAMPION_LANCE)).toBe(true); expect(p.world.state.events.has(EVENT_BEAT_ELITE_FOUR)).toBe(true); expect(p.world.state.hallOfFame).toBe(true); expect(p.world.state.hallOfFameCount).toBe(1); expect(p.world.state.hallOfFameTeams[0].mons.map((mon) => mon.species)).toEqual( p.world.state.party.map((mon) => mon.species), ); expect(p.world.state.hallOfFameTeams[0].mons[0].species).toBe(TYPHLOSION); // Continue after the credits: SPAWN_NEW_BARK, outside the lab. expect(p.world.mapId).toBe(NEW_BARK); expect({ x: p.world.x, y: p.world.y }).toEqual({ x: 13, y: 6 }); expect(p.world.busy).toBe(false); reload(); writeTestSave("hall-of-fame-new-bark"); }, 300_000); /** A Kanto gym leader in front of the player's way in: the battle * and the badge. */ const beatLeader = (id: number, badge: number) => { talkTo(id); play(); expect(p.world.state.flags.has(badge), p.log.slice(-3).join(" ")).toBe(true); }; it("takes the S.S. Ticket from Elm and sails the S.S. Aqua to Vermilion", () => { traceFailures(); p.world.move("down"); play(); enter(ELMS_LAB, "up"); play(); talkTo(2); // Elm play(); expect(p.world.state.events.has(EVENT_GOT_SS_TICKET_FROM_ELM)).toBe(true); expect(p.world.state.bag.keyItems).toContain(S_S_TICKET); stairs(NEW_BARK); cross("left", ROUTE_29); cross("left", CHERRYGROVE); cross("up", ROUTE_30); cross("up", ROUTE_31); stairs(ROUTE_31_GATE); stairs(VIOLET); cross("left", ROUTE_36); cross("up", ROUTE_37); cross("up", ECRUTEAK); stairs(ROUTE_38_GATE); stairs(ROUTE_38); cross("left", ROUTE_39); cross("down", OLIVINE); travel(OLIVINE_PORT, [OLIVINE_PORT_PASSAGE]); // OlivinePortWalkUpToShipScript stops the player on the way to the // gangway: YES, the ticket, and on board. try { walk(7, 16); } catch { // The script takes over the walk. } play(); expect(p.world.mapId).toBe(FAST_SHIP_1F); // FastShipB1FSailorBlocksLeft: a sailor steps in front of the // corridor west until the lazy sailor in the north cabins goes back // to work. stairs(FAST_SHIP_B1F); walk(30, 8); p.world.move("up"); play(); if (p.world.save().direction !== "up") p.world.move("up"); p.world.interact(); play(); walk(31, 13); // back up the east ladder play(); expect(route(FAST_SHIP_CABINS_NNW, [FAST_SHIP_1F], 3, { x: 4, y: 27 })).toBe(true); talkTo(6); // the lazy sailor play(); // Down to B1F and along it to the captain's cabin, where the lost // girl is found and the ship comes in. expect(route(FAST_SHIP_CABINS_SE, [FAST_SHIP_B1F, FAST_SHIP_1F], 6, { x: 1, y: 26 })).toBe(true); talkTo(5); play(); // Off the ship: the sailor at the gangway lets the player out. expect(route(FAST_SHIP_1F, [FAST_SHIP_B1F], 4, { x: 25, y: 3 })).toBe(true); talkTo(2); play(); travel(VERMILION, [VERMILION_PORT_PASSAGE]); expect(p.world.state.events.has(EVENT_FAST_SHIP_FIRST_TIME)).toBe(true); reload(); writeTestSave("vermilion-city"); }, 300_000); it("wins the THUNDERBADGE from Lt. Surge", () => { traceFailures(); heal(VERMILION_CENTER, VERMILION); // A tree hides the gym's yard; Bellsprout cuts it. p.cut(13, 18); enter(VERMILION_GYM, "up"); beatLeader(p.world.map.objects[0].id, ENGINE_THUNDERBADGE); reload(); }, 240_000); it("wins the MARSHBADGE from Sabrina", () => { traceFailures(); exit(VERMILION); p.cut(13, 18); // the tree has grown back cross("up", ROUTE_6); stairs(ROUTE_6_SAFFRON_GATE); stairs(SAFFRON); heal(SAFFRON_CENTER, SAFFRON); writeTestSave("saffron-city"); enter(SAFFRON_GYM, "up"); // Sabrina at (9, 8), past the gym's warp panels. expect(route(SAFFRON_GYM, [SAFFRON_GYM], 12, { x: 9, y: 9 })).toBe(true); beatLeader(p.world.map.objects[0].id, ENGINE_MARSHBADGE); reload(); }, 240_000); it("meets the Power Plant's manager, finds the Machine Part and wins the CASCADEBADGE", () => { traceFailures(); expect(route(SAFFRON, [SAFFRON_GYM], 12)).toBe(true); stairs(ROUTE_5_SAFFRON_GATE); stairs(ROUTE_5); cross("up", CERULEAN); heal(CERULEAN_CENTER, CERULEAN); cross("right", ROUTE_9); p.cut(5, 8); // Route 10's fence lets no one down from its Pokémon Center: the // river from Route 9's east end leads down to the Power Plant. sail(56, 17); p.world.move("down"); play(); sail(3, 11); stairs(POWER_PLANT); talkTo(7); // the manager play(); expect(p.world.state.events.has(EVENT_MET_MANAGER_AT_POWER_PLANT)).toBe(true); stairs(ROUTE_10_NORTH); // Back up the river, ashore at its top and west to Cerulean; the // tree has grown back. sail(16, 0); p.world.move("up"); play(); sail(50, 4); p.cut(5, 8); cross("left", CERULEAN); // The thief Rocket's scene on the way in, and the grunt on Route 24 // tells where he hid the part. enter(CERULEAN_GYM, "up"); play(); exit(CERULEAN); cross("up", ROUTE_24); talkTo(2); play(); cross("down", CERULEAN); enter(CERULEAN_GYM, "up"); p.inspect(3, 8); expect(p.world.state.events.has(EVENT_FOUND_MACHINE_PART_IN_CERULEAN_GYM)).toBe(true); expect(p.world.state.bag.keyItems).toContain(MACHINE_PART); exit(CERULEAN); cross("up", ROUTE_24); cross("up", ROUTE_25); // Route25MistyDate: the date breaks up as the player walks by. walk(41, 6); p.world.move("right"); play(); expect(p.log.join(" ")).toContain("MISTY"); cross("down", ROUTE_24); cross("down", CERULEAN); heal(CERULEAN_CENTER, CERULEAN); enter(CERULEAN_GYM, "up"); beatLeader(3, ENGINE_CASCADEBADGE); // Misty, on the pool's island reload(); }, 300_000); it("restores the power, crosses Rock Tunnel and takes the EXPN card in Lavender", () => { traceFailures(); exit(CERULEAN); cross("right", ROUTE_9); p.cut(5, 8); sail(56, 17); p.world.move("down"); play(); sail(3, 11); stairs(POWER_PLANT); talkTo(7); play(); expect(p.world.state.events.has(EVENT_RETURNED_MACHINE_PART)).toBe(true); expect(p.world.state.events.has(EVENT_RESTORED_POWER_TO_KANTO)).toBe(true); stairs(ROUTE_10_NORTH); sail(16, 0); p.world.move("up"); play(); sail(50, 4); // Rock Tunnel's door on Route 9 is reached from below: Route 10's // Pokémon Center grounds lead north into Route 9's corner by it. cross("down", ROUTE_10_NORTH); walk(8, 0); p.world.move("up"); play(); walk(48, 16); p.world.move("up"); play(); travel(ROUTE_10_SOUTH, ROCK_TUNNEL); cross("down", LAVENDER); heal(LAVENDER_CENTER, LAVENDER); enter(LAVENDER_RADIO_TOWER, "up"); talkTo(5); // the director, by the stairs play(); expect(p.world.state.flags.has(ENGINE_EXPN_CARD)).toBe(true); reload(); }, 300_000); it("wakes Vermilion's Snorlax with the Poké Flute channel", () => { traceFailures(); exit(LAVENDER); cross("left", ROUTE_8); stairs(ROUTE_8_SAFFRON_GATE); stairs(SAFFRON); stairs(ROUTE_6_SAFFRON_GATE); stairs(ROUTE_6); cross("down", VERMILION); // SnorlaxAwake: the radio left on the Poké Flute channel, beside it. walk(34, 10); p.tuneRadio("20.0"); if (p.world.save().direction !== "up") p.world.move("up"); p.world.interact(); play(); expect(p.world.state.events.has(EVENT_FOUGHT_SNORLAX)).toBe(true); reload(); }, 240_000); it("goes through Diglett's Cave and wins the BOULDERBADGE from Brock", () => { traceFailures(); heal(VERMILION_CENTER, VERMILION); travel(ROUTE_2, [DIGLETTS_CAVE]); p.cut(5, 8); cross("up", PEWTER); heal(PEWTER_CENTER, PEWTER); enter(PEWTER_GYM, "up"); beatLeader(p.world.map.objects[0].id, ENGINE_BOULDERBADGE); reload(); }, 240_000); it("surfs from Pallet Town to Cinnabar and wins the VOLCANOBADGE at Seafoam", () => { traceFailures(); exit(PEWTER); cross("down", ROUTE_2); p.cut(5, 8); cross("down", VIRIDIAN); heal(VIRIDIAN_CENTER, VIRIDIAN); cross("down", ROUTE_1); cross("down", PALLET); cross("down", ROUTE_21, { water: true }); cross("down", CINNABAR, { water: true }); heal(CINNABAR_CENTER, CINNABAR); cross("right", ROUTE_20, { water: true }); travel(SEAFOAM_GYM, [], { water: true }); beatLeader(2, ENGINE_VOLCANOBADGE); // Blaine reload(); }, 300_000); it("hears Blue out on Cinnabar and wins the SOULBADGE from Janine", () => { traceFailures(); stairs(ROUTE_20); cross("left", CINNABAR, { water: true }); talkTo(2); // Blue play(); expect(p.log.join(" ")).toContain("Who are you?"); expect(p.world.state.events.has(EVENT_BLUE_IN_CINNABAR)).toBe(true); cross("right", ROUTE_20, { water: true }); cross("right", ROUTE_19, { water: true }); travel(FUCHSIA, [ROUTE_19_FUCHSIA_GATE], { water: true }); heal(FUCHSIA_CENTER, FUCHSIA); enter(FUCHSIA_GYM, "up"); beatLeader(2, ENGINE_SOULBADGE); reload(); }, 300_000); it("rides up Cycling Road and wins the RAINBOWBADGE from Erika", () => { traceFailures(); exit(FUCHSIA); cross("left", ROUTE_18); // Cycling Road: on the Bicycle through the gate and up Route 17, // which continues north into Route 16 between x 4 and 9. p.world.useBicycle(); play(); stairs(ROUTE_17_ROUTE_18_GATE); stairs(ROUTE_17); p.rideUp(6, 0); // Through the Route 16 gate to its east side, then Celadon. walk(9, 7); p.world.move("right"); play(); walk(9, 5); p.world.move("right"); play(); cross("right", CELADON); if (p.world.state.playerState === 1) { p.world.useBicycle(); play(); } heal(CELADON_CENTER, CELADON); writeTestSave("celadon-city"); p.cut(28, 35); // the tree by the gym's yard enter(CELADON_GYM, "up"); beatLeader(2, ENGINE_RAINBOWBADGE); reload(); }, 300_000); it("restocks in Saffron and wins the EARTHBADGE from Blue", () => { traceFailures(); exit(CELADON); p.cut(28, 35); // grown back heal(CELADON_CENTER, CELADON); cross("right", ROUTE_7); stairs(ROUTE_7_SAFFRON_GATE); stairs(SAFFRON); // Saffron's mart sells Max Potions. enter(SAFFRON_MART, "up"); buy(2, C.MAX_POTION, Math.min(99, Math.floor((p.world.state.money - 3000) / 2500))); exit(SAFFRON); stairs(ROUTE_6_SAFFRON_GATE); stairs(ROUTE_6); cross("down", VERMILION); travel(ROUTE_2, [DIGLETTS_CAVE]); p.cut(5, 8); cross("down", VIRIDIAN); heal(VIRIDIAN_CENTER, VIRIDIAN); enter(VIRIDIAN_GYM, "up"); beatLeader(2, ENGINE_EARTHBADGE); expect(p.world.state.events.has(EVENT_BEAT_BLUE)).toBe(true); reload(); }, 300_000); it("has Oak open Mt. Silver for sixteen badges", () => { traceFailures(); exit(VIRIDIAN); heal(VIRIDIAN_CENTER, VIRIDIAN); cross("down", ROUTE_1); cross("down", PALLET); enter(OAKS_LAB, "up"); talkTo(2); // Oak play(); expect(p.world.state.events.has(EVENT_TALKED_TO_OAK_IN_KANTO)).toBe(true); expect(p.world.state.events.has(EVENT_OPENED_MT_SILVER)).toBe(true); expect(p.log.join(" ")).toContain("POKéMON seen"); reload(); }, 240_000); it("reaches Mt. Silver by Route 28 and trains outside Silver Cave", () => { traceFailures(); exit(PALLET); cross("up", ROUTE_1); cross("up", VIRIDIAN); cross("left", ROUTE_22); stairs(VICTORY_ROAD_GATE); stairs(ROUTE_28); cross("left", SILVER_CAVE_OUTSIDE); heal(SILVER_CAVE_CENTER, SILVER_CAVE_OUTSIDE); writeTestSave("mt-silver"); // Red's team is in the 70s and 80s: the lead trains in steps. travel(SILVER_CAVE_ROOM_3, SILVER_CAVE); travel(SILVER_CAVE_OUTSIDE, SILVER_CAVE); const rest = () => heal(SILVER_CAVE_CENTER, SILVER_CAVE_OUTSIDE); for (const level of [84, 88, 91, 93, 95]) train(level, rest); expect(lead().level).toBeGreaterThanOrEqual(95); reload(); }, 900_000); it("takes TM26 from Victory Road, beats Red and sees the credits", () => { traceFailures(); cross("right", ROUTE_28); stairs(VICTORY_ROAD_GATE); stairs(VICTORY_ROAD); expect(route(VICTORY_ROAD, [VICTORY_ROAD], 8, { x: 4, y: 28 })).toBe(true); talkTo(3); // TM26's ball play(); p.teach(tmhmItem(26), 0, 2); // Earthquake for Smokescreen expect(lead().moves[2]).toBe(EARTHQUAKE); travel(VICTORY_ROAD_GATE, [VICTORY_ROAD]); stairs(ROUTE_28); cross("left", SILVER_CAVE_OUTSIDE); heal(SILVER_CAVE_CENTER, SILVER_CAVE_OUTSIDE); travel(SILVER_CAVE_ROOM_3, SILVER_CAVE); talkTo(2); // Red play(); const log = p.log.join(" "); expect(log).toContain("RED\nwas defeated!"); // RedCredits, then SPAWN_MT_SILVER outside the cave, with Red gone // for good and the party healed. expect(p.world.state.events.has(EVENT_RED_IN_MT_SILVER)).toBe(true); expect(p.world.mapId).toBe(SILVER_CAVE_OUTSIDE); expect({ x: p.world.x, y: p.world.y }).toEqual({ x: 23, y: 20 }); expect(p.world.state.party.every((mon) => mon.hp === mon.stats[0])).toBe(true); expect(p.world.busy).toBe(false); reload(); writeTestSave("red-beaten"); }, 300_000); }, );