From 8b656d1a218d9c759b81fe85fe3e4f7ed16112a4 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 10:20:41 -0800 Subject: [PATCH 01/28] FRLG story drafts 00.29-00.31: Lavender to the Silph Scope Unverified authored-checkpoint drafts, outside the embedded StoryData set: the Lavender Mart, Route 8, the Underground Path, a Pidgey for Fly, and the Rocket Hideout to Giovanni. The README lists the whole 00.29-00.51 draft, its conventions, the user's open decisions and the leg check. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../AutoStory/StoryData/Draft/README.md | 135 ++++++++++++++++++ .../StoryData/Draft/frlg-00.29.plan.json | 91 ++++++++++++ .../StoryData/Draft/frlg-00.30.plan.json | 86 +++++++++++ .../StoryData/Draft/frlg-00.31.plan.json | 105 ++++++++++++++ 4 files changed, 417 insertions(+) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.29.plan.json create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.31.plan.json diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md new file mode 100644 index 0000000000..8046824952 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md @@ -0,0 +1,135 @@ +# FRLG story drafts, 00.29 to the Hall of Fame + +**Every file here is an unverified draft.** None has run on a console, and the +story table doesn't load them: `pa_embed_text_files()` embeds only +`StoryData/*.json`, not this folder. The authored format has no status field, +so this README is where they are marked unverified. + +Each file is a `pokesim-authored-checkpoint/1` plan in the same format as +`../frlg-parcel-00.05.plan.json`. The order and the item stops follow PokeGuy's +LeafGreen run (PokemonSimulation `data/runs/pokeguy-leafgreen-15934/events.json` +and `external/pokeguy-notes.md`). They start where 00.28 (Rock Tunnel to +Lavender, on `frlg/stage8-rocktunnel`) is expected to end: at Lavender's nurse. + +| Id | Title | Events | +|---|---|---:| +| 00.29 | Lavender Mart, then west over Route 8: Gamer Rich, a Pidgey for Fly, the Underground Path to Celadon | 12 | +| 00.30 | Celadon Game Corner and the Rocket Hideout down to B4F: the poster guard, the Lift Key Grunt, the LIFT KEY and B3F's Rare Candy | 12 | +| 00.31 | Rocket Hideout: the lift to B4F, the two door guards, two Rare Candies, Blastoise, Giovanni and the SILPH SCOPE | 16 | +| 00.32 | Celadon: Department Store 5F battle items, HM02 Fly on Route 16 for the Pidgey, the Tea, then the Center | 20 | +| 00.32b | Celadon Mansion roof room: the Eevee (Gift RNG placeholder) | 4 | +| 00.33 | Fly to Lavender, the Center, Pokemon Tower 2F: the rival | 8 | +| 00.34 | Pokemon Tower 3F to 7F: Tammy, Jennifer, Emilia, the Marowak, three Grunts, Mr. Fuji | 21 | +| 00.35 | The POKE FLUTE, Fly to Celadon, Route 16's Snorlax, Cycling Road's Rare Candy and Max Elixir, Fuchsia | 16 | +| 00.36 | Safari Zone: a Full Restore, the GOLD TEETH, HM03 Surf from the Secret House | 12 | +| 00.37 | Fuchsia: Surf to the lead, Juggler Kirk, Koga, then HM04 Strength from the Warden | 14 | +| 00.38 | Fly to Pallet, Surf down Route 21 to Cinnabar, the Mansion: Elixir, TM14 Blizzard and the SECRET KEY | 15 | +| 00.38b | Cinnabar Lab: the Helix Fossil revived (Gift RNG placeholder) | 6 | +| 00.39 | Cinnabar Gym: the quiz doors, Blaine | 9 | +| 00.40 | Fly to Celadon: two Rare Candies, Blizzard, Celadon Gym: Tamia, Lori, Erika | 21 | +| 00.41 | East to Saffron: the TEA at the gate, the Fighting Dojo's Hitmonchan (Gift RNG placeholder), the Center | 13 | +| 00.42 | Silph Co.: the CARD KEY, the rival on 7F, the Lapras (Gift RNG placeholder) | 10 | +| 00.43 | Silph Co. 11F: the boardroom Grunt, Giovanni; then Sabrina | 13 | +| 00.44 | Fly to Viridian: Viridian Gym: Takashi, Warren, Giovanni | 14 | +| 00.45 | Route 22: the rival; Route 23's badge gates; Strength | 7 | +| 00.46 | Victory Road's boulders, Indigo Plateau: Full Restores, the nurse, the save | 8 | +| 00.47 | Lorelei | 4 | +| 00.48 | Bruno | 3 | +| 00.49 | Agatha | 3 | +| 00.50 | Lance | 3 | +| 00.51 | The Champion and the Hall of Fame | 4 | + +## Conventions + +- A `battle` event's `id` is the battle's id in PokemonSimulation + `docs/battles/pokeguy-trainers.json`, e.g. `route8-rich`, `koga`. Its strategy + belongs to the battle agent, not to these files. Two battles have no entry + there: `tower-marowak` (an `events.json` scripted fight) and + `dojo-karate-master` (not in PokeGuy's run). +- Shopping is a `mart ...` `game_action` with the quantities from + PokemonSimulation `docs/battles/pokeguy-conditions.md` bags and PokeGuy's + receipts: Lavender (4 Escape Ropes, 7 Super Repels), Celadon 5F (6 X Speed, + 21 X Special, 5 X Accuracy, 2 Guard Spec), Indigo Plateau (12 Full Restores). +- A pickup faces the ball and talks (A). Hidden items (Cycling Road's Rare Candy + and Max Elixir, the Mansion's Elixir) have no tile in the candidate maps. +- The story's shiny gifts are placeholders: a `party_room free=1` check, then a + `gift_rng ` handoff, for the Eevee (00.32b), the Omanyte from the Helix + Fossil (00.38b), the Hitmonchan (00.41) and the Lapras (00.42). The authored + format can't yet say `handoff`/`live_start` as 00.12/00.12b do in C++. +- New `game_action` variants that no executor has yet: `party_room`, + `gift_rng`, `use_item`, `fly`, `flee_wild`, `surf`, `strength_puzzle`, + `reenter_map`. The `mart` variants name Marts the Mart table doesn't list + yet (Lavender, Celadon 5F, Indigo Plateau). +- HMs and key items come in story order: LIFT KEY, SILPH SCOPE, HM02 Fly, TEA, + POKE FLUTE, HM03 Surf, GOLD TEETH, HM04 Strength, SECRET KEY, CARD KEY. + +## Decisions for the user + +- **A Pidgey for Fly.** PokeGuy flies everywhere from 1:03; the story party has + no flier. 00.29 catches a Pidgey in Route 7's grass (Route 8's grass is behind + two Cut trees). Without Fly, Cinnabar needs a long walk round to Pallet. +- **TM14 Blizzard over Mega Kick** (00.40) is a one-shot TM: it needs the team + plan first. +- **Strength over Water Pulse** (00.45) and **Surf over Water Gun** (00.37) + follow PokeGuy. +- **The Hitmonchan** needs the Dojo's Karate Master beaten, a fight PokeGuy + skipped. + +## Leg check + +Every `route` event was planned on PokemonSimulation `6c2f5c8` +(`data/games/frlg/maps/source-candidate`, plus `runtime` for the maps the +candidate set lacks) with `RuntimeWorld` and the shared `shortest_path()`, from +the previous event's pose. 74 legs plan; 36 are skipped, none fails. The +planner ignores trainer sight, Surf, Strength, spinner and quiz tiles, and holds +NPC wander boxes as walls. No map was edited. Where a leg crosses a map edge on +an open tile (Lavender to Route 8, Route 7 to Celadon), the executor should hold +it to a wall, because of the known off-by-one on unheld crossings. + +Two candidate maps look wrong; these are map questions for the user, not edits: +the Elite Four rooms' arrival points (e.g. Lorelei's `warp_0` (6, 12)) are +wall tiles, and Indigo Plateau's door glyph is at (11, 5) with its warp at +(11, 6). + +| Skipped leg | Why | +|---|---| +| 00.29/celadon_nurse | map not in source-candidate | +| 00.31/b2f_lift | B2F's lift corner is reached over arrow tiles the planner does not solve | +| 00.31/b4f_below_guards | RocketHideout_Elevator map not in source-candidate | +| 00.32/x_item_clerk | CeladonCity_DepartmentStore_Elevator map not in source-candidate | +| 00.32/route16_below_tree | CeladonCity_DepartmentStore_Elevator map not in source-candidate | +| 00.32/celadon_nurse | map not in source-candidate | +| 00.32b/eevee_ball | Condominiums 2F/3F/RoofRoom map not in source-candidate | +| 00.35/cycling_road_rare_candy | Route17 map not in source-candidate | +| 00.35/cycling_road_max_elixir | Route17 map not in source-candidate | +| 00.35/fuchsia_below_center | Route17 map not in source-candidate | +| 00.35/fuchsia_nurse | map not in source-candidate | +| 00.37/fuchsia_nurse | map not in source-candidate | +| 00.38/mansion_b1f | the statue-switch gates and the floor drop are not modeled | +| 00.38/hidden_elixir_spot | hidden item tiles are not in the candidate maps | +| 00.38/below_tm14 | the statue-switch gates are not modeled | +| 00.38/below_secret_key | the statue-switch gates are not modeled | +| 00.38/cinnabar_nurse | map not in source-candidate | +| 00.38b/fossil_scientist | CinnabarIsland_PokemonLab_* map not in source-candidate | +| 00.39/below_blaine | the quiz doors are not modeled | +| 00.40/celadon_nurse | map not in source-candidate | +| 00.41/karate_master | SaffronCity_Dojo map not in source-candidate | +| 00.41/hitmonchan_ball | SaffronCity_Dojo map not in source-candidate | +| 00.41/saffron_center_door | SaffronCity_Dojo map not in source-candidate | +| 00.41/saffron_nurse | map not in source-candidate | +| 00.43/below_sabrina | warp-tile room order not planned: check SaffronCity_Gym warps | +| 00.44/takashi_line | arrow tiles and trainer lines not planned | +| 00.44/warren_line | arrow tiles and trainer lines not planned | +| 00.44/below_gym_giovanni | start unknown | +| 00.45/victory_road_door | Surf legs between the gates are not planned | +| 00.46/indigo_door | Victory Road boulder states are not modeled | +| 00.46/indigo_clerk | the Plateau door (11, 6) does not lead in on the candidate maps (no door glyph under the credits NPCs) | +| 00.47/below_lorelei | E4 arrival: the room's arrival point is a wall tile in the candidate map (map question, not edited) | +| 00.48/below_bruno | E4 arrival: the room's arrival point is a wall tile in the candidate map (map question, not edited) | +| 00.49/below_agatha | E4 arrival: the room's arrival point is a wall tile in the candidate map (map question, not edited) | +| 00.50/below_lance | E4 arrival: the room's arrival point is a wall tile in the candidate map (map question, not edited) | +| 00.51/below_champion | E4 arrival: the room's arrival point is a wall tile in the candidate map (map question, not edited) | + +The native codec (`Pokemon::read_authored_checkpoint()`, built alone from the +story-data-tests sources) and PokemonSimulation's `read_authored_checkpoint()` +both read all 25 files. diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.29.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.29.plan.json new file mode 100644 index 0000000000..135608d5d2 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.29.plan.json @@ -0,0 +1,91 @@ +{ + "format": "pokesim-authored-checkpoint/1", + "game": "frlg", + "checkpoint": { + "id": "00.29", + "title": "Lavender Mart, then west over Route 8: Gamer Rich, a Pidgey for Fly, the Underground Path to Celadon", + "start": {"certainty": "tile", "map": "LavenderTown_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, + "events": [ + { + "id": "lavender_mart_clerk", + "kind": "route", + "target": {"certainty": "tile", "map": "LavenderTown_Mart", "where": {"facing": "west", "kind": "tiles", "tiles": [[4, 3]]}}, + "variant": "run", + "why": "out of the Center, east along row 6 and down to the Mart (20, 15), to its clerk" + }, + { + "id": "buy_ropes_and_super_repels", + "kind": "game_action", + "variant": "mart LavenderTown_Mart budget=5700 floor=9000 escape-rope=4 super-repel=7", + "why": "pokeguy 54:18: 4 Escape Ropes and 7 Super Repels" + }, + { + "id": "east_of_rich_line", + "kind": "route", + "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "west", "kind": "tiles", "tiles": [[56, 18]]}}, + "variant": "run", + "why": "west out of Lavender onto Route 8, down column 64 east of Julia (62, 14) and west along row 18" + }, + { + "id": "rich_line", + "kind": "step", + "direction": "west", + "variant": "trainer_line", + "why": "onto (55, 18), the end of Rich's line (55, 16..18): he walks down to (55, 17)" + }, + { + "id": "route8-rich", + "kind": "battle", + "declared_after": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[55, 18]]}}, + "why": "Gamer Rich (TRAINER_GAMER_RICH): Growlithe, Vulpix Lv24" + }, + { + "id": "below_underground_door", + "kind": "route", + "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[13, 5]]}}, + "variant": "run", + "why": "west along rows 14..18, up column 14 east of Aidan's line (10..13, 6), to below the Underground Path door (13, 4)" + }, + { + "id": "route7_underground_exit", + "kind": "route", + "target": {"certainty": "tile", "map": "Route7", "where": {"facing": "south", "kind": "tiles", "tiles": [[7, 15]]}}, + "variant": "run", + "why": "through the door, the east entrance, the east-west tunnel and the west entrance, out onto Route 7" + }, + { + "id": "pidgey_grass", + "kind": "route", + "target": {"certainty": "tile", "map": "Route7", "where": {"facing": "east", "kind": "tiles", "tiles": [[14, 5]]}}, + "variant": "run", + "why": "north round the ledges into Route 7's grass (11..16, 2..5), a grass tile east of it free; Route 8's grass is behind two Cut trees" + }, + { + "id": "catch_pidgey", + "kind": "game_action", + "variant": "capture pidgey catches=1 encounters=30 balls=5 pace=east", + "why": "a Pidgey to carry HM02 Fly (pokeguy flies with a Pidgey from 1:03 on); the party has no other flier" + }, + { + "id": "celadon_below_center", + "kind": "route", + "target": {"certainty": "tile", "map": "CeladonCity", "where": {"facing": "north", "kind": "tiles", "tiles": [[48, 12]]}}, + "variant": "run", + "why": "north round the grass, west onto Celadon (row 12..15) and along row 12 to below the Center door (48, 11)" + }, + { + "id": "celadon_nurse", + "kind": "route", + "target": {"certainty": "tile", "map": "CeladonCity_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, + "variant": "run", + "why": "into the Center, to the nurse" + }, + { + "id": "heal_celadon", + "kind": "heal", + "why": "the nurse heals the party" + } + ] + }, + "execution": {"menu": "full"} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json new file mode 100644 index 0000000000..0b256ee6be --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json @@ -0,0 +1,86 @@ +{ + "format": "pokesim-authored-checkpoint/1", + "game": "frlg", + "checkpoint": { + "id": "00.30", + "title": "Celadon Game Corner and the Rocket Hideout down to B4F: the poster guard, the Lift Key Grunt, the LIFT KEY and B3F's Rare Candy", + "start": {"certainty": "tile", "map": "CeladonCity_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, + "events": [ + { + "id": "celadon_below_game_corner", + "kind": "route", + "target": {"certainty": "tile", "map": "CeladonCity", "where": {"facing": "north", "kind": "tiles", "tiles": [[34, 22]]}}, + "variant": "run", + "why": "out of the Center, west along row 12, down column 41..43 to row 22, west to below the Game Corner (34, 21)" + }, + { + "id": "below_poster_guard", + "kind": "route", + "target": {"certainty": "tile", "map": "CeladonCity_GameCorner", "where": {"facing": "north", "kind": "tiles", "tiles": [[11, 3]]}}, + "variant": "run", + "why": "into the Game Corner, up the slot rows to below the Grunt (11, 2) in front of the poster" + }, + { + "id": "talk_poster_guard", + "kind": "talk", + "why": "the Grunt guarding the poster" + }, + { + "id": "hideout-poster-guard", + "kind": "battle", + "declared_after": {"certainty": "tile", "map": "CeladonCity_GameCorner", "where": {"facing": "north", "kind": "tiles", "tiles": [[11, 3]]}}, + "why": "Rocket Grunt (TRAINER_TEAM_ROCKET_GRUNT_7): Raticate, Zubat Lv20; he runs off" + }, + { + "id": "poster_switch", + "kind": "talk", + "why": "the poster (11, 1): the switch opens the stairs (15, 2)" + }, + { + "id": "hideout_b4f_west", + "kind": "route", + "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "west", "kind": "tiles", "tiles": [[5, 3]]}}, + "variant": "run", + "why": "the stairs (15, 2) down to B1F, B1F to B2F (17, 2), B2F's arrow tiles to B3F (21, 2), B3F to B4F (15, 18), round to (5, 3), east of the tile below the Grunt (4, 2)" + }, + { + "id": "key_grunt_line", + "kind": "step", + "direction": "west", + "variant": "trainer_line", + "why": "onto (4, 3), in the Grunt's line (sight 1)" + }, + { + "id": "hideout-key-grunt", + "kind": "battle", + "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "west", "kind": "tiles", "tiles": [[4, 3]]}}, + "why": "Rocket Grunt (TRAINER_TEAM_ROCKET_GRUNT_18): Koffing, Zubat Lv21; he drops the LIFT KEY" + }, + { + "id": "below_lift_key", + "kind": "route", + "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[3, 3]]}}, + "variant": "run", + "why": "a tile west, below the LIFT KEY (3, 2)" + }, + { + "id": "lift_key", + "kind": "talk", + "why": "pick up the LIFT KEY (pokeguy 57:33)" + }, + { + "id": "beside_b3f_rare_candy", + "kind": "route", + "target": {"certainty": "tile", "map": "RocketHideout_B3F", "where": {"facing": "north", "kind": "tiles", "tiles": [[12, 13]]}}, + "variant": "run", + "why": "back up the stairs to B3F, below the Rare Candy (12, 12) beside the arrow tiles" + }, + { + "id": "b3f_rare_candy", + "kind": "talk", + "why": "pick up the Rare Candy: two, with Route 9's" + } + ] + }, + "execution": {"menu": "full"} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.31.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.31.plan.json new file mode 100644 index 0000000000..e040f84d88 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.31.plan.json @@ -0,0 +1,105 @@ +{ + "format": "pokesim-authored-checkpoint/1", + "game": "frlg", + "checkpoint": { + "id": "00.31", + "title": "Rocket Hideout: the lift to B4F, the two door guards, two Rare Candies, Blastoise, Giovanni and the SILPH SCOPE", + "start": {"certainty": "tile", "map": "RocketHideout_B3F", "where": {"facing": "north", "kind": "tiles", "tiles": [[12, 13]]}}, + "events": [ + { + "id": "b2f_lift", + "kind": "route", + "target": {"certainty": "tile", "map": "RocketHideout_B2F", "where": {"facing": "north", "kind": "tiles", "tiles": [[28, 17]]}}, + "variant": "run", + "why": "up to B2F, along the arrow tiles to the lift (28..29, 16)" + }, + { + "id": "lift_to_b4f", + "kind": "scene", + "why": "the LIFT KEY works the lift: B4F" + }, + { + "id": "b4f_below_guards", + "kind": "route", + "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[17, 16]]}}, + "variant": "run", + "why": "out of the lift to below the two Grunts (16, 14) and (19, 14)" + }, + { + "id": "talk_left_guard", + "kind": "talk", + "why": "the left door guard" + }, + { + "id": "hideout-left-guard", + "kind": "battle", + "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[17, 16]]}}, + "why": "Rocket Grunt (TRAINER_TEAM_ROCKET_GRUNT_16): Sandshrew, Sandslash, Ekans Lv23" + }, + { + "id": "talk_right_guard", + "kind": "talk", + "why": "the right door guard" + }, + { + "id": "hideout-right-guard", + "kind": "battle", + "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[17, 16]]}}, + "why": "Rocket Grunt (TRAINER_TEAM_ROCKET_GRUNT_17): Ekans, Sandshrew, Arbok Lv23; the door opens" + }, + { + "id": "rare_candy_1", + "kind": "game_action", + "variant": "use_item rare-candy slot=1", + "why": "Rare Candy: Lv35 (pokeguy 1:00:25)" + }, + { + "id": "rare_candy_2", + "kind": "game_action", + "variant": "use_item rare-candy slot=1", + "why": "Rare Candy: Lv36, Wartortle evolves" + }, + { + "id": "evolve_blastoise", + "kind": "scene", + "why": "Wartortle evolves into Blastoise; do not press B" + }, + { + "id": "below_giovanni", + "kind": "route", + "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[19, 5]]}}, + "why": "through the door to below Giovanni (19, 4)" + }, + { + "id": "talk_giovanni", + "kind": "talk", + "why": "Giovanni" + }, + { + "id": "hideout-giovanni", + "kind": "battle", + "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[19, 5]]}}, + "why": "Giovanni (TRAINER_BOSS_GIOVANNI): Onix, Rhyhorn, Kangaskhan" + }, + { + "id": "beside_silph_scope", + "kind": "route", + "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "west", "kind": "tiles", "tiles": [[21, 5]]}}, + "why": "round to east of the SILPH SCOPE (20, 5)" + }, + { + "id": "silph_scope", + "kind": "talk", + "why": "pick up the SILPH SCOPE" + }, + { + "id": "escape_rope_hideout", + "kind": "game_action", + "declared_after": {"certainty": "tile", "map": "CeladonCity", "where": {"facing": "south", "kind": "tiles", "tiles": [[34, 22]]}}, + "variant": "use_item escape-rope", + "why": "out to the Game Corner's door (pokeguy 1:02:10)" + } + ] + }, + "execution": {"menu": "full"} +} From 5a1c77026f5565a1943fee4a496db55199207acf Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 17:00:04 -0800 Subject: [PATCH 02/28] FRLG drafts: drop the 00.29 draft for #293's row; say why 00.30-00.31 aren't rows #293's 00.29 (Route 10 to Lavender's nurse) passed on the console, so the draft that reused the id is dropped. The README says what blocks 00.30 and 00.31 from becoming table rows: no Celadon or Rocket Hideout maps in the locked runtime pack, no row to Celadon's Center, no spinner or lift step. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../AutoStory/StoryData/Draft/README.md | 29 ++++-- .../StoryData/Draft/frlg-00.29.plan.json | 91 ------------------- 2 files changed, 23 insertions(+), 97 deletions(-) delete mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.29.plan.json diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md index 8046824952..bff9a09871 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md @@ -1,4 +1,4 @@ -# FRLG story drafts, 00.29 to the Hall of Fame +# FRLG story drafts, 00.30 to the Hall of Fame **Every file here is an unverified draft.** None has run on a console, and the story table doesn't load them: `pa_embed_text_files()` embeds only @@ -8,12 +8,14 @@ so this README is where they are marked unverified. Each file is a `pokesim-authored-checkpoint/1` plan in the same format as `../frlg-parcel-00.05.plan.json`. The order and the item stops follow PokeGuy's LeafGreen run (PokemonSimulation `data/runs/pokeguy-leafgreen-15934/events.json` -and `external/pokeguy-notes.md`). They start where 00.28 (Rock Tunnel to -Lavender, on `frlg/stage8-rocktunnel`) is expected to end: at Lavender's nurse. +and `external/pokeguy-notes.md`). 00.28 and 00.29 are table rows on +`frlg/stage8-rocktunnel` (#293), not drafts: 00.29 ends at Lavender's nurse. +The draft that also used the id 00.29 (Lavender Mart, Route 8, the Underground +Path to Celadon's Center) was dropped in favour of #293's row, so nothing here +covers Lavender's nurse to Celadon's yet; its events are in commit 8b656d1a2. | Id | Title | Events | |---|---|---:| -| 00.29 | Lavender Mart, then west over Route 8: Gamer Rich, a Pidgey for Fly, the Underground Path to Celadon | 12 | | 00.30 | Celadon Game Corner and the Rocket Hideout down to B4F: the poster guard, the Lift Key Grunt, the LIFT KEY and B3F's Rare Candy | 12 | | 00.31 | Rocket Hideout: the lift to B4F, the two door guards, two Rare Candies, Blastoise, Giovanni and the SILPH SCOPE | 16 | | 00.32 | Celadon: Department Store 5F battle items, HM02 Fly on Route 16 for the Pidgey, the Tea, then the Center | 20 | @@ -39,6 +41,22 @@ Lavender, on `frlg/stage8-rocktunnel`) is expected to end: at Lavender's nurse. | 00.50 | Lance | 3 | | 00.51 | The Champion and the Hall of Fame | 4 | +## Why 00.30 and 00.31 are not table rows yet + +They can't run or be simulated on this branch's data: + +- The map resource lock (PokemonSimulation `6c2f5c8`, `runtime-manifest.json`) + has none of CeladonCity, CeladonCity_GameCorner or RocketHideout_B1F..B4F; + they exist only in `source-candidate`. CeladonCity_PokemonCenter_1F (00.30's + start) and RocketHideout_Elevator (00.31's lift) are in neither. Adding them + to the runtime pack is a PokemonSimulation publish and a lock bump. +- 00.30's start, Celadon's nurse, isn't reached by any row: 00.29 ends in + Lavender. +- The walker has no spinner (arrow) tiles, which B2F and B3F need, and no + step for the lift's floor menu or the poster switch. +- `STORY_TRAINERS` names none of the hideout's Grunts or Giovanni yet; + their parties are in `BattleData/`. + ## Conventions - A `battle` event's `id` is the battle's id in PokemonSimulation @@ -66,7 +84,7 @@ Lavender, on `frlg/stage8-rocktunnel`) is expected to end: at Lavender's nurse. ## Decisions for the user - **A Pidgey for Fly.** PokeGuy flies everywhere from 1:03; the story party has - no flier. 00.29 catches a Pidgey in Route 7's grass (Route 8's grass is behind + no flier. The dropped Lavender-to-Celadon draft caught a Pidgey in Route 7's grass (Route 8's grass is behind two Cut trees). Without Fly, Cinnabar needs a long walk round to Pallet. - **TM14 Blizzard over Mega Kick** (00.40) is a one-shot TM: it needs the team plan first. @@ -93,7 +111,6 @@ wall tiles, and Indigo Plateau's door glyph is at (11, 5) with its warp at | Skipped leg | Why | |---|---| -| 00.29/celadon_nurse | map not in source-candidate | | 00.31/b2f_lift | B2F's lift corner is reached over arrow tiles the planner does not solve | | 00.31/b4f_below_guards | RocketHideout_Elevator map not in source-candidate | | 00.32/x_item_clerk | CeladonCity_DepartmentStore_Elevator map not in source-candidate | diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.29.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.29.plan.json deleted file mode 100644 index 135608d5d2..0000000000 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.29.plan.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "format": "pokesim-authored-checkpoint/1", - "game": "frlg", - "checkpoint": { - "id": "00.29", - "title": "Lavender Mart, then west over Route 8: Gamer Rich, a Pidgey for Fly, the Underground Path to Celadon", - "start": {"certainty": "tile", "map": "LavenderTown_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, - "events": [ - { - "id": "lavender_mart_clerk", - "kind": "route", - "target": {"certainty": "tile", "map": "LavenderTown_Mart", "where": {"facing": "west", "kind": "tiles", "tiles": [[4, 3]]}}, - "variant": "run", - "why": "out of the Center, east along row 6 and down to the Mart (20, 15), to its clerk" - }, - { - "id": "buy_ropes_and_super_repels", - "kind": "game_action", - "variant": "mart LavenderTown_Mart budget=5700 floor=9000 escape-rope=4 super-repel=7", - "why": "pokeguy 54:18: 4 Escape Ropes and 7 Super Repels" - }, - { - "id": "east_of_rich_line", - "kind": "route", - "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "west", "kind": "tiles", "tiles": [[56, 18]]}}, - "variant": "run", - "why": "west out of Lavender onto Route 8, down column 64 east of Julia (62, 14) and west along row 18" - }, - { - "id": "rich_line", - "kind": "step", - "direction": "west", - "variant": "trainer_line", - "why": "onto (55, 18), the end of Rich's line (55, 16..18): he walks down to (55, 17)" - }, - { - "id": "route8-rich", - "kind": "battle", - "declared_after": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[55, 18]]}}, - "why": "Gamer Rich (TRAINER_GAMER_RICH): Growlithe, Vulpix Lv24" - }, - { - "id": "below_underground_door", - "kind": "route", - "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[13, 5]]}}, - "variant": "run", - "why": "west along rows 14..18, up column 14 east of Aidan's line (10..13, 6), to below the Underground Path door (13, 4)" - }, - { - "id": "route7_underground_exit", - "kind": "route", - "target": {"certainty": "tile", "map": "Route7", "where": {"facing": "south", "kind": "tiles", "tiles": [[7, 15]]}}, - "variant": "run", - "why": "through the door, the east entrance, the east-west tunnel and the west entrance, out onto Route 7" - }, - { - "id": "pidgey_grass", - "kind": "route", - "target": {"certainty": "tile", "map": "Route7", "where": {"facing": "east", "kind": "tiles", "tiles": [[14, 5]]}}, - "variant": "run", - "why": "north round the ledges into Route 7's grass (11..16, 2..5), a grass tile east of it free; Route 8's grass is behind two Cut trees" - }, - { - "id": "catch_pidgey", - "kind": "game_action", - "variant": "capture pidgey catches=1 encounters=30 balls=5 pace=east", - "why": "a Pidgey to carry HM02 Fly (pokeguy flies with a Pidgey from 1:03 on); the party has no other flier" - }, - { - "id": "celadon_below_center", - "kind": "route", - "target": {"certainty": "tile", "map": "CeladonCity", "where": {"facing": "north", "kind": "tiles", "tiles": [[48, 12]]}}, - "variant": "run", - "why": "north round the grass, west onto Celadon (row 12..15) and along row 12 to below the Center door (48, 11)" - }, - { - "id": "celadon_nurse", - "kind": "route", - "target": {"certainty": "tile", "map": "CeladonCity_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, - "variant": "run", - "why": "into the Center, to the nurse" - }, - { - "id": "heal_celadon", - "kind": "heal", - "why": "the nurse heals the party" - } - ] - }, - "execution": {"menu": "full"} -} From cf9400fb38c88d4d2b126ac6c4c46da781aebb58 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 17:12:55 -0800 Subject: [PATCH 03/28] FRLG overworld: spinner tiles, the lift's floor and the poster switch on the sim Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Overworld/PokemonFRLG_MovementMechanics.h | 7 + .../Overworld/PokemonFRLG_OverworldMap.cpp | 62 ++++ .../Overworld/PokemonFRLG_OverworldMap.h | 31 ++ .../Overworld/PokemonFRLG_OverworldRoute.cpp | 45 ++- .../Overworld/PokemonFRLG_OverworldRoute.h | 4 + .../PokemonFRLG_OverworldRouteStrings.cpp | 3 + .../Overworld/PokemonFRLG_OverworldSim.cpp | 61 +++- .../Overworld/PokemonFRLG_OverworldSim.h | 24 ++ .../PokemonFRLG_OverworldSim_Tests.cpp | 1 + .../PokemonFRLG_OverworldSpin_Tests.cpp | 279 ++++++++++++++++++ .../Overworld/PokemonFRLG_OverworldWalk.cpp | 4 + SerialPrograms/cmake/SourceFiles.cmake | 1 + 12 files changed, 516 insertions(+), 6 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h index f7062858bc..0a20f37a7a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h @@ -62,6 +62,10 @@ inline constexpr Milliseconds STEP_TAP_SPACING_WORKED(666); // the D-pad this long. Not measured: pokefirered's fade from black is 16 // frames, then the field takes input; a tap sent at once was lost. inline constexpr Milliseconds BATTLE_EXIT_LOCKOUT(500); +// A spinner tile (Rocket Hideout, Viridian Gym) slides the player a tile +// per 8 frames (pokefirered PlayerGoSpin(): MOVE_SPEED_FAST_1). Not +// measured on the console. +inline constexpr Milliseconds SPIN_TILE_TIME(133); // The walker's presses. @@ -87,6 +91,9 @@ inline constexpr Milliseconds STEP_TAP_RELEASE(700); // How long the overworld must show nothing (no box, fade or battle) after // a battle before the next tap: twice the lockout. inline constexpr Milliseconds AFTER_BATTLE_QUIET(1000); +// The wait after a slide's last tile before the next press: the spin's +// last frames and the facing it leaves. Not measured. +inline constexpr Milliseconds SPIN_STOP_SETTLE(300); static_assert(TURN_TAP <= LONGEST_TAP && FACE_HOLD <= LONGEST_TAP && STEP_TAP <= LONGEST_TAP); static_assert(STEP_TAP + STEP_TAP_RELEASE > STEP_TAP_SPACING_WORKED); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp index 03efa4db3f..fb2317c92a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp @@ -230,6 +230,68 @@ StepKind classify_step_onto( return StepKind::WALK; } +const char SPIN_NORTH_REGION[] = "spin_north"; +const char SPIN_SOUTH_REGION[] = "spin_south"; +const char SPIN_WEST_REGION[] = "spin_west"; +const char SPIN_EAST_REGION[] = "spin_east"; +const char SPIN_STOP_REGION[] = "spin_stop"; + +namespace{ + +bool in_region(const MapData& map, const char* region, const TilePos& point){ + const auto tiles = map.regions.find(region); + return tiles != map.regions.end() && std::find(tiles->second.begin(), tiles->second.end(), point) != tiles->second.end(); +} + +} // namespace + +std::optional spin_direction(const MapData& map, const TilePos& point){ + static const std::pair ARROWS[] = { + {SPIN_NORTH_REGION, Direction::NORTH}, + {SPIN_SOUTH_REGION, Direction::SOUTH}, + {SPIN_WEST_REGION, Direction::WEST}, + {SPIN_EAST_REGION, Direction::EAST}, + }; + for (const auto& [region, direction] : ARROWS){ + if (in_region(map, region, point)){ + return direction; + } + } + return std::nullopt; +} +bool stops_spinning(const MapData& map, const TilePos& point){ + return in_region(map, SPIN_STOP_REGION, point); +} + +std::optional spin_slide(const MapData& map, const PlayerState& state){ + std::optional arrow = spin_direction(map, state.position); + if (!arrow){ + return std::nullopt; + } + SpinSlide slide{state, *arrow, 0}; + // A slide longer than the map has tiles has gone round a loop. + const int max_tiles = map.tiles.width * map.tiles.height; + while (slide.tiles <= max_tiles){ + const StepKind kind = classify_step(map, slide.end, slide.facing); + if (kind != StepKind::WALK){ + return slide; + } + slide.end = after_step(map, slide.end, slide.facing, kind); + slide.tiles++; + if (warp_at(map, slide.end.position) != nullptr){ + return std::nullopt; + } + if (stops_spinning(map, slide.end.position)){ + return slide; + } + arrow = spin_direction(map, slide.end.position); + if (arrow){ + slide.facing = *arrow; + } + } + return std::nullopt; +} + PlayerState after_step(const MapData& map, const PlayerState& state, Direction direction, StepKind kind){ return after_step_onto(map, state, map, state.position.next(direction), kind); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h index 7e1935b643..e6d7490370 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h @@ -160,6 +160,37 @@ PlayerState after_step_onto( ); +// Spinner tiles (Rocket Hideout B2F and B3F, Viridian Gym): pokefirered's +// MB_SPIN_UP, MB_SPIN_DOWN, MB_SPIN_LEFT, MB_SPIN_RIGHT and +// MB_STOP_SPINNING. The tile glyphs can't say so, so a map lists them in +// these regions, as it does WILD_REGION. +extern const char SPIN_NORTH_REGION[]; +extern const char SPIN_SOUTH_REGION[]; +extern const char SPIN_WEST_REGION[]; +extern const char SPIN_EAST_REGION[]; +extern const char SPIN_STOP_REGION[]; +// The way the arrow on this tile slides the player, if it is a spinner. +std::optional spin_direction(const MapData& map, const TilePos& point); +// Whether this tile ends a slide (MB_STOP_SPINNING). +bool stops_spinning(const MapData& map, const TilePos& point); + +// Where a slide leaves the player, the way it last moved them, and how +// many tiles it took. +struct SpinSlide{ + PlayerState end; + Direction facing = Direction::NORTH; + int tiles = 0; +}; +// The slide of a player who has just stepped onto the spinner `state` +// stands on (pokefirered TryUpdatePlayerSpinDirection()): a tile at a +// time the last arrow's way, turned by each arrow on the way, until a stop +// tile, or a step the step rules refuse (a wall, a person: the game's +// collision ends the forced movement there). Nothing if `state` isn't on a +// spinner, if the arrows turn the player round in a loop, or if the slide +// crosses a warp, which isn't modelled. +std::optional spin_slide(const MapData& map, const PlayerState& state); + + } // namespace PokemonFRLG } // namespace NintendoSwitch diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp index 8556442dd0..cdea398515 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp @@ -81,6 +81,9 @@ struct Transition{ bool warped = false; bool walked_out = false; // the warp ended with the game walking the player out bool dismounted = false; // stepped off the water onto land + // The step landed on a spinner, which slid the player on: how far, + // and the way they last moved. + std::optional spin; }; // The player taking `warp`, or nothing if it leads off the loaded maps. @@ -182,6 +185,13 @@ std::optional press(const std::vector& maps, const Located& if (transition.action == RouteAction::WALK && warps_on_step(map, landed) && warp_at(map, landed) != nullptr){ return take_warp(maps, warp_at(map, landed)); } + if (transition.action == RouteAction::WALK && spin_direction(map, landed)){ + transition.spin = spin_slide(map, transition.next.state); + if (!transition.spin){ + return std::nullopt; + } + transition.next.state = transition.spin->end; + } return transition; } @@ -268,9 +278,14 @@ class StepModel{ step.action = transition->action == RouteAction::SURF ? ACTION_SURF : 0; // Leaving the water: the player walks at half the speed from here. step.starts_leg = transition->dismounted; - step.ends_leg = transition->warped || transition->dismounted; + // A slide ends the leg: the hold must let go on the spinner. + step.ends_leg = transition->warped || transition->dismounted || transition->spin; step.warped = transition->warped; step.facing = transition->walked_out ? Direction::SOUTH : direction; + if (transition->spin){ + step.facing = transition->spin->facing; + step.extra_cost += transition->spin->tiles * route_cost(m_costs, RouteCost::TILE); + } if (transition->action == RouteAction::SURF){ step.extra_cost += route_cost(m_costs, RouteCost::SURF); } @@ -306,6 +321,24 @@ Pokemon::LegCosts to_leg_costs(const RouteCosts& costs){ return leg_costs; } +// The tiles the slide at the end of WALK `leg` takes, if it ends on a +// spinner: only a leg's last step can slide (StepModel ends the leg). +std::optional last_spin_tiles(const std::vector& maps, const Pokemon::Leg& leg){ + Located player = leg.start; + std::optional transition; + for (int c = 0; c < leg.tiles; c++){ + transition = press(maps, player, leg.direction); + if (!transition){ + return std::nullopt; + } + player = transition->next; + } + if (!transition || !transition->spin){ + return std::nullopt; + } + return transition->spin->tiles; +} + RouteStep to_route_step(const std::vector& maps, const Pokemon::Leg& leg){ RouteStep step; switch (leg.kind){ @@ -333,6 +366,10 @@ RouteStep to_route_step(const std::vector& maps, const Pokemon::Leg tiles = leg_tiles(maps, MapPosition{maps[leg.start.map].name, leg.start.state.position}, step); for (int c : std::views::iota(1, (int)tiles.size()) | std::views::filter([&](int n){ return tiles[n].map != tiles[n - 1].map; })){ step.connection_steps.push_back(c); @@ -361,7 +398,7 @@ std::optional predict_overworld_press( .position = {maps[next->next.map].name, next->next.state.position}, .elevation = next->next.state.elevation, .surfing = next->next.state.surfing, - .facing = next->walked_out ? Direction::SOUTH : direction, + .facing = next->spin ? next->spin->facing : next->walked_out ? Direction::SOUTH : direction, }, .warped = next->warped, }; @@ -424,7 +461,7 @@ std::optional locate(const std::vector& maps, const MapPositio // A plain step: no warp, no Surf, no getting off the water. std::optional plain_step(const std::vector& maps, const Located& player, Direction direction){ const std::optional transition = press(maps, player, direction); - if (!transition || transition->warped || transition->dismounted || transition->action != RouteAction::WALK){ + if (!transition || transition->warped || transition->dismounted || transition->spin || transition->action != RouteAction::WALK){ return std::nullopt; } return transition->next; @@ -473,7 +510,7 @@ std::vector leg_tiles( ){ std::vector tiles{start}; std::optional player = locate(maps, start, step.surfing); - const int along = step.tiles - (step.warps ? 1 : 0); + const int along = step.tiles - (step.warps || step.spin_tiles ? 1 : 0); for (int c = 0; player && c < along; c++){ player = plain_step(maps, *player, step.direction); if (player){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h index 82a9d65676..2d5f45f8e4 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h @@ -76,6 +76,10 @@ struct RouteStep{ // WALK: a wall stops the player at the end, so the leg can be held for // longer than it takes and still ends on its tile. bool to_wall = false; + // WALK: the last tile is a spinner, which slides the player this many + // tiles on, to `end` (spin_slide()). The hold must end on the spinner: + // held on, the player walks on after the slide. + std::optional spin_tiles; // WALK, SURF: a wild Pokemon can appear on the way: the step crosses // tall grass or water. Elsewhere (indoors, streets) no wild battle can // start, so the walker doesn't watch for one. diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRouteStrings.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRouteStrings.cpp index 4c082f2c76..8b7175b066 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRouteStrings.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRouteStrings.cpp @@ -21,6 +21,9 @@ std::string to_string(const RouteStep& step){ if (step.to_wall){ text += " (to wall)"; } + if (step.spin_tiles){ + text += std::format(" (spins {})", *step.spin_tiles); + } break; case RouteAction::SURF: text = "surf " + std::string(direction_name(step.direction)); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp index e8625e13b3..0b9483bf2c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp @@ -243,7 +243,19 @@ std::optional OverworldSim::watch_until(const std::vector& std::optional OverworldSim::take_warp(int map_index, const TilePos& tile) const{ const MapWarp* warp = warp_at(m_maps[map_index], tile); - if (warp == nullptr || !warp->to){ + if (warp == nullptr){ + return std::nullopt; + } + const auto lift = m_script.lift_floors.find(m_maps[map_index].name); + if (lift != m_script.lift_floors.end() && m_lift_floor){ + const MapPosition& out = lift->second[*m_lift_floor]; + const int destination = find_map(m_maps, out.map); + if (destination < 0){ + return std::nullopt; + } + return Step{destination, PlayerState{out.point, elevation_at(m_maps[destination], out.point), false}, true, false, false}; + } + if (!warp->to){ return std::nullopt; } const int destination = find_map(m_maps, warp->to->map); @@ -309,10 +321,17 @@ std::optional OverworldSim::press(Direction direction) const if (kind != StepKind::WALK){ return std::nullopt; } - const Step step{m_map, after_step_onto(map(), m_player, map(), ahead, kind), false, false, false}; + Step step{m_map, after_step_onto(map(), m_player, map(), ahead, kind), false, false, false}; if (warps_on_step(map(), ahead)){ return take_warp(m_map, ahead); } + if (spin_direction(map(), ahead)){ + step.spin = spin_slide(map(), step.player); + if (!step.spin){ + return std::nullopt; + } + step.player = step.spin->end; + } return step; } @@ -491,6 +510,13 @@ void OverworldSim::take_steps(Direction direction, Milliseconds pressed, Millise if (step->walked_out){ m_state.facing = Direction::SOUTH; } + if (step->spin){ + // The game moves the player through the slide; the hold, if + // it goes on, walks on from its end. + m_state.facing = step->spin->facing; + note(std::format("slid {} tile(s) to {}.", step->spin->tiles, to_string(step->spin->end.position))); + pressed += MovementMechanics::SPIN_TILE_TIME * step->spin->tiles; + } if (after_step(*step, pressed)){ return; } @@ -669,6 +695,34 @@ bool OverworldSim::cut_tree(){ return false; } +bool OverworldSim::choose_lift_floor(size_t floor){ + const auto lift = m_script.lift_floors.find(map().name); + if (lift == m_script.lift_floors.end() || floor >= lift->second.size() || m_frozen){ + violation("no lift panel on " + to_string(m_state.position) + " with a floor " + std::to_string(floor) + "."); + return false; + } + m_lift_floor = floor; + advance_to(m_now + m_script.timing.message); + note("the lift goes to " + to_string(lift->second[floor]) + "."); + return true; +} + +bool OverworldSim::push_poster_switch(){ + std::vector& objects = m_maps[m_map].objects; + const TilePos ahead = m_player.position.next(m_state.facing); + const auto poster = std::find_if(objects.begin(), objects.end(), [&](const MapObject& object){ + return object.name == "poster" && object.x == ahead.x && object.y == ahead.y; + }); + if (poster == objects.end() || m_state.screen != SimScreen::OVERWORLD || m_frozen){ + violation("no poster " + std::string(direction_name(m_state.facing)) + " of " + to_string(m_state.position) + "."); + return false; + } + std::erase_if(objects, [](const MapObject& object){ return object.name == "poster_stairs"; }); + advance_to(m_now + m_script.timing.message); + note("pushed the switch behind the poster: the stairs are open."); + return true; +} + // @@ -846,6 +900,9 @@ std::optional OverworldSimWalker::walk_step(const RouteStep& step, if (result.stopped){ return dark_stop(step, walked_near(step, result.tiles, TILE_TIME)); } + if (step.spin_tiles){ + m_sim.watch({}, SPIN_TILE_TIME * *step.spin_tiles + SPIN_STOP_SETTLE); + } const Milliseconds fade_watched_from = m_rules.watch_fade_along_hold ? m_leg_start : m_sim.state().time; if (step.warps && !after_warp(step, warps_before, fade_watched_from)){ if (!m_rules.find_player_after_warp_battle){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h index 21038720d4..4eb0884abc 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h @@ -137,6 +137,11 @@ struct SimScript{ // check and screen observer read as a battle on its way (console B, // 2026-11-09, 00.28), unless SimWalkerRules::dark_cave_not_battle. std::set dark_maps; + // A lift's map, and where its door lets the player out for each floor + // on its panel's menu, top first (pokefirered setdynamicwarp in + // RocketHideout_Elevator's scripts): the door's own warp is the + // game's placeholder. + std::map> lift_floors = {}; SimTiming timing; }; @@ -255,6 +260,18 @@ class OverworldSim{ // as its hide flag is a FLAG_TEMP. Returns whether there was one. bool cut_tree(); + // A on the panel of the lift the player stands in (SimScript:: + // lift_floors) and floor `floor` picked: the lift's door now lets out + // there. Returns whether there was such a lift and floor. + bool choose_lift_floor(size_t floor); + + // A on the poster the player faces (an object named "poster"): the + // switch behind it opens the stairs, which takes away the objects + // named "poster_stairs" for good (pokefirered + // CeladonCity_GameCorner_EventScript_Poster's setmetatile). Returns + // whether there was a poster. + bool push_poster_switch(); + private: // A screen that shows at `at`; `unfreeze`: the player can move again. struct Pending{ @@ -269,6 +286,8 @@ class OverworldSim{ bool warped = false; bool walked_out = false; // the warp ended with the game walking the player a tile south bool ledge = false; + // The step landed on a spinner, which slid the player to `player`. + std::optional spin = {}; }; void advance_to(Milliseconds time); @@ -323,6 +342,8 @@ class OverworldSim{ std::optional m_trainer; // The trees cut since the last warp, and their maps. std::vector> m_cut_trees; + // The floor picked on the lift's panel, if any. + std::optional m_lift_floor; int m_grass_steps = 0; // The game's wild encounter state (SimScript::encounter_rates). std::mt19937 m_encounter_rng; @@ -515,6 +536,9 @@ class OverworldSimWalker{ void add_tests_OverworldSim(UnitTestDatabase& database); // Register the tests of a warp's fade over before the hold onto it ends. void add_tests_OverworldWarpFade(UnitTestDatabase& database); +// Register the tests of spinner tiles, the lift's panel and the poster +// switch on made-up maps. +void add_tests_OverworldSpin(UnitTestDatabase& database); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp index 7b7fb39c46..ce0f2054ae 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp @@ -986,6 +986,7 @@ void add_tests_OverworldSim(UnitTestDatabase& database){ } add_tests_OverworldLateBattle(database); add_tests_OverworldWarpFade(database); + add_tests_OverworldSpin(database); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp new file mode 100644 index 0000000000..afabcff1dd --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp @@ -0,0 +1,279 @@ +/* Overworld Spinner, Lift and Poster Tests + * + * From: https://github.com/PokemonAutomation/ + * + * The Rocket Hideout's walking on small made-up maps: spinner tiles that + * slide the player (the map functions, the planner, the sim and its + * walker), the lift's floor panel and the Game Corner's poster switch + * on the sim. + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "PokemonFRLG_MovementMechanics.h" +#include "PokemonFRLG_OverworldMaps.h" +#include "PokemonFRLG_OverworldRecovery.h" +#include "PokemonFRLG_OverworldRoute.h" +#include "PokemonFRLG_OverworldSim.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +class Test_OverworldSpin : public UnitTest{ +public: + Test_OverworldSpin(std::string name, std::function body) + : UnitTest("PokemonFRLG::OverworldSpin - " + std::move(name)) + , m_body(std::move(body)) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + try{ + return m_body(); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + std::function m_body; +}; + +// A room with spinners: +// - (3, 3) north, up a dead end to (3, 1) against the wall; (3, 2) is only +// ever slid over; +// - (5, 4) east onto (6, 4), which turns the slide south to (6, 5); +// - (7, 3) west, onto the stop tile (5, 3). +const MapData& spinner_room(){ + static const MapData MAP = parse_map(R"({ + "name": "Spinners", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, + "allow_running": false, + "tiles": ["#########", "###.#####", "###.#####", "#.......#", "#.......#", "#.......#", "#########"], + "regions": { + "spin_north": [[3, 3]], "spin_east": [[5, 4]], "spin_south": [[6, 4]], + "spin_west": [[7, 3]], "spin_stop": [[5, 3]] + } + })", "Spinners"); + return MAP; +} + +// Two arrows that face each other. +const MapData& spinner_loop(){ + static const MapData MAP = parse_map(R"({ + "name": "SpinLoop", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, + "tiles": ["####", "#..#", "####"], + "regions": {"spin_east": [[1, 1]], "spin_west": [[2, 1]]} + })", "SpinLoop"); + return MAP; +} + +std::string describe(const std::vector& route){ + std::string text; + for (const RouteStep& step : route){ + text += "\n " + to_string(step); + } + return text; +} + +UnitTestResult check_slide(const TilePos& from, const TilePos& end, Direction facing, int tiles){ + const std::optional slide = spin_slide(spinner_room(), PlayerState{from, ELEVATION_GROUND, false}); + if (!slide){ + return "No slide from " + to_string(from) + "."; + } + if (!(slide->end.position == end) || slide->facing != facing || slide->tiles != tiles){ + return std::format( + "From {}: slid {} tile(s) to {} facing {}; expected {} to {} facing {}.", + to_string(from), slide->tiles, to_string(slide->end.position), direction_name(slide->facing), + tiles, to_string(end), direction_name(facing) + ); + } + return true; +} + +UnitTestResult test_slide_into_wall(){ + return check_slide({3, 3}, {3, 1}, Direction::NORTH, 2); +} +UnitTestResult test_slide_turned_by_arrow(){ + return check_slide({5, 4}, {6, 5}, Direction::SOUTH, 2); +} +UnitTestResult test_slide_to_stop_tile(){ + return check_slide({7, 3}, {5, 3}, Direction::WEST, 2); +} +UnitTestResult test_slide_loop(){ + if (spin_slide(spinner_loop(), PlayerState{{1, 1}, ELEVATION_GROUND, false})){ + return "A slide round two arrows that face each other stopped."; + } + if (spin_slide(spinner_room(), PlayerState{{1, 3}, ELEVATION_GROUND, false})){ + return "A slide from a tile with no arrow."; + } + return true; +} + +const MapPosition SPIN_START{"Spinners", {1, 5}}; +const MapPosition DEAD_END{"Spinners", {3, 1}}; + +UnitTestResult test_route_ends_on_slide(){ + const std::vector maps{spinner_room()}; + const auto route = plan_overworld_route(maps, SPIN_START, RouteGoal{DEAD_END, std::nullopt}); + if (!route || route->empty()){ + return std::string("No route to the dead end, which only a slide reaches."); + } + const RouteStep& last = route->back(); + if (last.spin_tiles != 2 || last.to_wall || !(last.end == DEAD_END)){ + return "Expected a last leg onto (3, 3) that slides 2 tiles:" + describe(*route); + } + const MapPosition end = follow_overworld_route(maps, SPIN_START, *route); + if (!(end == DEAD_END)){ + return "The route replays to " + to_string(end) + ":" + describe(*route); + } + return true; +} + +UnitTestResult test_no_stop_mid_slide(){ + const std::vector maps{spinner_room()}; + const auto route = plan_overworld_route(maps, SPIN_START, RouteGoal{MapPosition{"Spinners", {3, 2}}, std::nullopt}); + if (route){ + return "Planned a stop on (3, 2), which a slide only passes:" + describe(*route); + } + return true; +} + +UnitTestResult test_sim_slides(){ + OverworldSim sim({spinner_room()}, MapPosition{"Spinners", {3, 4}}, Direction::NORTH); + sim.hold(Direction::NORTH, MovementMechanics::TILE_TIME, MovementMechanics::STEP_RELEASE, MovementMechanics::TILE_TIME, {}); + if (!(sim.state().position == DEAD_END)){ + return "One step north onto (3, 3) ended on " + to_string(sim.state().position) + "."; + } + if (sim.state().facing != Direction::NORTH){ + return std::string("The slide left the player facing ") + direction_name(sim.state().facing) + "."; + } + return true; +} + +UnitTestResult test_sim_walker_walks_slide(){ + const std::vector maps{spinner_room()}; + const auto route = plan_overworld_route(maps, SPIN_START, RouteGoal{DEAD_END, std::nullopt}); + if (!route){ + return std::string("No route."); + } + OverworldSim sim(maps, SPIN_START, Direction::NORTH); + OverworldSimWalker walker(sim); + const RoutePlanning planning{.maps = &maps, .costs = default_route_costs(), .start = SPIN_START, .label = "Sim"}; + const std::string failure = walker.run(*route, planning); + if (!failure.empty() || !(sim.state().position == DEAD_END)){ + std::string log; + for (const std::string& line : walker.log()){ + log += "\n " + line; + } + return "Walked to " + to_string(sim.state().position) + ", failure \"" + failure + "\":" + describe(*route) + log; + } + return true; +} + + +// A lift whose door, the exit mat (1, 2), lets out on the floor picked. +std::vector lift_maps(){ + const char* FLOOR = R"({ + "name": "%s", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, + "tiles": ["###", "#.#", "###"] + })"; + std::vector maps; + maps.push_back(parse_map(R"({ + "name": "Lift", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, + "tiles": ["###", "#.#", "#.#", "###"], + "warps": [{"x": 1, "y": 2, "enter": "walk_south"}] + })", "Lift")); + for (const char* name : {"FloorA", "FloorB"}){ + std::string json = FLOOR; + json.replace(json.find("%s"), 2, name); + maps.push_back(parse_map(json, name)); + } + return maps; +} + +UnitTestResult lift_ride(bool pick, const MapPosition& expected){ + SimScript script; + script.lift_floors["Lift"] = {MapPosition{"FloorA", {1, 1}}, MapPosition{"FloorB", {1, 1}}}; + OverworldSim sim(lift_maps(), MapPosition{"Lift", {1, 2}}, Direction::SOUTH, script); + if (pick && !sim.choose_lift_floor(1)){ + return std::string("No floor 1 on the lift's panel."); + } + sim.hold(Direction::SOUTH, MovementMechanics::TILE_TIME, MovementMechanics::STEP_RELEASE, std::chrono::milliseconds(3000), {}); + if (!(sim.state().position == expected)){ + return "Expected " + to_string(expected) + ", ended on " + to_string(sim.state().position) + "."; + } + return true; +} +UnitTestResult test_lift_floor(){ + return lift_ride(true, MapPosition{"FloorB", {1, 1}}); +} +UnitTestResult test_lift_no_floor(){ + return lift_ride(false, MapPosition{"Lift", {1, 2}}); +} + + +// The poster (2, 1) north of the player; the stairs behind it, (3, 1), +// closed until its switch is pushed. +const MapData& poster_room(){ + static const MapData MAP = parse_map(R"({ + "name": "Corner", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, + "tiles": ["#####", "#...#", "#...#", "#####"], + "objects": [ + {"name": "poster", "kind": "sign", "x": 2, "y": 1}, + {"name": "poster_stairs", "kind": "npc", "x": 3, "y": 1} + ] + })", "Corner"); + return MAP; +} + +MapPosition poster_walk(bool push){ + OverworldSim sim({poster_room()}, MapPosition{"Corner", {2, 2}}, Direction::NORTH); + if (push){ + sim.push_poster_switch(); + } + const Milliseconds tile = MovementMechanics::TILE_TIME; + sim.hold(Direction::EAST, tile, MovementMechanics::STEP_RELEASE, tile, {}); + sim.hold(Direction::NORTH, tile * 2, MovementMechanics::STEP_RELEASE, tile * 2, {}); + return sim.state().position; +} +UnitTestResult test_poster_opens_stairs(){ + const MapPosition shut = poster_walk(false); + if (!(shut == MapPosition{"Corner", {3, 2}})){ + return "Before the switch, walked onto " + to_string(shut) + "."; + } + const MapPosition open = poster_walk(true); + if (!(open == MapPosition{"Corner", {3, 1}})){ + return "After the switch, walked onto " + to_string(open) + ", not the stairs."; + } + return true; +} + +} // namespace + + +void add_tests_OverworldSpin(UnitTestDatabase& database){ + database.add("slide up to a wall", test_slide_into_wall); + database.add("slide turned by an arrow", test_slide_turned_by_arrow); + database.add("slide to a stop tile", test_slide_to_stop_tile); + database.add("arrows in a loop never stop", test_slide_loop); + database.add("route ends with a slide", test_route_ends_on_slide); + database.add("no stop in the middle of a slide", test_no_stop_mid_slide); + database.add("sim slides the player", test_sim_slides); + database.add("sim walker walks a slide", test_sim_walker_walks_slide); + database.add("lift lets out on the floor picked", test_lift_floor); + database.add("lift with no floor picked stays", test_lift_no_floor); + database.add("poster switch opens the stairs", test_poster_opens_stairs); +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp index 80cf4de310..80c8d798f6 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp @@ -536,6 +536,10 @@ void walk_step_leg( console, context, leg_mover(step, speed), step.wild ? interrupts.wild : interrupts.other, leg_spec(step, speed), MAX_INTERRUPTIONS_PER_LEG, warp_hold_watchers(step, fade) ); + if (step.spin_tiles){ + // The hold let go on the spinner; the game slides the player on. + pbf_wait(context, SPIN_TILE_TIME * *step.spin_tiles + SPIN_STOP_SETTLE); + } if (!step.warps){ return; } diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 976060be1f..08e57e3244 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1869,6 +1869,7 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp From 5ca3d8f6030591e5b23ae80d85e3b63831eec699 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 17:18:16 -0800 Subject: [PATCH 04/28] FRLG AutoStory: lift floor and poster switch steps; the Rocket Hideout's trainers Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../PokemonFRLG_OverworldSpin_Tests.cpp | 13 +- .../PokemonFRLG/PokemonFRLG_Tests.cpp | 2 + .../PokemonFRLG_AutoStoryBattleData.cpp | 10 + .../PokemonFRLG_AutoStoryBattleData_Tests.cpp | 36 +++ .../AutoStory/PokemonFRLG_AutoStoryEvents.cpp | 10 + .../PokemonFRLG_AutoStoryHideout.cpp | 116 ++++++++++ .../AutoStory/PokemonFRLG_AutoStoryHideout.h | 86 ++++++++ .../PokemonFRLG_AutoStoryHideout_Tests.cpp | 207 ++++++++++++++++++ .../AutoStory/PokemonFRLG_AutoStoryRowSim.cpp | 30 +++ .../AutoStory/PokemonFRLG_AutoStoryRowSim.h | 2 + .../PokemonFRLG_AutoStoryTableSteps.cpp | 13 ++ .../PokemonFRLG_AutoStoryTableSteps.h | 7 + SerialPrograms/cmake/SourceFiles.cmake | 3 + 13 files changed, 530 insertions(+), 5 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp index afabcff1dd..298e3cb156 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp @@ -136,11 +136,13 @@ UnitTestResult test_route_ends_on_slide(){ return true; } -UnitTestResult test_no_stop_mid_slide(){ +// Walking back down from the dead end steps onto (3, 3), which slides the +// player north again: there is no way out. +UnitTestResult test_dead_end_traps(){ const std::vector maps{spinner_room()}; - const auto route = plan_overworld_route(maps, SPIN_START, RouteGoal{MapPosition{"Spinners", {3, 2}}, std::nullopt}); + const auto route = plan_overworld_route(maps, DEAD_END, RouteGoal{SPIN_START, std::nullopt}); if (route){ - return "Planned a stop on (3, 2), which a slide only passes:" + describe(*route); + return "Planned a way out past the spinner that slides back in:" + describe(*route); } return true; } @@ -239,7 +241,8 @@ MapPosition poster_walk(bool push){ sim.push_poster_switch(); } const Milliseconds tile = MovementMechanics::TILE_TIME; - sim.hold(Direction::EAST, tile, MovementMechanics::STEP_RELEASE, tile, {}); + // Each hold turns the player first, which costs a tile's time. + sim.hold(Direction::EAST, tile * 2, MovementMechanics::STEP_RELEASE, tile * 2, {}); sim.hold(Direction::NORTH, tile * 2, MovementMechanics::STEP_RELEASE, tile * 2, {}); return sim.state().position; } @@ -264,7 +267,7 @@ void add_tests_OverworldSpin(UnitTestDatabase& database){ database.add("slide to a stop tile", test_slide_to_stop_tile); database.add("arrows in a loop never stop", test_slide_loop); database.add("route ends with a slide", test_route_ends_on_slide); - database.add("no stop in the middle of a slide", test_no_stop_mid_slide); + database.add("a spinner back into a dead end traps", test_dead_end_traps); database.add("sim slides the player", test_sim_slides); database.add("sim walker walks a slide", test_sim_walker_walks_slide); database.add("lift lets out on the floor picked", test_lift_floor); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index 2bf7183092..916b0837a2 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -48,6 +48,7 @@ #include "Programs/AutoStory/PokemonFRLG_AutoStoryAbilities.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryBattle.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h" +#include "Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryPcStorage.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.h" @@ -938,6 +939,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStorySimulation(database); add_tests_AutoStoryRows(database); add_tests_AutoStoryTrashCans(database); + add_tests_AutoStoryHideout(database); add_tests_AutoStoryPaceSim(database); add_tests_AutoStoryItems(database); add_tests_AutoStoryRngLoop(database); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData.cpp index 0deab3ef7c..17281e2e83 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData.cpp @@ -86,6 +86,16 @@ constexpr StoryTrainer STORY_TRAINERS[] = { {.map = "RockTunnel_B1F", .battle = "hiker_dudley", .trainer = "TRAINER_HIKER_DUDLEY"}, {.map = "RockTunnel_B1F", .battle = "pokemaniac_cooper", .trainer = "TRAINER_POKEMANIAC_COOPER"}, {.map = "RockTunnel_1F", .battle = "picnicker_dana", .trainer = "TRAINER_PICNICKER_DANA"}, + // The Rocket Hideout, as PokeGuy's run fights it (the 00.31 and 00.32 + // drafts): the Grunt at the Game Corner's poster (11, 2) + // (CeladonCity_GameCorner_EventScript_RocketGrunt); on B4F, Grunt1 at + // (4, 2), who drops the LIFT KEY, the door guards Grunt2 (16, 14) and + // Grunt3 (19, 14), and Giovanni (19, 4) (RocketHideout_B4F scripts.inc). + {.map = "CeladonCity_GameCorner", .battle = "rocket_grunt", .trainer = "TRAINER_TEAM_ROCKET_GRUNT_7"}, + {.map = "RocketHideout_B4F", .battle = "lift_key_grunt", .trainer = "TRAINER_TEAM_ROCKET_GRUNT_18"}, + {.map = "RocketHideout_B4F", .battle = "left_guard", .trainer = "TRAINER_TEAM_ROCKET_GRUNT_16"}, + {.map = "RocketHideout_B4F", .battle = "right_guard", .trainer = "TRAINER_TEAM_ROCKET_GRUNT_17"}, + {.map = "RocketHideout_B4F", .battle = "giovanni", .trainer = "TRAINER_BOSS_GIOVANNI"}, }; using JsonTable = std::map>; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData_Tests.cpp index d2e1cda312..f9e53a5779 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData_Tests.cpp @@ -140,6 +140,38 @@ UnitTestResult test_story_trainers(){ return true; } +// The Rocket Hideout's battles, which no table row fights yet (the +// 00.31 and 00.32 drafts): each names its map's trainer, with pret's +// party. +UnitTestResult test_hideout_trainers(){ + struct Expected{ + const char* map; + const char* battle; + std::vector party; + }; + const Expected EXPECTED[] = { + {"CeladonCity_GameCorner", "rocket_grunt", {"raticate", "zubat"}}, + {"RocketHideout_B4F", "lift_key_grunt", {"koffing", "zubat"}}, + {"RocketHideout_B4F", "left_guard", {"sandshrew", "ekans", "sandslash"}}, + {"RocketHideout_B4F", "right_guard", {"ekans", "sandshrew", "arbok"}}, + {"RocketHideout_B4F", "giovanni", {"onix", "rhyhorn", "kangaskhan"}}, + }; + for (const Expected& expected : EXPECTED){ + const std::optional trainer = story_trainer(expected.map, expected.battle); + if (!trainer){ + return std::format("{} on {}: no trainer.", expected.battle, expected.map); + } + const std::vector on_map = map_trainers(expected.map); + if (std::find(on_map.begin(), on_map.end(), *trainer) == on_map.end()){ + return std::format("{} on {}: {} isn't one of the map's trainers.", expected.battle, expected.map, *trainer); + } + if (trainer_party(*trainer) != expected.party){ + return std::format("{} on {}: {} hasn't the expected party.", expected.battle, expected.map, *trainer); + } + } + return true; +} + // Mt. Moon: Super Nerd Miguel's Grimer (Poison) takes Water Gun, with or // without Bite known: Bite (60, Dark) ties Water Gun (40 x 1.5 STAB), both // special, and the tie goes to the plan's Water Gun. @@ -208,6 +240,10 @@ const AutoStoryBattleDataTestCase AUTO_STORY_BATTLE_DATA_CASES[] = { .input = {.name = "every story battle fights a trainer with a party", .body = test_story_trainers}, .expectation = {.description = "ensures all scripted story battles encounter valid teams"}, }, + { + .input = {.name = "the Rocket Hideout's Grunts and Giovanni", .body = test_hideout_trainers}, + .expectation = {.description = "names each hideout battle's trainer and party"}, + }, { .input = {.name = "Miguel's Grimer: Water Gun", .body = test_miguel_grimer}, .expectation = {.description = "verifies move matchup choice against Miguel Grimer"}, diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp index 13a39e6cae..12ace02fe6 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp @@ -22,6 +22,7 @@ #include "PokemonFRLG_AutoStoryCapture.h" #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryGrind.h" +#include "PokemonFRLG_AutoStoryHideout.h" #include "PokemonFRLG_AutoStoryScreenEvents.h" #include "PokemonFRLG_AutoStorySummary.h" #include "PokemonFRLG_AutoStoryTable.h" @@ -684,6 +685,15 @@ Outcome open_gym_door(StoryEnv& env, RunState& state, const StoryStep& step){ // The GAME steps without a row of their own: a capture or a PC deposit or // withdrawal, a MART, TEACH, LEVEL, GRIND or fossil CHOICE. Outcome other_game_step(StoryEnv& env, RunState& state, const StoryStep& step){ + // The Rocket Hideout's steps leave the player where they stand. + if (const std::optional ride = parse_lift_floor_variant(step.variant)){ + ride_lift(env.console, env.context, *ride); + return Pokemon::outcome_ok(); + } + if (step.variant == POSTER_SWITCH_VARIANT){ + push_poster_switch(env.console, env.context); + return Pokemon::outcome_ok(); + } const Pokemon::Pose patch = state.pose; ConsoleCaptureActions capture(env.console, env.context, env.language, [&]{ pace_trainer_battle(env, state, step, patch); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp new file mode 100644 index 0000000000..f1208ac841 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp @@ -0,0 +1,116 @@ +/* AutoStory Rocket Hideout Steps + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonFRLG_AutoStoryActions.h" +#include "PokemonFRLG_AutoStoryHideout.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +using namespace std::chrono_literals; + + +const std::vector& rocket_hideout_lift_menu(){ + static const std::vector MENU{"B1F", "B2F", "B4F", "EXIT"}; + return MENU; +} + +std::string lift_floor_variant(std::string_view from, std::string_view to){ + return std::string(LIFT_FLOOR_PREFIX) + std::string(from) + " " + std::string(to); +} + +namespace{ + +// The row of `floor` on the panel's menu, if it is a floor (not EXIT). +std::optional floor_row(std::string_view floor){ + const std::vector& menu = rocket_hideout_lift_menu(); + for (size_t row = 0; row + 1 < menu.size(); row++){ + if (menu[row] == floor){ + return row; + } + } + return std::nullopt; +} + +} // namespace + +std::optional parse_lift_floor_variant(std::string_view variant){ + if (!variant.starts_with(LIFT_FLOOR_PREFIX)){ + return std::nullopt; + } + const std::string_view floors = variant.substr(LIFT_FLOOR_PREFIX.size()); + const size_t space = floors.find(' '); + if (space == std::string_view::npos){ + return std::nullopt; + } + const std::optional from = floor_row(floors.substr(0, space)); + const std::optional to = floor_row(floors.substr(space + 1)); + if (!from || !to){ + return std::nullopt; + } + return LiftRide{*from, *to}; +} + +MenuPresses lift_menu_presses(const LiftRide& ride, size_t entries){ + const size_t down = (ride.to + entries - ride.from) % entries; + const size_t up = (ride.from + entries - ride.to) % entries; + if (entries <= 3){ + // No wrap: the cursor goes straight there. + return ride.to >= ride.from + ? MenuPresses{Pokemon::Direction::SOUTH, ride.to - ride.from} + : MenuPresses{Pokemon::Direction::NORTH, ride.from - ride.to}; + } + return down <= up ? MenuPresses{Pokemon::Direction::SOUTH, down} : MenuPresses{Pokemon::Direction::NORTH, up}; +} + +bool is_hideout_variant(std::string_view variant){ + return variant == POSTER_SWITCH_VARIANT || parse_lift_floor_variant(variant).has_value(); +} + + +namespace{ + +// How long a message and its menu take to show on fast text, before the +// menu takes the D-pad. Not measured on the console. +constexpr Milliseconds LIFT_MENU_WAIT = 1500ms; +constexpr Milliseconds MENU_TAP = 80ms; +constexpr Milliseconds MENU_TAP_RELEASE = 240ms; +// The lift's shake and the doors, then the overworld, A press to the end. +constexpr Milliseconds HIDEOUT_SCENE_TIMEOUT = 30000ms; + +} // namespace + +void ride_lift(ConsoleHandle& console, ProControllerContext& context, const LiftRide& ride){ + const MenuPresses presses = lift_menu_presses(ride, rocket_hideout_lift_menu().size()); + console.log( + "Story: lift from " + rocket_hideout_lift_menu()[ride.from] + " to " + rocket_hideout_lift_menu()[ride.to] + + ": " + std::to_string(presses.presses) + " press(es) " + Pokemon::direction_name(presses.direction) + "." + ); + pbf_press_button(context, BUTTON_A, 160ms, LIFT_MENU_WAIT); + const DpadPosition dpad = presses.direction == Pokemon::Direction::SOUTH ? DPAD_DOWN : DPAD_UP; + for (size_t c = 0; c < presses.presses; c++){ + pbf_press_dpad(context, dpad, MENU_TAP, MENU_TAP_RELEASE); + } + pbf_press_button(context, BUTTON_A, 160ms, 800ms); + // B during the ride does nothing; the menu took the A. + mash_until_overworld(console, context, HIDEOUT_SCENE_TIMEOUT); +} + +void push_poster_switch(ConsoleHandle& console, ProControllerContext& context){ + console.log("Story: the switch behind the poster."); + pbf_press_button(context, BUTTON_A, 160ms, 800ms); + mash_until_overworld(console, context, HIDEOUT_SCENE_TIMEOUT); +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h new file mode 100644 index 0000000000..4e4249af77 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h @@ -0,0 +1,86 @@ +/* AutoStory Rocket Hideout Steps + * + * From: https://github.com/PokemonAutomation/ + * + * The two story steps the Rocket Hideout needs that no other place does: + * the switch behind the Celadon Game Corner's poster, which opens the + * stairs down, and the lift's floor panel. Both are GAME steps named by + * their variant, as the draft plans write them (StoryData/Draft). + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryHideout_H +#define PokemonAutomation_PokemonFRLG_AutoStoryHideout_H + +#include +#include +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "Pokemon/Navigation/Pokemon_Grid.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The GAME step that pushes the switch behind the Game Corner's poster, +// the player facing it (pokefirered CeladonCity_GameCorner_EventScript_ +// Poster): A, "Hey! A switch behind the POSTER!? Let's push it!", and the +// stairs (15..17, 2..3) open for good. The Grunt in front of it must be +// beaten first. +inline const std::string POSTER_SWITCH_VARIANT = "poster_switch"; + +// The GAME step that rides the Rocket Hideout's lift, the player facing +// its panel with the LIFT KEY: "lift_floor ", floors as the +// panel names them (B1F, B2F, B4F). `from` is the floor the lift stands +// at, where the panel's cursor starts. +inline constexpr std::string_view LIFT_FLOOR_PREFIX = "lift_floor "; + +// The lift panel's menu, top first (pokefirered +// sMultichoiceList_RocketHideoutElevator): B1F, B2F, B4F, EXIT. +const std::vector& rocket_hideout_lift_menu(); + +// A ride: the menu rows the cursor starts on and is moved to. +struct LiftRide{ + size_t from = 0; + size_t to = 0; +}; +// The variant for a ride from floor `from` to `to`. +std::string lift_floor_variant(std::string_view from, std::string_view to); +// The ride a "lift_floor " variant names, or nothing if it is +// not one, or names a floor the panel hasn't. +std::optional parse_lift_floor_variant(std::string_view variant); + +// The fewest presses that move the cursor from `ride.from` to `ride.to` +// on a menu of `entries` rows. A multichoice of more than three rows +// wraps round (pokefirered DrawVerticalMultichoiceMenu()). +struct MenuPresses{ + Pokemon::Direction direction = Pokemon::Direction::SOUTH; + size_t presses = 0; +}; +MenuPresses lift_menu_presses(const LiftRide& ride, size_t entries); + +// Whether `variant` is one of this file's steps. +bool is_hideout_variant(std::string_view variant); + +// On the console: A on the panel, the floor picked, the ride waited out +// back to the overworld, the player still in the lift. +void ride_lift(ConsoleHandle& console, ProControllerContext& context, const LiftRide& ride); +// On the console: A on the poster, its message closed back to the +// overworld. +void push_poster_switch(ConsoleHandle& console, ProControllerContext& context); + + +// Registers the hideout steps' tests. +void add_tests_AutoStoryHideout(UnitTestDatabase& database); + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp new file mode 100644 index 0000000000..a9518e3890 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp @@ -0,0 +1,207 @@ +/* AutoStory Rocket Hideout Steps Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h" +#include "PokemonFRLG_AutoStoryHideout.h" +#include "PokemonFRLG_AutoStoryRowSim.h" +#include "PokemonFRLG_AutoStoryTableSteps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +struct LiftInput{ + std::string name; + std::string variant; +}; +struct LiftExpectation{ + // Nothing: the variant isn't a ride. + std::optional presses; +}; +struct LiftCase{ + LiftInput input; + LiftExpectation expectation; +}; + +class Test_AutoStoryHideout : public UnitTest{ +public: + explicit Test_AutoStoryHideout(LiftCase test_case) + : UnitTest("PokemonFRLG::AutoStoryHideout - " + test_case.input.name) + , m_case(std::move(test_case)) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::optional ride = parse_lift_floor_variant(m_case.input.variant); + const std::optional& expected = m_case.expectation.presses; + if (!ride || !expected){ + if (ride.has_value() != expected.has_value()){ + return std::format("\"{}\": a ride {}, expected {}.", m_case.input.variant, ride.has_value(), expected.has_value()); + } + return true; + } + const MenuPresses got = lift_menu_presses(*ride, rocket_hideout_lift_menu().size()); + if (got.direction != expected->direction || got.presses != expected->presses){ + return std::format( + "\"{}\": {} press(es) {}, expected {} {}.", m_case.input.variant, + got.presses, Pokemon::direction_name(got.direction), expected->presses, Pokemon::direction_name(expected->direction) + ); + } + if (!is_hideout_variant(m_case.input.variant)){ + return "A ride that isn't a hideout variant."; + } + return true; + } + +private: + LiftCase m_case; +}; + +class Test_AutoStoryHideoutVariants : public UnitTest{ +public: + Test_AutoStoryHideoutVariants() + : UnitTest("PokemonFRLG::AutoStoryHideout - the poster and lift variants") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + if (!is_hideout_variant(POSTER_SWITCH_VARIANT) || is_hideout_variant("cut_tree") || is_hideout_variant("")){ + return "Expected only the poster switch and lift rides as hideout variants."; + } + if (lift_floor_variant("B2F", "B4F") != "lift_floor B2F B4F"){ + return "Expected \"lift_floor B2F B4F\", got \"" + lift_floor_variant("B2F", "B4F") + "\"."; + } + // A three-row menu doesn't wrap. + const MenuPresses no_wrap = lift_menu_presses(LiftRide{0, 2}, 3); + if (no_wrap.direction != Pokemon::Direction::SOUTH || no_wrap.presses != 2){ + return "Expected 2 down on a three-row menu."; + } + return true; + } +}; + +// The table's steps, run as a row runs them on the sim: the poster's +// switch opens the stairs north-east of it, and the lift lets out on the +// floor picked. +class Test_AutoStoryHideoutSim : public UnitTest{ +public: + Test_AutoStoryHideoutSim() + : UnitTest("PokemonFRLG::AutoStoryHideout - table steps on the sim") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::string poster = poster_on_sim(); + if (!poster.empty()){ + return poster; + } + return lift_on_sim(); + } + +private: + static std::string poster_on_sim(){ + const MapData room = parse_map(R"({ + "name": "Corner", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, + "tiles": ["#####", "#...#", "#...#", "#####"], + "objects": [ + {"name": "poster", "kind": "sign", "x": 2, "y": 1}, + {"name": "poster_stairs", "kind": "npc", "x": 3, "y": 1} + ] + })", "Corner"); + OverworldSim sim({room}, MapPosition{"Corner", {2, 2}}, Direction::NORTH); + StoryPose at{"Corner", {2, 2}, Direction::NORTH}; + const std::string failure = row_step_on_sim(sim, at, StoryTable::poster_switch("poster_switch", "test"), "00.31"); + if (!failure.empty()){ + return failure; + } + const Milliseconds tile = MovementMechanics::TILE_TIME; + sim.hold(Direction::EAST, tile * 2, MovementMechanics::STEP_RELEASE, tile * 2, {}); + sim.hold(Direction::NORTH, tile * 2, MovementMechanics::STEP_RELEASE, tile * 2, {}); + if (!(sim.state().position == MapPosition{"Corner", {3, 1}})){ + return "After the poster step, walked onto " + to_string(sim.state().position) + ", not the stairs."; + } + return ""; + } + static std::string lift_on_sim(){ + std::vector maps; + maps.push_back(parse_map(R"({ + "name": "Lift", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, + "tiles": ["###", "#.#", "#.#", "###"], + "warps": [{"x": 1, "y": 2, "enter": "walk_south"}] + })", "Lift")); + maps.push_back(parse_map(R"({ + "name": "B4F", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, + "tiles": ["###", "#.#", "###"] + })", "B4F")); + SimScript script; + const MapPosition b4f{"B4F", {1, 1}}; + script.lift_floors["Lift"] = {MapPosition{"Lift", {1, 1}}, MapPosition{"Lift", {1, 1}}, b4f}; + OverworldSim sim(maps, MapPosition{"Lift", {1, 2}}, Direction::SOUTH, script); + StoryPose at{"Lift", {1, 2}, Direction::SOUTH}; + const std::string failure = row_step_on_sim(sim, at, StoryTable::lift_floor("lift_to_b4f", "B2F", "B4F", "test"), "00.32"); + if (!failure.empty()){ + return failure; + } + sim.hold(Direction::SOUTH, MovementMechanics::TILE_TIME, MovementMechanics::STEP_RELEASE, std::chrono::milliseconds(3000), {}); + if (!(sim.state().position == b4f)){ + return "After the lift step, the door let out on " + to_string(sim.state().position) + "."; + } + return ""; + } +}; + +const LiftCase LIFT_CASES[] = { + { + .input = {.name = "B2F to B4F: one down", .variant = "lift_floor B2F B4F"}, + .expectation = {.presses = MenuPresses{Pokemon::Direction::SOUTH, 1}}, + }, + { + .input = {.name = "B4F to B1F: two up", .variant = "lift_floor B4F B1F"}, + .expectation = {.presses = MenuPresses{Pokemon::Direction::NORTH, 2}}, + }, + { + // B1F up past the top wraps to EXIT, then B4F: two up equal two + // down; down is taken. + .input = {.name = "B1F to B4F: two down", .variant = "lift_floor B1F B4F"}, + .expectation = {.presses = MenuPresses{Pokemon::Direction::SOUTH, 2}}, + }, + { + .input = {.name = "the lift's own floor: no press", .variant = "lift_floor B4F B4F"}, + .expectation = {.presses = MenuPresses{Pokemon::Direction::SOUTH, 0}}, + }, + { + .input = {.name = "no B3F on the panel", .variant = "lift_floor B2F B3F"}, + .expectation = {.presses = std::nullopt}, + }, + { + .input = {.name = "EXIT is no floor", .variant = "lift_floor B2F EXIT"}, + .expectation = {.presses = std::nullopt}, + }, + { + .input = {.name = "one floor only", .variant = "lift_floor B2F"}, + .expectation = {.presses = std::nullopt}, + }, +}; + +} // namespace + + +void add_tests_AutoStoryHideout(UnitTestDatabase& database){ + for (const LiftCase& item : LIFT_CASES){ + database.add(item); + } + database.add(); + database.add(); +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp index ff86b55359..adf518879f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp @@ -15,6 +15,7 @@ #include "Pokemon/AutoStory/Pokemon_TilePose.h" #include "PokemonFRLG_AutoStoryActions.h" #include "PokemonFRLG_AutoStoryBattleDecisions.h" +#include "PokemonFRLG_AutoStoryHideout.h" #include "PokemonFRLG_AutoStoryRowSim.h" namespace PokemonAutomation{ @@ -370,6 +371,8 @@ Pokemon::StoryAction OverworldStepSim::action(StoryActionId id){ case StoryActionId::BATTLE: body = &OverworldStepSim::battle; break; + case StoryActionId::GAME: + return hideout_or_stub(); default: return m_stubs.stub(); } @@ -519,6 +522,33 @@ std::string OverworldStepSim::cut(const StoryStep& step){ return ""; } +// A Rocket Hideout GAME step on the sim: the poster's switch, or the +// lift's floor (SimScript::lift_floors, rows as on the panel); any other +// GAME step is stubbed. +Pokemon::StoryAction OverworldStepSim::hideout_or_stub(){ + Pokemon::StoryAction stub = m_stubs.stub(); + Pokemon::StoryAction sim = played([this](RunState&, const StoryStep& step){ + const std::string failure = hideout(step); + return failure.empty() ? Pokemon::outcome_ok() : Pokemon::outcome_failed("sim_failure", failure); + }); + sim.act = [stub, played_act = sim.act](RunState& state, const StoryStep& step){ + return is_hideout_variant(step.variant) ? played_act(state, step) : stub.act(state, step); + }; + sim.needs_console = stub.needs_console; + return sim; +} + +std::string OverworldStepSim::hideout(const StoryStep& step){ + if (step.variant == POSTER_SWITCH_VARIANT){ + return m_sim.push_poster_switch() ? "" : "no poster ahead."; + } + const std::optional ride = parse_lift_floor_variant(step.variant); + if (!ride || !m_sim.choose_lift_floor(ride->to)){ + return "no lift panel for \"" + step.variant + "\"."; + } + return ""; +} + // A battle from the trainer's message, which must be up, ending where the // table says, the battle loop's end played on the overworld after it. std::string OverworldStepSim::battle(const StoryStep& step){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.h index 15655a8162..aa36f4f023 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.h @@ -183,6 +183,8 @@ class OverworldStepSim : public Pokemon::TableExecutor{ std::string step_onto(const StoryStep& step); std::string cut(const StoryStep& step); std::string battle(const StoryStep& step); + Pokemon::StoryAction hideout_or_stub(); + std::string hideout(const StoryStep& step); OverworldSim& m_sim; StoryPose m_at; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp index acd9a79de8..372aa1ed5e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp @@ -4,6 +4,7 @@ * */ +#include "PokemonFRLG_AutoStoryHideout.h" #include "PokemonFRLG_AutoStoryTableSteps.h" namespace PokemonAutomation{ @@ -154,6 +155,18 @@ StoryStep cut_tree(std::string name, Pokemon::Pose tree, std::string why){ return step; } +StoryStep poster_switch(std::string name, std::string why){ + StoryStep step = make(StoryStepKind::GAME, std::move(name), std::move(why)); + step.variant = POSTER_SWITCH_VARIANT; + return step; +} + +StoryStep lift_floor(std::string name, std::string_view from, std::string_view to, std::string why){ + StoryStep step = make(StoryStepKind::GAME, std::move(name), std::move(why)); + step.variant = lift_floor_variant(from, to); + return step; +} + } // namespace StoryTable diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h index d0a314a2ac..d8e92f3d45 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h @@ -13,6 +13,7 @@ #include #include +#include #include #include "PokemonFRLG_AutoStoryEvents.h" @@ -182,6 +183,12 @@ StoryStep options_step(); // Cuts the tree the player faces (CUT_TREE_VARIANT) and steps onto its // tile, `tree`, facing the way the step goes. StoryStep cut_tree(std::string name, Pokemon::Pose tree, std::string why); +// Pushes the switch behind the Game Corner's poster the player faces +// (POSTER_SWITCH_VARIANT). +StoryStep poster_switch(std::string name, std::string why); +// Rides the Rocket Hideout's lift, the player facing its panel, from the +// floor it stands at to `to` (lift_floor_variant()): "B1F", "B2F", "B4F". +StoryStep lift_floor(std::string name, std::string_view from, std::string_view to, std::string why); // Rows in files of their own (PokemonFRLG_AutoStoryTableVermilionGym.cpp, diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 08e57e3244..36df2c5b96 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1917,6 +1917,9 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItems_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFaultSweep_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFaultSweep_Tests.h From cf123e733a97d99ae7ecc77021808c2fca407d08 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 17:22:16 -0800 Subject: [PATCH 05/28] FRLG AutoStory: use items from the bag, renew a Repel, ride the Bicycle Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Menus/PokemonFRLG_ItemListReader.cpp | 5 + .../Menus/PokemonFRLG_ItemListReader.h | 3 +- .../Overworld/PokemonFRLG_MovementMechanics.h | 4 + .../Overworld/PokemonFRLG_OverworldWalk.cpp | 12 +- .../Overworld/PokemonFRLG_OverworldWalk.h | 2 + .../PokemonFRLG/PokemonFRLG_Tests.cpp | 2 + .../AutoStory/PokemonFRLG_AutoStoryEvents.cpp | 14 ++ .../AutoStory/PokemonFRLG_AutoStoryEvents.h | 7 +- .../PokemonFRLG_AutoStoryHideout_Tests.cpp | 16 +- .../PokemonFRLG_AutoStoryItemUse.cpp | 165 +++++++++++++ .../AutoStory/PokemonFRLG_AutoStoryItemUse.h | 127 ++++++++++ .../PokemonFRLG_AutoStoryItemUse_Tests.cpp | 223 ++++++++++++++++++ .../PokemonFRLG_AutoStoryTableSteps.cpp | 14 ++ .../PokemonFRLG_AutoStoryTableSteps.h | 5 + SerialPrograms/cmake/SourceFiles.cmake | 3 + 15 files changed, 596 insertions(+), 6 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_ItemListReader.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_ItemListReader.cpp index 4c89385dbb..9b8a5c1212 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_ItemListReader.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_ItemListReader.cpp @@ -70,6 +70,11 @@ class ItemNameMatcher : public OCR::SmallDictionaryMatcher{ {"parlyz-heal", "Parlyz Heal"}, {"awakening", "Awakening"}, {"poke-ball", "Poke Ball"}, + {"repel", "Repel"}, + {"super-repel", "Super Repel"}, + {"max-repel", "Max Repel"}, + {"escape-rope", "Escape Rope"}, + {"bicycle", "Bicycle"}, {"cancel", "Cancel"}, }){ JsonArray names; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_ItemListReader.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_ItemListReader.h index 0b36e39959..6565fcbfdd 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_ItemListReader.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_ItemListReader.h @@ -36,7 +36,8 @@ ImageFloatBox item_list_name_box(int row); ImageFloatBox bag_item_count_box(int row); // The item named in row `row`: a slug such as "potion", "antidote", -// "parlyz-heal", "awakening", "poke-ball", or "cancel" for the list's last row, in a +// "parlyz-heal", "awakening", "poke-ball", "repel", +// "super-repel", "max-repel", "escape-rope", "bicycle", or "cancel" for the list's last row, in a // game in `language`; empty if it can't be read, as in a language the // reader has no names for. std::string read_item_list_name(Logger& logger, Language language, const ImageViewRGB32& screen, int row); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h index 0a20f37a7a..d2f761596c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h @@ -48,6 +48,10 @@ inline constexpr Milliseconds TILE_TIME(267); inline constexpr Milliseconds RUN_TILE_TIME(133); // Surfing: a tile per 8 frames. inline constexpr Milliseconds SURF_TILE_TIME(133); +// Riding the Bicycle: a tile per 4 frames (pokefirered bike.c, +// PlayerRideWaterCurrent(): MOVE_SPEED_FAST_2). An estimate from the +// decompilation, not measured on the console. +inline constexpr Milliseconds BIKE_TILE_TIME(67); // The longest tap measured to only turn a player facing another way, and // to take exactly one step for one facing its way. inline constexpr Milliseconds LONGEST_TAP(133); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp index 80c8d798f6..35214508be 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp @@ -191,7 +191,15 @@ Milliseconds tile_time(const RouteStep& step, MoveSpeed speed){ if (step.surfing){ return SURF_TILE_TIME; } - return step_speed(step, speed) == MoveSpeed::RUN ? RUN_TILE_TIME : TILE_TIME; + switch (step_speed(step, speed)){ + case MoveSpeed::RUN: + return RUN_TILE_TIME; + case MoveSpeed::BIKE: + return BIKE_TILE_TIME; + case MoveSpeed::WALK: + break; + } + return TILE_TIME; } Pokemon::LegMover leg_mover(const RouteStep& step, MoveSpeed speed){ @@ -469,6 +477,8 @@ const char* move_speed_name(MoveSpeed speed){ return "walk"; case MoveSpeed::RUN: return "run"; + case MoveSpeed::BIKE: + return "bike"; } return "?"; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h index d2120b8e04..22eb2eea05 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h @@ -77,6 +77,8 @@ void use_max_repel_from_bag(ConsoleHandle& console, ProControllerContext& contex enum class MoveSpeed{ WALK, RUN, + // On the Bicycle, already mounted (MovementMechanics::BIKE_TILE_TIME). + BIKE, }; // Name of a movement speed. const char* move_speed_name(MoveSpeed speed); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index 916b0837a2..28816b82aa 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -49,6 +49,7 @@ #include "Programs/AutoStory/PokemonFRLG_AutoStoryBattle.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h" +#include "Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryPcStorage.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.h" @@ -940,6 +941,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStoryRows(database); add_tests_AutoStoryTrashCans(database); add_tests_AutoStoryHideout(database); + add_tests_AutoStoryItemUse(database); add_tests_AutoStoryPaceSim(database); add_tests_AutoStoryItems(database); add_tests_AutoStoryRngLoop(database); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp index 12ace02fe6..b3bb43b2ec 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp @@ -23,6 +23,7 @@ #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryGrind.h" #include "PokemonFRLG_AutoStoryHideout.h" +#include "PokemonFRLG_AutoStoryItemUse.h" #include "PokemonFRLG_AutoStoryScreenEvents.h" #include "PokemonFRLG_AutoStorySummary.h" #include "PokemonFRLG_AutoStoryTable.h" @@ -366,6 +367,9 @@ const std::vector& story_maps(){ } MoveSpeed move_speed(const StoryStep& step){ + if (step.variant == BIKE_VARIANT){ + return MoveSpeed::BIKE; + } return step.variant == RUN_VARIANT ? MoveSpeed::RUN : MoveSpeed::WALK; } @@ -694,6 +698,16 @@ Outcome other_game_step(StoryEnv& env, RunState& state, const StoryStep& step){ push_poster_switch(env.console, env.context); return Pokemon::outcome_ok(); } + // An item from the bag; an Escape Rope ends where the table says. + if (is_item_use_variant(step.variant)){ + const std::string item = parse_use_item_variant(step.variant).value_or("bicycle"); + ConsoleItemUseActions bag(env.console, env.context, env.language); + const ItemUseEnd end = use_item_from_bag(env.console.logger(), bag, item); + if (end != ItemUseEnd::USED){ + return Pokemon::outcome_failed(std::string("use_item_") + item_use_end_name(end), step.variant); + } + return step.pose_after ? Pokemon::outcome_ok(*step.pose_after) : Pokemon::outcome_ok(); + } const Pokemon::Pose patch = state.pose; ConsoleCaptureActions capture(env.console, env.context, env.language, [&]{ pace_trainer_battle(env, state, step, patch); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h index 9e089b767c..4ecbd72ab7 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h @@ -63,6 +63,10 @@ struct StoryPose{ // The variant of a ROUTE or LEGS step that runs, with B held: the player // has the Running Shoes (00.10 on). inline const std::string RUN_VARIANT = "run"; +// The variant of a ROUTE or LEGS step ridden on the Bicycle, mounted by a +// RIDE_BIKE_VARIANT step before it (PokemonFRLG_AutoStoryItemUse.h). +// Indoors, where the game doesn't let the player ride, it walks. +inline const std::string BIKE_VARIANT = "bike"; // The variant of a STEP into a trainer's line: a tile at a time, at most // TRAINER_LINE_TILES, until the trainer sees the player @@ -101,7 +105,8 @@ inline const std::string CLOSE_TRAINER_CARD = "close_trainer_card"; // the overworld. inline const std::string LEAVE_GIFT_SUMMARY = "leave_gift_summary"; -// How `step`, a ROUTE or LEGS, moves the player: RUN_VARIANT runs. +// How `step`, a ROUTE or LEGS, moves the player: RUN_VARIANT runs, +// BIKE_VARIANT rides. MoveSpeed move_speed(const StoryStep& step); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp index a9518e3890..0c29251b6a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp @@ -101,7 +101,11 @@ class Test_AutoStoryHideoutSim : public UnitTest{ if (!poster.empty()){ return poster; } - return lift_on_sim(); + const std::string lift = lift_on_sim(); + if (!lift.empty()){ + return lift; + } + return true; } private: @@ -162,8 +166,14 @@ const LiftCase LIFT_CASES[] = { .expectation = {.presses = MenuPresses{Pokemon::Direction::SOUTH, 1}}, }, { - .input = {.name = "B4F to B1F: two up", .variant = "lift_floor B4F B1F"}, - .expectation = {.presses = MenuPresses{Pokemon::Direction::NORTH, 2}}, + // Down past EXIT wraps to B1F: as few presses as up, and down is + // taken. + .input = {.name = "B4F to B1F: two down past EXIT", .variant = "lift_floor B4F B1F"}, + .expectation = {.presses = MenuPresses{Pokemon::Direction::SOUTH, 2}}, + }, + { + .input = {.name = "B4F to B2F: one up", .variant = "lift_floor B4F B2F"}, + .expectation = {.presses = MenuPresses{Pokemon::Direction::NORTH, 1}}, }, { // B1F up past the top wraps to EXIT, then B4F: two up equal two diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp new file mode 100644 index 0000000000..d0a719798d --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp @@ -0,0 +1,165 @@ +/* AutoStory Item Use + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" +#include "PokemonFRLG_AutoStoryActions.h" +#include "PokemonFRLG_AutoStoryItemUse.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +using namespace std::chrono_literals; + + +std::optional usable_item_pocket(std::string_view item){ + if (is_repel(item) || item == "escape-rope"){ + return BagPocket::ITEMS; + } + if (item == "bicycle"){ + return BagPocket::KEY_ITEMS; + } + return std::nullopt; +} +bool is_repel(std::string_view item){ + return item == "repel" || item == "super-repel" || item == "max-repel"; +} + +std::string use_item_variant(std::string_view item){ + return std::string(USE_ITEM_PREFIX) + std::string(item); +} +std::optional parse_use_item_variant(std::string_view variant){ + if (!variant.starts_with(USE_ITEM_PREFIX)){ + return std::nullopt; + } + const std::string_view item = variant.substr(USE_ITEM_PREFIX.size()); + if (!usable_item_pocket(item)){ + return std::nullopt; + } + return std::string(item); +} +bool is_item_use_variant(std::string_view variant){ + return variant == RIDE_BIKE_VARIANT || parse_use_item_variant(variant).has_value(); +} + +std::optional bag_row_of(const std::vector& rows, std::string_view item){ + for (size_t row = 0; row < rows.size(); row++){ + if (rows[row].item == item && rows[row].count != 0){ + return row; + } + } + return std::nullopt; +} + +const char* item_use_end_name(ItemUseEnd end){ + switch (end){ + case ItemUseEnd::USED: + return "used"; + case ItemUseEnd::NONE_LEFT: + return "none_left"; + case ItemUseEnd::NO_BAG: + return "no_bag"; + case ItemUseEnd::UNKNOWN_ITEM: + return "unknown_item"; + } + return "?"; +} + +ItemUseEnd use_item_from_bag(Logger& logger, ItemUseActions& actions, std::string_view item){ + const std::optional pocket = usable_item_pocket(item); + if (!pocket){ + return ItemUseEnd::UNKNOWN_ITEM; + } + if (!actions.open_pocket(*pocket)){ + logger.log("Bag: the bag didn't open."); + return ItemUseEnd::NO_BAG; + } + const std::optional row = bag_row_of(actions.read_pocket(), item); + if (!row){ + logger.log("Bag: no " + std::string(item) + " left."); + actions.close_bag(); + return ItemUseEnd::NONE_LEFT; + } + logger.log("Bag: using the " + std::string(item) + " on row " + std::to_string(*row + 1) + "."); + actions.use_row(*row); + actions.close_bag(); + return ItemUseEnd::USED; +} + +ItemUseEnd renew_repel(Logger& logger, ItemUseActions& actions, std::string_view item){ + if (!is_repel(item)){ + return ItemUseEnd::UNKNOWN_ITEM; + } + logger.log("Bag: the " + std::string(item) + " wore off."); + return use_item_from_bag(logger, actions, item); +} + + +namespace{ + +// The bag slides in, and ignores presses until it is done (as +// use_max_repel_from_bag()). +constexpr Milliseconds BAG_SLIDE_IN = 1000ms; +// Out of the bag and its messages, or the Escape Rope's warp. +constexpr Milliseconds BAG_CLOSE_TIMEOUT = 30000ms; + +} // namespace + +ConsoleItemUseActions::ConsoleItemUseActions(ConsoleHandle& console, ProControllerContext& context, Language language) + : m_console(console) + , m_context(context) + , m_language(language) +{} + +bool ConsoleItemUseActions::open_pocket(BagPocket pocket){ + open_bag_from_overworld(m_console, m_context); + pbf_wait(m_context, BAG_SLIDE_IN); + // Left to the Items pocket, which doesn't wrap, then right to `pocket`. + for (int c = 0; c < 3; c++){ + pbf_press_dpad(m_context, DPAD_LEFT, 100ms, 600ms); + } + for (int c = 0; c < (int)pocket; c++){ + pbf_press_dpad(m_context, DPAD_RIGHT, 100ms, 600ms); + } + for (int c = 0; c < 20; c++){ + pbf_press_dpad(m_context, DPAD_UP, 100ms, 100ms); + } + return true; +} + +std::vector ConsoleItemUseActions::read_pocket(){ + m_context.wait_for_all_requests(); + VideoSnapshot screen = m_console.video().snapshot(); + if (!screen){ + return {}; + } + return read_bag_pocket(m_console.logger(), m_language, screen); +} + +void ConsoleItemUseActions::use_row(size_t row){ + for (size_t c = 0; c < row; c++){ + pbf_press_dpad(m_context, DPAD_DOWN, 100ms, 300ms); + } + // The item's menu, USE on top. + pbf_press_button(m_context, BUTTON_A, 160ms, 800ms); + pbf_press_button(m_context, BUTTON_A, 160ms, 1500ms); +} + +void ConsoleItemUseActions::close_bag(){ + // B until the overworld: past " used the REPEL.", out of the + // bag and the START menu. The Escape Rope and the Bicycle leave the + // bag by themselves. + mash_until_overworld(m_console, m_context, BAG_CLOSE_TIMEOUT); +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h new file mode 100644 index 0000000000..bf0a5c86c7 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h @@ -0,0 +1,127 @@ +/* AutoStory Item Use + * + * From: https://github.com/PokemonAutomation/ + * + * Items the story uses from the bag on the overworld, as PokeGuy's run + * does: a Repel or Super Repel before long walks through grass and caves, + * renewed when it wears off; an Escape Rope out of the Rocket Hideout; + * and the Bicycle. The bag is read with read_bag_pocket(), so the step + * finds its item wherever the pocket keeps it. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryItemUse_H +#define PokemonAutomation_PokemonFRLG_AutoStoryItemUse_H + +#include +#include +#include +#include +#include +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "CommonFramework/Language.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_ItemListReader.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The GAME step that uses an item from the bag on the overworld: +// "use_item ", the slug as read_item_list_name() names it. +inline constexpr std::string_view USE_ITEM_PREFIX = "use_item "; +// The GAME step that gets on the Bicycle (the Key Items pocket's BICYCLE, +// USE). The ROUTE steps after it ride (BIKE_VARIANT). +inline const std::string RIDE_BIKE_VARIANT = "ride_bike"; + +// The bag's pockets, in the order LEFT and RIGHT go through them. +enum class BagPocket{ + ITEMS, + KEY_ITEMS, + POKE_BALLS, +}; + +// The pocket of an item the story uses from the bag: repel, super-repel, +// max-repel, escape-rope (Items) or bicycle (Key Items); nothing for +// another. +std::optional usable_item_pocket(std::string_view item); +// Whether `item` is a Repel of any kind. +bool is_repel(std::string_view item); + +// The variant that uses `item`. +std::string use_item_variant(std::string_view item); +// The item a "use_item " variant uses, or nothing if it isn't one +// or the item isn't one the story uses from the bag. +std::optional parse_use_item_variant(std::string_view variant); +// Whether `variant` is one of this file's steps. +bool is_item_use_variant(std::string_view variant); + +// The row of `item` in `rows` (read_bag_pocket()'s, from the top), if it +// has one left: a count of -1 (unread) counts as some. +std::optional bag_row_of(const std::vector& rows, std::string_view item); + + +// The bag, as use_item_from_bag() drives it: the console's, or a test's. +class ItemUseActions{ +public: + virtual ~ItemUseActions() = default; + // From the overworld, opens the bag on `pocket`, scrolled to its top. + virtual bool open_pocket(BagPocket pocket) = 0; + // The open pocket's rows, from the top. + virtual std::vector read_pocket() = 0; + // Moves to row `row`, USE, and through what the item says. + virtual void use_row(size_t row) = 0; + // Back to the overworld (nothing to do if the item closed the bag). + virtual void close_bag() = 0; +}; + +enum class ItemUseEnd{ + USED, + // The pocket has none left (or the name isn't read). + NONE_LEFT, + // The bag didn't open. + NO_BAG, + // Not an item the story uses from the bag. + UNKNOWN_ITEM, +}; +const char* item_use_end_name(ItemUseEnd end); + +// Opens the item's pocket, finds it and uses it, then back to the +// overworld. +ItemUseEnd use_item_from_bag(Logger& logger, ItemUseActions& actions, std::string_view item); + +// After "REPEL's effect wore off..." has closed: FRLG asks nothing, so +// another of the same Repel is used if the bag has one. NONE_LEFT is no +// failure: the walk goes on without. +ItemUseEnd renew_repel(Logger& logger, ItemUseActions& actions, std::string_view item); + + +// The console's bag. +class ConsoleItemUseActions : public ItemUseActions{ +public: + ConsoleItemUseActions(ConsoleHandle& console, ProControllerContext& context, Language language); + + virtual bool open_pocket(BagPocket pocket) override; + virtual std::vector read_pocket() override; + virtual void use_row(size_t row) override; + virtual void close_bag() override; + +private: + ConsoleHandle& m_console; + ProControllerContext& m_context; + Language m_language; +}; + + +// Registers the item use tests. +void add_tests_AutoStoryItemUse(UnitTestDatabase& database); + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp new file mode 100644 index 0000000000..391644a4cf --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp @@ -0,0 +1,223 @@ +/* AutoStory Item Use Tests + * + * From: https://github.com/PokemonAutomation/ + * + * use_item_from_bag() and renew_repel() on a made-up bag, and the bike's + * pace. + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" +#include "PokemonFRLG_AutoStoryEvents.h" +#include "PokemonFRLG_AutoStoryItemUse.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +// A bag on the sim: its pockets, each item used takes one, a bag that may +// not open. +class SimBag : public ItemUseActions{ +public: + std::map> pockets; + bool opens = true; + std::vector used; + bool open = false; + + virtual bool open_pocket(BagPocket pocket) override{ + m_pocket = pocket; + open = opens; + return opens; + } + virtual std::vector read_pocket() override{ + return pockets[m_pocket]; + } + virtual void use_row(size_t row){ + BagRow& item = pockets[m_pocket][row]; + used.push_back(item.item); + if (item.count > 0){ + item.count--; + } + } + virtual void close_bag() override{ + open = false; + } + +private: + BagPocket m_pocket = BagPocket::ITEMS; +}; + +struct ItemUseInput{ + std::string name; + std::string variant; + std::vector items; + std::vector key_items; + bool opens = true; + // renew_repel() instead of use_item_from_bag(). + bool renew = false; +}; +struct ItemUseExpectation{ + ItemUseEnd end; + std::vector used; +}; +struct ItemUseCase{ + ItemUseInput input; + ItemUseExpectation expectation; +}; + +class Test_AutoStoryItemUse : public UnitTest{ +public: + explicit Test_AutoStoryItemUse(ItemUseCase test_case) + : UnitTest("PokemonFRLG::AutoStoryItemUse - " + test_case.input.name) + , m_case(std::move(test_case)) + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + SimBag bag; + bag.pockets[BagPocket::ITEMS] = m_case.input.items; + bag.pockets[BagPocket::KEY_ITEMS] = m_case.input.key_items; + bag.opens = m_case.input.opens; + const std::string variant = m_case.input.variant; + const std::string item = variant == RIDE_BIKE_VARIANT ? "bicycle" : parse_use_item_variant(variant).value_or(variant); + const ItemUseEnd end = m_case.input.renew ? renew_repel(logger, bag, item) : use_item_from_bag(logger, bag, item); + if (end != m_case.expectation.end || bag.used != m_case.expectation.used || bag.open){ + return std::format( + "\"{}\": {} using {} item(s), bag {}; expected {} using {}.", variant, + item_use_end_name(end), bag.used.size(), bag.open ? "open" : "closed", + item_use_end_name(m_case.expectation.end), m_case.expectation.used.size() + ); + } + return true; + } + +private: + ItemUseCase m_case; +}; + +const ItemUseCase ITEM_USE_CASES[] = { + { + .input = { + .name = "a Repel below the Potions", + .variant = "use_item repel", + .items = {{"potion", 15}, {"repel", 5}, {"escape-rope", 2}}, + }, + .expectation = {.end = ItemUseEnd::USED, .used = {"repel"}}, + }, + { + .input = { + .name = "the Escape Rope with its count unread", + .variant = "use_item escape-rope", + .items = {{"potion", 15}, {"", -1}, {"escape-rope", -1}}, + }, + .expectation = {.end = ItemUseEnd::USED, .used = {"escape-rope"}}, + }, + { + .input = { + .name = "no Super Repel bought", + .variant = "use_item super-repel", + .items = {{"potion", 15}, {"repel", 5}}, + }, + .expectation = {.end = ItemUseEnd::NONE_LEFT, .used = {}}, + }, + { + .input = { + .name = "the bag doesn't open", + .variant = "use_item repel", + .items = {{"repel", 5}}, + .opens = false, + }, + .expectation = {.end = ItemUseEnd::NO_BAG, .used = {}}, + }, + { + .input = { + .name = "the Bicycle from the Key Items", + .variant = "ride_bike", + .items = {{"bicycle", 1}}, + .key_items = {{"", -1}, {"bicycle", -1}}, + }, + .expectation = {.end = ItemUseEnd::USED, .used = {"bicycle"}}, + }, + { + .input = { + .name = "a Repel wore off: another used", + .variant = "use_item repel", + .items = {{"repel", 1}}, + .renew = true, + }, + .expectation = {.end = ItemUseEnd::USED, .used = {"repel"}}, + }, + { + .input = { + .name = "the last Repel wore off: none left", + .variant = "use_item repel", + .items = {{"repel", 0}}, + .renew = true, + }, + .expectation = {.end = ItemUseEnd::NONE_LEFT, .used = {}}, + }, + { + .input = { + .name = "an Escape Rope is no Repel to renew", + .variant = "use_item escape-rope", + .items = {{"escape-rope", 2}}, + .renew = true, + }, + .expectation = {.end = ItemUseEnd::UNKNOWN_ITEM, .used = {}}, + }, +}; + +class Test_AutoStoryItemUseVariants : public UnitTest{ +public: + Test_AutoStoryItemUseVariants() + : UnitTest("PokemonFRLG::AutoStoryItemUse - variants and the bike's pace") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + if (parse_use_item_variant("use_item potion") || parse_use_item_variant("use_item") || !parse_use_item_variant(use_item_variant("super-repel"))){ + return "Expected only the story's bag items as use_item variants."; + } + if (!is_item_use_variant(RIDE_BIKE_VARIANT) || is_item_use_variant("poster_switch")){ + return "Expected ride_bike, not poster_switch, as an item use."; + } + StoryStep ride; + ride.kind = Pokemon::StoryStepKind::ROUTE; + ride.variant = BIKE_VARIANT; + if (move_speed(ride) != MoveSpeed::BIKE){ + return "A bike ROUTE isn't ridden."; + } + RouteStep outdoors; + outdoors.tiles = 10; + RouteStep indoors = outdoors; + indoors.walk_only = true; + if (OverworldWalkSteps::tile_time(outdoors, MoveSpeed::BIKE) != MovementMechanics::BIKE_TILE_TIME + || OverworldWalkSteps::tile_time(indoors, MoveSpeed::BIKE) != MovementMechanics::TILE_TIME + ){ + return "Expected the bike's pace outdoors and a walk indoors."; + } + return true; + } +}; + +} // namespace + + +void add_tests_AutoStoryItemUse(UnitTestDatabase& database){ + for (const ItemUseCase& item : ITEM_USE_CASES){ + database.add(item); + } + database.add(); +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp index 372aa1ed5e..81140d3520 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp @@ -5,6 +5,7 @@ */ #include "PokemonFRLG_AutoStoryHideout.h" +#include "PokemonFRLG_AutoStoryItemUse.h" #include "PokemonFRLG_AutoStoryTableSteps.h" namespace PokemonAutomation{ @@ -167,6 +168,19 @@ StoryStep lift_floor(std::string name, std::string_view from, std::string_view t return step; } +StoryStep use_item(std::string name, std::string_view item, std::string why, std::optional pose_after){ + StoryStep step = make(StoryStepKind::GAME, std::move(name), std::move(why)); + step.variant = use_item_variant(item); + step.pose_after = std::move(pose_after); + return step; +} + +StoryStep ride_bike(std::string name, std::string why){ + StoryStep step = make(StoryStepKind::GAME, std::move(name), std::move(why)); + step.variant = RIDE_BIKE_VARIANT; + return step; +} + } // namespace StoryTable diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h index d8e92f3d45..0e28ad3048 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h @@ -189,6 +189,11 @@ StoryStep poster_switch(std::string name, std::string why); // Rides the Rocket Hideout's lift, the player facing its panel, from the // floor it stands at to `to` (lift_floor_variant()): "B1F", "B2F", "B4F". StoryStep lift_floor(std::string name, std::string_view from, std::string_view to, std::string why); +// Uses `item` from the bag on the overworld (use_item_variant()): a +// Repel, or an Escape Rope, which leaves the player on `pose_after`. +StoryStep use_item(std::string name, std::string_view item, std::string why, std::optional pose_after = std::nullopt); +// Gets on the Bicycle (RIDE_BIKE_VARIANT). +StoryStep ride_bike(std::string name, std::string why); // Rows in files of their own (PokemonFRLG_AutoStoryTableVermilionGym.cpp, diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 36df2c5b96..9de4d7fee3 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1920,6 +1920,9 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItems_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFaultSweep_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFaultSweep_Tests.h From a078ce6c5dec2f80624625399ec7b35027a13641 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 17:22:52 -0800 Subject: [PATCH 06/28] FRLG AutoStory: mark the sim bag's use_row override Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp index 391644a4cf..2ea359f252 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp @@ -42,7 +42,7 @@ class SimBag : public ItemUseActions{ virtual std::vector read_pocket() override{ return pockets[m_pocket]; } - virtual void use_row(size_t row){ + virtual void use_row(size_t row) override{ BagRow& item = pockets[m_pocket][row]; used.push_back(item.item); if (item.count > 0){ From 620d6ceca27bed1f776c0af380da2b63faaf3b8a Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 17:28:43 -0800 Subject: [PATCH 07/28] FRLG: code health: field actions in their own file, route tests in a case table Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Overworld/PokemonFRLG_OverworldMap.h | 5 + .../Overworld/PokemonFRLG_OverworldRoute.cpp | 466 --------------- .../PokemonFRLG_OverworldRoute_Tests.cpp | 552 ++++++++++++++++++ .../Overworld/PokemonFRLG_OverworldSim.cpp | 83 +-- .../Overworld/PokemonFRLG_OverworldSim.h | 62 +- .../PokemonFRLG_OverworldSimField.cpp | 125 ++++ .../PokemonFRLG_OverworldSpin_Tests.cpp | 88 ++- .../PokemonFRLG/PokemonFRLG_Tests.cpp | 4 - .../AutoStory/PokemonFRLG_AutoStoryEvents.cpp | 32 +- .../PokemonFRLG_AutoStoryHideout.cpp | 24 + .../AutoStory/PokemonFRLG_AutoStoryHideout.h | 18 +- .../PokemonFRLG_AutoStoryHideout_Tests.cpp | 193 +++--- .../PokemonFRLG_AutoStoryItemUse.cpp | 8 + .../AutoStory/PokemonFRLG_AutoStoryItemUse.h | 2 + .../PokemonFRLG_AutoStoryItemUse_Tests.cpp | 81 ++- .../AutoStory/PokemonFRLG_AutoStoryRowSim.cpp | 6 +- .../PokemonFRLG_AutoStoryStepTable_Tests.cpp | 5 + ...okemonFRLG_AutoStoryVermilionGym_Tests.cpp | 2 +- SerialPrograms/cmake/SourceFiles.cmake | 2 + 19 files changed, 1032 insertions(+), 726 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h index e6d7490370..84bf33ea49 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h @@ -164,10 +164,15 @@ PlayerState after_step_onto( // MB_SPIN_UP, MB_SPIN_DOWN, MB_SPIN_LEFT, MB_SPIN_RIGHT and // MB_STOP_SPINNING. The tile glyphs can't say so, so a map lists them in // these regions, as it does WILD_REGION. +// "spin_north": MB_SPIN_UP. extern const char SPIN_NORTH_REGION[]; +// "spin_south": MB_SPIN_DOWN. extern const char SPIN_SOUTH_REGION[]; +// "spin_west": MB_SPIN_LEFT. extern const char SPIN_WEST_REGION[]; +// "spin_east": MB_SPIN_RIGHT. extern const char SPIN_EAST_REGION[]; +// "spin_stop": MB_STOP_SPINNING. extern const char SPIN_STOP_REGION[]; // The way the arrow on this tile slides the player, if it is a spinner. std::optional spin_direction(const MapData& map, const TilePos& point); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp index cdea398515..d16f0ca592 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp @@ -561,472 +561,6 @@ MapPosition follow_overworld_route( -namespace{ - -class Test_OverworldRoute : public UnitTest{ -public: - Test_OverworldRoute(std::string name, std::function body) - : UnitTest("PokemonFRLG::OverworldRoute - " + std::move(name)) - , m_body(std::move(body)) - {} - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ - try{ - return m_body(); - }catch (Exception& e){ - return std::string(e.name()) + ": " + e.message(); - } - } - -private: - std::function m_body; -}; - -std::string describe(const std::vector& route){ - std::string text; - for (const RouteStep& step : route){ - text += "\n " + to_string(step); - } - return text; -} - -// A 5x4 map for the step rules: a pond, a raised ledge reached by a stair -// tile, and a tile that can't be entered moving south. -const MapData& small_map(){ - static const MapData MAP = parse_map(R"({ - "name": "Small", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, - "tiles": ["#####", "#.8.#", "#.w.#", "#####"], - "elevation": ["33333", "33403", "33133", "33333"] - })", "Small"); - return MAP; -} - -UnitTestResult test_step_rules(){ - const MapData& map = small_map(); - PlayerState state{{1, 1}, 3, false}; - if (classify_step(map, state, Direction::EAST) != StepKind::BLOCKED){ - return "Walked from elevation 3 onto elevation 4."; - } - if (classify_step(map, PlayerState{{2, 0}, 3, false}, Direction::SOUTH) != StepKind::BLOCKED){ - return "Entered a north-blocked tile moving south."; - } - state = PlayerState{{1, 2}, 3, false}; - if (classify_step(map, state, Direction::EAST) != StepKind::START_SURF){ - return "Can't start surfing onto the pond."; - } - state = after_step(map, state, Direction::EAST, StepKind::START_SURF); - if (!state.surfing || state.elevation != 1){ - return "Surfing didn't put the player on the water."; - } - if (classify_step(map, state, Direction::EAST) != StepKind::STOP_SURFING){ - return "Can't land from the pond."; - } - if (classify_step(map, PlayerState{{3, 2}, 3, false}, Direction::NORTH) != StepKind::WALK){ - return "Can't step from elevation 3 onto the elevation-0 stair."; - } - return true; -} - -// The maps after the Hall of Fame, which clears the way to Cerulean Cave. -std::vector post_game_maps(){ - return overworld_maps(map_flag_set(overworld_maps(), "after_hall_of_fame")); -} - -MapPosition point_on(const std::vector& maps, const std::string& map, const std::string& point){ - return MapPosition{map, map_point(maps, map, point)}; -} - -const MapData* map_named(const std::vector& maps, const std::string& name){ - const int index = find_map(maps, name); - return index < 0 ? nullptr : &maps[index]; -} - -UnitTestResult test_maps(){ - const std::vector maps = post_game_maps(); - const MapData* b1f = map_named(maps, "CeruleanCave_B1F"); - const MapData* city = map_named(maps, "CeruleanCity"); - if (b1f == nullptr || city == nullptr || map_named(maps, "CeruleanCity_PokemonCenter_1F") == nullptr){ - return "Missing a map."; - } - if (b1f->tiles.width != 40 || b1f->tiles.height != 23 || city->tiles.width != 48 || city->tiles.height != 40){ - return "Unexpected map size."; - } - const TilePos mewtwo_tile = map_point(maps, "CeruleanCave_B1F", "mewtwo"); - const MapObject* mewtwo = object_at(*b1f, mewtwo_tile); - if (mewtwo == nullptr || mewtwo->name != "mewtwo"){ - return "Mewtwo is not on B1F at " + to_string(mewtwo_tile) + "."; - } - if (object_at(*city, {1, 13}) != nullptr){ - return "The cave guard is still there after the Hall of Fame."; - } - if (object_at(*map_named(overworld_maps(), "CeruleanCity"), {1, 13}) == nullptr){ - return "The cave guard is missing before the Hall of Fame."; - } - return true; -} - -// Plans `start` to `goal`, replays it with the step rules and checks where it ends. -UnitTestResult check_route( - const std::vector& maps, - const MapPosition& start, const RouteGoal& goal, - std::vector& route -){ - const std::optional> planned = plan_overworld_route(maps, start, goal); - if (!planned){ - return "No route from " + to_string(start) + " to " + to_string(goal.position) + "."; - } - route = *planned; - const MapPosition end = follow_overworld_route(maps, start, route); - if (!(end == goal.position)){ - return "Route ends at " + to_string(end) + ":" + describe(route); - } - if (goal.facing && route.back().direction != *goal.facing){ - return "Doesn't end facing " + std::string(direction_name(*goal.facing)) + ":" + describe(route); - } - return true; -} - -// Below Mewtwo, facing it, to talk to it. -RouteGoal facing_mewtwo(const std::vector& maps){ - const TilePos mewtwo = map_point(maps, "CeruleanCave_B1F", "mewtwo"); - return RouteGoal{{"CeruleanCave_B1F", mewtwo.next(Direction::SOUTH)}, Direction::NORTH}; -} - -UnitTestResult test_entrance_to_mewtwo(){ - const std::vector maps = post_game_maps(); - std::vector route; - UnitTestResult result = check_route(maps, point_on(maps, "CeruleanCave_1F", "entrance"), facing_mewtwo(maps), route); - if (result.result != UnitTestResult::PASSED){ - return result; - } - bool surfs = false; - bool reaches_b1f = false; - for (const RouteStep& step : route){ - surfs |= step.action == RouteAction::SURF; - reaches_b1f |= step.warps && step.end.map == "CeruleanCave_B1F"; - } - if (!surfs || !reaches_b1f){ - return "Expected to take the ladder to B1F and to surf:" + describe(route); - } - return true; -} - -UnitTestResult test_pokemon_center_to_mewtwo(){ - const std::vector maps = post_game_maps(); - std::vector route; - UnitTestResult result = check_route( - maps, point_on(maps, "CeruleanCity_PokemonCenter_1F", "pc"), facing_mewtwo(maps), route - ); - if (result.result != UnitTestResult::PASSED){ - return result; - } - // Out through the exit mat, onto the city's step below the door, and - // into the cave by its entrance. - std::vector maps_in_order; - for (const RouteStep& step : route){ - if (step.warps){ - maps_in_order.push_back(step.end.map); - } - } - if (maps_in_order.size() < 2 || maps_in_order[0] != "CeruleanCity" || maps_in_order[1] != "CeruleanCave_1F"){ - return "Expected to leave by the mat and enter the cave next:" + describe(route); - } - if (!(route[0].end.map == "CeruleanCity_PokemonCenter_1F" || route[0].warps)){ - return "Unexpected first step:" + describe(route); - } - return true; -} - -UnitTestResult test_cave_guard_blocks(){ - // Before the Hall of Fame the guard stands in the cave entrance. - const std::vector maps = overworld_maps(); - if (plan_overworld_route(maps, point_on(maps, "CeruleanCity_PokemonCenter_1F", "pc"), facing_mewtwo(maps))){ - return "Planned past the cave guard."; - } - return true; -} - -UnitTestResult test_mewtwo_tile_unreachable(){ - const std::vector maps = post_game_maps(); - const std::optional> route = plan_overworld_route( - maps, - point_on(maps, "CeruleanCave_1F", "entrance"), - {point_on(maps, "CeruleanCave_B1F", "mewtwo"), std::nullopt} - ); - if (route){ - return "Planned a route onto Mewtwo itself:" + describe(*route); - } - return true; -} - -UnitTestResult test_story_maps(){ - const std::vector maps = overworld_maps(); - for (const char* name : { - "PalletTown_PlayersHouse_2F", "PalletTown_ProfessorOaksLab", "Route1", "ViridianCity_Mart", - "Route22", "Route2", "ViridianForest", "PewterCity", "PewterCity_Museum_1F", "PewterCity_Gym", - }){ - if (map_named(maps, name) == nullptr){ - return std::string("Missing map ") + name + "."; - } - } - return true; -} - -// Which maps `route` warps into, in order. -std::vector maps_warped_into(const std::vector& route){ - std::vector maps; - for (const RouteStep& step : route){ - if (step.warps){ - maps.push_back(step.end.map); - } - } - return maps; -} - -UnitTestResult test_bedroom_to_oaks_lab(){ - const std::vector maps = overworld_maps(); - std::vector route; - UnitTestResult result = check_route( - maps, point_on(maps, "PalletTown_PlayersHouse_2F", "start"), - {point_on(maps, "PalletTown", "oaks_lab_front"), Direction::NORTH}, route - ); - if (result.result != UnitTestResult::PASSED){ - return result; - } - const std::vector warped = maps_warped_into(route); - if (warped != std::vector{"PalletTown_PlayersHouse_1F", "PalletTown"}){ - return "Expected to take the stairs and leave by the front door:" + describe(route); - } - return true; -} - -UnitTestResult test_viridian_mart_to_pewter_gym(){ - const std::vector maps = overworld_maps(); - std::vector route; - UnitTestResult result = check_route( - maps, point_on(maps, "ViridianCity_Mart", "clerk_front"), - {point_on(maps, "PewterCity", "gym_front"), Direction::NORTH}, route - ); - if (result.result != UnitTestResult::PASSED){ - return result; - } - const std::vector warped = maps_warped_into(route); - if (std::find(warped.begin(), warped.end(), "ViridianForest") == warped.end()){ - return "Expected to go through Viridian Forest:" + describe(route); - } - return true; -} - -// A room with a sign on its north wall, above the third column. -const char* const TEST_SIGN_ROOM_JSON = R"({ - "name": "SignRoom", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, - "tiles": ["##S##", "#...#", "#...#", "#...#", "#####"], - "elevation": ["33333", "33333", "33333", "33333", "33333"] -})"; - -// Walking north under a sign stops by counting tiles, never by holding into -// the sign; under the wall beside it, the wall stops the player. -UnitTestResult test_no_hold_into_sign(){ - const std::vector maps{parse_map(TEST_SIGN_ROOM_JSON, "SignRoom")}; - std::vector route; - UnitTestResult result = check_route(maps, {"SignRoom", {2, 3}}, {{"SignRoom", {2, 1}}, std::nullopt}, route); - if (result.result != UnitTestResult::PASSED){ - return result; - } - for (const RouteStep& step : route){ - if (step.to_wall && step.end.point == TilePos{2, 1}){ - return "Expected no hold into the sign:" + describe(route); - } - } - route.clear(); - result = check_route(maps, {"SignRoom", {1, 3}}, {{"SignRoom", {1, 1}}, std::nullopt}, route); - if (result.result != UnitTestResult::PASSED){ - return result; - } - if (route.empty() || !route.back().to_wall){ - return "Expected the wall to stop the player beside the sign:" + describe(route); - } - return true; -} - -// A yard with a row of ledges across it. -const char* const TEST_LEDGE_YARD_JSON = R"({ - "name": "LedgeYard", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, - "tiles": ["#####", "#...#", "#...#", "#vvv#", "#...#", "#####"], - "elevation": ["33333", "33333", "33333", "33333", "33333", "33333"] -})"; - -// A ledge isn't a wall to hold into: moving the way it faces the player -// would hop it, and routes don't hop ledges. Viridian City, -// from the Pokemon Center's door south to the row above the ledge. -UnitTestResult test_no_hold_into_ledge(){ - const std::vector maps{parse_map(TEST_LEDGE_YARD_JSON, "LedgeYard")}; - std::vector route; - UnitTestResult result = check_route(maps, {"LedgeYard", {2, 1}}, {{"LedgeYard", {2, 2}}, std::nullopt}, route); - if (result.result != UnitTestResult::PASSED){ - return result; - } - for (const RouteStep& step : route){ - if (step.to_wall){ - return "Expected no hold into the ledge:" + describe(route); - } - } - route.clear(); - const std::vector world = overworld_maps(); - result = check_route(world, {"ViridianCity", {26, 27}}, {{"ViridianCity", {26, 29}}, std::nullopt}, route); - if (result.result != UnitTestResult::PASSED){ - return result; - } - for (const RouteStep& step : route){ - if (step.to_wall && step.end.point == TilePos{26, 29}){ - return "Expected no hold into the ledge below the Pokemon Center:" + describe(route); - } - } - return true; -} - -// Two small maps that don't need the map files: a room left by the exit mat -// at its bottom, onto the door of a yard, out of which the game walks the -// player. -// No wild battle can start between the bedroom and Oak's Lab: Pallet Town's -// streets have no tall grass. Route 2 and Viridian Forest do. -UnitTestResult test_wild_steps(){ - const std::vector maps = overworld_maps(); - std::vector route; - UnitTestResult result = check_route( - maps, point_on(maps, "PalletTown_PlayersHouse_2F", "start"), - {point_on(maps, "PalletTown", "oaks_lab_front"), Direction::NORTH}, route - ); - if (result.result != UnitTestResult::PASSED){ - return result; - } - for (const RouteStep& step : route){ - if (step.wild){ - return "Expected no wild step from the bedroom to Oak's Lab, got " + to_string(step) + ":" + describe(route); - } - } - route.clear(); - result = check_route( - maps, point_on(maps, "ViridianCity_Mart", "clerk_front"), - {point_on(maps, "PewterCity", "gym_front"), Direction::NORTH}, route - ); - if (result.result != UnitTestResult::PASSED){ - return result; - } - bool forest = false; - for (const RouteStep& step : route){ - forest = forest || (step.wild && step.end.map == "ViridianForest"); - } - if (!forest){ - return "Expected a wild step in Viridian Forest:" + describe(route); - } - // The leg from Pallet Town north onto Route 1 crosses Route 1's grass - // after the map's edge. - route.clear(); - result = check_route( - maps, point_on(maps, "PalletTown", "oaks_lab_front"), - {point_on(maps, "ViridianCity_Mart", "clerk_front"), std::nullopt}, route - ); - if (result.result != UnitTestResult::PASSED){ - return result; - } - for (const RouteStep& step : route){ - if (step.end.map == "Route1" && step.direction == Direction::NORTH && step.tiles > 14 && !step.wild){ - return "Expected the leg from Pallet Town onto Route 1 to be wild, got " + to_string(step) + ":" + describe(route); - } - } - return true; -} - -const char* const TEST_ROOM_JSON = R"({ - "name": "Room", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, - "tiles": ["#####", "#...#", "#...#", "##.##"], - "elevation": ["33333", "33333", "33333", "33333"], - "points": {"warp_0": [2, 3]}, - "warps": [{"x": 2, "y": 3, "enter": "walk_south", "to_map": "Yard", "to_point": "warp_0"}] -})"; -const char* const TEST_YARD_JSON = R"({ - "name": "Yard", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, - "tiles": ["#####", "##D##", "#...#", "#####"], - "elevation": ["33333", "33333", "33333", "33333"], - "points": {"warp_0": [2, 1]}, - "warps": [{"x": 2, "y": 1, "enter": "walk_into", "to_map": "Room", "to_point": "warp_0"}] -})"; - -UnitTestResult test_route_through_warp(){ - const std::vector maps{ - parse_map(TEST_ROOM_JSON, "Room"), - parse_map(TEST_YARD_JSON, "Yard"), - }; - std::vector route; - UnitTestResult result = check_route(maps, {"Room", {2, 1}}, {{"Yard", {1, 2}}, std::nullopt}, route); - if (result.result != UnitTestResult::PASSED){ - return result; - } - for (const RouteStep& step : route){ - if (step.warps && step.end.map == "Yard"){ - return true; - } - } - return "Expected to warp into the yard:" + describe(route); -} - -// `json` with its elevation grid replaced by `rows`, or left out if empty. -std::string with_elevation(std::string json, const std::string& rows){ - const size_t begin = json.find("\"elevation\""); - const size_t end = json.find("\n", begin); - json.erase(begin, end - begin + 1); - if (!rows.empty()){ - json.insert(begin, "\"elevation\": " + rows + ",\n"); - } - return json; -} - -// Room and yard as the decompilation writes them, their walls and the -// door at elevation 0, plan the same as with no elevation grid, which -// frlg_maps.py leaves out when every tile the player can stand on is ground. -UnitTestResult test_ground_only_elevation_left_out(){ - const std::vector written{ - parse_map(with_elevation(TEST_ROOM_JSON, R"(["00000", "03330", "03330", "00300"])"), "Room"), - parse_map(with_elevation(TEST_YARD_JSON, R"(["00000", "00000", "03330", "00000"])"), "Yard"), - }; - const std::vector left_out{ - parse_map(with_elevation(TEST_ROOM_JSON, ""), "Room"), - parse_map(with_elevation(TEST_YARD_JSON, ""), "Yard"), - }; - for (const MapPosition& start : {MapPosition{"Room", {1, 1}}, MapPosition{"Room", {3, 2}}}){ - for (const MapPosition& goal : {MapPosition{"Yard", {1, 2}}, MapPosition{"Yard", {3, 2}}, MapPosition{"Room", {2, 2}}}){ - const std::optional> a = plan_overworld_route(written, start, {goal, std::nullopt}); - const std::optional> b = plan_overworld_route(left_out, start, {goal, std::nullopt}); - if (!a || !b || describe(*a) != describe(*b)){ - return "Plans differ from " + to_string(start) + " to " + to_string(goal) + ":" - + (a ? describe(*a) : " none") + "\nwithout elevation:" + (b ? describe(*b) : " none"); - } - } - } - return true; -} - -} // namespace - -void add_tests_OverworldRoute(UnitTestDatabase& database){ - database.add("route through a warp", test_route_through_warp); - database.add("ground-only elevation left out", test_ground_only_elevation_left_out); - database.add("maps to Brock", test_story_maps); - database.add("bedroom to Oak's Lab", test_bedroom_to_oaks_lab); - database.add("Viridian Mart to Pewter Gym", test_viridian_mart_to_pewter_gym); - database.add("wild steps", test_wild_steps); - database.add("no hold into a sign", test_no_hold_into_sign); - database.add("no hold into a ledge", test_no_hold_into_ledge); - database.add("step rules", test_step_rules); - database.add("maps", test_maps); - database.add("Cerulean Cave entrance to Mewtwo", test_entrance_to_mewtwo); - database.add("Cerulean Pokemon Center to Mewtwo", test_pokemon_center_to_mewtwo); - database.add("cave guard blocks the way", test_cave_guard_blocks); - database.add("Mewtwo's tile is blocked", test_mewtwo_tile_unreachable); -} - - - } // namespace PokemonFRLG } // namespace NintendoSwitch } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp new file mode 100644 index 0000000000..419fe87757 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp @@ -0,0 +1,552 @@ +/* Overworld Route Tests + * + * From: https://github.com/PokemonAutomation/ + * + * Moved from PokemonFRLG_OverworldRoute.cpp, the tests registered from a + * table of cases. + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "PokemonFRLG_OverworldMaps.h" +#include "PokemonFRLG_OverworldRoute.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +struct RouteTestInput{ + std::string name; + UnitTestResult (*body)(); +}; +struct RouteTestExpectation{ + const char* description; +}; +struct RouteTestCase{ + RouteTestInput input; + RouteTestExpectation expectation; +}; + +class Test_OverworldRoute : public UnitTest{ +public: + explicit Test_OverworldRoute(RouteTestCase test_case) + : UnitTest("PokemonFRLG::OverworldRoute - " + test_case.input.name) + , m_case(std::move(test_case)) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + try{ + return m_case.input.body(); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + RouteTestCase m_case; +}; + +std::string describe(const std::vector& route){ + std::string text; + for (const RouteStep& step : route){ + text += "\n " + to_string(step); + } + return text; +} + +// A 5x4 map for the step rules: a pond, a raised ledge reached by a stair +// tile, and a tile that can't be entered moving south. +const MapData& small_map(){ + static const MapData MAP = parse_map(R"({ + "name": "Small", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, + "tiles": ["#####", "#.8.#", "#.w.#", "#####"], + "elevation": ["33333", "33403", "33133", "33333"] + })", "Small"); + return MAP; +} + +UnitTestResult test_step_rules(){ + const MapData& map = small_map(); + PlayerState state{{1, 1}, 3, false}; + if (classify_step(map, state, Direction::EAST) != StepKind::BLOCKED){ + return "Walked from elevation 3 onto elevation 4."; + } + if (classify_step(map, PlayerState{{2, 0}, 3, false}, Direction::SOUTH) != StepKind::BLOCKED){ + return "Entered a north-blocked tile moving south."; + } + state = PlayerState{{1, 2}, 3, false}; + if (classify_step(map, state, Direction::EAST) != StepKind::START_SURF){ + return "Can't start surfing onto the pond."; + } + state = after_step(map, state, Direction::EAST, StepKind::START_SURF); + if (!state.surfing || state.elevation != 1){ + return "Surfing didn't put the player on the water."; + } + if (classify_step(map, state, Direction::EAST) != StepKind::STOP_SURFING){ + return "Can't land from the pond."; + } + if (classify_step(map, PlayerState{{3, 2}, 3, false}, Direction::NORTH) != StepKind::WALK){ + return "Can't step from elevation 3 onto the elevation-0 stair."; + } + return true; +} + +// The maps after the Hall of Fame, which clears the way to Cerulean Cave. +std::vector post_game_maps(){ + return overworld_maps(map_flag_set(overworld_maps(), "after_hall_of_fame")); +} + +MapPosition point_on(const std::vector& maps, const std::string& map, const std::string& point){ + return MapPosition{map, map_point(maps, map, point)}; +} + +const MapData* map_named(const std::vector& maps, const std::string& name){ + const int index = find_map(maps, name); + return index < 0 ? nullptr : &maps[index]; +} + +UnitTestResult test_maps(){ + const std::vector maps = post_game_maps(); + const MapData* b1f = map_named(maps, "CeruleanCave_B1F"); + const MapData* city = map_named(maps, "CeruleanCity"); + if (b1f == nullptr || city == nullptr || map_named(maps, "CeruleanCity_PokemonCenter_1F") == nullptr){ + return "Missing a map."; + } + if (b1f->tiles.width != 40 || b1f->tiles.height != 23 || city->tiles.width != 48 || city->tiles.height != 40){ + return "Unexpected map size."; + } + const TilePos mewtwo_tile = map_point(maps, "CeruleanCave_B1F", "mewtwo"); + const MapObject* mewtwo = object_at(*b1f, mewtwo_tile); + if (mewtwo == nullptr || mewtwo->name != "mewtwo"){ + return "Mewtwo is not on B1F at " + to_string(mewtwo_tile) + "."; + } + if (object_at(*city, {1, 13}) != nullptr){ + return "The cave guard is still there after the Hall of Fame."; + } + if (object_at(*map_named(overworld_maps(), "CeruleanCity"), {1, 13}) == nullptr){ + return "The cave guard is missing before the Hall of Fame."; + } + return true; +} + +// Plans `start` to `goal`, replays it with the step rules and checks where it ends. +UnitTestResult check_route( + const std::vector& maps, + const MapPosition& start, const RouteGoal& goal, + std::vector& route +){ + const std::optional> planned = plan_overworld_route(maps, start, goal); + if (!planned){ + return "No route from " + to_string(start) + " to " + to_string(goal.position) + "."; + } + route = *planned; + const MapPosition end = follow_overworld_route(maps, start, route); + if (!(end == goal.position)){ + return "Route ends at " + to_string(end) + ":" + describe(route); + } + if (goal.facing && route.back().direction != *goal.facing){ + return "Doesn't end facing " + std::string(direction_name(*goal.facing)) + ":" + describe(route); + } + return true; +} + +// Below Mewtwo, facing it, to talk to it. +RouteGoal facing_mewtwo(const std::vector& maps){ + const TilePos mewtwo = map_point(maps, "CeruleanCave_B1F", "mewtwo"); + return RouteGoal{{"CeruleanCave_B1F", mewtwo.next(Direction::SOUTH)}, Direction::NORTH}; +} + +UnitTestResult test_entrance_to_mewtwo(){ + const std::vector maps = post_game_maps(); + std::vector route; + UnitTestResult result = check_route(maps, point_on(maps, "CeruleanCave_1F", "entrance"), facing_mewtwo(maps), route); + if (result.result != UnitTestResult::PASSED){ + return result; + } + bool surfs = false; + bool reaches_b1f = false; + for (const RouteStep& step : route){ + surfs |= step.action == RouteAction::SURF; + reaches_b1f |= step.warps && step.end.map == "CeruleanCave_B1F"; + } + if (!surfs || !reaches_b1f){ + return "Expected to take the ladder to B1F and to surf:" + describe(route); + } + return true; +} + +UnitTestResult test_pokemon_center_to_mewtwo(){ + const std::vector maps = post_game_maps(); + std::vector route; + UnitTestResult result = check_route( + maps, point_on(maps, "CeruleanCity_PokemonCenter_1F", "pc"), facing_mewtwo(maps), route + ); + if (result.result != UnitTestResult::PASSED){ + return result; + } + // Out through the exit mat, onto the city's step below the door, and + // into the cave by its entrance. + std::vector maps_in_order; + for (const RouteStep& step : route){ + if (step.warps){ + maps_in_order.push_back(step.end.map); + } + } + if (maps_in_order.size() < 2 || maps_in_order[0] != "CeruleanCity" || maps_in_order[1] != "CeruleanCave_1F"){ + return "Expected to leave by the mat and enter the cave next:" + describe(route); + } + if (!(route[0].end.map == "CeruleanCity_PokemonCenter_1F" || route[0].warps)){ + return "Unexpected first step:" + describe(route); + } + return true; +} + +UnitTestResult test_cave_guard_blocks(){ + // Before the Hall of Fame the guard stands in the cave entrance. + const std::vector maps = overworld_maps(); + if (plan_overworld_route(maps, point_on(maps, "CeruleanCity_PokemonCenter_1F", "pc"), facing_mewtwo(maps))){ + return "Planned past the cave guard."; + } + return true; +} + +UnitTestResult test_mewtwo_tile_unreachable(){ + const std::vector maps = post_game_maps(); + const std::optional> route = plan_overworld_route( + maps, + point_on(maps, "CeruleanCave_1F", "entrance"), + {point_on(maps, "CeruleanCave_B1F", "mewtwo"), std::nullopt} + ); + if (route){ + return "Planned a route onto Mewtwo itself:" + describe(*route); + } + return true; +} + +UnitTestResult test_story_maps(){ + const std::vector maps = overworld_maps(); + for (const char* name : { + "PalletTown_PlayersHouse_2F", "PalletTown_ProfessorOaksLab", "Route1", "ViridianCity_Mart", + "Route22", "Route2", "ViridianForest", "PewterCity", "PewterCity_Museum_1F", "PewterCity_Gym", + }){ + if (map_named(maps, name) == nullptr){ + return std::string("Missing map ") + name + "."; + } + } + return true; +} + +// Which maps `route` warps into, in order. +std::vector maps_warped_into(const std::vector& route){ + std::vector maps; + for (const RouteStep& step : route){ + if (step.warps){ + maps.push_back(step.end.map); + } + } + return maps; +} + +UnitTestResult test_bedroom_to_oaks_lab(){ + const std::vector maps = overworld_maps(); + std::vector route; + UnitTestResult result = check_route( + maps, point_on(maps, "PalletTown_PlayersHouse_2F", "start"), + {point_on(maps, "PalletTown", "oaks_lab_front"), Direction::NORTH}, route + ); + if (result.result != UnitTestResult::PASSED){ + return result; + } + const std::vector warped = maps_warped_into(route); + if (warped != std::vector{"PalletTown_PlayersHouse_1F", "PalletTown"}){ + return "Expected to take the stairs and leave by the front door:" + describe(route); + } + return true; +} + +UnitTestResult test_viridian_mart_to_pewter_gym(){ + const std::vector maps = overworld_maps(); + std::vector route; + UnitTestResult result = check_route( + maps, point_on(maps, "ViridianCity_Mart", "clerk_front"), + {point_on(maps, "PewterCity", "gym_front"), Direction::NORTH}, route + ); + if (result.result != UnitTestResult::PASSED){ + return result; + } + const std::vector warped = maps_warped_into(route); + if (std::find(warped.begin(), warped.end(), "ViridianForest") == warped.end()){ + return "Expected to go through Viridian Forest:" + describe(route); + } + return true; +} + +// A room with a sign on its north wall, above the third column. +const char* const TEST_SIGN_ROOM_JSON = R"({ + "name": "SignRoom", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, + "tiles": ["##S##", "#...#", "#...#", "#...#", "#####"], + "elevation": ["33333", "33333", "33333", "33333", "33333"] +})"; + +// Walking north under a sign stops by counting tiles, never by holding into +// the sign; under the wall beside it, the wall stops the player. +UnitTestResult test_no_hold_into_sign(){ + const std::vector maps{parse_map(TEST_SIGN_ROOM_JSON, "SignRoom")}; + std::vector route; + UnitTestResult result = check_route(maps, {"SignRoom", {2, 3}}, {{"SignRoom", {2, 1}}, std::nullopt}, route); + if (result.result != UnitTestResult::PASSED){ + return result; + } + for (const RouteStep& step : route){ + if (step.to_wall && step.end.point == TilePos{2, 1}){ + return "Expected no hold into the sign:" + describe(route); + } + } + route.clear(); + result = check_route(maps, {"SignRoom", {1, 3}}, {{"SignRoom", {1, 1}}, std::nullopt}, route); + if (result.result != UnitTestResult::PASSED){ + return result; + } + if (route.empty() || !route.back().to_wall){ + return "Expected the wall to stop the player beside the sign:" + describe(route); + } + return true; +} + +// A yard with a row of ledges across it. +const char* const TEST_LEDGE_YARD_JSON = R"({ + "name": "LedgeYard", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, + "tiles": ["#####", "#...#", "#...#", "#vvv#", "#...#", "#####"], + "elevation": ["33333", "33333", "33333", "33333", "33333", "33333"] +})"; + +// A ledge isn't a wall to hold into: moving the way it faces the player +// would hop it, and routes don't hop ledges. Viridian City, +// from the Pokemon Center's door south to the row above the ledge. +UnitTestResult test_no_hold_into_ledge(){ + const std::vector maps{parse_map(TEST_LEDGE_YARD_JSON, "LedgeYard")}; + std::vector route; + UnitTestResult result = check_route(maps, {"LedgeYard", {2, 1}}, {{"LedgeYard", {2, 2}}, std::nullopt}, route); + if (result.result != UnitTestResult::PASSED){ + return result; + } + for (const RouteStep& step : route){ + if (step.to_wall){ + return "Expected no hold into the ledge:" + describe(route); + } + } + route.clear(); + const std::vector world = overworld_maps(); + result = check_route(world, {"ViridianCity", {26, 27}}, {{"ViridianCity", {26, 29}}, std::nullopt}, route); + if (result.result != UnitTestResult::PASSED){ + return result; + } + for (const RouteStep& step : route){ + if (step.to_wall && step.end.point == TilePos{26, 29}){ + return "Expected no hold into the ledge below the Pokemon Center:" + describe(route); + } + } + return true; +} + +// Two small maps that don't need the map files: a room left by the exit mat +// at its bottom, onto the door of a yard, out of which the game walks the +// player. +// No wild battle can start between the bedroom and Oak's Lab: Pallet Town's +// streets have no tall grass. Route 2 and Viridian Forest do. +UnitTestResult test_wild_steps(){ + const std::vector maps = overworld_maps(); + std::vector route; + UnitTestResult result = check_route( + maps, point_on(maps, "PalletTown_PlayersHouse_2F", "start"), + {point_on(maps, "PalletTown", "oaks_lab_front"), Direction::NORTH}, route + ); + if (result.result != UnitTestResult::PASSED){ + return result; + } + for (const RouteStep& step : route){ + if (step.wild){ + return "Expected no wild step from the bedroom to Oak's Lab, got " + to_string(step) + ":" + describe(route); + } + } + route.clear(); + result = check_route( + maps, point_on(maps, "ViridianCity_Mart", "clerk_front"), + {point_on(maps, "PewterCity", "gym_front"), Direction::NORTH}, route + ); + if (result.result != UnitTestResult::PASSED){ + return result; + } + bool forest = false; + for (const RouteStep& step : route){ + forest = forest || (step.wild && step.end.map == "ViridianForest"); + } + if (!forest){ + return "Expected a wild step in Viridian Forest:" + describe(route); + } + // The leg from Pallet Town north onto Route 1 crosses Route 1's grass + // after the map's edge. + route.clear(); + result = check_route( + maps, point_on(maps, "PalletTown", "oaks_lab_front"), + {point_on(maps, "ViridianCity_Mart", "clerk_front"), std::nullopt}, route + ); + if (result.result != UnitTestResult::PASSED){ + return result; + } + for (const RouteStep& step : route){ + if (step.end.map == "Route1" && step.direction == Direction::NORTH && step.tiles > 14 && !step.wild){ + return "Expected the leg from Pallet Town onto Route 1 to be wild, got " + to_string(step) + ":" + describe(route); + } + } + return true; +} + +const char* const TEST_ROOM_JSON = R"({ + "name": "Room", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, + "tiles": ["#####", "#...#", "#...#", "##.##"], + "elevation": ["33333", "33333", "33333", "33333"], + "points": {"warp_0": [2, 3]}, + "warps": [{"x": 2, "y": 3, "enter": "walk_south", "to_map": "Yard", "to_point": "warp_0"}] +})"; +const char* const TEST_YARD_JSON = R"({ + "name": "Yard", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, + "tiles": ["#####", "##D##", "#...#", "#####"], + "elevation": ["33333", "33333", "33333", "33333"], + "points": {"warp_0": [2, 1]}, + "warps": [{"x": 2, "y": 1, "enter": "walk_into", "to_map": "Room", "to_point": "warp_0"}] +})"; + +UnitTestResult test_route_through_warp(){ + const std::vector maps{ + parse_map(TEST_ROOM_JSON, "Room"), + parse_map(TEST_YARD_JSON, "Yard"), + }; + std::vector route; + UnitTestResult result = check_route(maps, {"Room", {2, 1}}, {{"Yard", {1, 2}}, std::nullopt}, route); + if (result.result != UnitTestResult::PASSED){ + return result; + } + for (const RouteStep& step : route){ + if (step.warps && step.end.map == "Yard"){ + return true; + } + } + return "Expected to warp into the yard:" + describe(route); +} + +// `json` with its elevation grid replaced by `rows`, or left out if empty. +std::string with_elevation(std::string json, const std::string& rows){ + const size_t begin = json.find("\"elevation\""); + const size_t end = json.find("\n", begin); + json.erase(begin, end - begin + 1); + if (!rows.empty()){ + json.insert(begin, "\"elevation\": " + rows + ",\n"); + } + return json; +} + +// Room and yard as the decompilation writes them, their walls and the +// door at elevation 0, plan the same as with no elevation grid, which +// frlg_maps.py leaves out when every tile the player can stand on is ground. +UnitTestResult test_ground_only_elevation_left_out(){ + const std::vector written{ + parse_map(with_elevation(TEST_ROOM_JSON, R"(["00000", "03330", "03330", "00300"])"), "Room"), + parse_map(with_elevation(TEST_YARD_JSON, R"(["00000", "00000", "03330", "00000"])"), "Yard"), + }; + const std::vector left_out{ + parse_map(with_elevation(TEST_ROOM_JSON, ""), "Room"), + parse_map(with_elevation(TEST_YARD_JSON, ""), "Yard"), + }; + for (const MapPosition& start : {MapPosition{"Room", {1, 1}}, MapPosition{"Room", {3, 2}}}){ + for (const MapPosition& goal : {MapPosition{"Yard", {1, 2}}, MapPosition{"Yard", {3, 2}}, MapPosition{"Room", {2, 2}}}){ + const std::optional> a = plan_overworld_route(written, start, {goal, std::nullopt}); + const std::optional> b = plan_overworld_route(left_out, start, {goal, std::nullopt}); + if (!a || !b || describe(*a) != describe(*b)){ + return "Plans differ from " + to_string(start) + " to " + to_string(goal) + ":" + + (a ? describe(*a) : " none") + "\nwithout elevation:" + (b ? describe(*b) : " none"); + } + } + } + return true; +} + +const RouteTestCase ROUTE_CASES[] = { + { + .input = {.name = "route through a warp", .body = test_route_through_warp}, + .expectation = {.description = "route through a warp"}, + }, + { + .input = {.name = "ground-only elevation left out", .body = test_ground_only_elevation_left_out}, + .expectation = {.description = "ground-only elevation left out"}, + }, + { + .input = {.name = "maps to Brock", .body = test_story_maps}, + .expectation = {.description = "maps to Brock"}, + }, + { + .input = {.name = "bedroom to Oak's Lab", .body = test_bedroom_to_oaks_lab}, + .expectation = {.description = "bedroom to Oak's Lab"}, + }, + { + .input = {.name = "Viridian Mart to Pewter Gym", .body = test_viridian_mart_to_pewter_gym}, + .expectation = {.description = "Viridian Mart to Pewter Gym"}, + }, + { + .input = {.name = "wild steps", .body = test_wild_steps}, + .expectation = {.description = "wild steps"}, + }, + { + .input = {.name = "no hold into a sign", .body = test_no_hold_into_sign}, + .expectation = {.description = "no hold into a sign"}, + }, + { + .input = {.name = "no hold into a ledge", .body = test_no_hold_into_ledge}, + .expectation = {.description = "no hold into a ledge"}, + }, + { + .input = {.name = "step rules", .body = test_step_rules}, + .expectation = {.description = "step rules"}, + }, + { + .input = {.name = "maps", .body = test_maps}, + .expectation = {.description = "maps"}, + }, + { + .input = {.name = "Cerulean Cave entrance to Mewtwo", .body = test_entrance_to_mewtwo}, + .expectation = {.description = "Cerulean Cave entrance to Mewtwo"}, + }, + { + .input = {.name = "Cerulean Pokemon Center to Mewtwo", .body = test_pokemon_center_to_mewtwo}, + .expectation = {.description = "Cerulean Pokemon Center to Mewtwo"}, + }, + { + .input = {.name = "cave guard blocks the way", .body = test_cave_guard_blocks}, + .expectation = {.description = "cave guard blocks the way"}, + }, + { + .input = {.name = "Mewtwo's tile is blocked", .body = test_mewtwo_tile_unreachable}, + .expectation = {.description = "Mewtwo's tile is blocked"}, + }, +}; + +} // namespace + +void add_tests_OverworldRoute(UnitTestDatabase& database){ + for (const RouteTestCase& item : ROUTE_CASES){ + database.add(item); + } +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp index 0b9483bf2c..b0cd746e07 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp @@ -96,8 +96,6 @@ namespace{ constexpr Milliseconds NO_BATTLE_WAIT(15000); constexpr Milliseconds BATTLE_TIME(10000); constexpr Milliseconds MESSAGE_CLOSE(1000); -// The questions, " used CUT!" and the tree falling. -constexpr Milliseconds CUT_TIME(4000); // Rows of black below a small room, seen from this near its bottom. constexpr int BLACK_BELOW_ROWS = 5; constexpr int SMALL_ROOM_ROWS = 20; @@ -246,14 +244,8 @@ std::optional OverworldSim::take_warp(int map_index, const T if (warp == nullptr){ return std::nullopt; } - const auto lift = m_script.lift_floors.find(m_maps[map_index].name); - if (lift != m_script.lift_floors.end() && m_lift_floor){ - const MapPosition& out = lift->second[*m_lift_floor]; - const int destination = find_map(m_maps, out.map); - if (destination < 0){ - return std::nullopt; - } - return Step{destination, PlayerState{out.point, elevation_at(m_maps[destination], out.point), false}, true, false, false}; + if (m_lift_floor && m_script.lift_floors.contains(m_maps[map_index].name)){ + return lift_warp(map_index); } if (!warp->to){ return std::nullopt; @@ -325,12 +317,8 @@ std::optional OverworldSim::press(Direction direction) const if (warps_on_step(map(), ahead)){ return take_warp(m_map, ahead); } - if (spin_direction(map(), ahead)){ - step.spin = spin_slide(map(), step.player); - if (!step.spin){ - return std::nullopt; - } - step.player = step.spin->end; + if (!slide_on(step)){ + return std::nullopt; } return step; } @@ -510,13 +498,7 @@ void OverworldSim::take_steps(Direction direction, Milliseconds pressed, Millise if (step->walked_out){ m_state.facing = Direction::SOUTH; } - if (step->spin){ - // The game moves the player through the slide; the hold, if - // it goes on, walks on from its end. - m_state.facing = step->spin->facing; - note(std::format("slid {} tile(s) to {}.", step->spin->tiles, to_string(step->spin->end.position))); - pressed += MovementMechanics::SPIN_TILE_TIME * step->spin->tiles; - } + pressed += after_slide(*step); if (after_step(*step, pressed)){ return; } @@ -677,51 +659,6 @@ bool OverworldSim::close_message(){ return true; } -bool OverworldSim::cut_tree(){ - std::vector& objects = m_maps[m_map].objects; - const TilePos ahead = m_player.position.next(m_state.facing); - for (size_t c = 0; c < objects.size(); c++){ - if (objects[c].name == "cut_tree" && objects[c].x == ahead.x && objects[c].y == ahead.y - && m_state.screen == SimScreen::OVERWORLD && !m_frozen - ){ - m_cut_trees.emplace_back(m_map, objects[c]); - objects.erase(objects.begin() + c); - advance_to(m_now + CUT_TIME); - note("cut the tree on " + to_string(MapPosition{map().name, ahead}) + "."); - return true; - } - } - violation("no tree to cut " + std::string(direction_name(m_state.facing)) + " of " + to_string(m_state.position) + "."); - return false; -} - -bool OverworldSim::choose_lift_floor(size_t floor){ - const auto lift = m_script.lift_floors.find(map().name); - if (lift == m_script.lift_floors.end() || floor >= lift->second.size() || m_frozen){ - violation("no lift panel on " + to_string(m_state.position) + " with a floor " + std::to_string(floor) + "."); - return false; - } - m_lift_floor = floor; - advance_to(m_now + m_script.timing.message); - note("the lift goes to " + to_string(lift->second[floor]) + "."); - return true; -} - -bool OverworldSim::push_poster_switch(){ - std::vector& objects = m_maps[m_map].objects; - const TilePos ahead = m_player.position.next(m_state.facing); - const auto poster = std::find_if(objects.begin(), objects.end(), [&](const MapObject& object){ - return object.name == "poster" && object.x == ahead.x && object.y == ahead.y; - }); - if (poster == objects.end() || m_state.screen != SimScreen::OVERWORLD || m_frozen){ - violation("no poster " + std::string(direction_name(m_state.facing)) + " of " + to_string(m_state.position) + "."); - return false; - } - std::erase_if(objects, [](const MapObject& object){ return object.name == "poster_stairs"; }); - advance_to(m_now + m_script.timing.message); - note("pushed the switch behind the poster: the stairs are open."); - return true; -} @@ -813,6 +750,12 @@ size_t OverworldSimWalker::clear_late(const std::vector& interrupts, } } +void OverworldSimWalker::wait_out_slide(const RouteStep& step){ + if (step.spin_tiles){ + m_sim.watch({}, SPIN_TILE_TIME * *step.spin_tiles + SPIN_STOP_SETTLE); + } +} + void OverworldSimWalker::check_overworld(const RouteStep& step){ const SimScreen screen = m_sim.state().screen; if (m_rules.check_overworld && (screen == SimScreen::START_MENU || screen == SimScreen::MESSAGE)){ @@ -900,9 +843,7 @@ std::optional OverworldSimWalker::walk_step(const RouteStep& step, if (result.stopped){ return dark_stop(step, walked_near(step, result.tiles, TILE_TIME)); } - if (step.spin_tiles){ - m_sim.watch({}, SPIN_TILE_TIME * *step.spin_tiles + SPIN_STOP_SETTLE); - } + wait_out_slide(step); const Milliseconds fade_watched_from = m_rules.watch_fade_along_hold ? m_leg_start : m_sim.state().time; if (step.warps && !after_warp(step, warps_before, fade_watched_from)){ if (!m_rules.find_player_after_warp_battle){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h index 4eb0884abc..f2dc04326b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h @@ -60,7 +60,7 @@ enum class SimScreen{ BATTLE, // a battle's text box with the HP boxes FADE, // a warp's fade out and in }; -// The human-readable name of a simulated screen. +// The human-readable name of a simulated screen. const char* sim_screen_name(SimScreen screen); // What the walker watches the screen with. @@ -206,14 +206,30 @@ enum class SimBattleChoice{ RUN, FIGHT, }; -// How a simulated battle concluded. +// How a simulated battle concluded. enum class SimBattleEnd{ ENDED, CANT_RUN, // a trainer's battle: "No! There's no running from a TRAINER battle!" NO_BATTLE, // there was none, on screen or about to show }; -// Simulates overworld movement, encounters and dialogs. +// What A does on the overworld, facing something (OverworldSim::use_field()). +enum class SimField{ + // Cuts the tree the player faces (an object named "cut_tree") with + // YES to the game's question: it is gone until the player next warps, + // as its hide flag is a FLAG_TEMP. + CUT_TREE, + // The poster the player faces (an object named "poster"): the switch + // behind it opens the stairs, which takes away the objects named + // "poster_stairs" for good (pokefirered + // CeladonCity_GameCorner_EventScript_Poster's setmetatile). + POSTER_SWITCH, + // The panel of the lift the player stands in (SimScript::lift_floors): + // the floor picked, the lift's door now lets out there. + LIFT_FLOOR, +}; + +// Simulates overworld movement, encounters and dialogs. class OverworldSim{ public: OverworldSim(std::vector maps, MapPosition start, Direction facing, SimScript script = {}); @@ -255,22 +271,9 @@ class OverworldSim{ // battle follows it. bool close_message(); - // Cuts the tree the player faces (an object named "cut_tree") with - // YES to the game's question: it is gone until the player next warps, - // as its hide flag is a FLAG_TEMP. Returns whether there was one. - bool cut_tree(); - - // A on the panel of the lift the player stands in (SimScript:: - // lift_floors) and floor `floor` picked: the lift's door now lets out - // there. Returns whether there was such a lift and floor. - bool choose_lift_floor(size_t floor); - - // A on the poster the player faces (an object named "poster"): the - // switch behind it opens the stairs, which takes away the objects - // named "poster_stairs" for good (pokefirered - // CeladonCity_GameCorner_EventScript_Poster's setmetatile). Returns - // whether there was a poster. - bool push_poster_switch(); + // A on what the player faces, as `field` says (`floor`: the lift's). + // Returns whether it was there. (PokemonFRLG_OverworldSimField.cpp) + bool use_field(SimField field, size_t floor = 0); private: // A screen that shows at `at`; `unfreeze`: the player can move again. @@ -314,6 +317,17 @@ class OverworldSim{ std::optional press(Direction direction) const; std::optional take_warp(int map_index, const TilePos& tile) const; std::optional cross_edge(Direction direction) const; + // The lift's door to the floor picked, if `map_index` is a lift and + // one was picked. + std::optional lift_warp(int map_index) const; + // A step that landed on a spinner slides on to the slide's end; false + // if the slide never stops. + bool slide_on(Step& step) const; + // The slide's time after `step`, and its facing; 0 for no slide. + Milliseconds after_slide(const Step& step); + bool cut_tree(); + bool choose_lift_floor(size_t floor); + bool push_poster_switch(); bool after_step(const Step& step, Milliseconds pressed); bool wild_battle(Milliseconds pressed); std::optional next_encounter(); @@ -516,6 +530,8 @@ class OverworldSimWalker{ StepEnd watch_for_trainer(const RouteStep& step, int approach_tiles); bool moved_past_wall(const RouteStep& step); void check_overworld(const RouteStep& step); + // After a leg onto a spinner: the slide's time. + void wait_out_slide(const RouteStep& step); const std::vector& interrupts(const RouteStep& step) const; std::vector watchers(const std::vector& interrupts) const; [[noreturn]] void fail(const std::string& message); @@ -532,7 +548,7 @@ class OverworldSimWalker{ }; -// Register overworld simulation unit tests. +// Register overworld simulation unit tests. void add_tests_OverworldSim(UnitTestDatabase& database); // Register the tests of a warp's fade over before the hold onto it ends. void add_tests_OverworldWarpFade(UnitTestDatabase& database); @@ -542,7 +558,7 @@ void add_tests_OverworldSpin(UnitTestDatabase& database); -} // namespace PokemonFRLG -} // namespace NintendoSwitch -} // namespace PokemonAutomation +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp new file mode 100644 index 0000000000..2f657677a6 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp @@ -0,0 +1,125 @@ +/* Overworld Sim: the Field + * + * From: https://github.com/PokemonAutomation/ + * + * What A does on the sim's overworld, facing something: a tree to cut, + * the Game Corner's poster, the lift's panel (OverworldSim::use_field()). + * + */ + +#include +#include +#include "PokemonFRLG_OverworldSim.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +// The questions, " used CUT!" and the tree falling. +constexpr Milliseconds CUT_TIME(4000); + +} // namespace + + +bool OverworldSim::use_field(SimField field, size_t floor){ + switch (field){ + case SimField::CUT_TREE: + return cut_tree(); + case SimField::POSTER_SWITCH: + return push_poster_switch(); + case SimField::LIFT_FLOOR: + return choose_lift_floor(floor); + } + return false; +} + +bool OverworldSim::slide_on(Step& step) const{ + if (!spin_direction(m_maps[step.map], step.player.position)){ + return true; + } + step.spin = spin_slide(m_maps[step.map], step.player); + if (!step.spin){ + return false; + } + step.player = step.spin->end; + return true; +} + +Milliseconds OverworldSim::after_slide(const Step& step){ + if (!step.spin){ + return Milliseconds(0); + } + // The game moves the player through the slide; the hold, if it goes + // on, walks on from its end. + m_state.facing = step.spin->facing; + note(std::format("slid {} tile(s) to {}.", step.spin->tiles, to_string(step.spin->end.position))); + return MovementMechanics::SPIN_TILE_TIME * step.spin->tiles; +} + +std::optional OverworldSim::lift_warp(int map_index) const{ + const auto lift = m_script.lift_floors.find(m_maps[map_index].name); + if (lift == m_script.lift_floors.end() || !m_lift_floor){ + return std::nullopt; + } + const MapPosition& out = lift->second[*m_lift_floor]; + const auto to = std::find_if(m_maps.begin(), m_maps.end(), [&](const MapData& map){ return map.name == out.map; }); + if (to == m_maps.end()){ + return std::nullopt; + } + return Step{(int)(to - m_maps.begin()), PlayerState{out.point, elevation_at(*to, out.point), false}, true, false, false}; +} + +bool OverworldSim::cut_tree(){ + std::vector& objects = m_maps[m_map].objects; + const TilePos ahead = m_player.position.next(m_state.facing); + for (size_t c = 0; c < objects.size(); c++){ + if (objects[c].name == "cut_tree" && objects[c].x == ahead.x && objects[c].y == ahead.y + && m_state.screen == SimScreen::OVERWORLD && !m_frozen + ){ + m_cut_trees.emplace_back(m_map, objects[c]); + objects.erase(objects.begin() + c); + advance_to(m_now + CUT_TIME); + note("cut the tree on " + to_string(MapPosition{map().name, ahead}) + "."); + return true; + } + } + violation("no tree to cut " + std::string(direction_name(m_state.facing)) + " of " + to_string(m_state.position) + "."); + return false; +} + +bool OverworldSim::choose_lift_floor(size_t floor){ + const auto lift = m_script.lift_floors.find(map().name); + if (lift == m_script.lift_floors.end() || floor >= lift->second.size() || m_frozen){ + violation("no lift panel on " + to_string(m_state.position) + " with a floor " + std::to_string(floor) + "."); + return false; + } + m_lift_floor = floor; + advance_to(m_now + m_script.timing.message); + note("the lift goes to " + to_string(lift->second[floor]) + "."); + return true; +} + +bool OverworldSim::push_poster_switch(){ + std::vector& objects = m_maps[m_map].objects; + const TilePos ahead = m_player.position.next(m_state.facing); + const auto poster = std::find_if(objects.begin(), objects.end(), [&](const MapObject& object){ + return object.name == "poster" && object.x == ahead.x && object.y == ahead.y; + }); + if (poster == objects.end() || m_state.screen != SimScreen::OVERWORLD || m_frozen){ + violation("no poster " + std::string(direction_name(m_state.facing)) + " of " + to_string(m_state.position) + "."); + return false; + } + std::erase_if(objects, [](const MapObject& object){ return object.name == "poster_stairs"; }); + advance_to(m_now + m_script.timing.message); + note("pushed the switch behind the poster: the stairs are open."); + return true; +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp index 298e3cb156..afe24a3f8b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp @@ -10,7 +10,6 @@ */ #include -#include #include #include #include "Common/Cpp/Exceptions.h" @@ -28,22 +27,34 @@ namespace PokemonFRLG{ namespace{ +struct SpinInput{ + std::string name; + UnitTestResult (*body)(); +}; +struct SpinExpectation{ + const char* description; +}; +struct SpinCase{ + SpinInput input; + SpinExpectation expectation; +}; + class Test_OverworldSpin : public UnitTest{ public: - Test_OverworldSpin(std::string name, std::function body) - : UnitTest("PokemonFRLG::OverworldSpin - " + std::move(name)) - , m_body(std::move(body)) + explicit Test_OverworldSpin(SpinCase test_case) + : UnitTest("PokemonFRLG::OverworldSpin - " + test_case.input.name) + , m_case(std::move(test_case)) {} virtual UnitTestResult run(Logger&, CancellableScope&) const override{ try{ - return m_body(); + return m_case.input.body(); }catch (Exception& e){ return std::string(e.name()) + ": " + e.message(); } } private: - std::function m_body; + SpinCase m_case; }; // A room with spinners: @@ -204,7 +215,7 @@ UnitTestResult lift_ride(bool pick, const MapPosition& expected){ SimScript script; script.lift_floors["Lift"] = {MapPosition{"FloorA", {1, 1}}, MapPosition{"FloorB", {1, 1}}}; OverworldSim sim(lift_maps(), MapPosition{"Lift", {1, 2}}, Direction::SOUTH, script); - if (pick && !sim.choose_lift_floor(1)){ + if (pick && !sim.use_field(SimField::LIFT_FLOOR, 1)){ return std::string("No floor 1 on the lift's panel."); } sim.hold(Direction::SOUTH, MovementMechanics::TILE_TIME, MovementMechanics::STEP_RELEASE, std::chrono::milliseconds(3000), {}); @@ -238,7 +249,7 @@ const MapData& poster_room(){ MapPosition poster_walk(bool push){ OverworldSim sim({poster_room()}, MapPosition{"Corner", {2, 2}}, Direction::NORTH); if (push){ - sim.push_poster_switch(); + sim.use_field(SimField::POSTER_SWITCH); } const Milliseconds tile = MovementMechanics::TILE_TIME; // Each hold turns the player first, which costs a tile's time. @@ -258,21 +269,60 @@ UnitTestResult test_poster_opens_stairs(){ return true; } +const SpinCase SPIN_CASES[] = { + { + .input = {.name = "slide up to a wall", .body = test_slide_into_wall}, + .expectation = {.description = "slide up to a wall"}, + }, + { + .input = {.name = "slide turned by an arrow", .body = test_slide_turned_by_arrow}, + .expectation = {.description = "slide turned by an arrow"}, + }, + { + .input = {.name = "slide to a stop tile", .body = test_slide_to_stop_tile}, + .expectation = {.description = "slide to a stop tile"}, + }, + { + .input = {.name = "arrows in a loop never stop", .body = test_slide_loop}, + .expectation = {.description = "arrows in a loop never stop"}, + }, + { + .input = {.name = "route ends with a slide", .body = test_route_ends_on_slide}, + .expectation = {.description = "route ends with a slide"}, + }, + { + .input = {.name = "a spinner back into a dead end traps", .body = test_dead_end_traps}, + .expectation = {.description = "a spinner back into a dead end traps"}, + }, + { + .input = {.name = "sim slides the player", .body = test_sim_slides}, + .expectation = {.description = "sim slides the player"}, + }, + { + .input = {.name = "sim walker walks a slide", .body = test_sim_walker_walks_slide}, + .expectation = {.description = "sim walker walks a slide"}, + }, + { + .input = {.name = "lift lets out on the floor picked", .body = test_lift_floor}, + .expectation = {.description = "lift lets out on the floor picked"}, + }, + { + .input = {.name = "lift with no floor picked stays", .body = test_lift_no_floor}, + .expectation = {.description = "lift with no floor picked stays"}, + }, + { + .input = {.name = "poster switch opens the stairs", .body = test_poster_opens_stairs}, + .expectation = {.description = "poster switch opens the stairs"}, + }, +}; + } // namespace void add_tests_OverworldSpin(UnitTestDatabase& database){ - database.add("slide up to a wall", test_slide_into_wall); - database.add("slide turned by an arrow", test_slide_turned_by_arrow); - database.add("slide to a stop tile", test_slide_to_stop_tile); - database.add("arrows in a loop never stop", test_slide_loop); - database.add("route ends with a slide", test_route_ends_on_slide); - database.add("a spinner back into a dead end traps", test_dead_end_traps); - database.add("sim slides the player", test_sim_slides); - database.add("sim walker walks a slide", test_sim_walker_walks_slide); - database.add("lift lets out on the floor picked", test_lift_floor); - database.add("lift with no floor picked stays", test_lift_no_floor); - database.add("poster switch opens the stairs", test_poster_opens_stairs); + for (const SpinCase& item : SPIN_CASES){ + database.add(item); + } } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index 28816b82aa..2bf7183092 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -48,8 +48,6 @@ #include "Programs/AutoStory/PokemonFRLG_AutoStoryAbilities.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryBattle.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h" -#include "Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h" -#include "Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryPcStorage.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop.h" @@ -940,8 +938,6 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStorySimulation(database); add_tests_AutoStoryRows(database); add_tests_AutoStoryTrashCans(database); - add_tests_AutoStoryHideout(database); - add_tests_AutoStoryItemUse(database); add_tests_AutoStoryPaceSim(database); add_tests_AutoStoryItems(database); add_tests_AutoStoryRngLoop(database); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp index b3bb43b2ec..e62adfbf53 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp @@ -23,7 +23,6 @@ #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryGrind.h" #include "PokemonFRLG_AutoStoryHideout.h" -#include "PokemonFRLG_AutoStoryItemUse.h" #include "PokemonFRLG_AutoStoryScreenEvents.h" #include "PokemonFRLG_AutoStorySummary.h" #include "PokemonFRLG_AutoStoryTable.h" @@ -366,13 +365,6 @@ const std::vector& story_maps(){ return maps; } -MoveSpeed move_speed(const StoryStep& step){ - if (step.variant == BIKE_VARIANT){ - return MoveSpeed::BIKE; - } - return step.variant == RUN_VARIANT ? MoveSpeed::RUN : MoveSpeed::WALK; -} - Pokemon::Pose to_pose(const StoryPose& pose){ return Pokemon::tile_pose(pose.map, {pose.tile}, pose.facing); } @@ -689,31 +681,15 @@ Outcome open_gym_door(StoryEnv& env, RunState& state, const StoryStep& step){ // The GAME steps without a row of their own: a capture or a PC deposit or // withdrawal, a MART, TEACH, LEVEL, GRIND or fossil CHOICE. Outcome other_game_step(StoryEnv& env, RunState& state, const StoryStep& step){ - // The Rocket Hideout's steps leave the player where they stand. - if (const std::optional ride = parse_lift_floor_variant(step.variant)){ - ride_lift(env.console, env.context, *ride); - return Pokemon::outcome_ok(); - } - if (step.variant == POSTER_SWITCH_VARIANT){ - push_poster_switch(env.console, env.context); - return Pokemon::outcome_ok(); - } - // An item from the bag; an Escape Rope ends where the table says. - if (is_item_use_variant(step.variant)){ - const std::string item = parse_use_item_variant(step.variant).value_or("bicycle"); - ConsoleItemUseActions bag(env.console, env.context, env.language); - const ItemUseEnd end = use_item_from_bag(env.console.logger(), bag, item); - if (end != ItemUseEnd::USED){ - return Pokemon::outcome_failed(std::string("use_item_") + item_use_end_name(end), step.variant); - } - return step.pose_after ? Pokemon::outcome_ok(*step.pose_after) : Pokemon::outcome_ok(); - } const Pokemon::Pose patch = state.pose; ConsoleCaptureActions capture(env.console, env.context, env.language, [&]{ pace_trainer_battle(env, state, step, patch); }); ConsolePcStorageActions pc(env.console, env.context); - std::optional done = run_storage_game_step(env.console.logger(), &env.console, step, capture, pc); + std::optional done = run_field_game_step(env.console, env.context, env.language, step); + if (!done){ + done = run_storage_game_step(env.console.logger(), &env.console, step, capture, pc); + } if (!done){ done = run_abilities(env, state, step); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp index f1208ac841..3726c116a4 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp @@ -8,6 +8,7 @@ #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "PokemonFRLG_AutoStoryActions.h" #include "PokemonFRLG_AutoStoryHideout.h" +#include "PokemonFRLG_AutoStoryItemUse.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -109,6 +110,29 @@ void push_poster_switch(ConsoleHandle& console, ProControllerContext& context){ mash_until_overworld(console, context, HIDEOUT_SCENE_TIMEOUT); } +std::optional run_field_game_step( + ConsoleHandle& console, ProControllerContext& context, Language language, const Pokemon::StoryStep& step +){ + if (const std::optional ride = parse_lift_floor_variant(step.variant)){ + ride_lift(console, context, *ride); + return Pokemon::outcome_ok(); + } + if (step.variant == POSTER_SWITCH_VARIANT){ + push_poster_switch(console, context); + return Pokemon::outcome_ok(); + } + if (!is_item_use_variant(step.variant)){ + return std::nullopt; + } + const std::string item = parse_use_item_variant(step.variant).value_or("bicycle"); + ConsoleItemUseActions bag(console, context, language); + const ItemUseEnd end = use_item_from_bag(console.logger(), bag, item); + if (end != ItemUseEnd::USED){ + return Pokemon::outcome_failed(std::string("use_item_") + item_use_end_name(end), step.variant); + } + return step.pose_after ? Pokemon::outcome_ok(*step.pose_after) : Pokemon::outcome_ok(); +} + } // namespace PokemonFRLG diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h index 4e4249af77..1282c2d880 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h @@ -20,6 +20,9 @@ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" #include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "CommonFramework/Language.h" +#include "Pokemon/AutoStory/Pokemon_StoryEvents.h" +#include "Pokemon/AutoStory/Pokemon_StorySteps.h" #include "Pokemon/Navigation/Pokemon_Grid.h" namespace PokemonAutomation{ @@ -55,13 +58,14 @@ std::string lift_floor_variant(std::string_view from, std::string_view to); // not one, or names a floor the panel hasn't. std::optional parse_lift_floor_variant(std::string_view variant); -// The fewest presses that move the cursor from `ride.from` to `ride.to` -// on a menu of `entries` rows. A multichoice of more than three rows -// wraps round (pokefirered DrawVerticalMultichoiceMenu()). +// Presses of the D-pad on a menu: which way, how many. struct MenuPresses{ Pokemon::Direction direction = Pokemon::Direction::SOUTH; size_t presses = 0; }; +// The fewest presses that move the cursor from `ride.from` to `ride.to` +// on a menu of `entries` rows. A multichoice of more than three rows +// wraps round (pokefirered DrawVerticalMultichoiceMenu()). MenuPresses lift_menu_presses(const LiftRide& ride, size_t entries); // Whether `variant` is one of this file's steps. @@ -74,6 +78,14 @@ void ride_lift(ConsoleHandle& console, ProControllerContext& context, const Lift // overworld. void push_poster_switch(ConsoleHandle& console, ProControllerContext& context); +// On the console, a GAME step of this file's or an item from the bag +// (is_item_use_variant()); nothing for another GAME step. The hideout's +// steps leave the player where they stand; an item used ends on the +// step's pose_after (an Escape Rope's), if it has one. +std::optional run_field_game_step( + ConsoleHandle& console, ProControllerContext& context, Language language, const Pokemon::StoryStep& step +); + // Registers the hideout steps' tests. void add_tests_AutoStoryHideout(UnitTestDatabase& database); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp index 0c29251b6a..12db0549e2 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp @@ -67,97 +67,125 @@ class Test_AutoStoryHideout : public UnitTest{ LiftCase m_case; }; -class Test_AutoStoryHideoutVariants : public UnitTest{ -public: - Test_AutoStoryHideoutVariants() - : UnitTest("PokemonFRLG::AutoStoryHideout - the poster and lift variants") - {} - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ - if (!is_hideout_variant(POSTER_SWITCH_VARIANT) || is_hideout_variant("cut_tree") || is_hideout_variant("")){ - return "Expected only the poster switch and lift rides as hideout variants."; - } - if (lift_floor_variant("B2F", "B4F") != "lift_floor B2F B4F"){ - return "Expected \"lift_floor B2F B4F\", got \"" + lift_floor_variant("B2F", "B4F") + "\"."; - } - // A three-row menu doesn't wrap. - const MenuPresses no_wrap = lift_menu_presses(LiftRide{0, 2}, 3); - if (no_wrap.direction != Pokemon::Direction::SOUTH || no_wrap.presses != 2){ - return "Expected 2 down on a three-row menu."; - } - return true; +UnitTestResult test_variants(){ + if (!is_hideout_variant(POSTER_SWITCH_VARIANT) || is_hideout_variant("cut_tree") || is_hideout_variant("")){ + return "Expected only the poster switch and lift rides as hideout variants."; } -}; + if (lift_floor_variant("B2F", "B4F") != "lift_floor B2F B4F"){ + return "Expected \"lift_floor B2F B4F\", got \"" + lift_floor_variant("B2F", "B4F") + "\"."; + } + // A three-row menu doesn't wrap. + const MenuPresses no_wrap = lift_menu_presses(LiftRide{0, 2}, 3); + if (no_wrap.direction != Pokemon::Direction::SOUTH || no_wrap.presses != 2){ + return "Expected 2 down on a three-row menu."; + } + return true; +} // The table's steps, run as a row runs them on the sim: the poster's // switch opens the stairs north-east of it, and the lift lets out on the // floor picked. -class Test_AutoStoryHideoutSim : public UnitTest{ +std::string poster_on_sim(); +std::string lift_on_sim(); + +UnitTestResult test_steps_on_sim(){ + const std::string poster = poster_on_sim(); + if (!poster.empty()){ + return poster; + } + const std::string lift = lift_on_sim(); + if (!lift.empty()){ + return lift; + } + return true; +} + +std::string poster_on_sim(){ + const MapData room = parse_map(R"({ + "name": "Corner", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, + "tiles": ["#####", "#...#", "#...#", "#####"], + "objects": [ + {"name": "poster", "kind": "sign", "x": 2, "y": 1}, + {"name": "poster_stairs", "kind": "npc", "x": 3, "y": 1} + ] + })", "Corner"); + OverworldSim sim({room}, MapPosition{"Corner", {2, 2}}, Direction::NORTH); + StoryPose at{"Corner", {2, 2}, Direction::NORTH}; + const std::string failure = row_step_on_sim(sim, at, StoryTable::poster_switch("poster_switch", "test"), "00.31"); + if (!failure.empty()){ + return failure; + } + const Milliseconds tile = MovementMechanics::TILE_TIME; + sim.hold(Direction::EAST, tile * 2, MovementMechanics::STEP_RELEASE, tile * 2, {}); + sim.hold(Direction::NORTH, tile * 2, MovementMechanics::STEP_RELEASE, tile * 2, {}); + if (!(sim.state().position == MapPosition{"Corner", {3, 1}})){ + return "After the poster step, walked onto " + to_string(sim.state().position) + ", not the stairs."; + } + return ""; +} + +std::string lift_on_sim(){ + std::vector maps; + maps.push_back(parse_map(R"({ + "name": "Lift", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, + "tiles": ["###", "#.#", "#.#", "###"], + "warps": [{"x": 1, "y": 2, "enter": "walk_south"}] + })", "Lift")); + maps.push_back(parse_map(R"({ + "name": "B4F", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, + "tiles": ["###", "#.#", "###"] + })", "B4F")); + SimScript script; + const MapPosition b4f{"B4F", {1, 1}}; + script.lift_floors["Lift"] = {MapPosition{"Lift", {1, 1}}, MapPosition{"Lift", {1, 1}}, b4f}; + OverworldSim sim(maps, MapPosition{"Lift", {1, 2}}, Direction::SOUTH, script); + StoryPose at{"Lift", {1, 2}, Direction::SOUTH}; + const std::string failure = row_step_on_sim(sim, at, StoryTable::lift_floor("lift_to_b4f", "B2F", "B4F", "test"), "00.32"); + if (!failure.empty()){ + return failure; + } + sim.hold(Direction::SOUTH, MovementMechanics::TILE_TIME, MovementMechanics::STEP_RELEASE, std::chrono::milliseconds(3000), {}); + if (!(sim.state().position == b4f)){ + return "After the lift step, the door let out on " + to_string(sim.state().position) + "."; + } + return ""; +} + +struct AutoStoryHideoutBodyInput{ + std::string name; + UnitTestResult (*body)(); +}; +struct AutoStoryHideoutBodyExpectation{ + const char* description; +}; +struct AutoStoryHideoutBodyCase{ + AutoStoryHideoutBodyInput input; + AutoStoryHideoutBodyExpectation expectation; +}; + +class Test_AutoStoryHideoutBody : public UnitTest{ public: - Test_AutoStoryHideoutSim() - : UnitTest("PokemonFRLG::AutoStoryHideout - table steps on the sim") + explicit Test_AutoStoryHideoutBody(AutoStoryHideoutBodyCase test_case) + : UnitTest("PokemonFRLG::AutoStoryHideout - " + test_case.input.name) + , m_case(std::move(test_case)) {} virtual UnitTestResult run(Logger&, CancellableScope&) const override{ - const std::string poster = poster_on_sim(); - if (!poster.empty()){ - return poster; - } - const std::string lift = lift_on_sim(); - if (!lift.empty()){ - return lift; - } - return true; + return m_case.input.body(); } private: - static std::string poster_on_sim(){ - const MapData room = parse_map(R"({ - "name": "Corner", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, - "tiles": ["#####", "#...#", "#...#", "#####"], - "objects": [ - {"name": "poster", "kind": "sign", "x": 2, "y": 1}, - {"name": "poster_stairs", "kind": "npc", "x": 3, "y": 1} - ] - })", "Corner"); - OverworldSim sim({room}, MapPosition{"Corner", {2, 2}}, Direction::NORTH); - StoryPose at{"Corner", {2, 2}, Direction::NORTH}; - const std::string failure = row_step_on_sim(sim, at, StoryTable::poster_switch("poster_switch", "test"), "00.31"); - if (!failure.empty()){ - return failure; - } - const Milliseconds tile = MovementMechanics::TILE_TIME; - sim.hold(Direction::EAST, tile * 2, MovementMechanics::STEP_RELEASE, tile * 2, {}); - sim.hold(Direction::NORTH, tile * 2, MovementMechanics::STEP_RELEASE, tile * 2, {}); - if (!(sim.state().position == MapPosition{"Corner", {3, 1}})){ - return "After the poster step, walked onto " + to_string(sim.state().position) + ", not the stairs."; - } - return ""; - } - static std::string lift_on_sim(){ - std::vector maps; - maps.push_back(parse_map(R"({ - "name": "Lift", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, - "tiles": ["###", "#.#", "#.#", "###"], - "warps": [{"x": 1, "y": 2, "enter": "walk_south"}] - })", "Lift")); - maps.push_back(parse_map(R"({ - "name": "B4F", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, - "tiles": ["###", "#.#", "###"] - })", "B4F")); - SimScript script; - const MapPosition b4f{"B4F", {1, 1}}; - script.lift_floors["Lift"] = {MapPosition{"Lift", {1, 1}}, MapPosition{"Lift", {1, 1}}, b4f}; - OverworldSim sim(maps, MapPosition{"Lift", {1, 2}}, Direction::SOUTH, script); - StoryPose at{"Lift", {1, 2}, Direction::SOUTH}; - const std::string failure = row_step_on_sim(sim, at, StoryTable::lift_floor("lift_to_b4f", "B2F", "B4F", "test"), "00.32"); - if (!failure.empty()){ - return failure; - } - sim.hold(Direction::SOUTH, MovementMechanics::TILE_TIME, MovementMechanics::STEP_RELEASE, std::chrono::milliseconds(3000), {}); - if (!(sim.state().position == b4f)){ - return "After the lift step, the door let out on " + to_string(sim.state().position) + "."; - } - return ""; - } + AutoStoryHideoutBodyCase m_case; +}; + +const AutoStoryHideoutBodyCase HIDEOUT_BODY_CASES[] = { + { + .input = {.name = "the poster and lift variants", .body = test_variants}, + .expectation = {.description = "only the poster switch and lift rides are hideout steps"}, + }, + { + .input = {.name = "table steps on the sim", .body = test_steps_on_sim}, + .expectation = {.description = "the poster opens the stairs; the lift lets out on the floor picked"}, + }, }; const LiftCase LIFT_CASES[] = { @@ -206,8 +234,9 @@ void add_tests_AutoStoryHideout(UnitTestDatabase& database){ for (const LiftCase& item : LIFT_CASES){ database.add(item); } - database.add(); - database.add(); + for (const AutoStoryHideoutBodyCase& item : HIDEOUT_BODY_CASES){ + database.add(item); + } } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp index d0a719798d..1f0c4bdeb9 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp @@ -8,6 +8,7 @@ #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" #include "PokemonFRLG_AutoStoryActions.h" +#include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryItemUse.h" namespace PokemonAutomation{ @@ -18,6 +19,13 @@ namespace PokemonFRLG{ using namespace std::chrono_literals; +MoveSpeed move_speed(const StoryStep& step){ + if (step.variant == BIKE_VARIANT){ + return MoveSpeed::BIKE; + } + return step.variant == RUN_VARIANT ? MoveSpeed::RUN : MoveSpeed::WALK; +} + std::optional usable_item_pocket(std::string_view item){ if (is_repel(item) || item == "escape-rope"){ return BagPocket::ITEMS; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h index bf0a5c86c7..3836b7a78a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h @@ -78,6 +78,7 @@ class ItemUseActions{ virtual void close_bag() = 0; }; +// How use_item_from_bag() ended. enum class ItemUseEnd{ USED, // The pocket has none left (or the name isn't read). @@ -87,6 +88,7 @@ enum class ItemUseEnd{ // Not an item the story uses from the bag. UNKNOWN_ITEM, }; +// "used", "none_left": the end's name in logs and failures. const char* item_use_end_name(ItemUseEnd end); // Opens the item's pocket, finds it and uses it, then back to the diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp index 2ea359f252..ab33b1a43c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp @@ -175,35 +175,62 @@ const ItemUseCase ITEM_USE_CASES[] = { }, }; -class Test_AutoStoryItemUseVariants : public UnitTest{ +UnitTestResult test_variants_and_bike(){ + if (parse_use_item_variant("use_item potion") || parse_use_item_variant("use_item") || !parse_use_item_variant(use_item_variant("super-repel"))){ + return "Expected only the story's bag items as use_item variants."; + } + if (!is_item_use_variant(RIDE_BIKE_VARIANT) || is_item_use_variant("poster_switch")){ + return "Expected ride_bike, not poster_switch, as an item use."; + } + StoryStep ride; + ride.kind = Pokemon::StoryStepKind::ROUTE; + ride.variant = BIKE_VARIANT; + if (move_speed(ride) != MoveSpeed::BIKE){ + return "A bike ROUTE isn't ridden."; + } + RouteStep outdoors; + outdoors.tiles = 10; + RouteStep indoors = outdoors; + indoors.walk_only = true; + if (OverworldWalkSteps::tile_time(outdoors, MoveSpeed::BIKE) != MovementMechanics::BIKE_TILE_TIME + || OverworldWalkSteps::tile_time(indoors, MoveSpeed::BIKE) != MovementMechanics::TILE_TIME + ){ + return "Expected the bike's pace outdoors and a walk indoors."; + } + return true; +} + +struct AutoStoryItemUseBodyInput{ + std::string name; + UnitTestResult (*body)(); +}; +struct AutoStoryItemUseBodyExpectation{ + const char* description; +}; +struct AutoStoryItemUseBodyCase{ + AutoStoryItemUseBodyInput input; + AutoStoryItemUseBodyExpectation expectation; +}; + +class Test_AutoStoryItemUseBody : public UnitTest{ public: - Test_AutoStoryItemUseVariants() - : UnitTest("PokemonFRLG::AutoStoryItemUse - variants and the bike's pace") + explicit Test_AutoStoryItemUseBody(AutoStoryItemUseBodyCase test_case) + : UnitTest("PokemonFRLG::AutoStoryItemUse - " + test_case.input.name) + , m_case(std::move(test_case)) {} virtual UnitTestResult run(Logger&, CancellableScope&) const override{ - if (parse_use_item_variant("use_item potion") || parse_use_item_variant("use_item") || !parse_use_item_variant(use_item_variant("super-repel"))){ - return "Expected only the story's bag items as use_item variants."; - } - if (!is_item_use_variant(RIDE_BIKE_VARIANT) || is_item_use_variant("poster_switch")){ - return "Expected ride_bike, not poster_switch, as an item use."; - } - StoryStep ride; - ride.kind = Pokemon::StoryStepKind::ROUTE; - ride.variant = BIKE_VARIANT; - if (move_speed(ride) != MoveSpeed::BIKE){ - return "A bike ROUTE isn't ridden."; - } - RouteStep outdoors; - outdoors.tiles = 10; - RouteStep indoors = outdoors; - indoors.walk_only = true; - if (OverworldWalkSteps::tile_time(outdoors, MoveSpeed::BIKE) != MovementMechanics::BIKE_TILE_TIME - || OverworldWalkSteps::tile_time(indoors, MoveSpeed::BIKE) != MovementMechanics::TILE_TIME - ){ - return "Expected the bike's pace outdoors and a walk indoors."; - } - return true; + return m_case.input.body(); } + +private: + AutoStoryItemUseBodyCase m_case; +}; + +const AutoStoryItemUseBodyCase ITEM_USE_BODY_CASES[] = { + { + .input = {.name = "variants and the bike's pace", .body = test_variants_and_bike}, + .expectation = {.description = "the story's bag items, ride_bike, and the bike's pace outdoors only"}, + }, }; } // namespace @@ -213,7 +240,9 @@ void add_tests_AutoStoryItemUse(UnitTestDatabase& database){ for (const ItemUseCase& item : ITEM_USE_CASES){ database.add(item); } - database.add(); + for (const AutoStoryItemUseBodyCase& item : ITEM_USE_BODY_CASES){ + database.add(item); + } } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp index adf518879f..936d342526 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp @@ -511,7 +511,7 @@ std::string OverworldStepSim::cut(const StoryStep& step){ if (m_sim.state().facing != step.direction){ sim_turn_to(m_sim, step.direction); } - if (!m_sim.cut_tree()){ + if (!m_sim.use_field(SimField::CUT_TREE)){ return "no tree to cut."; } sim_step_once(m_sim, step.direction); @@ -540,10 +540,10 @@ Pokemon::StoryAction OverworldStepSim::hideout_or_stub(){ std::string OverworldStepSim::hideout(const StoryStep& step){ if (step.variant == POSTER_SWITCH_VARIANT){ - return m_sim.push_poster_switch() ? "" : "no poster ahead."; + return m_sim.use_field(SimField::POSTER_SWITCH) ? "" : "no poster ahead."; } const std::optional ride = parse_lift_floor_variant(step.variant); - if (!ride || !m_sim.choose_lift_floor(ride->to)){ + if (!ride || !m_sim.use_field(SimField::LIFT_FLOOR, ride->to)){ return "no lift panel for \"" + step.variant + "\"."; } return ""; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryStepTable_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryStepTable_Tests.cpp index 33302b794b..8eeb70211c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryStepTable_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryStepTable_Tests.cpp @@ -19,6 +19,8 @@ #include "PokemonFRLG_AutoStoryStepInterrupts.h" #include "PokemonFRLG_AutoStoryStepTable.h" #include "PokemonFRLG_AutoStoryEvents.h" +#include "PokemonFRLG_AutoStoryHideout.h" +#include "PokemonFRLG_AutoStoryItemUse.h" #include "PokemonFRLG_AutoStoryTable.h" #include "PokemonFRLG_AutoStoryTrashCans.h" @@ -249,6 +251,9 @@ void add_tests_AutoStoryStepTable(UnitTestDatabase& database){ database.add(scenario); } add_tests_AutoStoryStepInterrupts(database); + // The GAME variants with logic of their own. + add_tests_AutoStoryHideout(database); + add_tests_AutoStoryItemUse(database); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryVermilionGym_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryVermilionGym_Tests.cpp index b59faf6d75..3188bb3189 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryVermilionGym_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryVermilionGym_Tests.cpp @@ -325,7 +325,7 @@ UnitTestResult test_tree_shuts_the_yard(){ // The sim: no step onto the tree; cut, it's gone; a warp grows it back. OverworldSim sim(story_maps(), MapPosition{StoryTable::VERMILION, {19, 23}}, Direction::SOUTH); sim.tap(Direction::SOUTH); - if (!(sim.state().position == MapPosition{StoryTable::VERMILION, {19, 23}}) || !sim.cut_tree()){ + if (!(sim.state().position == MapPosition{StoryTable::VERMILION, {19, 23}}) || !sim.use_field(SimField::CUT_TREE)){ return "Expected the tree to stop the player, then be cut."; } sim.tap(Direction::SOUTH); diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 9de4d7fee3..3b38e8424d 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1866,8 +1866,10 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRouteStrings.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp From ce645d1600df281b494eb593e45c972a4a6c038e Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 17:29:09 -0800 Subject: [PATCH 08/28] FRLG: route tests find maps without the route's internals Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Overworld/PokemonFRLG_OverworldRoute_Tests.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp index 419fe87757..0f9fcb753a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp @@ -108,8 +108,8 @@ MapPosition point_on(const std::vector& maps, const std::string& map, c } const MapData* map_named(const std::vector& maps, const std::string& name){ - const int index = find_map(maps, name); - return index < 0 ? nullptr : &maps[index]; + const auto map = std::find_if(maps.begin(), maps.end(), [&](const MapData& item){ return item.name == name; }); + return map == maps.end() ? nullptr : &*map; } UnitTestResult test_maps(){ From fd9f6b904a7371484c1daf79bb856a1b3874398a Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 17:32:55 -0800 Subject: [PATCH 09/28] FRLG drafts: restore Lavender to Celadon as 00.30, hideout as 00.31-00.32; the steps and maps they need Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../AutoStory/StoryData/Draft/README.md | 133 +++++++++++------- .../StoryData/Draft/frlg-00.30.plan.json | 103 ++++++++------ .../StoryData/Draft/frlg-00.31.plan.json | 104 ++++++-------- .../StoryData/Draft/frlg-00.32.plan.json | 106 ++++++++++++++ 4 files changed, 291 insertions(+), 155 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.32.plan.json diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md index bff9a09871..532ef68bfa 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md @@ -9,53 +9,80 @@ Each file is a `pokesim-authored-checkpoint/1` plan in the same format as `../frlg-parcel-00.05.plan.json`. The order and the item stops follow PokeGuy's LeafGreen run (PokemonSimulation `data/runs/pokeguy-leafgreen-15934/events.json` and `external/pokeguy-notes.md`). 00.28 and 00.29 are table rows on -`frlg/stage8-rocktunnel` (#293), not drafts: 00.29 ends at Lavender's nurse. -The draft that also used the id 00.29 (Lavender Mart, Route 8, the Underground -Path to Celadon's Center) was dropped in favour of #293's row, so nothing here -covers Lavender's nurse to Celadon's yet; its events are in commit 8b656d1a2. +`frlg/stage8-rocktunnel` (#293); 00.29 ends at Lavender's nurse, where 00.30 +starts. | Id | Title | Events | |---|---|---:| -| 00.30 | Celadon Game Corner and the Rocket Hideout down to B4F: the poster guard, the Lift Key Grunt, the LIFT KEY and B3F's Rare Candy | 12 | -| 00.31 | Rocket Hideout: the lift to B4F, the two door guards, two Rare Candies, Blastoise, Giovanni and the SILPH SCOPE | 16 | -| 00.32 | Celadon: Department Store 5F battle items, HM02 Fly on Route 16 for the Pidgey, the Tea, then the Center | 20 | -| 00.32b | Celadon Mansion roof room: the Eevee (Gift RNG placeholder) | 4 | -| 00.33 | Fly to Lavender, the Center, Pokemon Tower 2F: the rival | 8 | -| 00.34 | Pokemon Tower 3F to 7F: Tammy, Jennifer, Emilia, the Marowak, three Grunts, Mr. Fuji | 21 | -| 00.35 | The POKE FLUTE, Fly to Celadon, Route 16's Snorlax, Cycling Road's Rare Candy and Max Elixir, Fuchsia | 16 | -| 00.36 | Safari Zone: a Full Restore, the GOLD TEETH, HM03 Surf from the Secret House | 12 | -| 00.37 | Fuchsia: Surf to the lead, Juggler Kirk, Koga, then HM04 Strength from the Warden | 14 | -| 00.38 | Fly to Pallet, Surf down Route 21 to Cinnabar, the Mansion: Elixir, TM14 Blizzard and the SECRET KEY | 15 | -| 00.38b | Cinnabar Lab: the Helix Fossil revived (Gift RNG placeholder) | 6 | -| 00.39 | Cinnabar Gym: the quiz doors, Blaine | 9 | -| 00.40 | Fly to Celadon: two Rare Candies, Blizzard, Celadon Gym: Tamia, Lori, Erika | 21 | -| 00.41 | East to Saffron: the TEA at the gate, the Fighting Dojo's Hitmonchan (Gift RNG placeholder), the Center | 13 | -| 00.42 | Silph Co.: the CARD KEY, the rival on 7F, the Lapras (Gift RNG placeholder) | 10 | -| 00.43 | Silph Co. 11F: the boardroom Grunt, Giovanni; then Sabrina | 13 | -| 00.44 | Fly to Viridian: Viridian Gym: Takashi, Warren, Giovanni | 14 | -| 00.45 | Route 22: the rival; Route 23's badge gates; Strength | 7 | -| 00.46 | Victory Road's boulders, Indigo Plateau: Full Restores, the nurse, the save | 8 | -| 00.47 | Lorelei | 4 | -| 00.48 | Bruno | 3 | -| 00.49 | Agatha | 3 | -| 00.50 | Lance | 3 | -| 00.51 | The Champion and the Hall of Fame | 4 | - -## Why 00.30 and 00.31 are not table rows yet - -They can't run or be simulated on this branch's data: - -- The map resource lock (PokemonSimulation `6c2f5c8`, `runtime-manifest.json`) - has none of CeladonCity, CeladonCity_GameCorner or RocketHideout_B1F..B4F; - they exist only in `source-candidate`. CeladonCity_PokemonCenter_1F (00.30's - start) and RocketHideout_Elevator (00.31's lift) are in neither. Adding them - to the runtime pack is a PokemonSimulation publish and a lock bump. -- 00.30's start, Celadon's nurse, isn't reached by any row: 00.29 ends in - Lavender. -- The walker has no spinner (arrow) tiles, which B2F and B3F need, and no - step for the lift's floor menu or the poster switch. -- `STORY_TRAINERS` names none of the hideout's Grunts or Giovanni yet; - their parties are in `BattleData/`. +| 00.30 | Lavender Mart, then west over Route 8 on the Bicycle under a Repel: Gamer Rich, a Pidgey for Fly, the Underground Path to Celadon's Pokemon Center | 14 | +| 00.31 | Celadon Game Corner and the Rocket Hideout down to B4F: the poster guard, the poster switch, the Lift Key Grunt, the LIFT KEY and B3F's Rare Candy | 12 | +| 00.32 | Rocket Hideout: the lift to B4F, the two door guards, two Rare Candies, Giovanni, the SILPH SCOPE, an Escape Rope out | 16 | + +The later drafts (Celadon Department Store to the Hall of Fame) are on +`frlg/draft-story-50-51` (#303) under the ids 00.32 to 00.51; with 00.30 +restored here they each move up by one when that stack takes this one in. + +## Story order after the hideout + +As PokeGuy plays it: the Celadon Department Store 5F (6 X Speed, 21 X Special, +5 X Accuracy, 2 Guard Spec), then Pokemon Tower (a Super Repel on 3F to 7F, +the floors with wild Pokemon), Route 16's Snorlax, Fuchsia and Koga, the +Pokemon Mansion and Blaine, Erika, Silph Co., Sabrina, Viridian's Giovanni, and +the Elite Four. + +Items from the bag, as PokeGuy uses them: a Repel over Route 8 and the +Underground Path (00.30; 5 bought in 00.26), renewed when "REPEL's effect wore +off" shows (`renew_repel()`); Super Repels in Pokemon Tower; an Escape Rope out +of the hideout after Giovanni (00.32), which puts the player at the Game +Corner's door, CeladonCity (34, 22) (pokefirered `UpdateEscapeWarp()`: the last +outdoor door, a tile south). The Rocket Hideout has no wild Pokemon +(`wild_encounters.json` lists none of its floors), so no Repel is used there. +The BICYCLE (00.26) is ridden on Route 8's open legs (`bike`); it is +`BIKE_TILE_TIME`, an estimate from the decompilation, until measured. + +## The maps each draft needs + +The story can't run or simulate these drafts until their maps are in the map +resource lock (PokemonSimulation `6c2f5c8`, `data/maps/runtime-manifest.json`). +Status there, in PokemonSimulation's `data/games/frlg/maps/source-candidate`, +or neither: + +| Map | Draft | Status | +|---|---|---| +| LavenderTown | 00.30 | published | +| LavenderTown_Mart | 00.30 | candidate | +| Route8 | 00.30 | candidate | +| UndergroundPath_EastEntrance | 00.30 | candidate | +| UndergroundPath_EastWestTunnel | 00.30 | candidate | +| UndergroundPath_WestEntrance | 00.30 | candidate | +| Route7 | 00.30 | candidate | +| CeladonCity | 00.30 to 00.32 | candidate | +| CeladonCity_PokemonCenter_1F | 00.30, 00.31 | missing | +| CeladonCity_GameCorner | 00.31, 00.32 | candidate | +| RocketHideout_B1F | 00.31, 00.32 | candidate | +| RocketHideout_B2F | 00.31, 00.32 | candidate | +| RocketHideout_B3F | 00.31 | candidate | +| RocketHideout_B4F | 00.31, 00.32 | candidate | +| RocketHideout_Elevator | 00.32 | missing | + +Beyond publishing (a PokemonSimulation publish and a lock bump, which need the +user's approval), the code added for these drafts reads three things the +candidate maps don't carry yet: + +- B2F's and B3F's spinner tiles as regions `spin_north`, `spin_south`, + `spin_west`, `spin_east` and `spin_stop` (pokefirered `MB_SPIN_*`, + `MB_STOP_SPINNING`; `spin_slide()` in PokemonFRLG_OverworldMap.h); +- the Game Corner's poster as an object `poster` at (11, 1), and its stairs + (16..17, 2..3) as objects `poster_stairs`, which the switch removes + (`SimField::POSTER_SWITCH`); +- the lift's floors: its door is a dynamic warp (`setdynamicwarp`), so the sim + is told where each floor lets out (`SimScript::lift_floors`: B1F (24, 25), + B2F (28, 16), B4F (20, 23)) and the planner can't route through it yet. + +`STORY_TRAINERS` names the battles these drafts fight: CeladonCity_GameCorner +`rocket_grunt` (GRUNT_7), and on RocketHideout_B4F `lift_key_grunt` (GRUNT_18), +`left_guard` (GRUNT_16), `right_guard` (GRUNT_17) and `giovanni`. When the +drafts become table rows, their battle steps take those names. ## Conventions @@ -74,9 +101,13 @@ They can't run or be simulated on this branch's data: `gift_rng ` handoff, for the Eevee (00.32b), the Omanyte from the Helix Fossil (00.38b), the Hitmonchan (00.41) and the Lapras (00.42). The authored format can't yet say `handoff`/`live_start` as 00.12/00.12b do in C++. -- New `game_action` variants that no executor has yet: `party_room`, - `gift_rng`, `use_item`, `fly`, `flee_wild`, `surf`, `strength_puzzle`, - `reenter_map`. The `mart` variants name Marts the Mart table doesn't list +- `game_action` variants the C++ executor now has (no console run yet): + `poster_switch`, `lift_floor ` (PokemonFRLG_AutoStoryHideout.h), + `use_item ` and `ride_bike` + (PokemonFRLG_AutoStoryItemUse.h), and the `bike` route variant. Variants + that no executor has yet: `party_room`, `gift_rng`, `use_item rare-candy + slot=N` (an item used on a party member), `fly`, `flee_wild`, `surf`, + `strength_puzzle`, `reenter_map`. The `mart` variants name Marts the Mart table doesn't list yet (Lavender, Celadon 5F, Indigo Plateau). - HMs and key items come in story order: LIFT KEY, SILPH SCOPE, HM02 Fly, TEA, POKE FLUTE, HM03 Surf, GOLD TEETH, HM04 Strength, SECRET KEY, CARD KEY. @@ -84,8 +115,8 @@ They can't run or be simulated on this branch's data: ## Decisions for the user - **A Pidgey for Fly.** PokeGuy flies everywhere from 1:03; the story party has - no flier. The dropped Lavender-to-Celadon draft caught a Pidgey in Route 7's grass (Route 8's grass is behind - two Cut trees). Without Fly, Cinnabar needs a long walk round to Pallet. + no flier. 00.30 catches a Pidgey in Route 7's grass (Route 8's grass is + behind two Cut trees). Without Fly, Cinnabar needs a long walk round to Pallet. - **TM14 Blizzard over Mega Kick** (00.40) is a one-shot TM: it needs the team plan first. - **Strength over Water Pulse** (00.45) and **Surf over Water Gun** (00.37) @@ -111,8 +142,8 @@ wall tiles, and Indigo Plateau's door glyph is at (11, 5) with its warp at | Skipped leg | Why | |---|---| -| 00.31/b2f_lift | B2F's lift corner is reached over arrow tiles the planner does not solve | -| 00.31/b4f_below_guards | RocketHideout_Elevator map not in source-candidate | +| 00.32/b2f_lift | B2F's lift corner is reached over arrow tiles: the planner solves them now, but the candidate map has no `spin_*` regions | +| 00.32/b4f_below_guards | RocketHideout_Elevator map not in source-candidate | | 00.32/x_item_clerk | CeladonCity_DepartmentStore_Elevator map not in source-candidate | | 00.32/route16_below_tree | CeladonCity_DepartmentStore_Elevator map not in source-candidate | | 00.32/celadon_nurse | map not in source-candidate | diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json index 0b256ee6be..4e01206a12 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json @@ -3,82 +3,99 @@ "game": "frlg", "checkpoint": { "id": "00.30", - "title": "Celadon Game Corner and the Rocket Hideout down to B4F: the poster guard, the Lift Key Grunt, the LIFT KEY and B3F's Rare Candy", - "start": {"certainty": "tile", "map": "CeladonCity_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, + "title": "Lavender Mart, then west over Route 8: Gamer Rich, a Pidgey for Fly, the Underground Path to Celadon", + "start": {"certainty": "tile", "map": "LavenderTown_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, "events": [ { - "id": "celadon_below_game_corner", + "id": "lavender_mart_clerk", "kind": "route", - "target": {"certainty": "tile", "map": "CeladonCity", "where": {"facing": "north", "kind": "tiles", "tiles": [[34, 22]]}}, + "target": {"certainty": "tile", "map": "LavenderTown_Mart", "where": {"facing": "west", "kind": "tiles", "tiles": [[4, 3]]}}, "variant": "run", - "why": "out of the Center, west along row 12, down column 41..43 to row 22, west to below the Game Corner (34, 21)" + "why": "out of the Center, east along row 6 and down to the Mart (20, 15), to its clerk" }, { - "id": "below_poster_guard", - "kind": "route", - "target": {"certainty": "tile", "map": "CeladonCity_GameCorner", "where": {"facing": "north", "kind": "tiles", "tiles": [[11, 3]]}}, - "variant": "run", - "why": "into the Game Corner, up the slot rows to below the Grunt (11, 2) in front of the poster" + "id": "buy_ropes_and_super_repels", + "kind": "game_action", + "variant": "mart LavenderTown_Mart budget=5700 floor=9000 escape-rope=4 super-repel=7", + "why": "pokeguy 54:18: 4 Escape Ropes and 7 Super Repels" }, { - "id": "talk_poster_guard", - "kind": "talk", - "why": "the Grunt guarding the poster" - }, - { - "id": "hideout-poster-guard", - "kind": "battle", - "declared_after": {"certainty": "tile", "map": "CeladonCity_GameCorner", "where": {"facing": "north", "kind": "tiles", "tiles": [[11, 3]]}}, - "why": "Rocket Grunt (TRAINER_TEAM_ROCKET_GRUNT_7): Raticate, Zubat Lv20; he runs off" + "id": "repel_route8", + "kind": "game_action", + "variant": "use_item repel", + "why": "a Repel (5 bought in 00.26) for Route 8's grass and the Underground Path; renewed when it wears off" }, { - "id": "poster_switch", - "kind": "talk", - "why": "the poster (11, 1): the switch opens the stairs (15, 2)" + "id": "ride_bike", + "kind": "game_action", + "variant": "ride_bike", + "why": "on the BICYCLE (00.26) for Route 8; the Underground Path's gate, indoors, gets the player off" }, { - "id": "hideout_b4f_west", + "id": "east_of_rich_line", "kind": "route", - "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "west", "kind": "tiles", "tiles": [[5, 3]]}}, - "variant": "run", - "why": "the stairs (15, 2) down to B1F, B1F to B2F (17, 2), B2F's arrow tiles to B3F (21, 2), B3F to B4F (15, 18), round to (5, 3), east of the tile below the Grunt (4, 2)" + "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "west", "kind": "tiles", "tiles": [[56, 18]]}}, + "variant": "bike", + "why": "west out of Lavender onto Route 8, down column 64 east of Julia (62, 14) and west along row 18" }, { - "id": "key_grunt_line", + "id": "rich_line", "kind": "step", "direction": "west", "variant": "trainer_line", - "why": "onto (4, 3), in the Grunt's line (sight 1)" + "why": "onto (55, 18), the end of Rich's line (55, 16..18): he walks down to (55, 17)" }, { - "id": "hideout-key-grunt", + "id": "route8-rich", "kind": "battle", - "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "west", "kind": "tiles", "tiles": [[4, 3]]}}, - "why": "Rocket Grunt (TRAINER_TEAM_ROCKET_GRUNT_18): Koffing, Zubat Lv21; he drops the LIFT KEY" + "declared_after": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[55, 18]]}}, + "why": "Gamer Rich (TRAINER_GAMER_RICH): Growlithe, Vulpix Lv24" + }, + { + "id": "below_underground_door", + "kind": "route", + "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[13, 5]]}}, + "variant": "bike", + "why": "west along rows 14..18, up column 14 east of Aidan's line (10..13, 6), to below the Underground Path door (13, 4)" }, { - "id": "below_lift_key", + "id": "route7_underground_exit", "kind": "route", - "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[3, 3]]}}, + "target": {"certainty": "tile", "map": "Route7", "where": {"facing": "south", "kind": "tiles", "tiles": [[7, 15]]}}, "variant": "run", - "why": "a tile west, below the LIFT KEY (3, 2)" + "why": "through the door, the east entrance, the east-west tunnel and the west entrance, out onto Route 7" }, { - "id": "lift_key", - "kind": "talk", - "why": "pick up the LIFT KEY (pokeguy 57:33)" + "id": "pidgey_grass", + "kind": "route", + "target": {"certainty": "tile", "map": "Route7", "where": {"facing": "east", "kind": "tiles", "tiles": [[14, 5]]}}, + "variant": "run", + "why": "north round the ledges into Route 7's grass (11..16, 2..5), a grass tile east of it free; Route 8's grass is behind two Cut trees" + }, + { + "id": "catch_pidgey", + "kind": "game_action", + "variant": "capture pidgey catches=1 encounters=30 balls=5 pace=east", + "why": "a Pidgey to carry HM02 Fly (pokeguy flies with a Pidgey from 1:03 on); the party has no other flier" + }, + { + "id": "celadon_below_center", + "kind": "route", + "target": {"certainty": "tile", "map": "CeladonCity", "where": {"facing": "north", "kind": "tiles", "tiles": [[48, 12]]}}, + "variant": "run", + "why": "north round the grass, west onto Celadon (row 12..15) and along row 12 to below the Center door (48, 11)" }, { - "id": "beside_b3f_rare_candy", + "id": "celadon_nurse", "kind": "route", - "target": {"certainty": "tile", "map": "RocketHideout_B3F", "where": {"facing": "north", "kind": "tiles", "tiles": [[12, 13]]}}, + "target": {"certainty": "tile", "map": "CeladonCity_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, "variant": "run", - "why": "back up the stairs to B3F, below the Rare Candy (12, 12) beside the arrow tiles" + "why": "into the Center, to the nurse" }, { - "id": "b3f_rare_candy", - "kind": "talk", - "why": "pick up the Rare Candy: two, with Route 9's" + "id": "heal_celadon", + "kind": "heal", + "why": "the nurse heals the party" } ] }, diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.31.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.31.plan.json index e040f84d88..bef003ddb2 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.31.plan.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.31.plan.json @@ -3,101 +3,83 @@ "game": "frlg", "checkpoint": { "id": "00.31", - "title": "Rocket Hideout: the lift to B4F, the two door guards, two Rare Candies, Blastoise, Giovanni and the SILPH SCOPE", - "start": {"certainty": "tile", "map": "RocketHideout_B3F", "where": {"facing": "north", "kind": "tiles", "tiles": [[12, 13]]}}, + "title": "Celadon Game Corner and the Rocket Hideout down to B4F: the poster guard, the Lift Key Grunt, the LIFT KEY and B3F's Rare Candy", + "start": {"certainty": "tile", "map": "CeladonCity_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, "events": [ { - "id": "b2f_lift", + "id": "celadon_below_game_corner", "kind": "route", - "target": {"certainty": "tile", "map": "RocketHideout_B2F", "where": {"facing": "north", "kind": "tiles", "tiles": [[28, 17]]}}, + "target": {"certainty": "tile", "map": "CeladonCity", "where": {"facing": "north", "kind": "tiles", "tiles": [[34, 22]]}}, "variant": "run", - "why": "up to B2F, along the arrow tiles to the lift (28..29, 16)" + "why": "out of the Center, west along row 12, down column 41..43 to row 22, west to below the Game Corner (34, 21)" }, { - "id": "lift_to_b4f", - "kind": "scene", - "why": "the LIFT KEY works the lift: B4F" - }, - { - "id": "b4f_below_guards", + "id": "below_poster_guard", "kind": "route", - "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[17, 16]]}}, + "target": {"certainty": "tile", "map": "CeladonCity_GameCorner", "where": {"facing": "north", "kind": "tiles", "tiles": [[11, 3]]}}, "variant": "run", - "why": "out of the lift to below the two Grunts (16, 14) and (19, 14)" + "why": "into the Game Corner, up the slot rows to below the Grunt (11, 2) in front of the poster" }, { - "id": "talk_left_guard", + "id": "talk_poster_guard", "kind": "talk", - "why": "the left door guard" + "why": "the Grunt guarding the poster" }, { - "id": "hideout-left-guard", + "id": "hideout-poster-guard", "kind": "battle", - "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[17, 16]]}}, - "why": "Rocket Grunt (TRAINER_TEAM_ROCKET_GRUNT_16): Sandshrew, Sandslash, Ekans Lv23" - }, - { - "id": "talk_right_guard", - "kind": "talk", - "why": "the right door guard" + "declared_after": {"certainty": "tile", "map": "CeladonCity_GameCorner", "where": {"facing": "north", "kind": "tiles", "tiles": [[11, 3]]}}, + "why": "Rocket Grunt (TRAINER_TEAM_ROCKET_GRUNT_7): Raticate, Zubat Lv20; he runs off" }, { - "id": "hideout-right-guard", - "kind": "battle", - "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[17, 16]]}}, - "why": "Rocket Grunt (TRAINER_TEAM_ROCKET_GRUNT_17): Ekans, Sandshrew, Arbok Lv23; the door opens" + "id": "poster_switch", + "kind": "game_action", + "variant": "poster_switch", + "why": "the poster (11, 1): the switch opens the stairs (15, 2)" }, { - "id": "rare_candy_1", - "kind": "game_action", - "variant": "use_item rare-candy slot=1", - "why": "Rare Candy: Lv35 (pokeguy 1:00:25)" + "id": "hideout_b4f_west", + "kind": "route", + "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "west", "kind": "tiles", "tiles": [[5, 3]]}}, + "variant": "run", + "why": "the stairs (15, 2) down to B1F, B1F to B2F (17, 2), B2F's arrow tiles to B3F (21, 2), B3F to B4F (15, 18), round to (5, 3), east of the tile below the Grunt (4, 2)" }, { - "id": "rare_candy_2", - "kind": "game_action", - "variant": "use_item rare-candy slot=1", - "why": "Rare Candy: Lv36, Wartortle evolves" + "id": "key_grunt_line", + "kind": "step", + "direction": "west", + "variant": "trainer_line", + "why": "onto (4, 3), in the Grunt's line (sight 1)" }, { - "id": "evolve_blastoise", - "kind": "scene", - "why": "Wartortle evolves into Blastoise; do not press B" + "id": "hideout-key-grunt", + "kind": "battle", + "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "west", "kind": "tiles", "tiles": [[4, 3]]}}, + "why": "Rocket Grunt (TRAINER_TEAM_ROCKET_GRUNT_18): Koffing, Zubat Lv21; he drops the LIFT KEY" }, { - "id": "below_giovanni", + "id": "below_lift_key", "kind": "route", - "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[19, 5]]}}, - "why": "through the door to below Giovanni (19, 4)" + "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[3, 3]]}}, + "variant": "run", + "why": "a tile west, below the LIFT KEY (3, 2)" }, { - "id": "talk_giovanni", + "id": "lift_key", "kind": "talk", - "why": "Giovanni" - }, - { - "id": "hideout-giovanni", - "kind": "battle", - "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[19, 5]]}}, - "why": "Giovanni (TRAINER_BOSS_GIOVANNI): Onix, Rhyhorn, Kangaskhan" + "why": "pick up the LIFT KEY (pokeguy 57:33)" }, { - "id": "beside_silph_scope", + "id": "beside_b3f_rare_candy", "kind": "route", - "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "west", "kind": "tiles", "tiles": [[21, 5]]}}, - "why": "round to east of the SILPH SCOPE (20, 5)" + "target": {"certainty": "tile", "map": "RocketHideout_B3F", "where": {"facing": "north", "kind": "tiles", "tiles": [[12, 13]]}}, + "variant": "run", + "why": "back up the stairs to B3F, below the Rare Candy (12, 12) beside the arrow tiles" }, { - "id": "silph_scope", + "id": "b3f_rare_candy", "kind": "talk", - "why": "pick up the SILPH SCOPE" - }, - { - "id": "escape_rope_hideout", - "kind": "game_action", - "declared_after": {"certainty": "tile", "map": "CeladonCity", "where": {"facing": "south", "kind": "tiles", "tiles": [[34, 22]]}}, - "variant": "use_item escape-rope", - "why": "out to the Game Corner's door (pokeguy 1:02:10)" + "why": "pick up the Rare Candy: two, with Route 9's" } ] }, diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.32.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.32.plan.json new file mode 100644 index 0000000000..4351f396c4 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.32.plan.json @@ -0,0 +1,106 @@ +{ + "format": "pokesim-authored-checkpoint/1", + "game": "frlg", + "checkpoint": { + "id": "00.32", + "title": "Rocket Hideout: the lift to B4F, the two door guards, two Rare Candies, Blastoise, Giovanni and the SILPH SCOPE", + "start": {"certainty": "tile", "map": "RocketHideout_B3F", "where": {"facing": "north", "kind": "tiles", "tiles": [[12, 13]]}}, + "events": [ + { + "id": "b2f_lift", + "kind": "route", + "target": {"certainty": "tile", "map": "RocketHideout_B2F", "where": {"facing": "north", "kind": "tiles", "tiles": [[28, 17]]}}, + "variant": "run", + "why": "up to B2F, along the arrow tiles to the lift (28..29, 16)" + }, + { + "id": "lift_to_b4f", + "kind": "game_action", + "variant": "lift_floor B2F B4F", + "why": "the LIFT KEY works the lift: B4F" + }, + { + "id": "b4f_below_guards", + "kind": "route", + "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[17, 16]]}}, + "variant": "run", + "why": "out of the lift to below the two Grunts (16, 14) and (19, 14)" + }, + { + "id": "talk_left_guard", + "kind": "talk", + "why": "the left door guard" + }, + { + "id": "hideout-left-guard", + "kind": "battle", + "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[17, 16]]}}, + "why": "Rocket Grunt (TRAINER_TEAM_ROCKET_GRUNT_16): Sandshrew, Sandslash, Ekans Lv23" + }, + { + "id": "talk_right_guard", + "kind": "talk", + "why": "the right door guard" + }, + { + "id": "hideout-right-guard", + "kind": "battle", + "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[17, 16]]}}, + "why": "Rocket Grunt (TRAINER_TEAM_ROCKET_GRUNT_17): Ekans, Sandshrew, Arbok Lv23; the door opens" + }, + { + "id": "rare_candy_1", + "kind": "game_action", + "variant": "use_item rare-candy slot=1", + "why": "Rare Candy: Lv35 (pokeguy 1:00:25)" + }, + { + "id": "rare_candy_2", + "kind": "game_action", + "variant": "use_item rare-candy slot=1", + "why": "Rare Candy: Lv36, Wartortle evolves" + }, + { + "id": "evolve_blastoise", + "kind": "scene", + "why": "Wartortle evolves into Blastoise; do not press B" + }, + { + "id": "below_giovanni", + "kind": "route", + "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[19, 5]]}}, + "why": "through the door to below Giovanni (19, 4)" + }, + { + "id": "talk_giovanni", + "kind": "talk", + "why": "Giovanni" + }, + { + "id": "hideout-giovanni", + "kind": "battle", + "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[19, 5]]}}, + "why": "Giovanni (TRAINER_BOSS_GIOVANNI): Onix, Rhyhorn, Kangaskhan" + }, + { + "id": "beside_silph_scope", + "kind": "route", + "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "west", "kind": "tiles", "tiles": [[21, 5]]}}, + "why": "round to east of the SILPH SCOPE (20, 5)" + }, + { + "id": "silph_scope", + "kind": "talk", + "why": "pick up the SILPH SCOPE" + }, + { + "id": "escape_rope_hideout", + "kind": "game_action", + "declared_after": {"certainty": "tile", "map": "CeladonCity", "where": {"facing": "south", "kind": "tiles", "tiles": [[34, 22]]}}, + "variant": "use_item escape-rope", + "why": "out to the Game Corner's door (pokeguy 1:02:10)" + } + ] + }, + "execution": {"menu": "full"} +} From 8ebc21b3c7b3b109bd052caeea7e718e777aa0c2 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 18:02:19 -0800 Subject: [PATCH 10/28] FRLG: script Pokemon Centers and the hideout lift; code spinners and the poster Centers share one layout and the lift is 5x7 tiles, so the story walks them by fixed taps (center_enter, center_exit, lift_floor from below the door to the destination's door) and the sim builds their rooms from code read off pokefirered. B2F/B3F spinner tiles and the Game Corner poster and stairs are coded too, so the candidate maps need no extra regions or objects. Bike pace and bag waits from console B (2026-11-09). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Overworld/PokemonFRLG_CodedRooms.cpp | 211 ++++++++++++++++++ .../Overworld/PokemonFRLG_CodedRooms.h | 96 ++++++++ .../Overworld/PokemonFRLG_MovementMechanics.h | 10 +- .../Overworld/PokemonFRLG_OverworldMap.cpp | 6 +- .../Overworld/PokemonFRLG_OverworldMap.h | 5 +- .../Overworld/PokemonFRLG_OverworldSim.cpp | 1 + .../Overworld/PokemonFRLG_OverworldSim.h | 8 +- .../PokemonFRLG_OverworldSimField.cpp | 7 + .../PokemonFRLG_AutoStoryHideout.cpp | 21 +- .../AutoStory/PokemonFRLG_AutoStoryHideout.h | 21 +- .../PokemonFRLG_AutoStoryHideout_Tests.cpp | 107 ++++++--- .../PokemonFRLG_AutoStoryItemUse.cpp | 20 +- .../AutoStory/PokemonFRLG_AutoStoryRooms.cpp | 91 ++++++++ .../AutoStory/PokemonFRLG_AutoStoryRooms.h | 81 +++++++ .../PokemonFRLG_AutoStoryRooms_Tests.cpp | 125 +++++++++++ .../AutoStory/PokemonFRLG_AutoStoryRowSim.cpp | 59 ++++- .../AutoStory/PokemonFRLG_AutoStoryRowSim.h | 1 + .../PokemonFRLG_AutoStoryStepTable_Tests.cpp | 2 + .../PokemonFRLG_AutoStoryTableSteps.cpp | 20 ++ .../PokemonFRLG_AutoStoryTableSteps.h | 12 +- .../AutoStory/StoryData/Draft/README.md | 60 +++-- .../StoryData/Draft/frlg-00.30.plan.json | 17 +- .../StoryData/Draft/frlg-00.31.plan.json | 9 +- .../StoryData/Draft/frlg-00.32.plan.json | 5 +- SerialPrograms/cmake/SourceFiles.cmake | 5 + 25 files changed, 910 insertions(+), 90 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms_Tests.cpp diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.cpp new file mode 100644 index 0000000000..64a103cf0a --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.cpp @@ -0,0 +1,211 @@ +/* Coded Rooms and Tiles + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "PokemonFRLG_CodedRooms.h" +#include "PokemonFRLG_OverworldMaps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +// A floor's spinner tiles, by the way each slides the player. +struct SpinnerFloor{ + std::string_view map; + std::vector north; + std::vector south; + std::vector west; + std::vector east; + std::vector stop; +}; + +// Read off pokefirered: each floor's layout (data/layouts/RocketHideout_ +// B2F, _B3F map.bin, primary tileset gTileset_Building, secondary +// gTileset_SilphCo) with each metatile's behaviour from the tilesets' +// metatile_attributes.bin: MB_SPIN_UP (0x56), MB_SPIN_DOWN (0x57), +// MB_SPIN_LEFT (0x55), MB_SPIN_RIGHT (0x54), MB_STOP_SPINNING (0x58). +const std::vector& spinner_floors(){ + static const std::vector FLOORS{ + { + "RocketHideout_B2F", + {{9, 5}, {11, 6}, {7, 7}, {11, 8}, {15, 9}, {7, 10}, {9, 10}, {3, 11}, {15, 11}, {13, 12}, {15, 13}, {9, 14}, {5, 17}, {7, 18}, {5, 19}}, + {{12, 5}, {8, 9}, {10, 9}, {14, 11}, {10, 13}, {8, 17}}, + {{3, 4}, {7, 4}, {9, 4}, {11, 4}, {16, 5}, {16, 6}, {12, 13}, {3, 14}, {7, 14}, {12, 14}, {12, 17}, {12, 18}}, + {{3, 6}, {12, 7}, {4, 9}, {3, 10}, {10, 11}, {12, 11}, {9, 12}, {11, 12}, {9, 20}}, + {{1, 4}, {7, 6}, {13, 7}, {15, 8}, {13, 10}, {8, 11}, {14, 13}, {1, 14}, {5, 15}, {10, 15}, {8, 19}, {13, 20}}, + }, + { + "RocketHideout_B3F", + {{8, 11}, {10, 14}, {6, 15}, {6, 17}, {4, 18}}, + {{9, 10}, {7, 16}}, + {{4, 9}}, + {{6, 9}, {2, 11}, {6, 13}, {5, 14}, {7, 14}, {4, 15}, {3, 16}, {2, 17}}, + {{2, 9}, {8, 9}, {6, 11}, {10, 13}, {9, 14}, {7, 20}}, + }, + }; + return FLOORS; +} + +const SpinnerFloor* spinner_floor(std::string_view map){ + for (const SpinnerFloor& floor : spinner_floors()){ + if (floor.map == map){ + return &floor; + } + } + return nullptr; +} + +bool contains(const std::vector& tiles, const TilePos& point){ + return std::any_of(tiles.begin(), tiles.end(), [&](const TilePos& tile){ + return tile.x == point.x && tile.y == point.y; + }); +} + +bool has_object(const MapData& map, std::string_view name){ + return std::any_of(map.objects.begin(), map.objects.end(), [&](const MapObject& object){ + return object.name == name; + }); +} + +MapObject object(std::string name, Pokemon::ObjectKind kind, const TilePos& tile){ + MapObject out; + out.name = std::move(name); + out.kind = kind; + out.x = tile.x; + out.y = tile.y; + return out; +} + +// LAYOUT_POKEMON_CENTER_1F's collision and elevation, as the map lock's +// seven Centers (PokemonSimulation 6c2f5c8, data/games/frlg/maps/runtime/ +// *_PokemonCenter_1F.json) all have them. The mat's warps: (6..8, 8), +// the middle one MB_SOUTH_ARROW_WARP-like ("walk_south"); the stairs to +// 2F at (1, 6), which isn't loaded. +const char CENTER_ROOM_JSON[] = R"({ + "name": "PokemonCenter_1F", + "source": {"decomp_file": "data/layouts/PokemonCenter_1F/map.bin", "revision": "ce6406d8c2eaf46d543c8a2606de0c3cb1951982", "date": "2026-11-09"}, + "allow_running": false, + "tiles": ["#.#############", "###############", "....###..##....", "....#######....", "...............", "...............", "#..........##..", "...........##..", "...............", "###############"], + "elevation": ["030000000000000", "000000000000000", "333300033003333", "333300000003333", "333333333333333", "330333333333333", "044033333330033", "330333333330033", "333333333333333", "000000000000000"], + "points": {"warp_0": [6, 8], "warp_1": [7, 8], "warp_2": [8, 8], "nurse_front": [7, 4]}, + "warps": [ + {"x": 6, "y": 8, "enter": "walk_into"}, + {"x": 7, "y": 8, "enter": "walk_south"}, + {"x": 8, "y": 8, "enter": "walk_into"}, + {"x": 1, "y": 6, "enter": "step_on"} + ], + "objects": [{"name": "nurse", "kind": "npc", "x": 7, "y": 2}] +})"; + +// LAYOUT_ROCKET_HIDEOUT_ELEVATOR's collision (map.bin: rows 0 to 2 and 6 +// walls, 3 to 5 floor), the panel's bg event at (0, 2), and warp 1 at +// (2, 5), the one MB_SOUTH_ARROW_WARP tile (warp 0 at (1, 5) has no +// behaviour, so never fires). +const char LIFT_ROOM_JSON[] = R"({ + "name": "RocketHideout_Elevator", + "source": {"decomp_file": "data/layouts/RocketHideout_Elevator/map.bin", "revision": "ce6406d8c2eaf46d543c8a2606de0c3cb1951982", "date": "2026-11-09"}, + "tiles": ["#####", "#####", "S####", ".....", ".....", ".....", "#####"], + "points": {"warp_0": [1, 5], "warp_1": [2, 5]}, + "warps": [{"x": 2, "y": 5, "enter": "walk_south"}] +})"; + +} // namespace + + +std::optional coded_spin_direction(std::string_view map, const TilePos& point){ + const SpinnerFloor* floor = spinner_floor(map); + if (floor == nullptr){ + return std::nullopt; + } + if (contains(floor->north, point)){ + return Direction::NORTH; + } + if (contains(floor->south, point)){ + return Direction::SOUTH; + } + if (contains(floor->west, point)){ + return Direction::WEST; + } + if (contains(floor->east, point)){ + return Direction::EAST; + } + return std::nullopt; +} + +bool coded_spin_stop(std::string_view map, const TilePos& point){ + const SpinnerFloor* floor = spinner_floor(map); + return floor != nullptr && contains(floor->stop, point); +} + + +void add_coded_objects(MapData& map){ + if (map.name != GAME_CORNER_MAP){ + return; + } + if (!has_object(map, "poster")){ + map.objects.push_back(object("poster", Pokemon::ObjectKind::SIGN, GAME_CORNER_POSTER)); + } + if (!has_object(map, "poster_stairs")){ + for (const TilePos& tile : {TilePos{15, 2}, TilePos{16, 2}, TilePos{17, 2}, TilePos{16, 3}, TilePos{17, 3}}){ + map.objects.push_back(object("poster_stairs", Pokemon::ObjectKind::NPC, tile)); + } + } +} + + +bool is_pokemon_center(std::string_view map){ + return map.ends_with("_PokemonCenter_1F"); +} + +MapData pokemon_center_room(const std::string& name, const Pokemon::WarpDestination& outside){ + MapData room = parse_map(CENTER_ROOM_JSON, name); + room.name = name; + for (MapWarp& warp : room.warps){ + if (warp.y == CENTER_MAT.y){ + warp.to = outside; + } + } + return room; +} + +std::optional add_pokemon_center_room(std::vector& maps, size_t town){ + for (const MapWarp& warp : maps[town].warps){ + if (!warp.to || !is_pokemon_center(warp.to->map)){ + continue; + } + const std::string center = warp.to->map; + const std::string door = center + "_door"; + maps[town].points[door] = TilePos{warp.x, warp.y}; + Pokemon::WarpDestination outside{maps[town].name, door}; + maps.push_back(pokemon_center_room(center, outside)); + return center; + } + return std::nullopt; +} + + +MapData rocket_hideout_lift_room(){ + return parse_map(LIFT_ROOM_JSON, std::string(LIFT_MAP)); +} + +const std::vector& rocket_hideout_lift_doors(){ + static const std::vector DOORS{ + {"RocketHideout_B1F", {24, 25}}, + {"RocketHideout_B2F", {28, 16}}, + {"RocketHideout_B4F", {20, 23}}, + }; + return DOORS; +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h new file mode 100644 index 0000000000..348aba0492 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h @@ -0,0 +1,96 @@ +/* Coded Rooms and Tiles + * + * From: https://github.com/PokemonAutomation/ + * + * Places the story scripts rather than maps (the user's call, 2026-11-09): + * every Pokemon Center's ground floor, which all share one layout, the + * Rocket Hideout's lift, a room of 5 by 7 tiles, and the tiles that maps + * can't say in glyphs: the Rocket Hideout's spinners and the Game + * Corner's poster. All of it is read off pret/pokefirered, cited where it + * is defined, so no map file has to carry it. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_CodedRooms_H +#define PokemonAutomation_PokemonFRLG_CodedRooms_H + +#include +#include +#include +#include +#include "PokemonFRLG_OverworldMap.h" +#include "PokemonFRLG_OverworldRoute.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The spinner arrow on `point` of the map named `map`, from the +// decompilation's metatile behaviours, if the map is one of the hideout's +// spinner floors (RocketHideout_B2F, RocketHideout_B3F). +std::optional coded_spin_direction(std::string_view map, const TilePos& point); +// Whether `point` of the map named `map` is a coded MB_STOP_SPINNING tile. +bool coded_spin_stop(std::string_view map, const TilePos& point); + + +// The Game Corner, whose poster hides the hideout's switch. +inline constexpr std::string_view GAME_CORNER_MAP = "CeladonCity_GameCorner"; +// The poster (pokefirered CeladonCity_GameCorner map.json, the bg event +// CeladonCity_GameCorner_EventScript_Poster at (11, 1)). +inline constexpr TilePos GAME_CORNER_POSTER{11, 1}; +// Adds what the Game Corner's map can't say to `map`, if it is the Game +// Corner and hasn't them: the poster, an object "poster", and the tiles +// the switch turns into stairs (CeladonCity_GameCorner_EventScript_ +// OpenRocketHideout's setmetatile: (15, 2), (16..17, 2..3)), objects +// "poster_stairs" that block them until it is pushed. +void add_coded_objects(MapData& map); + + +// A Pokemon Center's ground floor. pokefirered gives all of Kanto's and +// the Sevii Islands' but the Indigo Plateau's and One Island's the one +// layout, LAYOUT_POKEMON_CENTER_1F; the seven in the map lock have the +// same tiles and differ only in who stands where, none of them on the +// nurse's column. +// The mat inside the door: the player arrives on it and leaves from it, +// walking south. +inline constexpr TilePos CENTER_MAT{7, 8}; +// Below the nurse's counter, facing her (the nurse at (7, 2)). +inline constexpr TilePos CENTER_NURSE_FRONT{7, 4}; +// Whether `map` names a Pokemon Center's ground floor. +bool is_pokemon_center(std::string_view map); +// The ground floor of the Center named `name`, whose mat lets out on +// `outside` (the door tile of the town's map): its tiles, the nurse, and +// the mat's warps. No person but the nurse: who stands where differs, and +// the script keeps to the nurse's column. +MapData pokemon_center_room(const std::string& name, const Pokemon::WarpDestination& outside); +// For a sim: adds the coded room of the Center whose door is on +// `maps[town]` to `maps`, and returns its name; nothing if the town has +// no Center door. The town gets a point "
_door" on its door tile +// for the room's mat to let out on. +std::optional add_pokemon_center_room(std::vector& maps, size_t town); + + +// The Rocket Hideout's lift (pokefirered LAYOUT_ROCKET_HIDEOUT_ELEVATOR, +// 5 by 7 tiles, rows 3 to 5 walkable). +inline constexpr std::string_view LIFT_MAP = "RocketHideout_Elevator"; +// Where the floors' lift doors put the player (warp 1, an +// MB_SOUTH_ARROW_WARP tile): south from it leaves the lift. +inline constexpr TilePos LIFT_ARRIVAL{2, 5}; +// Below the panel (a bg event at (0, 2), RocketHideout_Elevator_ +// EventScript_FloorSelect), facing north. +inline constexpr TilePos LIFT_PANEL_FRONT{0, 3}; +// The lift's room. Its door is a dynamic warp: the sim takes it to the +// floor picked (SimScript::lift_floors with rocket_hideout_lift_doors()). +MapData rocket_hideout_lift_room(); +// Where the lift lets out for each floor on its panel, top first +// (setdynamicwarp in RocketHideout_Elevator's scripts): B1F (24, 25), +// B2F (28, 16), B4F (20, 23), each its floor's lift door. +const std::vector& rocket_hideout_lift_doors(); + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h index d2f761596c..de3c682440 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h @@ -48,10 +48,12 @@ inline constexpr Milliseconds TILE_TIME(267); inline constexpr Milliseconds RUN_TILE_TIME(133); // Surfing: a tile per 8 frames. inline constexpr Milliseconds SURF_TILE_TIME(133); -// Riding the Bicycle: a tile per 4 frames (pokefirered bike.c, -// PlayerRideWaterCurrent(): MOVE_SPEED_FAST_2). An estimate from the -// decompilation, not measured on the console. -inline constexpr Milliseconds BIKE_TILE_TIME(67); +// Riding the Bicycle, measured on console B (2026-11-09, Lavender Town +// row 13, wall to wall, from rest): 16 tiles (1, 13) to (17, 13) in +// 1800 ms of motion, east and back west alike, 112 ms a tile; 5 tiles +// from rest in 600 ms (120 ms a tile, the start up included). Not the +// decompilation's 4 frames a tile (67 ms): the bike speeds up from rest. +inline constexpr Milliseconds BIKE_TILE_TIME(112); // The longest tap measured to only turn a player facing another way, and // to take exactly one step for one facing its way. inline constexpr Milliseconds LONGEST_TAP(133); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp index fb2317c92a..41a578fdde 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp @@ -6,6 +6,7 @@ #include #include "Common/Cpp/Exceptions.h" +#include "PokemonFRLG_CodedRooms.h" #include "PokemonFRLG_OverworldMap.h" namespace PokemonAutomation{ @@ -246,6 +247,9 @@ bool in_region(const MapData& map, const char* region, const TilePos& point){ } // namespace std::optional spin_direction(const MapData& map, const TilePos& point){ + if (const std::optional coded = coded_spin_direction(map.name, point)){ + return coded; + } static const std::pair ARROWS[] = { {SPIN_NORTH_REGION, Direction::NORTH}, {SPIN_SOUTH_REGION, Direction::SOUTH}, @@ -260,7 +264,7 @@ std::optional spin_direction(const MapData& map, const TilePos& point return std::nullopt; } bool stops_spinning(const MapData& map, const TilePos& point){ - return in_region(map, SPIN_STOP_REGION, point); + return coded_spin_stop(map.name, point) || in_region(map, SPIN_STOP_REGION, point); } std::optional spin_slide(const MapData& map, const PlayerState& state){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h index 84bf33ea49..aecc3af3c0 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h @@ -162,8 +162,9 @@ PlayerState after_step_onto( // Spinner tiles (Rocket Hideout B2F and B3F, Viridian Gym): pokefirered's // MB_SPIN_UP, MB_SPIN_DOWN, MB_SPIN_LEFT, MB_SPIN_RIGHT and -// MB_STOP_SPINNING. The tile glyphs can't say so, so a map lists them in -// these regions, as it does WILD_REGION. +// MB_STOP_SPINNING. The tile glyphs can't say so: the hideout's floors +// have them coded (coded_spin_direction(), PokemonFRLG_CodedRooms.h); +// another map may list them in these regions, as it does WILD_REGION. // "spin_north": MB_SPIN_UP. extern const char SPIN_NORTH_REGION[]; // "spin_south": MB_SPIN_DOWN. diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp index b0cd746e07..2e9b85afac 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp @@ -145,6 +145,7 @@ OverworldSim::OverworldSim(std::vector maps, MapPosition start, Directi if (m_map < 0){ throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Sim: start on a map that isn't loaded: " + start.map); } + add_coded_field(); m_player = PlayerState{start.point, elevation_at(map(), start.point), false}; m_state.facing = facing; show(m_script.start_menu_open ? SimScreen::START_MENU : SimScreen::OVERWORLD, m_now); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h index f2dc04326b..94fdd1d063 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h @@ -140,7 +140,8 @@ struct SimScript{ // A lift's map, and where its door lets the player out for each floor // on its panel's menu, top first (pokefirered setdynamicwarp in // RocketHideout_Elevator's scripts): the door's own warp is the - // game's placeholder. + // game's placeholder. The hideout's: rocket_hideout_lift_room() under + // LIFT_MAP, with rocket_hideout_lift_doors(). std::map> lift_floors = {}; SimTiming timing; }; @@ -222,7 +223,8 @@ enum class SimField{ // The poster the player faces (an object named "poster"): the switch // behind it opens the stairs, which takes away the objects named // "poster_stairs" for good (pokefirered - // CeladonCity_GameCorner_EventScript_Poster's setmetatile). + // CeladonCity_GameCorner_EventScript_Poster's setmetatile). The sim + // adds both to the Game Corner from code (add_coded_objects()). POSTER_SWITCH, // The panel of the lift the player stands in (SimScript::lift_floors): // the floor picked, the lift's door now lets out there. @@ -325,6 +327,8 @@ class OverworldSim{ bool slide_on(Step& step) const; // The slide's time after `step`, and its facing; 0 for no slide. Milliseconds after_slide(const Step& step); + // What the maps can't say, from code (add_coded_objects()). + void add_coded_field(); bool cut_tree(); bool choose_lift_floor(size_t floor); bool push_poster_switch(); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp index 2f657677a6..ac198790ac 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp @@ -9,6 +9,7 @@ #include #include +#include "PokemonFRLG_CodedRooms.h" #include "PokemonFRLG_OverworldSim.h" namespace PokemonAutomation{ @@ -36,6 +37,12 @@ bool OverworldSim::use_field(SimField field, size_t floor){ return false; } +void OverworldSim::add_coded_field(){ + for (MapData& loaded : m_maps){ + add_coded_objects(loaded); + } +} + bool OverworldSim::slide_on(Step& step) const{ if (!spin_direction(m_maps[step.map], step.player.position)){ return true; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp index 3726c116a4..8c11c1fa7d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp @@ -9,6 +9,7 @@ #include "PokemonFRLG_AutoStoryActions.h" #include "PokemonFRLG_AutoStoryHideout.h" #include "PokemonFRLG_AutoStoryItemUse.h" +#include "PokemonFRLG_AutoStoryRooms.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -88,7 +89,7 @@ constexpr Milliseconds HIDEOUT_SCENE_TIMEOUT = 30000ms; } // namespace -void ride_lift(ConsoleHandle& console, ProControllerContext& context, const LiftRide& ride){ +void pick_lift_floor(ConsoleHandle& console, ProControllerContext& context, const LiftRide& ride){ const MenuPresses presses = lift_menu_presses(ride, rocket_hideout_lift_menu().size()); console.log( "Story: lift from " + rocket_hideout_lift_menu()[ride.from] + " to " + rocket_hideout_lift_menu()[ride.to] @@ -104,6 +105,12 @@ void ride_lift(ConsoleHandle& console, ProControllerContext& context, const Lift mash_until_overworld(console, context, HIDEOUT_SCENE_TIMEOUT); } +void ride_lift(ConsoleHandle& console, ProControllerContext& context, const LiftRide& ride){ + walk_room_legs(console, context, lift_in_legs(), Pokemon::Direction::NORTH); + pick_lift_floor(console, context, ride); + walk_room_legs(console, context, lift_out_legs(), Pokemon::Direction::NORTH); +} + void push_poster_switch(ConsoleHandle& console, ProControllerContext& context){ console.log("Story: the switch behind the poster."); pbf_press_button(context, BUTTON_A, 160ms, 800ms); @@ -113,9 +120,17 @@ void push_poster_switch(ConsoleHandle& console, ProControllerContext& context){ std::optional run_field_game_step( ConsoleHandle& console, ProControllerContext& context, Language language, const Pokemon::StoryStep& step ){ + const auto done = [&step]{ + return step.pose_after ? Pokemon::outcome_ok(*step.pose_after) : Pokemon::outcome_ok(); + }; if (const std::optional ride = parse_lift_floor_variant(step.variant)){ ride_lift(console, context, *ride); - return Pokemon::outcome_ok(); + return done(); + } + if (is_center_variant(step.variant)){ + // Both start facing north: at the door, or at the nurse. + walk_room_legs(console, context, center_legs(step.variant), Pokemon::Direction::NORTH); + return done(); } if (step.variant == POSTER_SWITCH_VARIANT){ push_poster_switch(console, context); @@ -130,7 +145,7 @@ std::optional run_field_game_step( if (end != ItemUseEnd::USED){ return Pokemon::outcome_failed(std::string("use_item_") + item_use_end_name(end), step.variant); } - return step.pose_after ? Pokemon::outcome_ok(*step.pose_after) : Pokemon::outcome_ok(); + return done(); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h index 1282c2d880..bf8ed55c83 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h @@ -37,10 +37,12 @@ namespace PokemonFRLG{ // beaten first. inline const std::string POSTER_SWITCH_VARIANT = "poster_switch"; -// The GAME step that rides the Rocket Hideout's lift, the player facing -// its panel with the LIFT KEY: "lift_floor ", floors as the -// panel names them (B1F, B2F, B4F). `from` is the floor the lift stands -// at, where the panel's cursor starts. +// The GAME step that rides the Rocket Hideout's lift with the LIFT KEY: +// "lift_floor ", floors as the panel names them (B1F, B2F, +// B4F). `from` is the floor the lift stands at, where the panel's cursor +// starts. The player starts below `from`'s lift door, facing north, and +// ends on `to`'s lift door (rocket_hideout_lift_doors()): the lift is +// walked by a script (lift_in_legs(), lift_out_legs()), not a map. inline constexpr std::string_view LIFT_FLOOR_PREFIX = "lift_floor "; // The lift panel's menu, top first (pokefirered @@ -73,15 +75,18 @@ bool is_hideout_variant(std::string_view variant); // On the console: A on the panel, the floor picked, the ride waited out // back to the overworld, the player still in the lift. +void pick_lift_floor(ConsoleHandle& console, ProControllerContext& context, const LiftRide& ride); +// On the console, the whole ride: into the lift from below the door, +// the floor picked, out onto the floor's lift door. void ride_lift(ConsoleHandle& console, ProControllerContext& context, const LiftRide& ride); // On the console: A on the poster, its message closed back to the // overworld. void push_poster_switch(ConsoleHandle& console, ProControllerContext& context); -// On the console, a GAME step of this file's or an item from the bag -// (is_item_use_variant()); nothing for another GAME step. The hideout's -// steps leave the player where they stand; an item used ends on the -// step's pose_after (an Escape Rope's), if it has one. +// On the console, a GAME step of this file's, a Pokemon Center's +// (is_center_variant()) or an item from the bag (is_item_use_variant()); +// nothing for another GAME step. The step ends on its pose_after, if it +// has one (an Escape Rope's, a lift ride's, a Center's). std::optional run_field_game_step( ConsoleHandle& console, ProControllerContext& context, Language language, const Pokemon::StoryStep& step ); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp index 12db0549e2..ae6b342f3e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp @@ -8,6 +8,7 @@ #include #include #include "Common/Cpp/TestRunners/UnitTest.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h" @@ -100,57 +101,99 @@ UnitTestResult test_steps_on_sim(){ return true; } +// The Game Corner with no poster or stairs in its map: the sim adds them +// from code (add_coded_objects()). The room ends at x 16, so the walk +// east ends on (15, 2), the stairs' top, only once the switch opens it. std::string poster_on_sim(){ - const MapData room = parse_map(R"({ - "name": "Corner", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, - "tiles": ["#####", "#...#", "#...#", "#####"], - "objects": [ - {"name": "poster", "kind": "sign", "x": 2, "y": 1}, - {"name": "poster_stairs", "kind": "npc", "x": 3, "y": 1} - ] - })", "Corner"); - OverworldSim sim({room}, MapPosition{"Corner", {2, 2}}, Direction::NORTH); - StoryPose at{"Corner", {2, 2}, Direction::NORTH}; + std::string rows; + for (int y = 0; y < 5; y++){ + rows += std::string(y == 0 ? "" : ", ") + (y == 0 || y == 4 ? "\"#################\"" : "\"#...............#\""); + } + const MapData room = parse_map( + "{\"name\": \"CeladonCity_GameCorner\", \"source\": {\"decomp_file\": \"\", \"revision\": \"test\", \"date\": \"2026-11-09\"}," + " \"tiles\": [" + rows + "]}", + "CeladonCity_GameCorner" + ); + const MapPosition below_poster{"CeladonCity_GameCorner", {GAME_CORNER_POSTER.x, GAME_CORNER_POSTER.y + 1}}; + OverworldSim sim({room}, below_poster, Direction::NORTH); + StoryPose at{below_poster.map, below_poster.point, Direction::NORTH}; const std::string failure = row_step_on_sim(sim, at, StoryTable::poster_switch("poster_switch", "test"), "00.31"); if (!failure.empty()){ return failure; } const Milliseconds tile = MovementMechanics::TILE_TIME; - sim.hold(Direction::EAST, tile * 2, MovementMechanics::STEP_RELEASE, tile * 2, {}); - sim.hold(Direction::NORTH, tile * 2, MovementMechanics::STEP_RELEASE, tile * 2, {}); - if (!(sim.state().position == MapPosition{"Corner", {3, 1}})){ - return "After the poster step, walked onto " + to_string(sim.state().position) + ", not the stairs."; + sim.hold(Direction::EAST, tile * 6, MovementMechanics::STEP_RELEASE, tile * 6, {}); + if (!(sim.state().position == MapPosition{"CeladonCity_GameCorner", {15, 2}})){ + return "After the poster step, walked onto " + to_string(sim.state().position) + ", not the stairs' (15, 2)."; } return ""; } +// A hideout floor of 32 by 26 open tiles with its lift door, which lets +// into the lift as pokefirered's floors' warps do (warp 1 of the lift). +MapData hideout_floor(const std::string& name, const TilePos& door){ + std::string rows; + for (int y = 0; y < 26; y++){ + std::string row(32, '.'); + if (y == door.y){ + row[door.x] = 'D'; + } + rows += (y == 0 ? "\"" : ", \"") + row + "\""; + } + return parse_map(std::format( + "{{\"name\": \"{}\", \"source\": {{\"decomp_file\": \"\", \"revision\": \"test\", \"date\": \"2026-11-09\"}}," + " \"tiles\": [{}], \"warps\": [{{\"x\": {}, \"y\": {}, \"enter\": \"walk_into\", \"to_map\": \"{}\", \"to_point\": \"warp_1\"}}]}}", + name, rows, door.x, door.y, LIFT_MAP + ), name); +} + +// The ride from B2F to B4F with the lift's room coded, not mapped: from +// below B2F's lift door, in, the panel, out onto B4F's lift door. std::string lift_on_sim(){ - std::vector maps; - maps.push_back(parse_map(R"({ - "name": "Lift", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, - "tiles": ["###", "#.#", "#.#", "###"], - "warps": [{"x": 1, "y": 2, "enter": "walk_south"}] - })", "Lift")); - maps.push_back(parse_map(R"({ - "name": "B4F", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, - "tiles": ["###", "#.#", "###"] - })", "B4F")); + const std::vector& doors = rocket_hideout_lift_doors(); + std::vector maps{ + hideout_floor("RocketHideout_B2F", doors[1].point), + hideout_floor("RocketHideout_B4F", doors[2].point), + rocket_hideout_lift_room(), + }; SimScript script; - const MapPosition b4f{"B4F", {1, 1}}; - script.lift_floors["Lift"] = {MapPosition{"Lift", {1, 1}}, MapPosition{"Lift", {1, 1}}, b4f}; - OverworldSim sim(maps, MapPosition{"Lift", {1, 2}}, Direction::SOUTH, script); - StoryPose at{"Lift", {1, 2}, Direction::SOUTH}; + script.lift_floors[std::string(LIFT_MAP)] = doors; + const MapPosition below_door{"RocketHideout_B2F", doors[1].point.next(Direction::SOUTH)}; + OverworldSim sim(maps, below_door, Direction::NORTH, script); + StoryPose at{below_door.map, below_door.point, Direction::NORTH}; const std::string failure = row_step_on_sim(sim, at, StoryTable::lift_floor("lift_to_b4f", "B2F", "B4F", "test"), "00.32"); if (!failure.empty()){ return failure; } - sim.hold(Direction::SOUTH, MovementMechanics::TILE_TIME, MovementMechanics::STEP_RELEASE, std::chrono::milliseconds(3000), {}); - if (!(sim.state().position == b4f)){ + if (!(sim.state().position == doors[2])){ return "After the lift step, the door let out on " + to_string(sim.state().position) + "."; } + if (!sim.state().violations.empty()){ + return "The lift step broke the game's rules: " + sim.state().violations.front(); + } return ""; } +// The hideout's spinners are coded: a map named as B2F or B3F slides the +// player with no spin_* regions of its own. +UnitTestResult test_coded_spinners(){ + MapData b3f = parse_map(R"({ + "name": "RocketHideout_B3F", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, + "tiles": ["##########", "#........#", "#........#", "##########"] + })", "RocketHideout_B3F"); + if (spin_direction(b3f, TilePos{4, 9}) != Direction::WEST || !stops_spinning(b3f, TilePos{2, 9})){ + return "Expected B3F (4, 9) to slide west and (2, 9) to stop a slide."; + } + if (coded_spin_direction("RocketHideout_B2F", TilePos{3, 4}) != Direction::WEST + || coded_spin_direction("RocketHideout_B2F", TilePos{9, 5}) != Direction::NORTH + || !coded_spin_stop("RocketHideout_B2F", TilePos{1, 4}) + || coded_spin_direction("RocketHideout_B1F", TilePos{3, 4}).has_value() + ){ + return "Expected B2F's arrows and stops from pokefirered, and none on B1F."; + } + return true; +} + struct AutoStoryHideoutBodyInput{ std::string name; UnitTestResult (*body)(); @@ -186,6 +229,10 @@ const AutoStoryHideoutBodyCase HIDEOUT_BODY_CASES[] = { .input = {.name = "table steps on the sim", .body = test_steps_on_sim}, .expectation = {.description = "the poster opens the stairs; the lift lets out on the floor picked"}, }, + { + .input = {.name = "coded spinners", .body = test_coded_spinners}, + .expectation = {.description = "B2F's and B3F's arrows come from code, not the map"}, + }, }; const LiftCase LIFT_CASES[] = { diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp index 1f0c4bdeb9..3bce9b0ae6 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp @@ -111,9 +111,17 @@ ItemUseEnd renew_repel(Logger& logger, ItemUseActions& actions, std::string_view namespace{ -// The bag slides in, and ignores presses until it is done (as -// use_max_repel_from_bag()). -constexpr Milliseconds BAG_SLIDE_IN = 1000ms; +// The waits that worked on console B (2026-11-09, the Bicycle from Key +// Items: TEACHY TV, TM CASE, HELIX FOSSIL, FAME CHECKER, S.S. TICKET, +// BICYCLE; USE the first option, on the bike about 1.5 s after its A). +// The bag slides in, and ignores presses until it is done. +constexpr Milliseconds BAG_SLIDE_IN = 1500ms; +// A pocket switch (left or right), press and wait. +constexpr Milliseconds POCKET_TAP = 100ms; +constexpr Milliseconds POCKET_SWITCH_WAIT = 600ms; +// A cursor step in a pocket, press and wait: 250 ms in all. +constexpr Milliseconds CURSOR_TAP = 100ms; +constexpr Milliseconds CURSOR_WAIT = 150ms; // Out of the bag and its messages, or the Escape Rope's warp. constexpr Milliseconds BAG_CLOSE_TIMEOUT = 30000ms; @@ -130,10 +138,10 @@ bool ConsoleItemUseActions::open_pocket(BagPocket pocket){ pbf_wait(m_context, BAG_SLIDE_IN); // Left to the Items pocket, which doesn't wrap, then right to `pocket`. for (int c = 0; c < 3; c++){ - pbf_press_dpad(m_context, DPAD_LEFT, 100ms, 600ms); + pbf_press_dpad(m_context, DPAD_LEFT, POCKET_TAP, POCKET_SWITCH_WAIT); } for (int c = 0; c < (int)pocket; c++){ - pbf_press_dpad(m_context, DPAD_RIGHT, 100ms, 600ms); + pbf_press_dpad(m_context, DPAD_RIGHT, POCKET_TAP, POCKET_SWITCH_WAIT); } for (int c = 0; c < 20; c++){ pbf_press_dpad(m_context, DPAD_UP, 100ms, 100ms); @@ -152,7 +160,7 @@ std::vector ConsoleItemUseActions::read_pocket(){ void ConsoleItemUseActions::use_row(size_t row){ for (size_t c = 0; c < row; c++){ - pbf_press_dpad(m_context, DPAD_DOWN, 100ms, 300ms); + pbf_press_dpad(m_context, DPAD_DOWN, CURSOR_TAP, CURSOR_WAIT); } // The item's menu, USE on top. pbf_press_button(m_context, BUTTON_A, 160ms, 800ms); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.cpp new file mode 100644 index 0000000000..52de05543b --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.cpp @@ -0,0 +1,91 @@ +/* AutoStory Scripted Rooms + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" +#include "PokemonFRLG_AutoStoryActions.h" +#include "PokemonFRLG_AutoStoryRooms.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +bool is_center_variant(std::string_view variant){ + return variant == CENTER_ENTER_VARIANT || variant == CENTER_EXIT_VARIANT; +} + +const std::vector& center_enter_legs(){ + static const std::vector LEGS{ + {Pokemon::Direction::NORTH, 1, true}, + {Pokemon::Direction::NORTH, 4, false}, + }; + return LEGS; +} +const std::vector& center_exit_legs(){ + static const std::vector LEGS{ + {Pokemon::Direction::SOUTH, 4, false}, + {Pokemon::Direction::SOUTH, 1, true}, + }; + return LEGS; +} +const std::vector& lift_in_legs(){ + static const std::vector LEGS{ + {Pokemon::Direction::NORTH, 1, true}, + {Pokemon::Direction::NORTH, 2, false}, + {Pokemon::Direction::WEST, 2, false}, + {Pokemon::Direction::NORTH, 0, false}, + }; + return LEGS; +} +const std::vector& lift_out_legs(){ + static const std::vector LEGS{ + {Pokemon::Direction::EAST, 2, false}, + {Pokemon::Direction::SOUTH, 2, false}, + {Pokemon::Direction::SOUTH, 1, true}, + }; + return LEGS; +} +const std::vector& center_legs(std::string_view variant){ + static const std::vector NONE; + if (variant == CENTER_ENTER_VARIANT){ + return center_enter_legs(); + } + if (variant == CENTER_EXIT_VARIANT){ + return center_exit_legs(); + } + return NONE; +} + + +void walk_room_legs( + ConsoleHandle& console, ProControllerContext& context, + const std::vector& legs, std::optional facing +){ + for (const RoomLeg& leg : legs){ + if (leg.tiles == 0){ + OverworldWalkSteps::turn_to(console, context, leg.direction); + facing = leg.direction; + continue; + } + for (int tile = 0; tile < leg.tiles; tile++){ + step_once(console, context, leg.direction, facing); + facing = leg.direction; + } + if (leg.warps){ + RouteStep step; + step.direction = leg.direction; + step.tiles = 1; + step.warps = true; + OverworldWalkSteps::wait_out_warp(console, context, step); + } + } +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.h new file mode 100644 index 0000000000..ee5b3d9f91 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.h @@ -0,0 +1,81 @@ +/* AutoStory Scripted Rooms + * + * From: https://github.com/PokemonAutomation/ + * + * Rooms the story walks by a fixed script of taps, not a planned route: + * every Pokemon Center's ground floor (one layout, so one script) and the + * Rocket Hideout's lift. Their tiles are coded (PokemonFRLG_CodedRooms.h), + * not mapped: the user's call, 2026-11-09 ("mostly the same"; "small area, + * high overhead, just script it"). + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryRooms_H +#define PokemonAutomation_PokemonFRLG_AutoStoryRooms_H + +#include +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "Pokemon/Navigation/Pokemon_Grid.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The GAME step into a Pokemon Center: from the tile below its door, +// facing north, through the door onto the mat (CENTER_MAT) and up the +// nurse's column to below her counter (CENTER_NURSE_FRONT), facing her. +// The HEAL step after it talks to her. +inline const std::string CENTER_ENTER_VARIANT = "center_enter"; +// The GAME step out of a Pokemon Center: from below the nurse's counter +// down to the mat and out; the game walks the player a tile out of the +// door, onto the tile below it, facing south. +inline const std::string CENTER_EXIT_VARIANT = "center_exit"; +// Whether `variant` is one of the Center's steps. +bool is_center_variant(std::string_view variant); + +// A straight piece of a room's script: `tiles` steps `direction`, one +// tap a tile; none: a turn to face `direction`. `warps`: the last step +// takes a warp, whose fade is waited out. +struct RoomLeg{ + Pokemon::Direction direction = Pokemon::Direction::NORTH; + int tiles = 0; + bool warps = false; +}; +// Into a Center: north through the door, north 4 to the nurse. +const std::vector& center_enter_legs(); +// Out of a Center: south 4 to the mat, south through the door. +const std::vector& center_exit_legs(); +// Into the hideout's lift, from below a floor's lift door facing north: +// north through the door onto LIFT_ARRIVAL, north 2, west 2 to below the +// panel (LIFT_PANEL_FRONT), facing it. +const std::vector& lift_in_legs(); +// Out of the lift, from below the panel: east 2, south 2 onto the arrow +// warp, south through it, onto the floor picked's lift door. +const std::vector& lift_out_legs(); +// The legs of the Center's step `variant`; empty if it isn't one. +const std::vector& center_legs(std::string_view variant); + +// On the console: the legs, a step tap a tile (step_once()), each warp's +// fade waited out. `facing` is the way the player faces at the start, if +// known. +void walk_room_legs( + ConsoleHandle& console, ProControllerContext& context, + const std::vector& legs, std::optional facing +); + + +// Registers the scripted rooms' tests. +void add_tests_AutoStoryRooms(UnitTestDatabase& database); + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms_Tests.cpp new file mode 100644 index 0000000000..00884dc0b3 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms_Tests.cpp @@ -0,0 +1,125 @@ +/* AutoStory Scripted Rooms Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h" +#include "PokemonFRLG_AutoStoryRooms.h" +#include "PokemonFRLG_AutoStoryRowSim.h" +#include "PokemonFRLG_AutoStoryTableSteps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +struct CenterInput{ + // The town or route whose Center it is. + std::string town; +}; +struct CenterExpectation{ + std::string center; +}; +struct CenterCase{ + CenterInput input; + CenterExpectation expectation; +}; + +// The Center's coded room has the tiles of its map in the lock. +std::string same_tiles(const std::string& center){ + const MapData mapped = load_map(center); + const MapData coded = pokemon_center_room(center, {}); + if (mapped.tiles.width != coded.tiles.width || mapped.tiles.height != coded.tiles.height){ + return center + ": the coded room is not the map's size."; + } + for (int y = 0; y < coded.tiles.height; y++){ + for (int x = 0; x < coded.tiles.width; x++){ + if (tile_at(mapped, TilePos{x, y}) != tile_at(coded, TilePos{x, y})){ + return center + ": the coded room differs at (" + std::to_string(x) + ", " + std::to_string(y) + ")."; + } + } + } + return ""; +} + +// On the sim, with the town's map and the Center's coded room: in from +// below the door to the nurse, out again to below the door. +std::string in_and_out(const std::string& town, const std::string& center){ + std::vector maps{load_map(town)}; + const std::optional added = add_pokemon_center_room(maps, 0); + if (added != center){ + return town + ": no door to " + center + "."; + } + const TilePos door = maps[0].points.at(center + "_door"); + const MapPosition below_door{town, door.next(Direction::SOUTH)}; + OverworldSim sim(maps, below_door, Direction::NORTH); + StoryPose at{town, below_door.point, Direction::NORTH}; + std::string failure = row_step_on_sim(sim, at, StoryTable::center_enter("to_nurse", center.c_str(), "test"), "test"); + if (!failure.empty()){ + return failure; + } + const Pokemon::Pose outside = StoryTable::at(town.c_str(), below_door.point.x, below_door.point.y, Direction::SOUTH); + failure = row_step_on_sim(sim, at, StoryTable::center_exit("out", outside, "test"), "test"); + if (!failure.empty()){ + return failure; + } + if (!sim.state().violations.empty()){ + return town + ": " + sim.state().violations.front(); + } + return ""; +} + +class Test_AutoStoryCenter : public UnitTest{ +public: + explicit Test_AutoStoryCenter(CenterCase test_case) + : UnitTest("PokemonFRLG::AutoStoryRooms - Center: " + test_case.expectation.center) + , m_case(std::move(test_case)) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::string& center = m_case.expectation.center; + if (std::string failure = same_tiles(center); !failure.empty()){ + return failure; + } + if (std::string failure = in_and_out(m_case.input.town, center); !failure.empty()){ + return failure; + } + return true; + } + +private: + CenterCase m_case; +}; + +// The seven Centers in the map lock. +const CenterCase CENTER_CASES[] = { + {.input = {.town = "ViridianCity"}, .expectation = {.center = "ViridianCity_PokemonCenter_1F"}}, + {.input = {.town = "PewterCity"}, .expectation = {.center = "PewterCity_PokemonCenter_1F"}}, + {.input = {.town = "Route4"}, .expectation = {.center = "Route4_PokemonCenter_1F"}}, + {.input = {.town = "CeruleanCity"}, .expectation = {.center = "CeruleanCity_PokemonCenter_1F"}}, + {.input = {.town = "VermilionCity"}, .expectation = {.center = "VermilionCity_PokemonCenter_1F"}}, + {.input = {.town = "Route10"}, .expectation = {.center = "Route10_PokemonCenter_1F"}}, + {.input = {.town = "LavenderTown"}, .expectation = {.center = "LavenderTown_PokemonCenter_1F"}}, +}; + +} // namespace + + +void add_tests_AutoStoryRooms(UnitTestDatabase& database){ + for (const CenterCase& item : CENTER_CASES){ + database.add(item); + } +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp index 936d342526..089904ef6a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp @@ -15,7 +15,9 @@ #include "Pokemon/AutoStory/Pokemon_TilePose.h" #include "PokemonFRLG_AutoStoryActions.h" #include "PokemonFRLG_AutoStoryBattleDecisions.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" #include "PokemonFRLG_AutoStoryHideout.h" +#include "PokemonFRLG_AutoStoryRooms.h" #include "PokemonFRLG_AutoStoryRowSim.h" namespace PokemonAutomation{ @@ -522,9 +524,30 @@ std::string OverworldStepSim::cut(const StoryStep& step){ return ""; } -// A Rocket Hideout GAME step on the sim: the poster's switch, or the -// lift's floor (SimScript::lift_floors, rows as on the panel); any other -// GAME step is stubbed. +namespace{ + +// walk_room_legs() on the sim: a step tap a tile, each warp's fade and +// the walk out of a door waited out. +void sim_walk_room_legs(OverworldSim& sim, const std::vector& legs){ + for (const RoomLeg& leg : legs){ + if (leg.tiles == 0){ + sim_turn_to(sim, leg.direction); + continue; + } + for (int tile = 0; tile < leg.tiles; tile++){ + sim_step_once(sim, leg.direction); + } + if (leg.warps){ + sim.watch({}, OverworldWalkSteps::AFTER_WARP); + } + } +} + +} // namespace + +// A scripted GAME step on the sim: the poster's switch, the lift's ride +// (its room coded, SimScript::lift_floors, rows as on the panel), or a +// Pokemon Center's way in or out; any other GAME step is stubbed. Pokemon::StoryAction OverworldStepSim::hideout_or_stub(){ Pokemon::StoryAction stub = m_stubs.stub(); Pokemon::StoryAction sim = played([this](RunState&, const StoryStep& step){ @@ -532,7 +555,9 @@ Pokemon::StoryAction OverworldStepSim::hideout_or_stub(){ return failure.empty() ? Pokemon::outcome_ok() : Pokemon::outcome_failed("sim_failure", failure); }); sim.act = [stub, played_act = sim.act](RunState& state, const StoryStep& step){ - return is_hideout_variant(step.variant) ? played_act(state, step) : stub.act(state, step); + return is_hideout_variant(step.variant) || is_center_variant(step.variant) + ? played_act(state, step) + : stub.act(state, step); }; sim.needs_console = stub.needs_console; return sim; @@ -542,10 +567,34 @@ std::string OverworldStepSim::hideout(const StoryStep& step){ if (step.variant == POSTER_SWITCH_VARIANT){ return m_sim.use_field(SimField::POSTER_SWITCH) ? "" : "no poster ahead."; } + if (is_center_variant(step.variant)){ + sim_walk_room_legs(m_sim, center_legs(step.variant)); + return ended_on(step); + } const std::optional ride = parse_lift_floor_variant(step.variant); - if (!ride || !m_sim.use_field(SimField::LIFT_FLOOR, ride->to)){ + if (!ride){ + return "no lift ride \"" + step.variant + "\"."; + } + sim_walk_room_legs(m_sim, lift_in_legs()); + if (!m_sim.use_field(SimField::LIFT_FLOOR, ride->to)){ return "no lift panel for \"" + step.variant + "\"."; } + sim_walk_room_legs(m_sim, lift_out_legs()); + return ended_on(step); +} + +// After a scripted step: the player's pose, and whether it is the step's +// pose_after, if it has one. +std::string OverworldStepSim::ended_on(const StoryStep& step){ + const SimState& state = m_sim.state(); + m_at = StoryPose{state.position.map, state.position.point, state.facing}; + if (!step.pose_after){ + return ""; + } + const std::optional expected = story_pose(*step.pose_after); + if (!expected || !(MapPosition{expected->map, expected->tile} == state.position)){ + return "expected to end on " + (expected ? to_string(MapPosition{expected->map, expected->tile}) : std::string("a pose")) + "."; + } return ""; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.h index aa36f4f023..401b5b9b34 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.h @@ -185,6 +185,7 @@ class OverworldStepSim : public Pokemon::TableExecutor{ std::string battle(const StoryStep& step); Pokemon::StoryAction hideout_or_stub(); std::string hideout(const StoryStep& step); + std::string ended_on(const StoryStep& step); OverworldSim& m_sim; StoryPose m_at; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryStepTable_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryStepTable_Tests.cpp index 8eeb70211c..c61f6c10ce 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryStepTable_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryStepTable_Tests.cpp @@ -20,6 +20,7 @@ #include "PokemonFRLG_AutoStoryStepTable.h" #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryHideout.h" +#include "PokemonFRLG_AutoStoryRooms.h" #include "PokemonFRLG_AutoStoryItemUse.h" #include "PokemonFRLG_AutoStoryTable.h" #include "PokemonFRLG_AutoStoryTrashCans.h" @@ -253,6 +254,7 @@ void add_tests_AutoStoryStepTable(UnitTestDatabase& database){ add_tests_AutoStoryStepInterrupts(database); // The GAME variants with logic of their own. add_tests_AutoStoryHideout(database); + add_tests_AutoStoryRooms(database); add_tests_AutoStoryItemUse(database); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp index 81140d3520..70d3a2b8f0 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp @@ -4,8 +4,10 @@ * */ +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h" #include "PokemonFRLG_AutoStoryHideout.h" #include "PokemonFRLG_AutoStoryItemUse.h" +#include "PokemonFRLG_AutoStoryRooms.h" #include "PokemonFRLG_AutoStoryTableSteps.h" namespace PokemonAutomation{ @@ -165,6 +167,24 @@ StoryStep poster_switch(std::string name, std::string why){ StoryStep lift_floor(std::string name, std::string_view from, std::string_view to, std::string why){ StoryStep step = make(StoryStepKind::GAME, std::move(name), std::move(why)); step.variant = lift_floor_variant(from, to); + if (const std::optional ride = parse_lift_floor_variant(step.variant)){ + const MapPosition& door = rocket_hideout_lift_doors()[ride->to]; + step.pose_after = to_pose(StoryPose{door.map, door.point, Direction::SOUTH}); + } + return step; +} + +StoryStep center_enter(std::string name, const char* center, std::string why){ + StoryStep step = make(StoryStepKind::GAME, std::move(name), std::move(why)); + step.variant = CENTER_ENTER_VARIANT; + step.pose_after = at(center, CENTER_NURSE_FRONT.x, CENTER_NURSE_FRONT.y, Direction::NORTH); + return step; +} + +StoryStep center_exit(std::string name, Pokemon::Pose below_door, std::string why){ + StoryStep step = make(StoryStepKind::GAME, std::move(name), std::move(why)); + step.variant = CENTER_EXIT_VARIANT; + step.pose_after = std::move(below_door); return step; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h index 0e28ad3048..8729198d44 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h @@ -186,9 +186,17 @@ StoryStep cut_tree(std::string name, Pokemon::Pose tree, std::string why); // Pushes the switch behind the Game Corner's poster the player faces // (POSTER_SWITCH_VARIANT). StoryStep poster_switch(std::string name, std::string why); -// Rides the Rocket Hideout's lift, the player facing its panel, from the -// floor it stands at to `to` (lift_floor_variant()): "B1F", "B2F", "B4F". +// Rides the Rocket Hideout's lift from the floor it stands at to `to` +// (lift_floor_variant()): "B1F", "B2F", "B4F". The player starts below +// `from`'s lift door, facing north, and ends on `to`'s, facing south. StoryStep lift_floor(std::string name, std::string_view from, std::string_view to, std::string why); +// Into the Pokemon Center `center` (a map name) from below its door, +// facing north, to below the nurse's counter (CENTER_ENTER_VARIANT): the +// same script in every Center, with no map of its rooms. +StoryStep center_enter(std::string name, const char* center, std::string why); +// Out of the Center from below the nurse's counter (CENTER_EXIT_VARIANT) +// to `below_door`, the tile below its door outside, facing south. +StoryStep center_exit(std::string name, Pokemon::Pose below_door, std::string why); // Uses `item` from the bag on the overworld (use_item_variant()): a // Repel, or an Escape Rope, which leaves the player on `pose_after`. StoryStep use_item(std::string name, std::string_view item, std::string why, std::optional pose_after = std::nullopt); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md index 532ef68bfa..0d23b8ff91 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md @@ -37,8 +37,8 @@ of the hideout after Giovanni (00.32), which puts the player at the Game Corner's door, CeladonCity (34, 22) (pokefirered `UpdateEscapeWarp()`: the last outdoor door, a tile south). The Rocket Hideout has no wild Pokemon (`wild_encounters.json` lists none of its floors), so no Repel is used there. -The BICYCLE (00.26) is ridden on Route 8's open legs (`bike`); it is -`BIKE_TILE_TIME`, an estimate from the decompilation, until measured. +The BICYCLE (00.26) is ridden on Route 8's open legs (`bike`) at +`BIKE_TILE_TIME`, 112 ms a tile, measured on console B (2026-11-09). ## The maps each draft needs @@ -57,27 +57,45 @@ or neither: | UndergroundPath_WestEntrance | 00.30 | candidate | | Route7 | 00.30 | candidate | | CeladonCity | 00.30 to 00.32 | candidate | -| CeladonCity_PokemonCenter_1F | 00.30, 00.31 | missing | | CeladonCity_GameCorner | 00.31, 00.32 | candidate | | RocketHideout_B1F | 00.31, 00.32 | candidate | | RocketHideout_B2F | 00.31, 00.32 | candidate | | RocketHideout_B3F | 00.31 | candidate | | RocketHideout_B4F | 00.31, 00.32 | candidate | -| RocketHideout_Elevator | 00.32 | missing | - -Beyond publishing (a PokemonSimulation publish and a lock bump, which need the -user's approval), the code added for these drafts reads three things the -candidate maps don't carry yet: - -- B2F's and B3F's spinner tiles as regions `spin_north`, `spin_south`, - `spin_west`, `spin_east` and `spin_stop` (pokefirered `MB_SPIN_*`, - `MB_STOP_SPINNING`; `spin_slide()` in PokemonFRLG_OverworldMap.h); -- the Game Corner's poster as an object `poster` at (11, 1), and its stairs - (16..17, 2..3) as objects `poster_stairs`, which the switch removes - (`SimField::POSTER_SWITCH`); -- the lift's floors: its door is a dynamic warp (`setdynamicwarp`), so the sim - is told where each floor lets out (`SimScript::lift_floors`: B1F (24, 25), - B2F (28, 16), B4F (20, 23)) and the planner can't route through it yet. + +### Scripted, not mapped + +Pokemon Centers and the hideout's lift have no maps (the user's call, +2026-11-09): every Center's ground floor shares pokefirered's +`LAYOUT_POKEMON_CENTER_1F`, and the lift is 5 by 7 tiles. The seven Centers in +the map lock have the same tiles; they differ only in the people standing in +them, none on the nurse's column (x 7). So the story walks both by a fixed +script (`PokemonFRLG_AutoStoryRooms.h`), and the sim builds their rooms from +code (`PokemonFRLG_CodedRooms.h`): + +- `center_enter`: from below the Center's door, facing north, through the door + onto the mat (7, 8) and north 4 to below the nurse (7, 4); `heal` follows. + `center_exit`: south 4 and out; the game walks the player back onto the tile + below the door. 00.30 and 00.31 use them for Lavender's and Celadon's + Centers; no `CeladonCity_PokemonCenter_1F` map is needed. +- `lift_floor `: from below `from`'s lift door, facing north: in + onto (2, 5), north 2, west 2 to below the panel (0, 2), the floor picked, + east 2, south 2 and south through the arrow warp onto `to`'s lift door + (`setdynamicwarp`: B1F (24, 25), B2F (28, 16), B4F (20, 23)). No + `RocketHideout_Elevator` map is needed. + +The two other things the candidate maps can't say in glyphs are coded too, +from pokefirered, so the maps need no extra regions or objects: + +- B2F's and B3F's spinner tiles (`MB_SPIN_*`, `MB_STOP_SPINNING` from the + floors' `map.bin` and the tilesets' metatile attributes; + `coded_spin_direction()`); +- the Game Corner's poster at (11, 1) and the tiles its switch turns into + stairs, (15, 2) and (16..17, 2..3) (`OpenRocketHideout`'s `setmetatile`), + added to the sim's Game Corner by `add_coded_objects()`. + +Beyond those, the drafts need a PokemonSimulation publish and a lock bump for +the candidate maps above, which need the user's approval. `STORY_TRAINERS` names the battles these drafts fight: CeladonCity_GameCorner `rocket_grunt` (GRUNT_7), and on RocketHideout_B4F `lift_key_grunt` (GRUNT_18), @@ -103,6 +121,7 @@ drafts become table rows, their battle steps take those names. format can't yet say `handoff`/`live_start` as 00.12/00.12b do in C++. - `game_action` variants the C++ executor now has (no console run yet): `poster_switch`, `lift_floor ` (PokemonFRLG_AutoStoryHideout.h), + `center_enter`, `center_exit` (PokemonFRLG_AutoStoryRooms.h), `use_item ` and `ride_bike` (PokemonFRLG_AutoStoryItemUse.h), and the `bike` route variant. Variants that no executor has yet: `party_room`, `gift_rng`, `use_item rare-candy @@ -140,10 +159,13 @@ the Elite Four rooms' arrival points (e.g. Lorelei's `warp_0` (6, 12)) are wall tiles, and Indigo Plateau's door glyph is at (11, 5) with its warp at (11, 6). +The `*_nurse` legs below (drafts on #303) need no Center maps once they become +`center_enter` steps, as 00.30's has. + | Skipped leg | Why | |---|---| | 00.32/b2f_lift | B2F's lift corner is reached over arrow tiles: the planner solves them now, but the candidate map has no `spin_*` regions | -| 00.32/b4f_below_guards | RocketHideout_Elevator map not in source-candidate | +| 00.32/b4f_below_guards | planned before the lift was scripted; it now starts on B4F's lift door (20, 23) | | 00.32/x_item_clerk | CeladonCity_DepartmentStore_Elevator map not in source-candidate | | 00.32/route16_below_tree | CeladonCity_DepartmentStore_Elevator map not in source-candidate | | 00.32/celadon_nurse | map not in source-candidate | diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json index 4e01206a12..6ccbd8d610 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json @@ -6,12 +6,19 @@ "title": "Lavender Mart, then west over Route 8: Gamer Rich, a Pidgey for Fly, the Underground Path to Celadon", "start": {"certainty": "tile", "map": "LavenderTown_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, "events": [ + { + "id": "out_of_lavender_center", + "kind": "game_action", + "declared_after": {"certainty": "tile", "map": "LavenderTown", "where": {"facing": "south", "kind": "tiles", "tiles": [[6, 6]]}}, + "variant": "center_exit", + "why": "the Center script out: south to the mat and out of the door (6, 5)" + }, { "id": "lavender_mart_clerk", "kind": "route", "target": {"certainty": "tile", "map": "LavenderTown_Mart", "where": {"facing": "west", "kind": "tiles", "tiles": [[4, 3]]}}, "variant": "run", - "why": "out of the Center, east along row 6 and down to the Mart (20, 15), to its clerk" + "why": "east along row 6 and down to the Mart (20, 15), to its clerk" }, { "id": "buy_ropes_and_super_repels", @@ -87,10 +94,10 @@ }, { "id": "celadon_nurse", - "kind": "route", - "target": {"certainty": "tile", "map": "CeladonCity_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, - "variant": "run", - "why": "into the Center, to the nurse" + "kind": "game_action", + "declared_after": {"certainty": "tile", "map": "CeladonCity_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, + "variant": "center_enter", + "why": "the Center script in: through the door and up to the nurse; no map of the Center" }, { "id": "heal_celadon", diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.31.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.31.plan.json index bef003ddb2..39dd599fd8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.31.plan.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.31.plan.json @@ -6,12 +6,19 @@ "title": "Celadon Game Corner and the Rocket Hideout down to B4F: the poster guard, the Lift Key Grunt, the LIFT KEY and B3F's Rare Candy", "start": {"certainty": "tile", "map": "CeladonCity_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, "events": [ + { + "id": "out_of_celadon_center", + "kind": "game_action", + "declared_after": {"certainty": "tile", "map": "CeladonCity", "where": {"facing": "south", "kind": "tiles", "tiles": [[48, 12]]}}, + "variant": "center_exit", + "why": "the Center script out: south to the mat and out of the door (48, 11)" + }, { "id": "celadon_below_game_corner", "kind": "route", "target": {"certainty": "tile", "map": "CeladonCity", "where": {"facing": "north", "kind": "tiles", "tiles": [[34, 22]]}}, "variant": "run", - "why": "out of the Center, west along row 12, down column 41..43 to row 22, west to below the Game Corner (34, 21)" + "why": "west along row 12, down column 41..43 to row 22, west to below the Game Corner (34, 21)" }, { "id": "below_poster_guard", diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.32.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.32.plan.json index 4351f396c4..d5170089d4 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.32.plan.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.32.plan.json @@ -16,15 +16,16 @@ { "id": "lift_to_b4f", "kind": "game_action", + "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "south", "kind": "tiles", "tiles": [[20, 23]]}}, "variant": "lift_floor B2F B4F", - "why": "the LIFT KEY works the lift: B4F" + "why": "the lift script: in through the door (28, 16), to the panel, B4F with the LIFT KEY, out onto B4F's lift door; no map of the lift" }, { "id": "b4f_below_guards", "kind": "route", "target": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "north", "kind": "tiles", "tiles": [[17, 16]]}}, "variant": "run", - "why": "out of the lift to below the two Grunts (16, 14) and (19, 14)" + "why": "from the lift door to below the two Grunts (16, 14) and (19, 14)" }, { "id": "talk_left_guard", diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 3b38e8424d..35375a29ec 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1846,6 +1846,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Inference/Sounds/PokemonFRLG_CatchFanfareDetector.h Source/VideoGames/PokemonFRLG/Inference/Sounds/PokemonFRLG_ShinySoundDetector.cpp Source/VideoGames/PokemonFRLG/Inference/Sounds/PokemonFRLG_ShinySoundDetector.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldLateBattle.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldLateBattle.h @@ -1951,6 +1953,9 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.h + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp From 2162103e5ce48e26a2b4f30ee84ffee932d655bd Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 19:35:06 -0800 Subject: [PATCH 11/28] FRLG AutoStory 00.30: Lavender Town to Celadon City as an authored plan From Lavender's Center: the Mart's Escape Ropes and Super Repels, Route 8 on the Bicycle with only Gamer Rich fought, the Underground Path, a Pidgey from Route 7's grass, and Celadon's Pokemon Center. The plan loader accepts the bike, Center, item and hideout variants from the merged drafts; the sim stubs a Center's way in when the Center has no map, and the fault sweep ends a row there. Route 7 and Route 8 get their land rates in the sweep; the story sim trace golden gains 00.30. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Menus/PokemonFRLG_SaveCardReader.cpp | 1 + .../Overworld/PokemonFRLG_OverworldMaps.cpp | 1 + .../AutoStory/Data/AuthoredStory_Tests.cpp | 2 +- .../Programs/AutoStory/Data/JsonParser.cpp | 8 +- .../AutoStory/PokemonFRLG_AutoStoryEvents.cpp | 1 + .../PokemonFRLG_AutoStoryFaultSweep_Tests.cpp | 28 ++- .../PokemonFRLG_AutoStoryLavender_Tests.cpp | 49 ++++- .../AutoStory/PokemonFRLG_AutoStoryRowSim.cpp | 22 +- .../ReplayData/frlg-story-sim-trace.json | 117 +++++++++- .../Programs/AutoStory/StoryData/README.md | 2 +- .../AutoStory/StoryData/frlg-00.30.plan.json | 207 ++++++++++++++++++ 11 files changed, 421 insertions(+), 17 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.cpp index 995ae469e7..6401dd7cc9 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.cpp @@ -76,6 +76,7 @@ class PlaceNameMatcher : public OCR::SmallDictionaryMatcher{ {"route-10", "Route 10"}, {"lavender-town", "Lavender Town"}, {"vermilion-city", "Vermilion City"}, + {"celadon-city", "Celadon City"}, }){ JsonArray names; names.push_back(name); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp index 4d83c0db45..69ac64c188 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp @@ -83,6 +83,7 @@ const char* const MAP_NAMES[] = { "RockTunnel_B1F", "LavenderTown", "LavenderTown_PokemonCenter_1F", + "LavenderTown_Mart", // West from Lavender: Route 8, the Underground Path under Saffron and // Route 7 to Celadon City. "Route8", diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/Data/AuthoredStory_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/Data/AuthoredStory_Tests.cpp index 35cb1317c6..9cba930d66 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/Data/AuthoredStory_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/Data/AuthoredStory_Tests.cpp @@ -120,7 +120,7 @@ void unknown_event_field(JsonObject& root){ first_event(root)["speed"] = "fast"; } void unknown_variant(JsonObject& root){ - first_event(root)["variant"] = "bike"; + first_event(root)["variant"] = "skate"; } void unknown_game_action(JsonObject& root){ JsonObject& event = first_event(root); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/Data/JsonParser.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/Data/JsonParser.cpp index e84e3eaa07..159da5b8b8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/Data/JsonParser.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/Data/JsonParser.cpp @@ -15,10 +15,13 @@ #include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h" #include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryChoice.h" #include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.h" +#include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h" +#include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h" #include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryMart.h" #include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryMember.h" #include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryParty.h" #include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPcStorage.h" +#include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.h" #include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTeach.h" #include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTrashCans.h" #include "LegacyJsonFormat.h" @@ -224,7 +227,8 @@ bool known_game_variant(const std::string& variant){ } return parse_capture_variant(variant) || parse_pc_variant(variant) || parse_mart_variant(variant) || parse_teach_variant(variant) || parse_grind_variant(variant) || parse_grind_check_variant(variant) - || parse_fossil_variant(variant) || parse_level_variant(variant); + || parse_fossil_variant(variant) || parse_level_variant(variant) + || is_center_variant(variant) || is_item_use_variant(variant) || is_hideout_variant(variant); } // Whether FRLG does `step`'s kind the way its variant names. @@ -232,7 +236,7 @@ bool known_variant(const StoryStep& step){ switch (step.kind){ case StoryStepKind::ROUTE: case StoryStepKind::LEGS: - return step.variant.empty() || step.variant == RUN_VARIANT; + return step.variant.empty() || step.variant == RUN_VARIANT || step.variant == BIKE_VARIANT; case StoryStepKind::STEP: return step.variant.empty() || step.variant == TRAINER_LINE_VARIANT || step.variant == TRAINER_WAIT_VARIANT; case StoryStepKind::GAME: diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp index 259f3beb3b..a992e5e73d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp @@ -393,6 +393,7 @@ std::string save_card_place(const std::string& map){ {"VermilionCity", "vermilion-city"}, {"Route10", "route-10"}, {"LavenderTown", "lavender-town"}, + {"CeladonCity", "celadon-city"}, }; for (const auto& [prefix, place] : PLACES){ if (map == prefix || map.rfind(prefix + "_", 0) == 0){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFaultSweep_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFaultSweep_Tests.cpp index bd2a0112bb..61a208d570 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFaultSweep_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFaultSweep_Tests.cpp @@ -98,7 +98,7 @@ const std::map KNOWN_FAILING{ // on every tile (WILD_FLOOR_MAPS). const std::map STORY_ENCOUNTER_RATES{ {"Route1", 21}, {"Route2", 21}, {"Route3", 21}, {"Route4", 21}, {"Route5", 21}, {"Route6", 21}, - {"Route9", 21}, {"Route10", 21}, {"Route11", 21}, {"Route22", 21}, {"Route24", 21}, {"Route25", 21}, + {"Route7", 21}, {"Route8", 21}, {"Route9", 21}, {"Route10", 21}, {"Route11", 21}, {"Route22", 21}, {"Route24", 21}, {"Route25", 21}, {"ViridianForest", 14}, {"MtMoon_1F", 7}, {"MtMoon_B1F", 5}, {"MtMoon_B2F", 7}, {"RockTunnel_1F", 7}, {"RockTunnel_B1F", 7}, }; @@ -161,6 +161,19 @@ struct RowRun{ // Where a stubbed step leaves the player instead of its pose_after: by step index. using Landings = std::map; +// Whether `map` is one of sweep_maps(). +bool map_loaded(const std::string& map){ + const std::vector& maps = sweep_maps(); + return std::any_of(maps.begin(), maps.end(), [&](const MapData& data){ return data.name == map; }); +} + +// Whether a step of `row` after its step `c` walks. +bool walks_after(const StoryCheckpoint& row, size_t c){ + return std::any_of(row.steps.begin() + c + 1, row.steps.end(), [](const StoryStep& next){ + return next.kind == StoryStepKind::ROUTE || next.kind == StoryStepKind::LEGS || next.kind == StoryStepKind::STEP; + }); +} + // Whether `failure` is a script the sim does not have: a scene's trigger // stepped onto (no trainer sees the player there) or a leader's battle // after a talk (no trainer's message up). @@ -180,6 +193,7 @@ RowRun run_row( const std::optional>& scripted = std::nullopt ){ RowRun out; + std::optional off_maps; StoryPose at = *story_pose(*row.start); auto sim = std::make_unique(sweep_maps(), MapPosition{at.map, at.tile}, at.facing.value_or(Direction::SOUTH), script); const auto add = [&out](const SimState& state){ @@ -204,6 +218,12 @@ RowRun run_row( at = *story_pose(*step.pose_after); const auto landing = landings.find(c); const MapPosition land{at.map, landing == landings.end() ? at.tile : landing->second}; + if (!map_loaded(land.map)){ + // A room with no map (Celadon's Pokemon Center) must end the row. + off_maps = land; + out.failure = walks_after(row, c) ? std::format("{}/{}: ends on {}, which has no map, with a walk after it.", row.id, step.name, land.map) : ""; + break; + } if (!(sim->state().position == land)){ add(sim->state()); sim = std::make_unique(sweep_maps(), land, at.facing.value_or(sim->state().facing), script); @@ -212,7 +232,7 @@ RowRun run_row( if (out.failure.empty() && !sim->state().violations.empty()){ out.failure = "a violation on the way: " + sim->state().violations.back(); } - out.end = sim->state().position; + out.end = off_maps.value_or(sim->state().position); add(sim->state()); return out; } @@ -282,9 +302,7 @@ UnitTestResult sweep_scene_end_pose(Logger& logger, const StoryCheckpoint& row, // ends where our walk did: not the game's end pose. const auto from = clean.before.find(c); // With no walk after it, nothing in the row goes on from the pose. - const bool walk_after = std::any_of(row.steps.begin() + c + 1, row.steps.end(), [](const StoryStep& next){ - return next.kind == StoryStepKind::ROUTE || next.kind == StoryStepKind::LEGS || next.kind == StoryStepKind::STEP; - }); + const bool walk_after = walks_after(row, c); if (sim_plays(step) || !walk_after || !after || (from != clean.before.end() && from->second == MapPosition{after->map, after->tile})){ continue; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryLavender_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryLavender_Tests.cpp index 28a8a12bb1..e8ca30c53c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryLavender_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryLavender_Tests.cpp @@ -4,7 +4,8 @@ * * Rows from 00.28 on (PokemonFRLG_AutoStoryTableLavender.cpp) on the * overworld sim: 00.28 from Route 10's Pokemon Center through Rock Tunnel - * to its south mouth, and 00.29 down Route 10 to Lavender Town's Center. + * to its south mouth, 00.29 down Route 10 to Lavender Town's Center, and + * 00.30 west over Route 8 and under Saffron to Celadon City. * Each row whole, each open leg a tile short or long, and a wild battle * on every open leg in the tunnel. * @@ -572,6 +573,49 @@ UnitTestResult test_save_places(Logger&){ return true; } +// 00.30 on the sim: the Mart, Route 8 with only Rich fought (the walker +// keeps out of the other trainers' lines), the Underground Path, the +// Pidgey's grass and Celadon City, below its Center's door. The Center +// has no map: its way in is stubbed. +UnitTestResult test_celadon_way_row(Logger&){ + return row_on_sim("00.30", SimScript{}, {{"old_man_1"}}, MapPosition{"CeladonCity", {48, 12}}); +} + +// 00.30's walk into Route 7's grass with a wild battle on each of its +// two grass steps in turn (Route 7's land rate is 21): it still ends on +// the Pidgey's tile. +UnitTestResult test_route7_grass_battles(Logger&){ + const StoryCheckpoint& row = checkpoint("00.30"); + const size_t index = step_index(row, "pidgey_grass"); + const MapPosition end{"Route7", {14, 5}}; + for (int grass_step = 1; grass_step <= 2; grass_step++){ + SimScript script; + script.encounters.push_back(SimEncounter{.grass_step = grass_step}); + StoryPose at{"Route7", {7, 15}, Direction::SOUTH}; + OverworldSim sim(maps_after_route10(), MapPosition{at.map, at.tile}, *at.facing, script); + const std::string failure = row_step_on_sim(sim, at, row.steps[index], row.id); + if (!failure.empty()){ + return std::format("A battle on grass step {}: {}", grass_step, failure); + } + if (!(sim.state().position == end)){ + return std::format("A battle on grass step {}: ended on {}, not {}.", grass_step, to_string(sim.state().position), to_string(end)); + } + if (sim.state().wild_battles != 1){ + return std::format("A battle on grass step {}: {} wild battles, not 1.", grass_step, sim.state().wild_battles); + } + } + return true; +} + +// 00.30 saves in Celadon City's Pokemon Center: CELADON CITY on the save +// card. +UnitTestResult test_celadon_save_place(Logger&){ + if (save_card_place("CeladonCity_PokemonCenter_1F") != "celadon-city"){ + return "Wrong save card place for Celadon City's Pokemon Center."; + } + return true; +} + } // namespace @@ -590,6 +634,9 @@ const AutoStoryLavenderCase AUTO_STORY_LAVENDER_CASES[] = { {.input = {"00.29 Route 10 south, the ledge, Lavender Town's Pokemon Center", test_lavender_row}, .expectation = {}}, {.input = {"00.29 a tile off after each open leg", test_lavender_tile_off}, .expectation = {}}, {.input = {"00.28 and 00.29 save on Route 10 and in Lavender Town", test_save_places}, .expectation = {}}, + {.input = {"00.30 Lavender's Mart, Route 8 past Rich alone, the Underground Path, Route 7 to Celadon City", test_celadon_way_row}, .expectation = {}}, + {.input = {"00.30 a wild battle on each grass step into Route 7's grass", test_route7_grass_battles}, .expectation = {}}, + {.input = {"00.30 saves in Celadon City's Pokemon Center", test_celadon_save_place}, .expectation = {}}, }; void add_tests_AutoStoryLavender(UnitTestDatabase& database){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp index 089904ef6a..273822e571 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp @@ -15,6 +15,7 @@ #include "Pokemon/AutoStory/Pokemon_TilePose.h" #include "PokemonFRLG_AutoStoryActions.h" #include "PokemonFRLG_AutoStoryBattleDecisions.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" #include "PokemonFRLG_AutoStoryHideout.h" #include "PokemonFRLG_AutoStoryRooms.h" @@ -545,9 +546,26 @@ void sim_walk_room_legs(OverworldSim& sim, const std::vector& legs){ } // namespace +namespace{ + +// Whether the sim can walk a Pokemon Center's way in or out of `step`: +// the map it ends on is built into the program. Celadon's Center has no +// map yet (00.30), so its way in is stubbed. +bool center_walkable(const StoryStep& step){ + if (!is_center_variant(step.variant)){ + return false; + } + const std::optional after = step.pose_after ? story_pose(*step.pose_after) : std::nullopt; + const std::vector>& maps = overworld_map_json(); + return after && std::any_of(maps.begin(), maps.end(), [&](const auto& map){ return map.first == after->map; }); +} + +} // namespace + // A scripted GAME step on the sim: the poster's switch, the lift's ride // (its room coded, SimScript::lift_floors, rows as on the panel), or a -// Pokemon Center's way in or out; any other GAME step is stubbed. +// Pokemon Center's way in or out (center_walkable()); any other GAME step +// is stubbed. Pokemon::StoryAction OverworldStepSim::hideout_or_stub(){ Pokemon::StoryAction stub = m_stubs.stub(); Pokemon::StoryAction sim = played([this](RunState&, const StoryStep& step){ @@ -555,7 +573,7 @@ Pokemon::StoryAction OverworldStepSim::hideout_or_stub(){ return failure.empty() ? Pokemon::outcome_ok() : Pokemon::outcome_failed("sim_failure", failure); }); sim.act = [stub, played_act = sim.act](RunState& state, const StoryStep& step){ - return is_hideout_variant(step.variant) || is_center_variant(step.variant) + return is_hideout_variant(step.variant) || center_walkable(step) ? played_act(state, step) : stub.act(state, step); }; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json index 282d79e0f5..13fc534f8b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json @@ -1,7 +1,7 @@ { - "about": "story_sim_trace() of the FRLG story table (Data/AuthoredStory_Tests.h), taken from the C++ rows before they moved to StoryData.", + "about": "story_sim_trace() of the FRLG story table (Data/AuthoredStory_Tests.h), taken from the C++ rows before they moved to StoryData; from 00.30 on, from their plans.", "trace": [ - "[Segment] 00: start, 33 checkpoints", + "[Segment] 00: start, 34 checkpoints", "begin 00.01", "[Checkpoint] 00.01: start, 2 events", "[Event] 00.01/1 intro: start", @@ -1200,9 +1200,116 @@ "save at LavenderTown_PokemonCenter_1F (7, 4) facing north (tile)", "[Checkpoint] 00.29: done, 7 run, 0 skipped", "[Checkpoint] 00.29: passed on attempt 1 (one-shot)", - "[Segment] 00: 33 of 33 checkpoints one-shot", - "[Segment] 00: done, 33 checkpoints", + "begin 00.30", + "[Checkpoint] 00.30: start, 23 events", + "[Event] 00.30/1 from_save: start", + "[Event] 00.30/1 from_save: done", + "[Event] 00.30/2 east_of_mart_door: start", + "00.30/east_of_mart_door: ok at LavenderTown (21, 16) facing east (tile)", + "[Event] 00.30/2 east_of_mart_door: done", + "[Event] 00.30/3 into_lavender_mart: start", + "00.30/into_lavender_mart: ok at LavenderTown_Mart (4, 2) facing north (tile)", + "[Event] 00.30/3 into_lavender_mart: done", + "[Event] 00.30/4 lavender_mart_clerk: start", + "00.30/lavender_mart_clerk: ok at LavenderTown_Mart (4, 3) facing west (tile)", + "[Event] 00.30/4 lavender_mart_clerk: done", + "[Event] 00.30/5 buy_ropes_and_super_repels: start", + "00.30/buy_ropes_and_super_repels: ok at LavenderTown_Mart (4, 3) facing west (tile)", + "[Event] 00.30/5 buy_ropes_and_super_repels: done", + "[Event] 00.30/6 out_of_lavender_mart: start", + "00.30/out_of_lavender_mart: ok at LavenderTown (20, 16) facing south (tile)", + "[Event] 00.30/6 out_of_lavender_mart: done", + "[Event] 00.30/7 ride_bike: start", + "00.30/ride_bike: ok at LavenderTown (20, 16) facing south (tile)", + "[Event] 00.30/7 ride_bike: done", + "[Event] 00.30/8 lavender_west_edge: start", + "00.30/lavender_west_edge: ok at LavenderTown (1, 10) facing south (tile)", + "[Event] 00.30/8 lavender_west_edge: done", + "[Event] 00.30/9 route8_east_wall: start", + "00.30/route8_east_wall: ok at Route8 (62, 10) facing west (tile)", + "[Event] 00.30/9 route8_east_wall: done", + "[Event] 00.30/10 east_of_rich_line: start", + "00.30/east_of_rich_line: ok at Route8 (57, 18) facing west (tile)", + "[Event] 00.30/10 east_of_rich_line: done", + "[Event] 00.30/11 rich_line: start", + "00.30/rich_line: ok at Route8 (55, 18) facing west (tile)", + "[Event] 00.30/11 rich_line: done", + "[Event] 00.30/12 gamer_rich: start", + "00.30/gamer_rich: ok at -", + "[Event] 00.30/12 gamer_rich: done", + "[Event] 00.30/13 west_of_glenn_column: start", + "00.30/west_of_glenn_column: ok at Route8 (48, 7) facing north (tile)", + "[Event] 00.30/13 west_of_glenn_column: done", + "[Event] 00.30/14 east_of_megan_column: start", + "00.30/east_of_megan_column: ok at Route8 (30, 13) facing south (tile)", + "[Event] 00.30/14 east_of_megan_column: done", + "[Event] 00.30/15 east_of_aidan_column: start", + "00.30/east_of_aidan_column: ok at Route8 (14, 13) facing north (tile)", + "[Event] 00.30/15 east_of_aidan_column: done", + "[Event] 00.30/16 east_of_underground_door: start", + "00.30/east_of_underground_door: ok at Route8 (14, 5) facing north (tile)", + "[Event] 00.30/16 east_of_underground_door: done", + "[Event] 00.30/17 below_underground_door: start", + "00.30/below_underground_door: ok at Route8 (13, 5) facing north (tile)", + "[Event] 00.30/17 below_underground_door: done", + "[Event] 00.30/18 route7_underground_exit: start", + "00.30/route7_underground_exit: ok at Route7 (7, 15) facing south (tile)", + "[Event] 00.30/18 route7_underground_exit: done", + "[Event] 00.30/19 pidgey_grass: start", + "00.30/pidgey_grass: ok at Route7 (14, 5) facing east (tile)", + "[Event] 00.30/19 pidgey_grass: done", + "[Event] 00.30/20 catch_pidgey: start", + "00.30/catch_pidgey: ok at Route7 (14, 5) (15, 5) (set)", + "[Event] 00.30/20 catch_pidgey: done", + "[Event] 00.30/21 celadon_below_center: start", + "00.30/celadon_below_center: ok at CeladonCity (48, 12) facing north (tile)", + "[Event] 00.30/21 celadon_below_center: done", + "[Event] 00.30/22 celadon_nurse: start", + "00.30/celadon_nurse: ok at CeladonCity (48, 12) facing north (tile)", + "[Event] 00.30/22 celadon_nurse: done", + "[Event] 00.30/23 heal_celadon: start", + "00.30/heal_celadon: ok at -", + "[Event] 00.30/23 heal_celadon: done", + "save at CeladonCity_PokemonCenter_1F (7, 4) facing north (tile)", + "[Checkpoint] 00.30: done, 23 run, 0 skipped", + "[Checkpoint] 00.30: passed on attempt 1 (one-shot)", + "[Segment] 00: 34 of 34 checkpoints one-shot", + "[Segment] 00: done, 34 checkpoints", "completed", - "00.01: 2 stubbed: intro, options\n00.02: 0 stubbed\n00.03: 1 stubbed: to_the_lab\n00.04: 5 stubbed: squirtle_ball, choose_squirtle, no_nickname, rival_takes_bulbasaur, rival_battle\n00.05: 1 stubbed: parcel_from_clerk\n00.06: 2 stubbed: give_parcel, pokedex_scene\n00.07: 2 stubbed: heal_viridian, old_man_tutorial\n00.08: 1 stubbed: heal_pewter\n00.09: 3 stubbed: camper_liam, challenge_brock, brock\n00.10: 1 stubbed: shoes_from_aide\n00.11: 4 stubbed: heal_pewter, bug_catcher_colton, bug_catcher_greg, bug_catcher_james\n00.12: 1 stubbed: heal_route4\n00.12c: 1 stubbed: magikarp_to_box\n00.13: 6 stubbed: heal_route4, moon_stone_ball, found_moon_stone, rocket_grunt, super_nerd_miguel, helix_fossil\n00.14: 2 stubbed: teach_mega_kick, heal_cerulean\n00.14b: 3 stubbed: buy_potions, grind_to_lv23, heal_cerulean\n00.14c: 4 stubbed: challenge_misty, misty, heal_cerulean, teach_water_pulse\n00.15: 4 stubbed: teach_water_pulse, buy_potions, grind_for_rival, heal_cerulean\n00.15b: 2 stubbed: teach_water_pulse, rival_cerulean\n00.16: 7 stubbed: heal_cerulean, bug_catcher_cale, lass_ali, youngster_timmy, lass_reli, camper_ethan, rocket_grunt\n00.17: 4 stubbed: hiker_franklin, picnicker_kelsey, hiker_nob, lass_haley\n00.18: 7 stubbed: bill_clefairy, help_bill, bill_into_teleporter, cell_separator, bill_comes_out, thank_bill, ss_ticket\n00.19: 1 stubbed: heal_cerulean\n00.20: 3 stubbed: buy_poke_balls, catch_oddish, heal_cerulean\n00.21: 3 stubbed: rocket_grunt, camper_jeff, heal_vermilion\n00.22: 6 stubbed: show_ss_ticket, rival_ss_anne, captain, hm01_cut, ss_anne_sails, heal_vermilion\n00.23: 9 stubbed: teach_cut, teach_dig, cut_gym_tree, sailor_dwayne, engineer_baily, gentleman_tucker, trash_cans, cut_tree_out, heal_vermilion\n00.24: 4 stubbed: surge_winnable, grind_for_surge, heal_vermilion, cut_gym_tree\n00.25: 4 stubbed: challenge_lt_surge, lt_surge, cut_tree_out, heal_vermilion\n00.26: 8 stubbed: chairman, hear_story, bike_voucher, cut_cerulean_tree, talk_bike_clerk, bicycle, buy_repels, heal_cerulean\n00.27: 6 stubbed: cut_cerulean_tree, cut_route9_tree, picnicker_alicia, hiker_jeremy, bug_catcher_conner, heal_route10\n00.28: 9 stubbed: cut_route10_tree, pokemaniac_ashton, pokemaniac_winston, picnicker_martha, hiker_allen, hiker_lenny, picnicker_sofia, hiker_dudley, picnicker_dana\n00.29: 1 stubbed: heal_lavender\n" + "00.01: 2 stubbed: intro, options", + "00.02: 0 stubbed", + "00.03: 1 stubbed: to_the_lab", + "00.04: 5 stubbed: squirtle_ball, choose_squirtle, no_nickname, rival_takes_bulbasaur, rival_battle", + "00.05: 1 stubbed: parcel_from_clerk", + "00.06: 2 stubbed: give_parcel, pokedex_scene", + "00.07: 2 stubbed: heal_viridian, old_man_tutorial", + "00.08: 1 stubbed: heal_pewter", + "00.09: 3 stubbed: camper_liam, challenge_brock, brock", + "00.10: 1 stubbed: shoes_from_aide", + "00.11: 4 stubbed: heal_pewter, bug_catcher_colton, bug_catcher_greg, bug_catcher_james", + "00.12: 1 stubbed: heal_route4", + "00.12c: 1 stubbed: magikarp_to_box", + "00.13: 6 stubbed: heal_route4, moon_stone_ball, found_moon_stone, rocket_grunt, super_nerd_miguel, helix_fossil", + "00.14: 2 stubbed: teach_mega_kick, heal_cerulean", + "00.14b: 3 stubbed: buy_potions, grind_to_lv23, heal_cerulean", + "00.14c: 4 stubbed: challenge_misty, misty, heal_cerulean, teach_water_pulse", + "00.15: 4 stubbed: teach_water_pulse, buy_potions, grind_for_rival, heal_cerulean", + "00.15b: 2 stubbed: teach_water_pulse, rival_cerulean", + "00.16: 7 stubbed: heal_cerulean, bug_catcher_cale, lass_ali, youngster_timmy, lass_reli, camper_ethan, rocket_grunt", + "00.17: 4 stubbed: hiker_franklin, picnicker_kelsey, hiker_nob, lass_haley", + "00.18: 7 stubbed: bill_clefairy, help_bill, bill_into_teleporter, cell_separator, bill_comes_out, thank_bill, ss_ticket", + "00.19: 1 stubbed: heal_cerulean", + "00.20: 3 stubbed: buy_poke_balls, catch_oddish, heal_cerulean", + "00.21: 3 stubbed: rocket_grunt, camper_jeff, heal_vermilion", + "00.22: 6 stubbed: show_ss_ticket, rival_ss_anne, captain, hm01_cut, ss_anne_sails, heal_vermilion", + "00.23: 9 stubbed: teach_cut, teach_dig, cut_gym_tree, sailor_dwayne, engineer_baily, gentleman_tucker, trash_cans, cut_tree_out, heal_vermilion", + "00.24: 4 stubbed: surge_winnable, grind_for_surge, heal_vermilion, cut_gym_tree", + "00.25: 4 stubbed: challenge_lt_surge, lt_surge, cut_tree_out, heal_vermilion", + "00.26: 8 stubbed: chairman, hear_story, bike_voucher, cut_cerulean_tree, talk_bike_clerk, bicycle, buy_repels, heal_cerulean", + "00.27: 6 stubbed: cut_cerulean_tree, cut_route9_tree, picnicker_alicia, hiker_jeremy, bug_catcher_conner, heal_route10", + "00.28: 9 stubbed: cut_route10_tree, pokemaniac_ashton, pokemaniac_winston, picnicker_martha, hiker_allen, hiker_lenny, picnicker_sofia, hiker_dudley, picnicker_dana", + "00.29: 1 stubbed: heal_lavender", + "00.30: 6 stubbed: buy_ropes_and_super_repels, ride_bike, gamer_rich, catch_pidgey, celadon_nurse, heal_celadon", + "" ] } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/README.md b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/README.md index ae63e57e6c..6802726fb5 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/README.md +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/README.md @@ -4,7 +4,7 @@ Every story checkpoint is an authored plan here, one file per checkpoint: `frlg- - A checkpoint has `sections: [{id, title, notes?, events}]`, or plain `events` (one unnamed section, as 00.05 keeps so its bytes stay those PokemonSimulation pins). Events are numbered across the sections, and an event's log line names its section: `[Event] 00.28/12 [b1f_east] winston_line: start`. - `notes` (a list of paragraphs) on the checkpoint, a section or an event hold what the C++ table's comments said: the maps, trainers, numbers and console runs a step was planned from. No executor reads them. -- `variant` says how FRLG does a step: `run` on a route or legs, `trainer_line`/`trainer_wait` on a step, and on a `game_action` the step's plan as its parser reads it (`mart ...`, `teach ...`, `grind ...`, `grind_check ...`, `capture ...`, `pc ...`, `fossil ...`, `level ...`, or `cut_tree`, `trash_cans`, `close_trainer_card`, `leave_gift_summary`). An unknown variant is rejected at load. +- `variant` says how FRLG does a step: `run` or `bike` on a route or legs, `trainer_line`/`trainer_wait` on a step, and on a `game_action` the step's plan as its parser reads it (`mart ...`, `teach ...`, `grind ...`, `grind_check ...`, `capture ...`, `pc ...`, `fossil ...`, `level ...`, or `cut_tree`, `trash_cans`, `close_trainer_card`, `leave_gift_summary`, `center_enter`, `center_exit`, `ride_bike`, `use_item ...` and the Rocket Hideout's steps from `PokemonFRLG_AutoStoryHideout.h`). An unknown variant is rejected at load. - `once_per_save` marks a step done once per save (a TEACH, a Mart). - `execution` holds FRLG's fields: `menu`, and when not a new game's, `supplies` (`potions`, `antidotes`, `parlyz_heals`, `lead: "after_brock"`), `live_start` and `handoff` (`sid_helper`, `starter_rng`, `gift_rng`). - Unknown fields anywhere are rejected. diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json new file mode 100644 index 0000000000..c5e5f335e2 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json @@ -0,0 +1,207 @@ +{ + "format": "pokesim-authored-checkpoint/1", + "game": "frlg", + "checkpoint": { + "id": "00.30", + "title": "Lavender Mart, then west over Route 8 on the Bicycle, the Underground Path, a Pidgey on Route 7, Celadon's Pokemon Center", + "notes": [ + "PokeGuy's way from Lavender Town to Celadon City (pokeguy 54:18 on): the Lavender Mart, Route 8 west on the BICYCLE (00.26), Gamer Rich, the Underground Path's east-west tunnel to Route 7, a Pidgey from Route 7's grass to carry HM02 Fly later, and Celadon's Pokemon Center.", + "No Repel: the way keeps off grass. Route 8's grass (34..46, 9..18) is penned in by the Cut trees (47, 12) and (33, 15); Route 7's (11..18, 2..6) is east of the way north from the Underground Path's door (7, 14). The Underground Path has no wild Pokemon (pokefirered wild_encounters.json lists none), and the Pidgey is caught where Route 7's grass is walked on purpose.", + "Route 8's trainers (Route8 map.json): Julia (62, 14) looks west 6, Rich (55, 15) south 3, Glenn (51, 6) all ways 4, Paige (29, 3) and Andrea (29, 5) west 3, Leslie (29, 4) and Megan (29, 6) east 6, Stan (21, 3) all ways 1, Eli and Anne (40, 3), (41, 3) south 1, Ricardo (14, 16) and Jaren (24, 17) all ways 1, Aidan (9, 6) east 4. Below the wall (56..61, 6..13) the only ways west are row 14, Julia's line, and rows 15..18, which cross Rich's column: his line (55, 16..18) is the one taken." + ], + "start": {"certainty": "tile", "map": "LavenderTown_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, + "sections": [ + { + "id": "lavender_mart", + "title": "From Lavender's Center to the Mart: Escape Ropes and Super Repels", + "events": [ + { + "id": "east_of_mart_door", + "kind": "route", + "variant": "run", + "target": {"certainty": "tile", "map": "LavenderTown", "where": {"facing": "east", "kind": "tiles", "tiles": [[21, 16]]}}, + "why": "out of the Center's door (6, 5), east and down to the wall (22, 16), east of the Mart's door (20, 15)" + }, + { + "id": "into_lavender_mart", + "kind": "route", + "variant": "run", + "target": {"certainty": "tile", "map": "LavenderTown_Mart", "where": {"facing": "north", "kind": "tiles", "tiles": [[4, 2]]}}, + "why": "a tile west, in at the Mart's door (20, 15) and up column 4 to the wall (4, 1)" + }, + { + "id": "lavender_mart_clerk", + "kind": "route", + "variant": "run", + "target": {"certainty": "tile", "map": "LavenderTown_Mart", "where": {"facing": "west", "kind": "tiles", "tiles": [[4, 3]]}}, + "why": "a tile back down, facing the clerk (2, 3) over the counter (3, 3)" + }, + { + "id": "buy_ropes_and_super_repels", + "kind": "game_action", + "variant": "mart LavenderTown_Mart budget=5700 floor=9000 escape-rope=4 super-repel=7", + "once_per_save": true, + "why": "pokeguy 54:18: 4 Escape Ropes (the Rocket Hideout, 00.31) and 7 Super Repels (Pokemon Tower)" + }, + { + "id": "out_of_lavender_mart", + "kind": "route", + "variant": "run", + "target": {"certainty": "tile", "map": "LavenderTown", "where": {"facing": "south", "kind": "tiles", "tiles": [[20, 16]]}}, + "why": "down to the mat and out of the door (20, 15)" + } + ] + }, + { + "id": "route8_rich", + "title": "On the Bicycle onto Route 8, Gamer Rich", + "events": [ + { + "id": "ride_bike", + "kind": "game_action", + "variant": "ride_bike", + "why": "on the BICYCLE (00.26, Key Items) for Route 8" + }, + { + "id": "lavender_west_edge", + "kind": "legs", + "variant": "bike", + "legs": [ + {"direction": "west", "tiles": 7}, {"direction": "north", "tiles": 9}, {"direction": "south", "tiles": 1}, {"direction": "west", "tiles": 12}, {"direction": "south", "tiles": 2} + ], + "why": "west along row 16 to the wall (12, 16), up column 13 to the wall (13, 6), back onto row 8, west along it to the wall (0, 8) and down to (1, 10), mid-way along the way out (rows 9..11)" + }, + { + "id": "route8_east_wall", + "kind": "route", + "variant": "bike", + "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "west", "kind": "tiles", "tiles": [[62, 10]]}}, + "why": "west over the connection onto Route 8, to the wall (56..61, 6..13): a row off still ends on this wall" + }, + { + "id": "east_of_rich_line", + "kind": "legs", + "variant": "bike", + "legs": [ + {"direction": "south", "tiles": 3}, {"direction": "east", "tiles": 1}, {"direction": "south", "tiles": 5}, {"direction": "west", "tiles": 6} + ], + "why": "down column 62 to Julia (62, 14), who looks west, a tile east and down column 63 to the wall (63, 19), west along row 18 to (57, 18), two tiles short of Rich's line" + }, + { + "id": "rich_line", + "kind": "step", + "variant": "trainer_line", + "direction": "west", + "why": "a tile at a time over (56, 18) onto (55, 18), the end of Rich's line (55, 16..18): he walks down to (55, 17)" + }, + { + "id": "gamer_rich", + "kind": "battle", + "declared_after": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[55, 18]]}}, + "why": "Gamer Rich (TRAINER_GAMER_RICH): Growlithe, Vulpix Lv24" + } + ] + }, + { + "id": "route8_west", + "title": "Route 8 west to the Underground Path", + "events": [ + { + "id": "west_of_glenn_column", + "kind": "route", + "variant": "bike", + "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[48, 7]]}}, + "why": "west along row 18 past Rich (55, 17) and up column 48 to row 7, west of Glenn's south line (51, 7..10) and below his west line (47..50, 6)" + }, + { + "id": "east_of_megan_column", + "kind": "route", + "variant": "bike", + "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "south", "kind": "tiles", "tiles": [[30, 13]]}}, + "why": "west along row 7, below Megan's line (30..35, 6), and down column 30 to (30, 13), west of the pen's wall (33, 8..18)" + }, + { + "id": "east_of_aidan_column", + "kind": "route", + "variant": "bike", + "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[14, 13]]}}, + "why": "down onto row 14, west along it above Jaren (24, 17) and Ricardo (14, 16), who see a tile, and up onto (14, 13)" + }, + { + "id": "east_of_underground_door", + "kind": "route", + "variant": "bike", + "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[14, 5]]}}, + "why": "up column 14, east of Aidan's line (10..13, 6), to the wall (14, 4)" + }, + { + "id": "below_underground_door", + "kind": "route", + "variant": "bike", + "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[13, 5]]}}, + "why": "a tile west, below the Underground Path's door (13, 4)" + } + ] + }, + { + "id": "underground_path", + "title": "The Underground Path to Route 7", + "events": [ + { + "id": "route7_underground_exit", + "kind": "route", + "variant": "run", + "target": {"certainty": "tile", "map": "Route7", "where": {"facing": "south", "kind": "tiles", "tiles": [[7, 15]]}}, + "why": "through the door (the game takes the player off the Bicycle indoors), the east entrance, the east-west tunnel and the west entrance, out onto Route 7" + } + ] + }, + { + "id": "route7_pidgey", + "title": "A Pidgey in Route 7's grass", + "events": [ + { + "id": "pidgey_grass", + "kind": "route", + "variant": "run", + "target": {"certainty": "tile", "map": "Route7", "where": {"facing": "east", "kind": "tiles", "tiles": [[14, 5]]}}, + "why": "north round the ledges into Route 7's grass (11..18, 2..6), a grass tile east of it free" + }, + { + "id": "catch_pidgey", + "kind": "game_action", + "variant": "capture pidgey catches=1 encounters=30 balls=5 pace=east", + "once_per_save": true, + "why": "a Pidgey to carry HM02 Fly (pokeguy flies with a Pidgey from 1:03 on); the party has no other flier" + } + ] + }, + { + "id": "celadon", + "title": "Celadon City's Pokemon Center", + "events": [ + { + "id": "celadon_below_center", + "kind": "route", + "variant": "run", + "target": {"certainty": "tile", "map": "CeladonCity", "where": {"facing": "north", "kind": "tiles", "tiles": [[48, 12]]}}, + "why": "out of the grass, west onto Celadon (rows 12..15) and along row 12 to below the Center's door (48, 11)" + }, + { + "id": "celadon_nurse", + "kind": "game_action", + "variant": "center_enter", + "declared_after": {"certainty": "tile", "map": "CeladonCity_PokemonCenter_1F", "where": {"facing": "north", "kind": "tiles", "tiles": [[7, 4]]}}, + "why": "the Center script in: through the door and up to the nurse; no map of the Center" + }, + { + "id": "heal_celadon", + "kind": "heal", + "why": "the nurse heals the party" + } + ] + } + ] + }, + "execution": {"menu": "full", "supplies": {"lead": "after_brock"}} +} From ddb1e7217ac177795ac0dfd68ba1548a88a7448b Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 19:44:50 -0800 Subject: [PATCH 12/28] FRLG AutoStory 00.30: the story-wide tests count 00.30 The run plans, the checkpoint count, Rich among the trainers fought, a bike leg after the BICYCLE, and the Pidgey's route ending on the first grass tile; the sim trace golden regenerated with the stub report whole. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../PokemonFRLG_AutoStoryLavender_Tests.cpp | 33 ++++++--------- .../PokemonFRLG_AutoStoryRunPlan_Tests.cpp | 8 ++-- .../AutoStory/PokemonFRLG_AutoStory_Tests.cpp | 17 ++++---- .../ReplayData/frlg-story-sim-trace.json | 40 ++----------------- .../AutoStory/StoryData/frlg-00.30.plan.json | 4 +- 5 files changed, 32 insertions(+), 70 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryLavender_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryLavender_Tests.cpp index e8ca30c53c..99cd1fb3af 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryLavender_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryLavender_Tests.cpp @@ -581,28 +581,21 @@ UnitTestResult test_celadon_way_row(Logger&){ return row_on_sim("00.30", SimScript{}, {{"old_man_1"}}, MapPosition{"CeladonCity", {48, 12}}); } -// 00.30's walk into Route 7's grass with a wild battle on each of its -// two grass steps in turn (Route 7's land rate is 21): it still ends on -// the Pidgey's tile. +// 00.30's walk into Route 7's grass with a wild battle on its one grass +// step (Route 7's land rate is 21): it still ends on the Pidgey's tile. UnitTestResult test_route7_grass_battles(Logger&){ const StoryCheckpoint& row = checkpoint("00.30"); const size_t index = step_index(row, "pidgey_grass"); - const MapPosition end{"Route7", {14, 5}}; - for (int grass_step = 1; grass_step <= 2; grass_step++){ - SimScript script; - script.encounters.push_back(SimEncounter{.grass_step = grass_step}); - StoryPose at{"Route7", {7, 15}, Direction::SOUTH}; - OverworldSim sim(maps_after_route10(), MapPosition{at.map, at.tile}, *at.facing, script); - const std::string failure = row_step_on_sim(sim, at, row.steps[index], row.id); - if (!failure.empty()){ - return std::format("A battle on grass step {}: {}", grass_step, failure); - } - if (!(sim.state().position == end)){ - return std::format("A battle on grass step {}: ended on {}, not {}.", grass_step, to_string(sim.state().position), to_string(end)); - } - if (sim.state().wild_battles != 1){ - return std::format("A battle on grass step {}: {} wild battles, not 1.", grass_step, sim.state().wild_battles); - } + const MapPosition end{"Route7", {14, 6}}; + SimScript script; + script.encounters.push_back(SimEncounter{.grass_step = 1}); + StoryPose at{"Route7", {7, 15}, Direction::SOUTH}; + OverworldSim sim(maps_after_route10(), MapPosition{at.map, at.tile}, *at.facing, script); + if (const std::string failure = row_step_on_sim(sim, at, row.steps[index], row.id); !failure.empty()){ + return failure; + } + if (!(sim.state().position == end) || sim.state().wild_battles != 1){ + return std::format("Ended on {} after {} wild battles, not on {} after 1.", to_string(sim.state().position), sim.state().wild_battles, to_string(end)); } return true; } @@ -635,7 +628,7 @@ const AutoStoryLavenderCase AUTO_STORY_LAVENDER_CASES[] = { {.input = {"00.29 a tile off after each open leg", test_lavender_tile_off}, .expectation = {}}, {.input = {"00.28 and 00.29 save on Route 10 and in Lavender Town", test_save_places}, .expectation = {}}, {.input = {"00.30 Lavender's Mart, Route 8 past Rich alone, the Underground Path, Route 7 to Celadon City", test_celadon_way_row}, .expectation = {}}, - {.input = {"00.30 a wild battle on each grass step into Route 7's grass", test_route7_grass_battles}, .expectation = {}}, + {.input = {"00.30 a wild battle on the step into Route 7's grass", test_route7_grass_battles}, .expectation = {}}, {.input = {"00.30 saves in Celadon City's Pokemon Center", test_celadon_save_place}, .expectation = {}}, }; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp index e595c875e0..778035ecdf 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp @@ -138,7 +138,7 @@ class Test_RunPlan : public UnitTest{ const RunPlanScenario m_scenario; }; -const std::string TODAY = "00.01 00.02 00.03 00.04 00.05 00.06 00.07 00.08 00.09 00.10 00.11 00.12 00.12c 00.13 00.14 00.14b 00.14c 00.15 00.15b 00.16 00.17 00.18 00.19 00.20 00.21 00.22 00.23 00.24 00.25 00.26 00.27 00.28 00.29"; +const std::string TODAY = "00.01 00.02 00.03 00.04 00.05 00.06 00.07 00.08 00.09 00.10 00.11 00.12 00.12c 00.13 00.14 00.14b 00.14c 00.15 00.15b 00.16 00.17 00.18 00.19 00.20 00.21 00.22 00.23 00.24 00.25 00.26 00.27 00.28 00.29 00.30"; const RunPlanScenario RUN_PLAN_CASES[] = { { @@ -229,11 +229,11 @@ const RunPlanScenario RUN_PLAN_CASES[] = { }, { .input = {.name = "Gift RNG off: 00.12b is passed over", .first = "00.11"}, - .expectation = {.expected_ids = "00.11 00.12 00.12c 00.13 00.14 00.14b 00.14c 00.15 00.15b 00.16 00.17 00.18 00.19 00.20 00.21 00.22 00.23 00.24 00.25 00.26 00.27 00.28 00.29"}, + .expectation = {.expected_ids = "00.11 00.12 00.12c 00.13 00.14 00.14b 00.14c 00.15 00.15b 00.16 00.17 00.18 00.19 00.20 00.21 00.22 00.23 00.24 00.25 00.26 00.27 00.28 00.29 00.30"}, }, { .input = {.name = "Gift RNG off: 00.12b runs when started at", .first = "00.12b"}, - .expectation = {.expected_ids = "00.12b 00.12c 00.13 00.14 00.14b 00.14c 00.15 00.15b 00.16 00.17 00.18 00.19 00.20 00.21 00.22 00.23 00.24 00.25 00.26 00.27 00.28 00.29"}, + .expectation = {.expected_ids = "00.12b 00.12c 00.13 00.14 00.14b 00.14c 00.15 00.15b 00.16 00.17 00.18 00.19 00.20 00.21 00.22 00.23 00.24 00.25 00.26 00.27 00.28 00.29 00.30"}, }, { .input = { .name = "Gift RNG: started at 00.12b, from the save beside the salesman", @@ -258,7 +258,7 @@ const RunPlanScenario RUN_PLAN_CASES[] = { }, { .input = {.name = "Starter RNG alone: 00.12b runs when started at", .first = "00.12b", .handoffs = {.starter_rng = true}}, - .expectation = {.expected_ids = "00.12b 00.12c 00.13 00.14 00.14b 00.14c 00.15 00.15b 00.16 00.17 00.18 00.19 00.20 00.21 00.22 00.23 00.24 00.25 00.26 00.27 00.28 00.29"}, + .expectation = {.expected_ids = "00.12b 00.12c 00.13 00.14 00.14b 00.14c 00.15 00.15b 00.16 00.17 00.18 00.19 00.20 00.21 00.22 00.23 00.24 00.25 00.26 00.27 00.28 00.29 00.30"}, }, { .input = { .name = "Starter RNG first, with both on", diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp index da54867cd9..e7c5039cae 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp @@ -230,8 +230,8 @@ UnitTestResult check_checkpoint(const StoryCheckpoint& checkpoint, StoryPose& po UnitTestResult test_routes_and_chain(){ const std::vector& checkpoints = story_checkpoints(); - if (checkpoints.size() != 36 || checkpoints[0].start){ - return "Expected 36 checkpoints, the first from a new game."; + if (checkpoints.size() != 37 || checkpoints[0].start){ + return "Expected 37 checkpoints, the first from a new game."; } StoryPose pose; for (size_t c = 0; c < checkpoints.size(); c++){ @@ -351,9 +351,11 @@ UnitTestResult test_pewter_aide_walk(){ return true; } -// Every ROUTE and LEGS after the Running Shoes (00.10) runs; none before. +// Every ROUTE and LEGS after the Running Shoes (00.10) runs, or after the +// BICYCLE (00.26) may ride; none before. UnitTestResult test_runs_after_shoes(){ bool has_shoes = false; + bool has_bike = false; size_t runs = 0; for (const StoryCheckpoint& checkpoint : story_checkpoints()){ for (const StoryStep& step : checkpoint.steps){ @@ -361,12 +363,13 @@ UnitTestResult test_runs_after_shoes(){ continue; } const MoveSpeed expected = has_shoes ? MoveSpeed::RUN : MoveSpeed::WALK; - if (move_speed(step) != expected){ + if (move_speed(step) != expected && !(has_bike && move_speed(step) == MoveSpeed::BIKE)){ return checkpoint.id + "/" + step.name + ": expected to " + move_speed_name(expected) + ", it would " + move_speed_name(move_speed(step)) + "."; } runs += has_shoes ? 1 : 0; } has_shoes = has_shoes || checkpoint.id == "00.10"; + has_bike = has_bike || checkpoint.id == "00.26"; } if (!has_shoes || runs == 0){ return "No checkpoint 00.10, where the player gets the Running Shoes, or no route after it."; @@ -600,7 +603,7 @@ UnitTestResult check_lines(const StoryCheckpoint& checkpoint, std::set Date: Mon, 9 Nov 2026 19:47:23 -0800 Subject: [PATCH 13/28] FRLG AutoStory 00.30: Gamer Rich in the story's trainers Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Programs/AutoStory/PokemonFRLG_AutoStoryBattleData.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData.cpp index 17281e2e83..a70c1da706 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData.cpp @@ -86,6 +86,7 @@ constexpr StoryTrainer STORY_TRAINERS[] = { {.map = "RockTunnel_B1F", .battle = "hiker_dudley", .trainer = "TRAINER_HIKER_DUDLEY"}, {.map = "RockTunnel_B1F", .battle = "pokemaniac_cooper", .trainer = "TRAINER_POKEMANIAC_COOPER"}, {.map = "RockTunnel_1F", .battle = "picnicker_dana", .trainer = "TRAINER_PICNICKER_DANA"}, + {.map = "Route8", .battle = "gamer_rich", .trainer = "TRAINER_GAMER_RICH"}, // The Rocket Hideout, as PokeGuy's run fights it (the 00.31 and 00.32 // drafts): the Grunt at the Game Corner's poster (11, 2) // (CeladonCity_GameCorner_EventScript_RocketGrunt); on B4F, Grunt1 at From c5ee094663d2bc33bef9e6c5f2f0adadfdb06770 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 21:17:22 -0800 Subject: [PATCH 14/28] FRLG walker: no turn tap on the Bicycle On the bike a tap never only turns the player: it takes a step. The walker's turn tap before each leg made every open leg one tile long on the bike, and 00.30's walk to Route 8 ended on Route 12 (console B, 2026-11-09). turns_first() now skips the tap on the bike, for the walker and the sim walker alike. The sim models the bike: a tap steps facing either way, a hold through a turn loses no tile, and no step tap is ignored. Bike holds measured on Route 12's pier go in the movement mechanics table. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Overworld/PokemonFRLG_MovementMechanics.h | 6 ++++++ .../PokemonFRLG_OverworldRecoveryWalk.cpp | 3 ++- .../Overworld/PokemonFRLG_OverworldSim.cpp | 16 +++++++++------- .../Overworld/PokemonFRLG_OverworldSim.h | 10 ++++++++++ .../Overworld/PokemonFRLG_OverworldWalk.cpp | 10 +++++++++- .../Overworld/PokemonFRLG_OverworldWalk.h | 6 ++++++ .../PokemonFRLG_OverworldWalk_Tests.cpp | 11 +++++++++++ 7 files changed, 53 insertions(+), 9 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h index de3c682440..aadf6fcef6 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h @@ -54,6 +54,12 @@ inline constexpr Milliseconds SURF_TILE_TIME(133); // from rest in 600 ms (120 ms a tile, the start up included). Not the // decompilation's 4 frames a tile (67 ms): the bike speeds up from rest. inline constexpr Milliseconds BIKE_TILE_TIME(112); +// On the Bicycle a tap never only turns the player: from rest, a 40ms tap +// moved a tile facing either way, and holds of 112, 150, 200, 250, 300, +// 336 and 400ms moved 1, 2, 2, 3, 3, 4 and 4 tiles (console B, +// 2026-11-09, Route 12's north pier). So a bike leg has no turn tap: its +// hold turns the player too, and loses no tile for it. +inline constexpr bool BIKE_TAP_ONLY_TURNS = false; // The longest tap measured to only turn a player facing another way, and // to take exactly one step for one facing its way. inline constexpr Milliseconds LONGEST_TAP(133); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp index 9200c0e02b..ca51c44820 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp @@ -85,7 +85,8 @@ StepWalk walk_step( // Turning first, with a tap, makes the hold start from a standstill // facing the right way: holding through a turn lost a tile on the // console, while holds from a standstill were exact over 15 tiles. - if (facing != step.direction){ + // Not on the Bicycle, where the tap is a step. + if (turns_first(step, speed, facing)){ turn_to(console, context, step.direction); } if (const std::string note = speed_note(step, speed); !note.empty()){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp index 2e9b85afac..ecc7a6eff8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp @@ -470,9 +470,11 @@ void OverworldSim::walk_hold(Direction direction, Milliseconds duration, int sho const Milliseconds end = m_now + duration; Milliseconds pressed = m_now; if (m_state.facing != direction){ - // Holding through a turn lost a tile on the console. + // Holding through a turn lost a tile on the console, on foot. + if (!m_script.on_bike){ + pressed += tile; + } m_state.facing = direction; - pressed += tile; } take_steps(direction, pressed, end, (int)((end - pressed + tile - 1ms) / tile) - short_by); } @@ -546,9 +548,9 @@ void OverworldSim::tap(Direction direction, Milliseconds duration, Milliseconds m_last_tap.reset(); }else if (m_last_tap && m_last_tap->direction == direction && m_last_tap->end == start){ hold_on(direction, start + duration); - }else if (m_state.facing != direction){ + }else if (m_state.facing != direction && !m_script.on_bike){ // From a standstill a tap only turns the player, even one as long - // as LONGEST_TAP. + // as LONGEST_TAP. Not on the Bicycle: there it steps. m_last_tap = TapPress{direction, start, start + duration, m_state.facing, 0}; if (drops_turn_tap()){ m_state.turn_taps_dropped++; @@ -567,7 +569,7 @@ void OverworldSim::tap(Direction direction, Milliseconds duration, Milliseconds void OverworldSim::step_tap(Direction direction, Milliseconds duration){ const Milliseconds start = m_now; m_last_tap = TapPress{direction, start, start + duration, direction, 0}; - if (m_last_step_tap && start < *m_last_step_tap + MovementMechanics::STEP_TAP_LOCKOUT){ + if (!m_script.on_bike && m_last_step_tap && start < *m_last_step_tap + MovementMechanics::STEP_TAP_LOCKOUT){ m_state.step_taps_ignored++; note(std::format( "the step tap {} is ignored, {} ms after the last.", @@ -813,7 +815,7 @@ std::optional OverworldSimWalker::walk_step(const RouteStep& step, if (step.action != RouteAction::WALK){ fail("Sim: Surf isn't modelled."); } - if (facing != step.direction){ + if (turns_first(step, m_rules.speed, facing)){ sim_turn_to(m_sim, step.direction, m_rules.confirm_turn); } const int warps_before = m_sim.state().warps; @@ -822,7 +824,7 @@ std::optional OverworldSimWalker::walk_step(const RouteStep& step, leg.direction = step.direction; leg.tiles = step.tiles + step.warp_slack; leg.to_wall = step.to_wall; - leg.tile_time = TILE_TIME; + leg.tile_time = tile_time(step, m_rules.speed); leg.stop_on_interrupt = !step.to_wall; leg.stop_if_blocked = step.to_wall && m_rules.stop_if_blocked; if (m_rules.hold_over_connections){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h index 94fdd1d063..e1761f1248 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h @@ -132,6 +132,13 @@ struct SimScript{ // (SimWalkerRules::confirm_turn) after 00.28 run 2 (console B, // 2026-11-09), where a one-tile leg only turned the player. std::set dropped_turn_taps; + // The player rides the Bicycle: a tap facing another way takes a step + // too, a hold through a turn loses no tile + // (MovementMechanics::BIKE_TAP_ONLY_TURNS), and no step tap is + // ignored: a 40ms turn tap and a 56ms hold 340ms after it took two + // tiles (console B, 2026-11-09, 00.30). Set timing.tile to + // MovementMechanics::BIKE_TILE_TIME with it. + bool on_bike = false; // Caves dark without Flash (Rock Tunnel): the overworld is black but // for a lit circle on the player, which the walker's battle text box // check and screen observer read as a battle on its way (console B, @@ -449,6 +456,9 @@ struct SimWalkerRules{ // Without it a dropped tap left a one-tile leg turning the player // only, taken as walked (console B, 2026-11-09, 00.28 run 2). bool confirm_turn = true; + // The speed the walker goes at: its turn taps + // (OverworldWalkSteps::turns_first()) and its holds' tile time. + MoveSpeed speed = MoveSpeed::WALK; // A dark cave's overworld isn't a battle on its way // (OverworldWalkSteps::dark_cave_screen()). Without it the check // before each step watched for a battle on it (console B, 2026-11-09, diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp index 35214508be..92c49a4ed5 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp @@ -202,6 +202,13 @@ Milliseconds tile_time(const RouteStep& step, MoveSpeed speed){ return TILE_TIME; } +bool turns_first(const RouteStep& step, MoveSpeed speed, std::optional facing){ + if (facing == step.direction){ + return false; + } + return step.surfing || step_speed(step, speed) != MoveSpeed::BIKE || BIKE_TAP_ONLY_TURNS; +} + Pokemon::LegMover leg_mover(const RouteStep& step, MoveSpeed speed){ if (step_speed(step, speed) == MoveSpeed::RUN && !step.surfing){ return hold_dpad_running; @@ -580,7 +587,8 @@ void walk_overworld_route( // Turning first, with a tap, makes the hold start from a standstill // facing the right way: holding through a turn lost a tile on the // console, while holds from a standstill were exact over 15 tiles. - if (facing != step.direction){ + // Not on the Bicycle, where the tap is a step. + if (turns_first(step, speed, facing)){ turn_to(console, context, step.direction); } if (const std::string note = speed_note(step, speed); !note.empty()){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h index 22eb2eea05..7be9b789cb 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h @@ -154,6 +154,12 @@ MoveSpeed step_speed(const RouteStep& step, MoveSpeed speed); // pace whatever the speed; where running isn't allowed, the walking pace. Milliseconds tile_time(const RouteStep& step, MoveSpeed speed); +// Whether a WALK `step` at `speed` starts with a turn tap, for a player +// facing `facing`: on foot facing another way (or an unknown way), so the +// hold starts facing the right way. Never on the Bicycle, where a tap +// takes a step (MovementMechanics::BIKE_TAP_ONLY_TURNS). +bool turns_first(const RouteStep& step, MoveSpeed speed, std::optional facing); + // The mover for `step` at `speed`: hold_dpad_running() for a run on foot // where the map allows it, hold_dpad() otherwise (B does nothing on the // water). diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp index a488f376ae..a70e8b363a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp @@ -127,6 +127,8 @@ struct TurnTapInput{ std::set dropped_turn_taps; // SimWalkerRules::confirm_turn. bool confirm_turn = true; + // On the Bicycle (SimScript::on_bike, SimWalkerRules::speed). + bool on_bike = false; }; struct TurnTapExpectation{ @@ -162,6 +164,11 @@ class Test_OverworldTurnTap : public UnitTest{ script.dropped_turn_taps = m_case.input.dropped_turn_taps; SimWalkerRules rules; rules.confirm_turn = m_case.input.confirm_turn; + if (m_case.input.on_bike){ + script.on_bike = true; + script.timing.tile = BIKE_TILE_TIME; + rules.speed = MoveSpeed::BIKE; + } OverworldSim sim(MAPS, TURN_START, Direction::NORTH, script); OverworldSimWalker walker(sim, rules); const RoutePlanning planning{.maps = &MAPS, .costs = default_route_costs(), .start = TURN_START, .label = "Sim"}; @@ -184,6 +191,10 @@ const TurnTapCase TURN_TAP_CASES[] = { {.input = {.name = "dropped, tapped again", .dropped_turn_taps = {1}}, .expectation = {.end = TURN_END}}, {.input = {.name = "dropped twice, tapped again", .dropped_turn_taps = {1, 2}}, .expectation = {.end = TURN_END}}, {.input = {.name = "dropped, not checked: the leg only turns", .dropped_turn_taps = {1}, .confirm_turn = false}, .expectation = {.end = TURN_START}}, + // Console B, 2026-11-09 (00.30): on the Bicycle the turn tap took a + // step, each one-tile leg went two, and the walk to Route 8 ended on + // Route 12. + {.input = {.name = "on the bike, no turn tap", .on_bike = true}, .expectation = {.end = TURN_END}}, }; From e615aaabb5e481665bab53cb23f1aa70613e881f Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 21:19:37 -0800 Subject: [PATCH 15/28] FRLG walker: keep the bike change inside the file and function limits Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp | 1 - .../PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp index ca51c44820..5a0e782939 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp @@ -85,7 +85,6 @@ StepWalk walk_step( // Turning first, with a tap, makes the hold start from a standstill // facing the right way: holding through a turn lost a tile on the // console, while holds from a standstill were exact over 15 tiles. - // Not on the Bicycle, where the tap is a step. if (turns_first(step, speed, facing)){ turn_to(console, context, step.direction); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp index ecc7a6eff8..0bbc6fdce8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp @@ -471,9 +471,7 @@ void OverworldSim::walk_hold(Direction direction, Milliseconds duration, int sho Milliseconds pressed = m_now; if (m_state.facing != direction){ // Holding through a turn lost a tile on the console, on foot. - if (!m_script.on_bike){ - pressed += tile; - } + pressed += m_script.on_bike ? Milliseconds(0) : tile; m_state.facing = direction; } take_steps(direction, pressed, end, (int)((end - pressed + tile - 1ms) / tile) - short_by); From f7273a8078d4a3b9aaa4bd54a5c7fb3b09c59857 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 21:35:25 -0800 Subject: [PATCH 16/28] FRLG AutoStory: 00.30's GAME steps, use_item and lift_floor carry args The use_item and lift_floor actions read their item and floors from StoryStep::args and declare them in the action table, as #314's actions do. 00.30's Mart and capture steps and the draft plans are converted; the sim trace is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Programs/AutoStory/Data/JsonParser.cpp | 2 +- .../PokemonFRLG_AutoStoryHideout.cpp | 29 +++++++------------ .../AutoStory/PokemonFRLG_AutoStoryHideout.h | 25 +++++++++------- .../PokemonFRLG_AutoStoryHideout_Tests.cpp | 28 +++++++++++++----- .../PokemonFRLG_AutoStoryItemUse.cpp | 15 +++++----- .../AutoStory/PokemonFRLG_AutoStoryItemUse.h | 26 ++++++++++------- .../PokemonFRLG_AutoStoryItemUse_Tests.cpp | 15 +++++++--- .../AutoStory/PokemonFRLG_AutoStoryRowSim.cpp | 4 +-- .../PokemonFRLG_AutoStoryStepTable.cpp | 4 +++ .../PokemonFRLG_AutoStoryTableSteps.cpp | 8 +++-- .../PokemonFRLG_AutoStoryTableSteps.h | 4 +-- .../AutoStory/StoryData/Draft/README.md | 10 +++---- .../StoryData/Draft/frlg-00.30.plan.json | 10 +++++-- .../StoryData/Draft/frlg-00.32.plan.json | 12 +++++--- .../AutoStory/StoryData/frlg-00.30.plan.json | 7 +++-- 15 files changed, 119 insertions(+), 80 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/Data/JsonParser.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/Data/JsonParser.cpp index 060aa1a899..efc9dfe566 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/Data/JsonParser.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/Data/JsonParser.cpp @@ -229,7 +229,7 @@ bool known_game_step(const StoryStep& step){ return parse_capture_step(step) || parse_pc_step(step) || parse_mart_step(step) || parse_teach_step(step) || parse_grind_step(step) || parse_grind_check_step(step) || parse_fossil_step(step) || parse_level_step(step) - || is_center_variant(step.variant) || is_item_use_variant(step.variant) || is_hideout_variant(step.variant); + || (is_center_variant(step.variant) && step.args.empty()) || is_item_use_step(step) || is_hideout_step(step); } // Whether FRLG does `step`'s kind the way its variant names. diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp index 8c11c1fa7d..d3eda919e7 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp @@ -10,6 +10,7 @@ #include "PokemonFRLG_AutoStoryHideout.h" #include "PokemonFRLG_AutoStoryItemUse.h" #include "PokemonFRLG_AutoStoryRooms.h" +#include "PokemonFRLG_AutoStoryVariantWords.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -24,10 +25,6 @@ const std::vector& rocket_hideout_lift_menu(){ return MENU; } -std::string lift_floor_variant(std::string_view from, std::string_view to){ - return std::string(LIFT_FLOOR_PREFIX) + std::string(from) + " " + std::string(to); -} - namespace{ // The row of `floor` on the panel's menu, if it is a floor (not EXIT). @@ -43,17 +40,13 @@ std::optional floor_row(std::string_view floor){ } // namespace -std::optional parse_lift_floor_variant(std::string_view variant){ - if (!variant.starts_with(LIFT_FLOOR_PREFIX)){ - return std::nullopt; - } - const std::string_view floors = variant.substr(LIFT_FLOOR_PREFIX.size()); - const size_t space = floors.find(' '); - if (space == std::string_view::npos){ +std::optional parse_lift_floor_step(const Pokemon::StoryStep& step){ + const Pokemon::StoryArgs* args = game_step_args(step, LIFT_FLOOR_VARIANT, LIFT_FLOOR_ARGS); + if (args == nullptr){ return std::nullopt; } - const std::optional from = floor_row(floors.substr(0, space)); - const std::optional to = floor_row(floors.substr(space + 1)); + const std::optional from = floor_row(*Pokemon::story_arg_text(*args, "from")); + const std::optional to = floor_row(*Pokemon::story_arg_text(*args, "to")); if (!from || !to){ return std::nullopt; } @@ -72,8 +65,8 @@ MenuPresses lift_menu_presses(const LiftRide& ride, size_t entries){ return down <= up ? MenuPresses{Pokemon::Direction::SOUTH, down} : MenuPresses{Pokemon::Direction::NORTH, up}; } -bool is_hideout_variant(std::string_view variant){ - return variant == POSTER_SWITCH_VARIANT || parse_lift_floor_variant(variant).has_value(); +bool is_hideout_step(const Pokemon::StoryStep& step){ + return (step.variant == POSTER_SWITCH_VARIANT && step.args.empty()) || parse_lift_floor_step(step).has_value(); } @@ -123,7 +116,7 @@ std::optional run_field_game_step( const auto done = [&step]{ return step.pose_after ? Pokemon::outcome_ok(*step.pose_after) : Pokemon::outcome_ok(); }; - if (const std::optional ride = parse_lift_floor_variant(step.variant)){ + if (const std::optional ride = parse_lift_floor_step(step)){ ride_lift(console, context, *ride); return done(); } @@ -136,10 +129,10 @@ std::optional run_field_game_step( push_poster_switch(console, context); return Pokemon::outcome_ok(); } - if (!is_item_use_variant(step.variant)){ + if (!is_item_use_step(step)){ return std::nullopt; } - const std::string item = parse_use_item_variant(step.variant).value_or("bicycle"); + const std::string item = parse_use_item_step(step).value_or("bicycle"); ConsoleItemUseActions bag(console, context, language); const ItemUseEnd end = use_item_from_bag(console.logger(), bag, item); if (end != ItemUseEnd::USED){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h index bf8ed55c83..475c5cc9cd 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h @@ -37,13 +37,18 @@ namespace PokemonFRLG{ // beaten first. inline const std::string POSTER_SWITCH_VARIANT = "poster_switch"; -// The GAME step that rides the Rocket Hideout's lift with the LIFT KEY: -// "lift_floor ", floors as the panel names them (B1F, B2F, +// The GAME step that rides the Rocket Hideout's lift with the LIFT KEY, +// its args `from` and `to`, floors as the panel names them (B1F, B2F, // B4F). `from` is the floor the lift stands at, where the panel's cursor // starts. The player starts below `from`'s lift door, facing north, and // ends on `to`'s lift door (rocket_hideout_lift_doors()): the lift is // walked by a script (lift_in_legs(), lift_out_legs()), not a map. -inline constexpr std::string_view LIFT_FLOOR_PREFIX = "lift_floor "; +inline constexpr std::string_view LIFT_FLOOR_VARIANT = "lift_floor"; +// Its args: the floors it rides from and to. +inline constexpr Pokemon::StoryArgSpec LIFT_FLOOR_ARGS[] = { + {"from", Pokemon::StoryArgType::TEXT, true}, + {"to", Pokemon::StoryArgType::TEXT, true}, +}; // The lift panel's menu, top first (pokefirered // sMultichoiceList_RocketHideoutElevator): B1F, B2F, B4F, EXIT. @@ -54,11 +59,9 @@ struct LiftRide{ size_t from = 0; size_t to = 0; }; -// The variant for a ride from floor `from` to `to`. -std::string lift_floor_variant(std::string_view from, std::string_view to); -// The ride a "lift_floor " variant names, or nothing if it is -// not one, or names a floor the panel hasn't. -std::optional parse_lift_floor_variant(std::string_view variant); +// The ride a lift_floor step names, or nothing if it is not one, or names +// a floor the panel hasn't. +std::optional parse_lift_floor_step(const Pokemon::StoryStep& step); // Presses of the D-pad on a menu: which way, how many. struct MenuPresses{ @@ -70,8 +73,8 @@ struct MenuPresses{ // wraps round (pokefirered DrawVerticalMultichoiceMenu()). MenuPresses lift_menu_presses(const LiftRide& ride, size_t entries); -// Whether `variant` is one of this file's steps. -bool is_hideout_variant(std::string_view variant); +// Whether `step` is one of this file's steps. +bool is_hideout_step(const Pokemon::StoryStep& step); // On the console: A on the panel, the floor picked, the ride waited out // back to the overworld, the player still in the lift. @@ -84,7 +87,7 @@ void ride_lift(ConsoleHandle& console, ProControllerContext& context, const Lift void push_poster_switch(ConsoleHandle& console, ProControllerContext& context); // On the console, a GAME step of this file's, a Pokemon Center's -// (is_center_variant()) or an item from the bag (is_item_use_variant()); +// (is_center_variant()) or an item from the bag (is_item_use_step()); // nothing for another GAME step. The step ends on its pose_after, if it // has one (an Escape Rope's, a lift ride's, a Center's). std::optional run_field_game_step( diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp index ae6b342f3e..0df832e8fb 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp @@ -15,6 +15,7 @@ #include "PokemonFRLG_AutoStoryHideout.h" #include "PokemonFRLG_AutoStoryRowSim.h" #include "PokemonFRLG_AutoStoryTableSteps.h" +#include "PokemonFRLG_AutoStoryVariantWords.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -43,7 +44,9 @@ class Test_AutoStoryHideout : public UnitTest{ , m_case(std::move(test_case)) {} virtual UnitTestResult run(Logger&, CancellableScope&) const override{ - const std::optional ride = parse_lift_floor_variant(m_case.input.variant); + const std::string_view floors[] = {"from", "to"}; + const StoryStep step = legacy_game_step(m_case.input.variant, floors); + const std::optional ride = parse_lift_floor_step(step); const std::optional& expected = m_case.expectation.presses; if (!ride || !expected){ if (ride.has_value() != expected.has_value()){ @@ -58,8 +61,8 @@ class Test_AutoStoryHideout : public UnitTest{ got.presses, Pokemon::direction_name(got.direction), expected->presses, Pokemon::direction_name(expected->direction) ); } - if (!is_hideout_variant(m_case.input.variant)){ - return "A ride that isn't a hideout variant."; + if (!is_hideout_step(step)){ + return "A ride that isn't a hideout step."; } return true; } @@ -69,11 +72,22 @@ class Test_AutoStoryHideout : public UnitTest{ }; UnitTestResult test_variants(){ - if (!is_hideout_variant(POSTER_SWITCH_VARIANT) || is_hideout_variant("cut_tree") || is_hideout_variant("")){ - return "Expected only the poster switch and lift rides as hideout variants."; + const auto game_step = [](std::string variant, Pokemon::StoryArgs args){ + StoryStep step; + step.kind = Pokemon::StoryStepKind::GAME; + step.variant = std::move(variant); + step.args = std::move(args); + return step; + }; + if (!is_hideout_step(game_step(POSTER_SWITCH_VARIANT, {})) || is_hideout_step(game_step("cut_tree", {})) + || is_hideout_step(game_step("", {})) || is_hideout_step(game_step(POSTER_SWITCH_VARIANT, {{"from", "B1F"}})) + ){ + return "Expected only the poster switch and lift rides as hideout steps."; } - if (lift_floor_variant("B2F", "B4F") != "lift_floor B2F B4F"){ - return "Expected \"lift_floor B2F B4F\", got \"" + lift_floor_variant("B2F", "B4F") + "\"."; + const StoryStep ride = StoryTable::lift_floor("ride", "B2F", "B4F", ""); + const Pokemon::StoryArgs expected{{"from", "B2F"}, {"to", "B4F"}}; + if (ride.variant != LIFT_FLOOR_VARIANT || ride.args != expected){ + return "Expected lift_floor with {from: B2F, to: B4F}, got \"" + ride.variant + "\"."; } // A three-row menu doesn't wrap. const MenuPresses no_wrap = lift_menu_presses(LiftRide{0, 2}, 3); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp index 3bce9b0ae6..59e0c00b81 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp @@ -10,6 +10,7 @@ #include "PokemonFRLG_AutoStoryActions.h" #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryItemUse.h" +#include "PokemonFRLG_AutoStoryVariantWords.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -39,21 +40,19 @@ bool is_repel(std::string_view item){ return item == "repel" || item == "super-repel" || item == "max-repel"; } -std::string use_item_variant(std::string_view item){ - return std::string(USE_ITEM_PREFIX) + std::string(item); -} -std::optional parse_use_item_variant(std::string_view variant){ - if (!variant.starts_with(USE_ITEM_PREFIX)){ +std::optional parse_use_item_step(const Pokemon::StoryStep& step){ + const Pokemon::StoryArgs* args = game_step_args(step, USE_ITEM_VARIANT, USE_ITEM_ARGS); + if (args == nullptr || args->contains("slot")){ return std::nullopt; } - const std::string_view item = variant.substr(USE_ITEM_PREFIX.size()); + const std::string_view item = *Pokemon::story_arg_text(*args, "item"); if (!usable_item_pocket(item)){ return std::nullopt; } return std::string(item); } -bool is_item_use_variant(std::string_view variant){ - return variant == RIDE_BIKE_VARIANT || parse_use_item_variant(variant).has_value(); +bool is_item_use_step(const Pokemon::StoryStep& step){ + return (step.variant == RIDE_BIKE_VARIANT && step.args.empty()) || parse_use_item_step(step).has_value(); } std::optional bag_row_of(const std::vector& rows, std::string_view item){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h index 3836b7a78a..74949be3d8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h @@ -23,6 +23,8 @@ #include "CommonFramework/Language.h" #include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" #include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "Pokemon/AutoStory/Types/Pokemon_StoryArgs.h" +#include "Pokemon/AutoStory/Types/Pokemon_StoryStep.h" #include "VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_ItemListReader.h" namespace PokemonAutomation{ @@ -30,9 +32,15 @@ namespace NintendoSwitch{ namespace PokemonFRLG{ -// The GAME step that uses an item from the bag on the overworld: -// "use_item ", the slug as read_item_list_name() names it. -inline constexpr std::string_view USE_ITEM_PREFIX = "use_item "; +// The GAME step that uses an item from the bag on the overworld, its +// args the item's slug as read_item_list_name() names it, and for an item +// used on a Pokemon the party slot (none the story uses yet). +inline constexpr std::string_view USE_ITEM_VARIANT = "use_item"; +// Its args: the item, and the party slot it is used on. +inline constexpr Pokemon::StoryArgSpec USE_ITEM_ARGS[] = { + {"item", Pokemon::StoryArgType::TEXT, true}, + {"slot", Pokemon::StoryArgType::NUMBER, false}, +}; // The GAME step that gets on the Bicycle (the Key Items pocket's BICYCLE, // USE). The ROUTE steps after it ride (BIKE_VARIANT). inline const std::string RIDE_BIKE_VARIANT = "ride_bike"; @@ -51,13 +59,11 @@ std::optional usable_item_pocket(std::string_view item); // Whether `item` is a Repel of any kind. bool is_repel(std::string_view item); -// The variant that uses `item`. -std::string use_item_variant(std::string_view item); -// The item a "use_item " variant uses, or nothing if it isn't one -// or the item isn't one the story uses from the bag. -std::optional parse_use_item_variant(std::string_view variant); -// Whether `variant` is one of this file's steps. -bool is_item_use_variant(std::string_view variant); +// The item a use_item step uses, or nothing if it isn't one, the item +// isn't one the story uses from the bag, or it names a slot. +std::optional parse_use_item_step(const Pokemon::StoryStep& step); +// Whether `step` is one of this file's steps. +bool is_item_use_step(const Pokemon::StoryStep& step); // The row of `item` in `rows` (read_bag_pocket()'s, from the top), if it // has one left: a count of -1 (unread) counts as some. diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp index ab33b1a43c..96674e8fd6 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp @@ -17,6 +17,7 @@ #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryItemUse.h" +#include "PokemonFRLG_AutoStoryVariantWords.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -25,6 +26,9 @@ namespace PokemonFRLG{ namespace{ +// A use_item step's positional key, for legacy_game_step(). +const std::string_view ITEM[] = {"item"}; + // A bag on the sim: its pockets, each item used takes one, a bag that may // not open. class SimBag : public ItemUseActions{ @@ -87,7 +91,7 @@ class Test_AutoStoryItemUse : public UnitTest{ bag.pockets[BagPocket::KEY_ITEMS] = m_case.input.key_items; bag.opens = m_case.input.opens; const std::string variant = m_case.input.variant; - const std::string item = variant == RIDE_BIKE_VARIANT ? "bicycle" : parse_use_item_variant(variant).value_or(variant); + const std::string item = variant == RIDE_BIKE_VARIANT ? "bicycle" : parse_use_item_step(legacy_game_step(variant, ITEM)).value_or(variant); const ItemUseEnd end = m_case.input.renew ? renew_repel(logger, bag, item) : use_item_from_bag(logger, bag, item); if (end != m_case.expectation.end || bag.used != m_case.expectation.used || bag.open){ return std::format( @@ -176,10 +180,13 @@ const ItemUseCase ITEM_USE_CASES[] = { }; UnitTestResult test_variants_and_bike(){ - if (parse_use_item_variant("use_item potion") || parse_use_item_variant("use_item") || !parse_use_item_variant(use_item_variant("super-repel"))){ - return "Expected only the story's bag items as use_item variants."; + if (parse_use_item_step(legacy_game_step("use_item potion", ITEM)) || parse_use_item_step(legacy_game_step("use_item", ITEM)) + || parse_use_item_step(legacy_game_step("use_item repel slot=1", ITEM)) + || !parse_use_item_step(legacy_game_step("use_item super-repel", ITEM)) + ){ + return "Expected only the story's bag items as use_item steps."; } - if (!is_item_use_variant(RIDE_BIKE_VARIANT) || is_item_use_variant("poster_switch")){ + if (!is_item_use_step(legacy_game_step(RIDE_BIKE_VARIANT)) || is_item_use_step(legacy_game_step("poster_switch"))){ return "Expected ride_bike, not poster_switch, as an item use."; } StoryStep ride; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp index 273822e571..e8e33c6b4a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp @@ -573,7 +573,7 @@ Pokemon::StoryAction OverworldStepSim::hideout_or_stub(){ return failure.empty() ? Pokemon::outcome_ok() : Pokemon::outcome_failed("sim_failure", failure); }); sim.act = [stub, played_act = sim.act](RunState& state, const StoryStep& step){ - return is_hideout_variant(step.variant) || center_walkable(step) + return is_hideout_step(step) || center_walkable(step) ? played_act(state, step) : stub.act(state, step); }; @@ -589,7 +589,7 @@ std::string OverworldStepSim::hideout(const StoryStep& step){ sim_walk_room_legs(m_sim, center_legs(step.variant)); return ended_on(step); } - const std::optional ride = parse_lift_floor_variant(step.variant); + const std::optional ride = parse_lift_floor_step(step); if (!ride){ return "no lift ride \"" + step.variant + "\"."; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryStepTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryStepTable.cpp index 5981e1b290..1e6ef673ce 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryStepTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryStepTable.cpp @@ -9,6 +9,8 @@ #include "PokemonFRLG_AutoStoryChoice.h" #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryGrind.h" +#include "PokemonFRLG_AutoStoryHideout.h" +#include "PokemonFRLG_AutoStoryItemUse.h" #include "PokemonFRLG_AutoStoryMart.h" #include "PokemonFRLG_AutoStoryMember.h" #include "PokemonFRLG_AutoStoryPcStorage.h" @@ -73,6 +75,8 @@ const std::vector& story_action_rows(){ {StoryStepKind::GAME, std::string(CAPTURE_VARIANT), StoryActionId::GAME, CAPTURE_ARGS}, {StoryStepKind::GAME, std::string(FOSSIL_VARIANT), StoryActionId::GAME, FOSSIL_ARGS}, {StoryStepKind::GAME, std::string(LEVEL_VARIANT), StoryActionId::GAME, LEVEL_ARGS}, + {StoryStepKind::GAME, std::string(USE_ITEM_VARIANT), StoryActionId::GAME, USE_ITEM_ARGS}, + {StoryStepKind::GAME, std::string(LIFT_FLOOR_VARIANT), StoryActionId::GAME, LIFT_FLOOR_ARGS}, {StoryStepKind::GAME, "", StoryActionId::GAME}, }; return ROWS; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp index 70d3a2b8f0..d9815af8dc 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp @@ -166,8 +166,9 @@ StoryStep poster_switch(std::string name, std::string why){ StoryStep lift_floor(std::string name, std::string_view from, std::string_view to, std::string why){ StoryStep step = make(StoryStepKind::GAME, std::move(name), std::move(why)); - step.variant = lift_floor_variant(from, to); - if (const std::optional ride = parse_lift_floor_variant(step.variant)){ + step.variant = LIFT_FLOOR_VARIANT; + step.args = {{"from", std::string(from)}, {"to", std::string(to)}}; + if (const std::optional ride = parse_lift_floor_step(step)){ const MapPosition& door = rocket_hideout_lift_doors()[ride->to]; step.pose_after = to_pose(StoryPose{door.map, door.point, Direction::SOUTH}); } @@ -190,7 +191,8 @@ StoryStep center_exit(std::string name, Pokemon::Pose below_door, std::string wh StoryStep use_item(std::string name, std::string_view item, std::string why, std::optional pose_after){ StoryStep step = make(StoryStepKind::GAME, std::move(name), std::move(why)); - step.variant = use_item_variant(item); + step.variant = USE_ITEM_VARIANT; + step.args = {{"item", std::string(item)}}; step.pose_after = std::move(pose_after); return step; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h index db580bd96b..f562000d58 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h @@ -187,7 +187,7 @@ StoryStep cut_tree(std::string name, Pokemon::Pose tree, std::string why); // (POSTER_SWITCH_VARIANT). StoryStep poster_switch(std::string name, std::string why); // Rides the Rocket Hideout's lift from the floor it stands at to `to` -// (lift_floor_variant()): "B1F", "B2F", "B4F". The player starts below +// (LIFT_FLOOR_VARIANT): "B1F", "B2F", "B4F". The player starts below // `from`'s lift door, facing north, and ends on `to`'s, facing south. StoryStep lift_floor(std::string name, std::string_view from, std::string_view to, std::string why); // Into the Pokemon Center `center` (a map name) from below its door, @@ -197,7 +197,7 @@ StoryStep center_enter(std::string name, const char* center, std::string why); // Out of the Center from below the nurse's counter (CENTER_EXIT_VARIANT) // to `below_door`, the tile below its door outside, facing south. StoryStep center_exit(std::string name, Pokemon::Pose below_door, std::string why); -// Uses `item` from the bag on the overworld (use_item_variant()): a +// Uses `item` from the bag on the overworld (USE_ITEM_VARIANT): a // Repel, or an Escape Rope, which leaves the player on `pose_after`. StoryStep use_item(std::string name, std::string_view item, std::string why, std::optional pose_after = std::nullopt); // Gets on the Bicycle (RIDE_BIKE_VARIANT). diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md index 0d23b8ff91..3ebe665a43 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/README.md @@ -78,7 +78,7 @@ code (`PokemonFRLG_CodedRooms.h`): `center_exit`: south 4 and out; the game walks the player back onto the tile below the door. 00.30 and 00.31 use them for Lavender's and Celadon's Centers; no `CeladonCity_PokemonCenter_1F` map is needed. -- `lift_floor `: from below `from`'s lift door, facing north: in +- `lift_floor` (args `from`, `to`): from below `from`'s lift door, facing north: in onto (2, 5), north 2, west 2 to below the panel (0, 2), the floor picked, east 2, south 2 and south through the arrow warp onto `to`'s lift door (`setdynamicwarp`: B1F (24, 25), B2F (28, 16), B4F (20, 23)). No @@ -120,12 +120,12 @@ drafts become table rows, their battle steps take those names. Fossil (00.38b), the Hitmonchan (00.41) and the Lapras (00.42). The authored format can't yet say `handoff`/`live_start` as 00.12/00.12b do in C++. - `game_action` variants the C++ executor now has (no console run yet): - `poster_switch`, `lift_floor ` (PokemonFRLG_AutoStoryHideout.h), + `poster_switch`, `lift_floor` (PokemonFRLG_AutoStoryHideout.h), `center_enter`, `center_exit` (PokemonFRLG_AutoStoryRooms.h), - `use_item ` and `ride_bike` + `use_item` with `item` repel, super-repel, max-repel or escape-rope, and `ride_bike` (PokemonFRLG_AutoStoryItemUse.h), and the `bike` route variant. Variants - that no executor has yet: `party_room`, `gift_rng`, `use_item rare-candy - slot=N` (an item used on a party member), `fly`, `flee_wild`, `surf`, + that no executor has yet: `party_room`, `gift_rng`, `use_item` with a + `slot` (rare-candy, an item used on a party member), `fly`, `flee_wild`, `surf`, `strength_puzzle`, `reenter_map`. The `mart` variants name Marts the Mart table doesn't list yet (Lavender, Celadon 5F, Indigo Plateau). - HMs and key items come in story order: LIFT KEY, SILPH SCOPE, HM02 Fly, TEA, diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json index 6ccbd8d610..9dfd0a659c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.30.plan.json @@ -23,13 +23,16 @@ { "id": "buy_ropes_and_super_repels", "kind": "game_action", - "variant": "mart LavenderTown_Mart budget=5700 floor=9000 escape-rope=4 super-repel=7", + "variant": "mart", + "args": {"map": "LavenderTown_Mart", "budget": 5700, "floor": 9000}, + "items": [{"item": "escape-rope", "quantity": 4}, {"item": "super-repel", "quantity": 7}], "why": "pokeguy 54:18: 4 Escape Ropes and 7 Super Repels" }, { "id": "repel_route8", "kind": "game_action", - "variant": "use_item repel", + "variant": "use_item", + "args": {"item": "repel"}, "why": "a Repel (5 bought in 00.26) for Route 8's grass and the Underground Path; renewed when it wears off" }, { @@ -82,7 +85,8 @@ { "id": "catch_pidgey", "kind": "game_action", - "variant": "capture pidgey catches=1 encounters=30 balls=5 pace=east", + "variant": "capture", + "args": {"species": "pidgey", "catches": 1, "encounters": 30, "balls": 5, "pace": "east"}, "why": "a Pidgey to carry HM02 Fly (pokeguy flies with a Pidgey from 1:03 on); the party has no other flier" }, { diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.32.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.32.plan.json index d5170089d4..fcb107b1f7 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.32.plan.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/Draft/frlg-00.32.plan.json @@ -17,7 +17,8 @@ "id": "lift_to_b4f", "kind": "game_action", "declared_after": {"certainty": "tile", "map": "RocketHideout_B4F", "where": {"facing": "south", "kind": "tiles", "tiles": [[20, 23]]}}, - "variant": "lift_floor B2F B4F", + "variant": "lift_floor", + "args": {"from": "B2F", "to": "B4F"}, "why": "the lift script: in through the door (28, 16), to the panel, B4F with the LIFT KEY, out onto B4F's lift door; no map of the lift" }, { @@ -52,13 +53,15 @@ { "id": "rare_candy_1", "kind": "game_action", - "variant": "use_item rare-candy slot=1", + "variant": "use_item", + "args": {"item": "rare-candy", "slot": 1}, "why": "Rare Candy: Lv35 (pokeguy 1:00:25)" }, { "id": "rare_candy_2", "kind": "game_action", - "variant": "use_item rare-candy slot=1", + "variant": "use_item", + "args": {"item": "rare-candy", "slot": 1}, "why": "Rare Candy: Lv36, Wartortle evolves" }, { @@ -98,7 +101,8 @@ "id": "escape_rope_hideout", "kind": "game_action", "declared_after": {"certainty": "tile", "map": "CeladonCity", "where": {"facing": "south", "kind": "tiles", "tiles": [[34, 22]]}}, - "variant": "use_item escape-rope", + "variant": "use_item", + "args": {"item": "escape-rope"}, "why": "out to the Game Corner's door (pokeguy 1:02:10)" } ] diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json index d49cbdf492..41268209a3 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json @@ -39,7 +39,9 @@ { "id": "buy_ropes_and_super_repels", "kind": "game_action", - "variant": "mart LavenderTown_Mart budget=5700 floor=9000 escape-rope=4 super-repel=7", + "variant": "mart", + "args": {"map": "LavenderTown_Mart", "budget": 5700, "floor": 9000}, + "items": [{"item": "escape-rope", "quantity": 4}, {"item": "super-repel", "quantity": 7}], "once_per_save": true, "why": "pokeguy 54:18: 4 Escape Ropes (the Rocket Hideout, 00.31) and 7 Super Repels (Pokemon Tower)" }, @@ -170,7 +172,8 @@ { "id": "catch_pidgey", "kind": "game_action", - "variant": "capture pidgey catches=1 encounters=30 balls=5 pace=east", + "variant": "capture", + "args": {"species": "pidgey", "catches": 1, "encounters": 30, "balls": 5, "pace": "east"}, "once_per_save": true, "why": "a Pidgey to carry HM02 Fly (pokeguy flies with a Pidgey from 1:03 on); the party has no other flier" } From 2f999190c699e414e8cf2b3b59de5c205c1284a2 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 21:44:09 -0800 Subject: [PATCH 17/28] FRLG walker: the Bicycle moves a tile per 100ms Holds counted back with one-tile taps on console B (Lavender Town, 2026-11-09) moved a tile per 100ms from rest, the same each time. At 112ms a tile, 00.30's "west 16 (open)" on Route 8 went 17 or 18 tiles and the walk missed the Underground Path's door. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Overworld/PokemonFRLG_MovementMechanics.h | 16 ++++++++++------ .../PokemonFRLG_OverworldWalk_Tests.cpp | 10 ++++++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h index aadf6fcef6..bfcda76680 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h @@ -48,12 +48,16 @@ inline constexpr Milliseconds TILE_TIME(267); inline constexpr Milliseconds RUN_TILE_TIME(133); // Surfing: a tile per 8 frames. inline constexpr Milliseconds SURF_TILE_TIME(133); -// Riding the Bicycle, measured on console B (2026-11-09, Lavender Town -// row 13, wall to wall, from rest): 16 tiles (1, 13) to (17, 13) in -// 1800 ms of motion, east and back west alike, 112 ms a tile; 5 tiles -// from rest in 600 ms (120 ms a tile, the start up included). Not the -// decompilation's 4 frames a tile (67 ms): the bike speeds up from rest. -inline constexpr Milliseconds BIKE_TILE_TIME(112); +// Riding the Bicycle, measured on console B (2026-11-09, Lavender Town, +// east from the rock west of the Center, from rest, counted back with +// one-tile taps): holds of 450, 500, 650, 700, 900, 950, 1100, 1250, +// 1300, 1450 and 1500ms moved 5, 5, 7, 7, 9, 10, 11, 13, 13, 15 and 15 +// tiles, the same each time: a tile per 100ms, so an open leg's hold of +// (n - 1) tiles and a half moves n. 112ms a tile (a wall-to-wall time +// of the motion on screen) held "west 16 (open)" for 17 or 18 tiles on +// Route 8 (00.30 run 2). Not the decompilation's 4 frames a tile (67ms): +// the bike speeds up from rest. +inline constexpr Milliseconds BIKE_TILE_TIME(100); // On the Bicycle a tap never only turns the player: from rest, a 40ms tap // moved a tile facing either way, and holds of 112, 150, 200, 250, 300, // 336 and 400ms moved 1, 2, 2, 3, 3, 4 and 4 tiles (console B, diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp index a70e8b363a..aa659b6101 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp @@ -118,6 +118,16 @@ const OverworldWalkTestCase OVERWORLD_WALK_CASES[] = { .input = {.name = "walk, surfing", .speed = MoveSpeed::WALK, .tiles = 3, .to_wall = false, .surfing = true}, .expectation = {.hold = Milliseconds(2 * 133 + 66), .note = ""}, }, + // Console B, 2026-11-09: on the bike, 1450ms moved 15 tiles and 50ms + // (a tap) one; 1736ms, held for "west 16 (open)", went past 16. + { + .input = {.name = "bike, open 16", .speed = MoveSpeed::BIKE, .tiles = 16, .to_wall = false, .surfing = false}, + .expectation = {.hold = Milliseconds(1550), .note = ""}, + }, + { + .input = {.name = "bike, open 1", .speed = MoveSpeed::BIKE, .tiles = 1, .to_wall = false, .surfing = false}, + .expectation = {.hold = Milliseconds(50), .note = ""}, + }, }; From 7aff29c105ca970653ceb827fbdfaa075a6e1ee1 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 22:22:27 -0800 Subject: [PATCH 18/28] FRLG AutoStory: sim test for 00.30's Route 8 walk from a start off by 3 Console B, 2026-11-09: three 00.30 runs stood 3 tiles east of the walker's belief after Route 8's open legs, the hold north to the Underground Path's door ran up to the cliff (16, 2), and the step onto the door bumped the house. The test starts the steps after Rich's battle from tiles up to 3 off the table's and expects Route 7 with no trainer met. Red on the current plan: from (45, 7) it fails as the console did. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../PokemonFRLG_AutoStoryLavender_Tests.cpp | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryLavender_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryLavender_Tests.cpp index 99cd1fb3af..3b4c0f3fc6 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryLavender_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryLavender_Tests.cpp @@ -16,10 +16,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/TestRunners/UnitTest.h" @@ -600,6 +602,104 @@ UnitTestResult test_route7_grass_battles(Logger&){ return true; } +// The maps after Gamer Rich's battle: Rich, beaten, stands where he +// walked down to (55, 17). +std::vector maps_after_rich(){ + std::vector maps = maps_after_route10(); + for (MapData& map : maps){ + if (map.name != "Route8"){ + continue; + } + for (MapObject& object : map.objects){ + if (object.name == "old_man_1"){ + object.y = 17; + object.trainer.reset(); + } + } + } + return maps; +} + +// The walker believes the player stands on `believed`, a tile 00.30's +// steps from Rich's battle on pass through, while the player stands on +// `actual`. +struct OffStart{ + TilePos believed; + TilePos actual; +}; + +const OffStart ROUTE8_OFF_STARTS[] = { + {{55, 18}, {58, 18}}, + {{55, 18}, {52, 18}}, + {{48, 7}, {45, 7}}, + {{48, 7}, {50, 7}}, + {{26, 7}, {29, 7}}, + {{26, 7}, {26, 6}}, + {{26, 7}, {26, 9}}, + {{26, 7}, {28, 10}}, +}; + +// 00.30's steps from `first` to `last`, as the table lays them out from +// `at`: the first that starts on `tile`, and where the table says the +// player stands then. Nothing if none does. +std::optional> step_starting_on( + const StoryCheckpoint& row, size_t first, size_t last, StoryPose at, const TilePos& tile +){ + for (size_t c = first; c <= last; c++){ + if (at.map == "Route8" && at.tile == tile){ + return std::make_pair(c, at); + } + const std::optional> route = walked_route(at, row.steps[c]); + if (!route || route->empty()){ + return std::nullopt; + } + at = StoryPose{route->back().end.map, route->back().end.point, route->back().facing_after}; + } + return std::nullopt; +} + +// Console B, 2026-11-09 (00.30, three runs): after Route 8's open legs +// the player stood 3 tiles east of where the walker believed, the hold +// north to the Underground Path's door ran up column 17 to the cliff +// (16, 2), and the step onto the door bumped the house: "no fade in and +// out after walking onto the warp". From a start up to 3 tiles off, the +// steps on to Route 7 meet no trainer and come out of the Underground +// Path where the table says. +UnitTestResult test_route8_west_off_start(Logger&){ + const StoryCheckpoint& row = checkpoint("00.30"); + const size_t first = step_index(row, "west_of_glenn_column"); + const size_t last = step_index(row, "route7_underground_exit"); + const StoryPose after_rich{"Route8", {55, 18}, Direction::NORTH}; + const std::vector maps = maps_after_rich(); + const MapPosition end{"Route7", {7, 15}}; + std::string failures; + for (const OffStart& off : ROUTE8_OFF_STARTS){ + const std::optional> from = step_starting_on(row, first, last, after_rich, off.believed); + std::string failure = from ? "" : "no step starts there."; + StoryPose at = from ? from->second : after_rich; + OverworldSim sim(maps, MapPosition{at.map, off.actual}, at.facing.value_or(Direction::NORTH)); + for (size_t c = from ? from->first : last + 1; c <= last && failure.empty(); c++){ + failure = row_step_on_sim(sim, at, row.steps[c], row.id); + } + if (failure.empty() && !(sim.state().position == end)){ + failure = std::format("ended on {}, not {}.", to_string(sim.state().position), to_string(end)); + } + if (failure.empty() && !sim.state().trainers_beaten.empty()){ + failure = "fought " + sim.state().trainers_beaten.front() + "."; + } + if (failure.empty() && !sim.state().violations.empty()){ + failure = "a violation on the way: " + sim.state().violations.back(); + } + if (!failure.empty()){ + failures += std::format("{}from {}, not {}: {}", failures.empty() ? "" : " | ", to_string(off.actual), to_string(off.believed), failure); + } + } + if (!failures.empty()){ + return failures; + } + return true; +} + // 00.30 saves in Celadon City's Pokemon Center: CELADON CITY on the save // card. UnitTestResult test_celadon_save_place(Logger&){ @@ -629,6 +729,7 @@ const AutoStoryLavenderCase AUTO_STORY_LAVENDER_CASES[] = { {.input = {"00.28 and 00.29 save on Route 10 and in Lavender Town", test_save_places}, .expectation = {}}, {.input = {"00.30 Lavender's Mart, Route 8 past Rich alone, the Underground Path, Route 7 to Celadon City", test_celadon_way_row}, .expectation = {}}, {.input = {"00.30 a wild battle on the step into Route 7's grass", test_route7_grass_battles}, .expectation = {}}, + {.input = {"00.30 Route 8 west from a start up to 3 tiles off", test_route8_west_off_start}, .expectation = {}}, {.input = {"00.30 saves in Celadon City's Pokemon Center", test_celadon_save_place}, .expectation = {}}, }; From 87f2e85bc4a4e9f5c27fd270e6c39ab95bc836fb Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 22:22:28 -0800 Subject: [PATCH 19/28] FRLG AutoStory: 00.30 re-anchors on Route 8's walls before the door The open legs from (48, 7) to (14, 13) had no wall between them, so an error anywhere on them reached the door. Now row 7 is held west to its wall (26, 7), column 26 south to the bottom wall, and row 14 west to its wall (12, 14); from there 2 Bicycle taps east and a hold north to the house's wall (14, 5) put the player below the door. Each hold that ends on a wall puts the player back on a known tile, so a start up to 3 tiles off still reaches the door. The golden trace changes only for events 14 and 15, which are new. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../ReplayData/frlg-story-sim-trace.json | 12 +++---- .../AutoStory/StoryData/frlg-00.30.plan.json | 36 +++++++++++-------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json index 4bb19c09de..179242eb0b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json @@ -1240,12 +1240,12 @@ "[Event] 00.30/13 west_of_glenn_column: start", "00.30/west_of_glenn_column: ok at Route8 (48, 7) facing north (tile)", "[Event] 00.30/13 west_of_glenn_column: done", - "[Event] 00.30/14 east_of_megan_column: start", - "00.30/east_of_megan_column: ok at Route8 (30, 13) facing south (tile)", - "[Event] 00.30/14 east_of_megan_column: done", - "[Event] 00.30/15 east_of_aidan_column: start", - "00.30/east_of_aidan_column: ok at Route8 (14, 13) facing north (tile)", - "[Event] 00.30/15 east_of_aidan_column: done", + "[Event] 00.30/14 route8_row7_west_wall: start", + "00.30/route8_row7_west_wall: ok at Route8 (26, 7) facing west (tile)", + "[Event] 00.30/14 route8_row7_west_wall: done", + "[Event] 00.30/15 route8_row14_west_wall: start", + "00.30/route8_row14_west_wall: ok at Route8 (12, 14) facing west (tile)", + "[Event] 00.30/15 route8_row14_west_wall: done", "[Event] 00.30/16 east_of_underground_door: start", "00.30/east_of_underground_door: ok at Route8 (14, 5) facing north (tile)", "[Event] 00.30/16 east_of_underground_door: done", diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json index 41268209a3..cd34645f8a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json @@ -110,31 +110,39 @@ "events": [ { "id": "west_of_glenn_column", - "kind": "route", + "kind": "legs", "variant": "bike", - "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[48, 7]]}}, - "why": "west along row 18 past Rich (55, 17) and up column 48 to row 7, west of Glenn's south line (51, 7..10) and below his west line (47..50, 6)" + "legs": [ + {"direction": "west", "tiles": 7}, {"direction": "north", "tiles": 11} + ], + "why": "west along row 18 past Rich (55, 17) to the wall (47, 18), then up column 48 to row 7, west of Glenn's south line (51, 7..10) and below his west line (47..50, 6): the wall puts the player on column 48 wherever on row 18 the battle left them" }, { - "id": "east_of_megan_column", - "kind": "route", + "id": "route8_row7_west_wall", + "kind": "legs", "variant": "bike", - "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "south", "kind": "tiles", "tiles": [[30, 13]]}}, - "why": "west along row 7, below Megan's line (30..35, 6), and down column 30 to (30, 13), west of the pen's wall (33, 8..18)" + "legs": [ + {"direction": "west", "tiles": 22} + ], + "why": "west along row 7, below Megan's line (30..35, 6) and Andrea's (26..28, 5), to the wall (25, 7): column 26 from anywhere on the row" }, { - "id": "east_of_aidan_column", - "kind": "route", + "id": "route8_row14_west_wall", + "kind": "legs", "variant": "bike", - "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[14, 13]]}}, - "why": "down onto row 14, west along it above Jaren (24, 17) and Ricardo (14, 16), who see a tile, and up onto (14, 13)" + "legs": [ + {"direction": "south", "tiles": 11}, {"direction": "north", "tiles": 4}, {"direction": "west", "tiles": 14} + ], + "why": "down column 26 to the wall (26, 19), east of Jaren's line (25, 17), 4 tiles back up onto row 14, and west along it above Jaren (24, 17) and Ricardo (14, 16), who see a tile, to the wall (11, 14): the walls put the player on row 18, then on column 12, wherever the walk along row 7 ended (console B, 2026-11-09: open legs from (48, 7) to (14, 13) ended 3 tiles east, and the door step bumped the house)" }, { "id": "east_of_underground_door", - "kind": "route", + "kind": "legs", "variant": "bike", - "target": {"certainty": "tile", "map": "Route8", "where": {"facing": "north", "kind": "tiles", "tiles": [[14, 5]]}}, - "why": "up column 14, east of Aidan's line (10..13, 6), to the wall (14, 4)" + "legs": [ + {"direction": "east", "tiles": 2}, {"direction": "north", "tiles": 9} + ], + "why": "2 taps east (a Bicycle tap is a tile) onto column 14, east of Aidan's line (10..13, 6), and up it to the house's wall (14, 4)" }, { "id": "below_underground_door", From 83d3be7294dcec06beb5207d0232ee5e38e805f5 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 22:57:48 -0800 Subject: [PATCH 20/28] FRLG walker: count open Bicycle legs in taps, get on with SELECT Console B, 2026-11-09: on the Bicycle a 40ms tap is always one tile, tap chains are exact at one per 100ms and drop one tap in 12 at one per 60ms, and the registered Bicycle gets on with SELECT in about 1s. Open legs on the Bicycle are now walked a tap at a time (40ms press, 120ms period, judged after a settle by the screen scrolling). A tap that doesn't move ends the leg blocked: run_route() finds the player on the tiles the count gives, up to 3 further for a start that was off, ahead of a block. An interrupted tap ends it with the count and one more. A warp takes the walker off the Bicycle; the next leg where it may be ridden gets on again with SELECT, checked by the standing sprite no longer showing. The sim models the taps (dropped under 80ms), SELECT and doors. New OverworldBike tests: tap chains, an 11-tile leg from a start 2 off (red with one hold), SELECT onto the registered Bicycle, speed after a door. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Overworld/PokemonFRLG_MovementMechanics.h | 29 +++ .../PokemonFRLG_OverworldBike_Tests.cpp | 234 ++++++++++++++++++ .../PokemonFRLG_OverworldRecovery.cpp | 50 +++- .../Overworld/PokemonFRLG_OverworldRecovery.h | 34 +++ .../PokemonFRLG_OverworldRecoveryWalk.cpp | 107 +++++++- .../Overworld/PokemonFRLG_OverworldSim.cpp | 117 ++++++++- .../Overworld/PokemonFRLG_OverworldSim.h | 62 ++++- .../PokemonFRLG_OverworldSim_Tests.cpp | 1 + .../Overworld/PokemonFRLG_OverworldWalk.cpp | 56 +++++ .../Overworld/PokemonFRLG_OverworldWalk.h | 36 +++ SerialPrograms/cmake/SourceFiles.cmake | 1 + 11 files changed, 699 insertions(+), 28 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h index bfcda76680..aa9fafaa3f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h @@ -64,6 +64,18 @@ inline constexpr Milliseconds BIKE_TILE_TIME(100); // 2026-11-09, Route 12's north pier). So a bike leg has no turn tap: its // hold turns the player too, and loses no tile for it. inline constexpr bool BIKE_TAP_ONLY_TURNS = false; +// Chains of 40ms taps on the Bicycle (console B, 2026-11-09): one tap per +// 100ms (60ms release) moved exactly a tile each; at one per 60ms, one +// tap in 12 was dropped. Somewhere between the two the game starts +// dropping taps; the sim drops one in BIKE_FAST_TAP_DROP_EVERY of the taps +// that start sooner than this after the last one. +inline constexpr Milliseconds BIKE_TAP_PERIOD_WORKED(100); +inline constexpr Milliseconds BIKE_TAP_DROP_PERIOD(80); +inline constexpr int BIKE_FAST_TAP_DROP_EVERY = 12; +// With the Bicycle registered (Key Items, BICYCLE, REGISTER), SELECT gets +// on and off in about 1s (console B, 2026-11-09). Going through a door +// takes the player off (not measured); a battle leaves them on. +inline constexpr Milliseconds BIKE_SELECT_TIME(1000); // The longest tap measured to only turn a player facing another way, and // to take exactly one step for one facing its way. inline constexpr Milliseconds LONGEST_TAP(133); @@ -104,6 +116,20 @@ inline constexpr Milliseconds STEP_TAP(66); // STEP_TAP_RELEASE after it, past the lockout. inline constexpr Milliseconds STEP_TAP_RELEASE(700); +// An open leg on the Bicycle is counted out in taps, each a tile: a 40ms +// press, released to a BIKE_TAP_PERIOD period, slower than the chains +// that worked, so none is dropped. +inline constexpr Milliseconds BIKE_TAP(40); +inline constexpr Milliseconds BIKE_TAP_PERIOD(120); +// After a tap, how long until the screen shows whether it moved: the +// tile's 100ms and video lag. The Command File Runner's tap check judged +// its first tap too early, as unmoved. Not measured: a guess. +inline constexpr Milliseconds BIKE_TAP_SETTLE(250); +// SELECT, pressed to get on the registered Bicycle, and the wait after +// it for the player to be up on it. +inline constexpr Milliseconds BIKE_SELECT_PRESS(160); +inline constexpr Milliseconds BIKE_SELECT_SETTLE(1200); + // How long the overworld must show nothing (no box, fade or battle) after // a battle before the next tap: twice the lockout. inline constexpr Milliseconds AFTER_BATTLE_QUIET(1000); @@ -115,6 +141,9 @@ static_assert(TURN_TAP <= LONGEST_TAP && FACE_HOLD <= LONGEST_TAP && STEP_TAP <= static_assert(STEP_TAP + STEP_TAP_RELEASE > STEP_TAP_SPACING_WORKED); static_assert(STEP_TAP_SPACING_WORKED > STEP_TAP_LOCKOUT); static_assert(AFTER_BATTLE_QUIET > BATTLE_EXIT_LOCKOUT); +static_assert(BIKE_TAP_PERIOD > BIKE_TAP_PERIOD_WORKED && BIKE_TAP_PERIOD_WORKED > BIKE_TAP_DROP_PERIOD); +static_assert(BIKE_TAP < BIKE_TILE_TIME && BIKE_TAP <= LONGEST_TAP); +static_assert(BIKE_SELECT_SETTLE > BIKE_SELECT_TIME); } // namespace MovementMechanics diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp new file mode 100644 index 0000000000..03d1e829a4 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp @@ -0,0 +1,234 @@ +/* Overworld Bicycle Tests + * + * From: https://github.com/PokemonAutomation/ + * + * The Bicycle on the sim, on small made-up maps: its taps, each a tile + * and some dropped when they come too fast, open legs counted out a tap + * at a time, and SELECT getting on (console B, 2026-11-09). + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "PokemonFRLG_MovementMechanics.h" +#include "PokemonFRLG_OverworldMaps.h" +#include "PokemonFRLG_OverworldRecovery.h" +#include "PokemonFRLG_OverworldRoute.h" +#include "PokemonFRLG_OverworldSim.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +using namespace MovementMechanics; +using OverworldWalkSteps::BikeSelectCalls; + +struct BikeInput{ + std::string name; + UnitTestResult (*body)(); +}; +struct BikeExpectation{ + const char* description; +}; +struct BikeCase{ + BikeInput input; + BikeExpectation expectation; +}; + +class Test_OverworldBike : public UnitTest{ +public: + explicit Test_OverworldBike(BikeCase test_case) + : UnitTest("PokemonFRLG::OverworldBike - " + test_case.input.name) + , m_case(std::move(test_case)) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + try{ + return m_case.input.body(); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + BikeCase m_case; +}; + +// A field 28 tiles wide with a rock at (7, 2). +const std::vector& field(){ + static const std::vector MAPS{parse_map(R"({ + "name": "BikeField", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, + "tiles": [ + "##############################", + "#............................#", + "#......#.....................#", + "#............................#", + "##############################" + ] + })", "BikeField")}; + return MAPS; +} + +MapPosition at(int x, int y){ + return MapPosition{"BikeField", {x, y}}; +} + +std::string log_text(const std::vector& lines){ + std::string text; + for (const std::string& line : lines){ + text += "\n " + line; + } + return text; +} + +// `taps` taps west on the Bicycle, `period` apart, start to start. +SimState tap_chain(int taps, Milliseconds period){ + SimScript script; + script.on_bike = true; + OverworldSim sim(field(), at(28, 1), Direction::WEST, script); + for (int c = 0; c < taps; c++){ + sim.tap(Direction::WEST, BIKE_TAP, period - BIKE_TAP); + } + return sim.state(); +} + +// Console B, 2026-11-09: a chain of 40ms taps one per 100ms moved a tile +// a tap; one per 60ms dropped one tap in 12. The walker taps one per +// BIKE_TAP_PERIOD. +UnitTestResult test_tap_chains(){ + const SimState fast = tap_chain(25, Milliseconds(60)); + if (fast.bike_taps_dropped != 2 || fast.steps != 23){ + return std::format("25 taps 60ms apart: {} dropped, {} tiles; expected 2 and 23.", fast.bike_taps_dropped, fast.steps); + } + for (const Milliseconds period : {BIKE_TAP_PERIOD_WORKED, BIKE_TAP_PERIOD}){ + const SimState state = tap_chain(25, period); + if (state.bike_taps_dropped != 0 || state.steps != 25){ + return std::format("25 taps {}ms apart: {} dropped, {} tiles; expected none and 25.", period.count(), state.bike_taps_dropped, state.steps); + } + } + return true; +} + +RouteStep open_leg(Direction direction, int tiles, MapPosition end){ + RouteStep step; + step.direction = direction; + step.tiles = tiles; + step.facing_after = direction; + step.end = std::move(end); + return step; +} + +// An open leg of 11 tiles west from (20, 2), believed, then a tile north. +// The player starts 2 tiles further west, on (18, 2), and the rock at +// (7, 2) stops them a tile past the leg's end. Counted in taps, the +// blocked tap places them against the rock, and the route walks back. +// One hold ends against the rock too, taken for the leg's end, and the +// tile north ends a column off. +SimState walk_off_start(bool count_taps, std::string& failure, std::vector& log){ + const std::vector route{ + open_leg(Direction::WEST, 11, at(9, 2)), + open_leg(Direction::NORTH, 1, at(9, 1)), + }; + SimScript script; + script.on_bike = true; + OverworldSim sim(field(), at(18, 2), Direction::WEST, script); + SimWalkerRules rules; + rules.speed = MoveSpeed::BIKE; + rules.count_bike_taps = count_taps; + OverworldSimWalker walker(sim, rules); + const RoutePlanning planning{.maps = &field(), .costs = default_route_costs(), .start = at(20, 2), .label = "Sim"}; + failure = walker.run(route, planning); + log = walker.log(); + log.insert(log.end(), sim.state().log.begin(), sim.state().log.end()); + return sim.state(); +} + +UnitTestResult test_counted_leg_off_start(){ + std::string failure; + std::vector log; + const SimState held = walk_off_start(false, failure, log); + if (failure.empty() && held.position == at(9, 1)){ + return "One hold should end off: the sim doesn't reproduce the start's error." + log_text(log); + } + const SimState counted = walk_off_start(true, failure, log); + if (!failure.empty()){ + return "Counted in taps: " + failure + log_text(log); + } + if (!(counted.position == at(9, 1))){ + return "Counted in taps, ended on " + to_string(counted.position) + ", not " + to_string(at(9, 1)) + "." + log_text(log); + } + if (counted.bike_taps_dropped != 0 || !counted.violations.empty()){ + return "Counted in taps: a tap dropped or a violation." + log_text(log); + } + return true; +} + +// SELECT gets on the registered Bicycle only: a reload of a save made +// before REGISTER loses it. +UnitTestResult test_select_onto_bike(){ + for (const bool registered : {false, true}){ + SimScript script; + script.bike_registered = registered; + OverworldSim sim(field(), at(10, 1), Direction::WEST, script); + BikeSelectCalls calls; + int presses = 0; + calls.press_select = [&]{ presses++; sim.press_select(); }; + calls.riding = [&]{ return sim.state().on_bike; }; + calls.log = [](const std::string&){}; + const bool riding = OverworldWalkSteps::select_onto_bike(calls); + if (riding != registered || presses != (registered ? 1 : 2)){ + return std::format( + "Registered {}: riding {} after {} press(es).", registered, riding, presses + ); + } + } + return true; +} + +// A door takes the player off the Bicycle: legs after it go on foot, +// until one where the Bicycle may be ridden gets on again. +UnitTestResult test_speed_after_door(){ + RouteStep outdoors = open_leg(Direction::WEST, 3, at(7, 1)); + RouteStep indoors = outdoors; + indoors.walk_only = true; + using OverworldWalkSteps::gets_on_bike; + if (!gets_on_bike(outdoors, MoveSpeed::BIKE, false) || gets_on_bike(outdoors, MoveSpeed::BIKE, true) + || gets_on_bike(indoors, MoveSpeed::BIKE, false) || gets_on_bike(outdoors, MoveSpeed::RUN, false) + ){ + return "Expected to get on again outdoors only, off the Bicycle, on a ride."; + } + if (on_foot(MoveSpeed::BIKE) != MoveSpeed::RUN || on_foot(MoveSpeed::WALK) != MoveSpeed::WALK){ + return "Expected a run off the Bicycle."; + } + if (!OverworldWalkSteps::counts_bike_taps(outdoors, MoveSpeed::BIKE) || OverworldWalkSteps::counts_bike_taps(outdoors, MoveSpeed::RUN)){ + return "Expected open legs counted in taps on the Bicycle only."; + } + return true; +} + +const BikeCase OVERWORLD_BIKE_CASES[] = { + {.input = {"taps a tile each, one in 12 dropped 60ms apart", test_tap_chains}, .expectation = {"23 of 25 tiles at 60ms; 25 at 100 and 120ms"}}, + {.input = {"an open leg of 11 from a start 2 off, counted in taps", test_counted_leg_off_start}, .expectation = {"found against the rock and walked back"}}, + {.input = {"SELECT onto the registered Bicycle", test_select_onto_bike}, .expectation = {"one press registered; two and off unregistered"}}, + {.input = {"on foot after a door, on again outdoors", test_speed_after_door}, .expectation = {"gets_on_bike(), on_foot()"}}, +}; + +} // namespace + + +void add_tests_OverworldBike(UnitTestDatabase& database){ + for (const BikeCase& item : OVERWORLD_BIKE_CASES){ + database.add(item); + } +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.cpp index cddb0e2026..08492373f2 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.cpp @@ -313,7 +313,34 @@ class RouteRun{ return false; } const WalkedTiles walked = outcome.walked.value_or(whole_leg(step)); - return rejoin(step, candidates_on(step, start, walked), walked); + if (!outcome.blocked){ + return rejoin(step, candidates_on(step, start, walked), walked); + } + // Past the leg's end too: a start that was off ahead takes the + // taps there. + RouteStep longer = step; + longer.tiles += BLOCKED_TAP_SLACK; + return rejoin(step, blocked_ahead(step, candidates_on(longer, start, walked)), walked); + } + + // Of `candidates`, where a Bicycle tap along `step` stopped the + // player, those the maps block the way ahead of; all of them if none. + std::vector blocked_ahead(const RouteStep& step, std::vector candidates) const{ + std::vector blocked; + for (const StopCandidate& candidate : candidates){ + RouteStep ahead; + ahead.direction = step.direction; + ahead.tiles = 1; + ahead.surfing = step.surfing; + if (!candidate.trainer && leg_tiles(*maps(), candidate.player, ahead).size() < 2){ + blocked.push_back(candidate); + } + } + if (blocked.empty()){ + m_calls.log("Route: a Bicycle tap was blocked where the maps show the way open: finding the player from the tiles the taps put them on."); + return candidates; + } + return blocked; } // Whether `step`, a leg with wild battles stopped part-way, was stopped @@ -550,6 +577,27 @@ std::optional trainer_crossing( return crossing; } +StepOutcome count_bike_taps(const RouteStep& step, const BikeTapCalls& calls){ + int moved = 0; + for (int tap = 1; tap <= step.tiles; tap++){ + const BikeTap result = calls.tap(); + if (result.interrupt || result.cleared_late){ + calls.log(std::format("Bike: tap {} of {}: interrupted after {} tile(s).", tap, step.tiles, moved)); + if (result.interrupt){ + calls.clear(*result.interrupt); + } + const int most = std::min(moved + 1, step.tiles); + return StepOutcome{.end = StepEnd::STOPPED, .walked = WalkedTiles{moved, most}}; + } + if (!result.moved){ + calls.log(std::format("Bike: tap {} of {} didn't move: blocked after {} tile(s).", tap, step.tiles, moved)); + return StepOutcome{.end = StepEnd::STOPPED, .walked = WalkedTiles{moved, moved + BLOCKED_TAP_SLACK}, .blocked = true}; + } + moved++; + } + return StepOutcome{}; +} + void run_route( const std::vector& route, std::optional facing, const RoutePlanning* planning, const RouteCalls& calls diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.h index 18325a293a..8744c0089f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.h @@ -68,8 +68,42 @@ enum class StepEnd{ struct StepOutcome{ StepEnd end = StepEnd::DONE; std::optional walked; + // STOPPED by a Bicycle tap that didn't move the player: the tile + // ahead of where they stand is blocked (count_bike_taps()). + bool blocked = false; }; + +// One tap of an open leg on the Bicycle, counted out a tile a tap: whether +// the screen moved, and which of the leg's interrupts showed, if one did. +struct BikeTap{ + bool moved = false; + std::optional interrupt; + // The tap didn't move, and then an interrupt showed, which the tap + // cleared already: a trainer who saw the player, a battle seen late. + bool cleared_late = false; +}; +// The console side of count_bike_taps(): `tap` taps once and judges it; +// `clear` clears interrupt i. +struct BikeTapCalls{ + std::function tap; + std::function clear; + std::function log; +}; +// How far a sure start may be off before a blocked tap finds the player: +// three runs of 00.30 stood 3 tiles off (console B, 2026-11-09). +constexpr int BLOCKED_TAP_SLACK = 3; + +// Walks the open leg `step` a tap at a time: on the Bicycle each tap is +// exactly a tile (MovementMechanics::BIKE_TAP). A tap that doesn't move +// the player ends the leg there, blocked, and one with an interrupt ends +// it once that is cleared. DONE once all of the leg's taps moved; else +// STOPPED, with the tiles walked: blocked, the taps that moved, and up +// to BLOCKED_TAP_SLACK more for a start that was off (run_route() keeps +// the tiles with a blocked one ahead); interrupted, the taps that moved +// and one more, which may have moved before its battle showed. +StepOutcome count_bike_taps(const RouteStep& step, const BikeTapCalls& calls); + // Whether, after each leg to a wall, the walker taps once more toward the // wall and logs whether the player moved (see RouteCalls). enum class WallCheck{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp index 5a0e782939..08e449666c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp @@ -54,8 +54,80 @@ struct StepWalk{ std::optional stopped; WallClock hold_start; bool dark = false; + // Stopped by a Bicycle tap that didn't move (StepOutcome::blocked). + bool blocked = false; }; +// TODO(forest drift, 2026-11-01): the threshold is a diagnostic's guess. +// How much the game screen's top quarter, clear of the player in the +// middle, may change when a tap toward a wall only bumps the player (the +// player's own animation, an animated tile). A step scrolls the whole map +// by a tile. Not yet measured on the console: a guess on the safe side. +constexpr double WALL_TAP_MOVED_RMSD = 20.0; + +// How much the top quarter of the game screen changed from `before` to +// `after`: a step scrolls it by a tile. +double top_band_change(const ImageViewRGB32& before, const ImageViewRGB32& after){ + const ImageFloatBox game = GameSettings::instance().GAME_BOX; + const ImageFloatBox band(game.x, game.y, game.width, game.height * 0.25); + return ImageMatch::pixel_RMSD(extract_box_reference(before, band), extract_box_reference(after, band)); +} + +// One tap of a leg counted out on the Bicycle (count_bike_taps()): a +// BIKE_TAP press released to a BIKE_TAP_PERIOD period, then +// BIKE_TAP_SETTLE for the screen to show the step, watching `interrupts` +// throughout. A tap that didn't scroll the screen is watched on for a +// late interrupt, which is cleared. +BikeTap console_bike_tap( + ConsoleHandle& console, ProControllerContext& context, + const RouteStep& step, const OverworldInterrupts& interrupts +){ + context.wait_for_all_requests(); + const VideoSnapshot before = console.video().snapshot(); + std::vector> watchers; + std::vector callbacks; + for (const auto& interrupt : interrupts){ + watchers.emplace_back(interrupt->make_watcher()); + callbacks.emplace_back(*watchers.back()); + } + const int ret = run_until( + console, context, + [&](ProControllerContext& context){ + pbf_press_dpad(context, dpad(step.direction), BIKE_TAP, BIKE_TAP_PERIOD - BIKE_TAP); + pbf_wait(context, BIKE_TAP_SETTLE); + context.wait_for_all_requests(); + }, + callbacks + ); + if (ret >= 0){ + console.log("Bike: " + interrupts[ret]->name() + " on a tap of " + to_string(step) + ".", COLOR_ORANGE); + return BikeTap{.moved = false, .interrupt = (size_t)ret}; + } + const VideoSnapshot after = console.video().snapshot(); + if (before && after && top_band_change(before, after) > WALL_TAP_MOVED_RMSD){ + return BikeTap{.moved = true}; + } + const bool late = Pokemon::clear_late_interrupts(console, context, interrupts, AFTER_LEG_WATCH, MAX_INTERRUPTIONS_PER_LEG) > 0; + return BikeTap{.moved = false, .interrupt = std::nullopt, .cleared_late = late}; +} + +// An open leg on the Bicycle, a tap at a time (count_bike_taps()). +StepWalk walk_bike_taps( + ConsoleHandle& console, ProControllerContext& context, + const RouteStep& step, const OverworldInterruptSet& interrupts +){ + StepWalk out{std::nullopt, current_time(), false, false}; + const OverworldInterrupts& watched = step.wild ? interrupts.wild : interrupts.other; + BikeTapCalls calls; + calls.tap = [&]{ return console_bike_tap(console, context, step, watched); }; + calls.clear = [&](size_t index){ watched[index]->clear(console, context); }; + calls.log = [&](const std::string& message){ console.log(message, COLOR_ORANGE); }; + const StepOutcome outcome = count_bike_taps(step, calls); + out.stopped = outcome.walked; + out.blocked = outcome.blocked; + return out; +} + // stop_tiles(), logged when the leg started in a dark cave. WalkedTiles logged_stop_tiles(ConsoleHandle& console, const RouteStep& step, bool dark, WalkedTiles timed){ if (dark){ @@ -79,7 +151,10 @@ StepWalk walk_step( const RouteStep& step, std::optional facing, const OverworldInterruptSet& interrupts, MoveSpeed speed ){ - StepWalk out{std::nullopt, current_time(), false}; + if (counts_bike_taps(step, speed)){ + return walk_bike_taps(console, context, step, interrupts); + } + StepWalk out{std::nullopt, current_time(), false, false}; switch (step.action){ case RouteAction::WALK:{ // Turning first, with a tap, makes the hold start from a standstill @@ -191,7 +266,7 @@ StepOutcome walk_and_watch( const LegRecorder recorder(console, context, interrupts, step); const StepWalk walked = walk_step(console, context, step, facing, interrupts, speed); if (walked.stopped){ - return StepOutcome{.end = StepEnd::STOPPED, .walked = walked.stopped}; + return StepOutcome{.end = StepEnd::STOPPED, .walked = walked.stopped, .blocked = walked.blocked}; } if (step.action == RouteAction::FACE){ return StepOutcome{}; @@ -285,12 +360,6 @@ StepEnd watch_for_trainer( } // TODO(forest drift, 2026-11-01): diagnostic; remove or turn into a recovery once the cause is known. -// How much the game screen's top quarter, clear of the player in the -// middle, may change when a tap toward a wall only bumps the player (the -// player's own animation, an animated tile). A step scrolls the whole map -// by a tile. Not yet measured on the console: a guess on the safe side. -constexpr double WALL_TAP_MOVED_RMSD = 20.0; - // Taps once toward the wall `step` ended at (the player faces it, so a tap // steps if it can) and says whether the map scrolled: whether the player // moved instead of bumping into the wall. @@ -303,9 +372,7 @@ bool moved_past_wall(ConsoleHandle& console, ProControllerContext& context, cons if (!before || !after){ return false; } - const ImageFloatBox game = GameSettings::instance().GAME_BOX; - const ImageFloatBox band(game.x, game.y, game.width, game.height * 0.25); - const double change = ImageMatch::pixel_RMSD(extract_box_reference(before, band), extract_box_reference(after, band)); + const double change = top_band_change(before, after); console.log(std::format("Position check: a tap toward the wall of {} changed the screen by {:.1f}.", to_string(step), change)); return change > WALL_TAP_MOVED_RMSD; } @@ -327,12 +394,26 @@ void walk_overworld_route_with_recovery( if (!facing){ facing = read_facing(console, context); } + // A door takes the player off the Bicycle; the next leg where it may + // be ridden gets on again with SELECT. A battle leaves them on. + bool riding = speed == MoveSpeed::BIKE; RouteCalls calls; calls.walk = [&](const RouteStep& step, std::optional from_facing){ - return walk_and_watch(console, context, step, from_facing, interrupts, speed); + if (gets_on_bike(step, speed, riding)){ + get_on_bike(console, context); + riding = true; + from_facing = std::nullopt; + } + const StepOutcome outcome = walk_and_watch(console, context, step, from_facing, interrupts, riding ? speed : on_foot(speed)); + if (riding && step.warps){ + console.log("Bike: through the warp to " + step.end.map + ", off the Bicycle."); + riding = false; + } + return outcome; }; calls.hold_to_warp = [&](const RouteStep& step){ - hold_to_warp(console, context, step, step.wild ? interrupts.wild : interrupts.other, speed); + hold_to_warp(console, context, step, step.wild ? interrupts.wild : interrupts.other, riding ? speed : on_foot(speed)); + riding = false; }; calls.log = [&](const std::string& message){ console.log(message, COLOR_ORANGE); }; calls.fail = [&](const std::string& message){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp index 0bbc6fdce8..bce5aa34b0 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp @@ -148,6 +148,7 @@ OverworldSim::OverworldSim(std::vector maps, MapPosition start, Directi add_coded_field(); m_player = PlayerState{start.point, elevation_at(map(), start.point), false}; m_state.facing = facing; + m_state.on_bike = m_script.on_bike; show(m_script.start_menu_open ? SimScreen::START_MENU : SimScreen::OVERWORLD, m_now); update_view(); } @@ -366,6 +367,12 @@ bool OverworldSim::after_step(const Step& step, Milliseconds pressed){ m_maps[index].objects.push_back(tree); } m_cut_trees.clear(); + if (m_state.on_bike){ + // A door takes the player off the Bicycle (the user's + // knowledge, not measured). + m_state.on_bike = false; + note("the warp takes the player off the Bicycle."); + } m_frozen = true; schedule(pressed + timing.tile, SimScreen::FADE, false); schedule(pressed + timing.tile + timing.fade, SimScreen::OVERWORLD, true); @@ -466,12 +473,12 @@ void OverworldSim::bump(Direction direction, Milliseconds pressed){ // The steps a hold of `duration` takes, `short_by` fewer. void OverworldSim::walk_hold(Direction direction, Milliseconds duration, int short_by){ - const Milliseconds tile = m_script.timing.tile; + const Milliseconds tile = tile_time(); const Milliseconds end = m_now + duration; Milliseconds pressed = m_now; if (m_state.facing != direction){ // Holding through a turn lost a tile on the console, on foot. - pressed += m_script.on_bike ? Milliseconds(0) : tile; + pressed += m_state.on_bike ? Milliseconds(0) : tile; m_state.facing = direction; } take_steps(direction, pressed, end, (int)((end - pressed + tile - 1ms) / tile) - short_by); @@ -480,7 +487,7 @@ void OverworldSim::walk_hold(Direction direction, Milliseconds duration, int sho // Up to `left` steps of a hold of `direction` until `end`, the first // pressed at `pressed`. void OverworldSim::take_steps(Direction direction, Milliseconds pressed, Milliseconds end, int left){ - const Milliseconds tile = m_script.timing.tile; + const Milliseconds tile = tile_time(); for (; left > 0 && pressed < end; left--){ const std::optional step = press(direction); if (!step){ @@ -528,6 +535,8 @@ std::optional OverworldSim::hold( m_last_tap.reset(); if (m_state.screen != SimScreen::OVERWORLD || m_frozen){ stuck_hold(direction); + }else if (m_state.on_bike && duration <= MovementMechanics::LONGEST_TAP && drops_bike_tap(start)){ + // Dropped: no step. }else if (duration <= MovementMechanics::LONGEST_TAP && m_state.facing == direction){ // A hold as short as a tap is one (a one-tile leg). step_tap(direction, duration); @@ -546,7 +555,9 @@ void OverworldSim::tap(Direction direction, Milliseconds duration, Milliseconds m_last_tap.reset(); }else if (m_last_tap && m_last_tap->direction == direction && m_last_tap->end == start){ hold_on(direction, start + duration); - }else if (m_state.facing != direction && !m_script.on_bike){ + }else if (m_state.on_bike && drops_bike_tap(start)){ + m_last_tap.reset(); + }else if (m_state.facing != direction && !m_state.on_bike){ // From a standstill a tap only turns the player, even one as long // as LONGEST_TAP. Not on the Bicycle: there it steps. m_last_tap = TapPress{direction, start, start + duration, m_state.facing, 0}; @@ -567,7 +578,7 @@ void OverworldSim::tap(Direction direction, Milliseconds duration, Milliseconds void OverworldSim::step_tap(Direction direction, Milliseconds duration){ const Milliseconds start = m_now; m_last_tap = TapPress{direction, start, start + duration, direction, 0}; - if (!m_script.on_bike && m_last_step_tap && start < *m_last_step_tap + MovementMechanics::STEP_TAP_LOCKOUT){ + if (!m_state.on_bike && m_last_step_tap && start < *m_last_step_tap + MovementMechanics::STEP_TAP_LOCKOUT){ m_state.step_taps_ignored++; note(std::format( "the step tap {} is ignored, {} ms after the last.", @@ -587,7 +598,7 @@ void OverworldSim::step_tap(Direction direction, Milliseconds duration){ // hold, from the last one's start until `end`. void OverworldSim::hold_on(Direction direction, Milliseconds end){ TapPress& press = *m_last_tap; - const Milliseconds tile = m_script.timing.tile; + const Milliseconds tile = tile_time(); Milliseconds pressed = press.start; if (press.facing_before != direction){ pressed += tile; @@ -602,6 +613,35 @@ void OverworldSim::hold_on(Direction direction, Milliseconds end){ press.end = end; } +bool OverworldSim::drops_bike_tap(Milliseconds start){ + const bool fast = m_last_bike_tap && start - *m_last_bike_tap < MovementMechanics::BIKE_TAP_DROP_PERIOD; + m_last_bike_tap = start; + if (!fast || ++m_fast_bike_taps % MovementMechanics::BIKE_FAST_TAP_DROP_EVERY != 0){ + return false; + } + m_state.bike_taps_dropped++; + note(std::format("a Bicycle tap is dropped, {} fast taps in.", m_fast_bike_taps)); + return true; +} + +bool OverworldSim::press_select(){ + const Milliseconds start = m_now; + if (m_state.screen != SimScreen::OVERWORLD || m_frozen || !m_script.bike_registered){ + note(m_script.bike_registered ? "SELECT does nothing off the overworld." : "SELECT does nothing: the Bicycle isn't registered."); + advance_to(start + MovementMechanics::BIKE_SELECT_PRESS); + return false; + } + if (!m_state.on_bike && !map().allow_running){ + note("SELECT: the Bicycle can't be ridden here."); + advance_to(start + MovementMechanics::BIKE_SELECT_TIME); + return false; + } + m_state.on_bike = !m_state.on_bike; + note(m_state.on_bike ? "gets on the Bicycle." : "gets off the Bicycle."); + advance_to(start + MovementMechanics::BIKE_SELECT_TIME); + return true; +} + bool OverworldSim::drops_turn_tap(){ m_turn_taps++; return m_script.dropped_turn_taps.contains(m_turn_taps); @@ -689,6 +729,8 @@ Milliseconds delay_of(const OverworldInterrupts& interrupts, size_t index){ OverworldSimWalker::OverworldSimWalker(OverworldSim& sim, SimWalkerRules rules, const OverworldInterruptSet& walker) : m_sim(sim) , m_rules(rules) + , m_riding(rules.speed == MoveSpeed::BIKE) + , m_speed(rules.speed) { // In the walker's order: battles first. m_wild = { @@ -813,7 +855,7 @@ std::optional OverworldSimWalker::walk_step(const RouteStep& step, if (step.action != RouteAction::WALK){ fail("Sim: Surf isn't modelled."); } - if (turns_first(step, m_rules.speed, facing)){ + if (turns_first(step, m_speed, facing)){ sim_turn_to(m_sim, step.direction, m_rules.confirm_turn); } const int warps_before = m_sim.state().warps; @@ -822,7 +864,7 @@ std::optional OverworldSimWalker::walk_step(const RouteStep& step, leg.direction = step.direction; leg.tiles = step.tiles + step.warp_slack; leg.to_wall = step.to_wall; - leg.tile_time = tile_time(step, m_rules.speed); + leg.tile_time = tile_time(step, m_speed); leg.stop_on_interrupt = !step.to_wall; leg.stop_if_blocked = step.to_wall && m_rules.stop_if_blocked; if (m_rules.hold_over_connections){ @@ -856,8 +898,56 @@ std::optional OverworldSimWalker::walk_step(const RouteStep& step, return std::nullopt; } -// walk_and_watch(). +BikeTap OverworldSimWalker::bike_tap(const RouteStep& step){ + const MapPosition before = m_sim.state().position; + const std::vector& ints = interrupts(step); + const Milliseconds release = m_rules.bike_tap_period - BIKE_TAP; + const std::optional seen = m_sim.hold(step.direction, BIKE_TAP, release, m_rules.bike_tap_period + BIKE_TAP_SETTLE, watchers(ints)); + if (seen){ + m_log.push_back("Bike: " + ints[seen->watcher].name + " on a tap of " + to_string(step) + "."); + return BikeTap{.moved = false, .interrupt = seen->watcher}; + } + if (!(m_sim.state().position == before)){ + return BikeTap{.moved = true}; + } + return BikeTap{.moved = false, .interrupt = std::nullopt, .cleared_late = clear_late(ints, AFTER_LEG_WATCH, MAX_INTERRUPTIONS_PER_LEG) > 0}; +} + +StepOutcome OverworldSimWalker::walk_bike_taps(const RouteStep& step){ + const std::vector& ints = interrupts(step); + BikeTapCalls calls; + calls.tap = [&]{ return bike_tap(step); }; + calls.clear = [&](size_t index){ clear(ints[index]); }; + calls.log = [&](const std::string& message){ m_log.push_back(message); }; + m_leg_start = m_sim.state().time; + m_leg_dark = m_sim.state().dark; + return count_bike_taps(step, calls); +} + +// walk_and_watch(), getting on the Bicycle first where +// walk_overworld_route_with_recovery() does. StepOutcome OverworldSimWalker::walk(const RouteStep& step, std::optional facing){ + if (m_rules.get_back_on_bike && gets_on_bike(step, m_rules.speed, m_riding)){ + BikeSelectCalls select; + select.press_select = [&]{ m_sim.press_select(); }; + select.riding = [&]{ return m_sim.state().on_bike; }; + select.log = [&](const std::string& message){ m_log.push_back(message); }; + if (!select_onto_bike(select)){ + fail("get_on_bike(): not on the Bicycle after SELECT twice. Is it registered?"); + } + m_riding = true; + facing = std::nullopt; + } + m_speed = m_riding || !m_rules.get_back_on_bike ? m_rules.speed : on_foot(m_rules.speed); + const StepOutcome outcome = walk_ridden(step, facing); + if (m_riding && step.warps){ + m_log.push_back("Bike: through the warp to " + step.end.map + ", off the Bicycle."); + m_riding = false; + } + return outcome; +} + +StepOutcome OverworldSimWalker::walk_ridden(const RouteStep& step, std::optional facing){ if (m_rules.battle_before_step && battle_on_its_way()){ m_log.push_back("Route: " + std::string(sim_screen_name(m_sim.state().screen)) + " before " + to_string(step) + ": watching on for a wild battle set off before it."); if (clear_late(m_wild, UNSEEN_BATTLE_WATCH, MAX_INTERRUPTIONS_PER_LEG) > 0){ @@ -869,7 +959,12 @@ StepOutcome OverworldSimWalker::walk(const RouteStep& step, std::optional walked = walk_step(step, facing)){ + if (m_rules.count_bike_taps && counts_bike_taps(step, m_speed)){ + const StepOutcome outcome = walk_bike_taps(step); + if (outcome.end != StepEnd::DONE){ + return outcome; + } + }else if (const std::optional walked = walk_step(step, facing)){ return StepOutcome{.end = StepEnd::STOPPED, .walked = walked}; } if (step.action == RouteAction::FACE){ @@ -920,6 +1015,8 @@ size_t OverworldSimWalker::clear_unseen_battle(const RouteStep& step){ } void OverworldSimWalker::hold_to_warp(const RouteStep& step){ + // The warp it holds on to takes the player off the Bicycle. + m_riding = false; const std::vector& ints = interrupts(step); std::vector fade_first{SimWatcher::FADE}; for (const SimWatcher watcher : watchers(ints)){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h index e1761f1248..2f6c0c83df 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h @@ -20,8 +20,10 @@ * A step onto a connected map waits a tile first, as the console does * while the game loads the map. * - * Not modelled: Surf, bikes, running, HP and status. Wandering people - * stand on their tile. + * Not modelled: Surf, running, HP and status. Wandering people stand on + * their tile. The Bicycle is: its pace, its taps (each a tile, some + * dropped when they come too fast), SELECT and doors taking the player + * off it. * */ @@ -87,6 +89,8 @@ struct SimEncounter{ // at these. struct SimTiming{ Milliseconds tile = MovementMechanics::TILE_TIME; + // A tile on the Bicycle. + Milliseconds bike_tile = MovementMechanics::BIKE_TILE_TIME; Milliseconds flash{300}; Milliseconds battle_screen{3700}; // A wild battle's text box, before the HP boxes (battle_screen). Not @@ -136,9 +140,15 @@ struct SimScript{ // too, a hold through a turn loses no tile // (MovementMechanics::BIKE_TAP_ONLY_TURNS), and no step tap is // ignored: a 40ms turn tap and a 56ms hold 340ms after it took two - // tiles (console B, 2026-11-09, 00.30). Set timing.tile to - // MovementMechanics::BIKE_TILE_TIME with it. + // tiles (console B, 2026-11-09, 00.30). At the start: SELECT gets on + // and off (OverworldSim::press_select()), a warp gets off. Each tap is + // a tile; one in MovementMechanics::BIKE_FAST_TAP_DROP_EVERY of those + // that start sooner than BIKE_TAP_DROP_PERIOD after the last is + // dropped. Its pace is timing.bike_tile. bool on_bike = false; + // The Bicycle is registered (Key Items, REGISTER): SELECT gets on. A + // reload of a save made before it was registered loses it. + bool bike_registered = false; // Caves dark without Flash (Rock Tunnel): the overworld is black but // for a lit circle on the player, which the walker's battle text box // check and screen observer read as a battle on its way (console B, @@ -180,6 +190,10 @@ struct SimState{ Milliseconds in_wild_battles{0}; int signs_read = 0; int turn_taps_dropped = 0; + // On the Bicycle (SimScript::on_bike). + bool on_bike = false; + // Bicycle taps the game dropped, too soon after the last one. + int bike_taps_dropped = 0; // Step taps the game ignored, inside the lockout after the last one // (MovementMechanics::STEP_TAP_LOCKOUT). int step_taps_ignored = 0; @@ -268,6 +282,13 @@ class OverworldSim{ Milliseconds release = MovementMechanics::TURN_SETTLE ); + // SELECT on the overworld: with the Bicycle registered, gets on it + // where running is allowed, or off it, taking about + // MovementMechanics::BIKE_SELECT_TIME. Returns whether it did. + bool press_select(); + // REGISTER on the Bicycle in the bag (SimScript::bike_registered). + void register_bike(){ m_script.bike_registered = true; } + // Watches `watchers` for `duration`, as wait_until(). std::optional watch(const std::vector& watchers, Milliseconds duration); @@ -344,6 +365,13 @@ class OverworldSim{ std::optional next_encounter(); bool rolls_encounter(int rate); bool drops_turn_tap(); + // Whether the Bicycle tap starting at `start` is dropped, too soon + // after the last one. + bool drops_bike_tap(Milliseconds start); + // How long a tile takes: on the Bicycle or on foot. + Milliseconds tile_time() const{ + return m_state.on_bike ? m_script.timing.bike_tile : m_script.timing.tile; + } bool trainer_sees(Milliseconds pressed); void update_view(); void note(const std::string& line); @@ -378,6 +406,9 @@ class OverworldSim{ int m_turn_taps = 0; // The last tap, until a hold. std::optional m_last_tap; + // When the last Bicycle tap started, and how many came too soon. + std::optional m_last_bike_tap; + int m_fast_bike_taps = 0; // When the last tap that stepped started. std::optional m_last_step_tap; // The end of the lockout after the last battle. @@ -459,6 +490,19 @@ struct SimWalkerRules{ // The speed the walker goes at: its turn taps // (OverworldWalkSteps::turns_first()) and its holds' tile time. MoveSpeed speed = MoveSpeed::WALK; + // An open leg on the Bicycle is counted out a tap at a time, each a + // tile (count_bike_taps()), and one that doesn't move finds the player + // ahead of a block. Without it the leg is one hold, and a start off + // along the leg ends it off with nothing to show it (console B, + // 2026-11-09, 00.30: three runs 3 tiles east after Route 8's legs). + bool count_bike_taps = true; + // How far apart, start to start, the counted taps are + // (MovementMechanics::BIKE_TAP_PERIOD). + Milliseconds bike_tap_period = MovementMechanics::BIKE_TAP_PERIOD; + // After a warp, which takes the player off the Bicycle, the next leg + // where it may be ridden gets on again with SELECT. Without it the + // walker held on-foot legs at the Bicycle's pace. + bool get_back_on_bike = true; // A dark cave's overworld isn't a battle on its way // (OverworldWalkSteps::dark_cave_screen()). Without it the check // before each step watched for a battle on it (console B, 2026-11-09, @@ -522,6 +566,10 @@ class OverworldSimWalker{ bool battle; }; StepOutcome walk(const RouteStep& step, std::optional facing); + StepOutcome walk_ridden(const RouteStep& step, std::optional facing); + // count_bike_taps() on the sim. + StepOutcome walk_bike_taps(const RouteStep& step); + BikeTap bike_tap(const RouteStep& step); std::optional late_stop(const RouteStep& step); // stop_tiles(); without SimWalkerRules::whole_leg_in_dark, the dark // screen places the stop at the hold's start. @@ -559,6 +607,10 @@ class OverworldSimWalker{ // and whether its map was dark then. Milliseconds m_leg_start{0}; bool m_leg_dark = false; + // Whether the walker takes the player to ride the Bicycle, and the + // speed of the step under way. + bool m_riding = false; + MoveSpeed m_speed = MoveSpeed::WALK; }; @@ -569,6 +621,8 @@ void add_tests_OverworldWarpFade(UnitTestDatabase& database); // Register the tests of spinner tiles, the lift's panel and the poster // switch on made-up maps. void add_tests_OverworldSpin(UnitTestDatabase& database); +// Register the tests of the Bicycle's taps and SELECT on a made-up map. +void add_tests_OverworldBike(UnitTestDatabase& database); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp index ce0f2054ae..cfa1e6b021 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp @@ -987,6 +987,7 @@ void add_tests_OverworldSim(UnitTestDatabase& database){ add_tests_OverworldLateBattle(database); add_tests_OverworldWarpFade(database); add_tests_OverworldSpin(database); + add_tests_OverworldBike(database); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp index 92c49a4ed5..99296be9bd 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp @@ -209,6 +209,58 @@ bool turns_first(const RouteStep& step, MoveSpeed speed, std::optional 0; +} + +bool gets_on_bike(const RouteStep& step, MoveSpeed speed, bool riding){ + return speed == MoveSpeed::BIKE && !riding && step.action == RouteAction::WALK + && !step.walk_only && !step.surfing; +} + +bool select_onto_bike(const BikeSelectCalls& calls){ + for (int press = 1; press <= 2; press++){ + calls.press_select(); + if (calls.riding()){ + return true; + } + calls.log(std::format("Bike: not riding after SELECT press {}.", press)); + } + return false; +} + +bool screen_shows_riding(const ImageViewRGB32& screen){ + if (StartMenuDetector(COLOR_RED).detect(screen) || SelectionDialogDetector(COLOR_RED).detect(screen) + || WhiteDialogDetector(COLOR_RED).detect(screen) + ){ + return false; + } + return !read_player_facing(screen).has_value(); +} + +void get_on_bike(ConsoleHandle& console, ProControllerContext& context){ + BikeSelectCalls calls; + calls.press_select = [&]{ + pbf_press_button(context, BUTTON_MINUS, BIKE_SELECT_PRESS, BIKE_SELECT_SETTLE); + }; + calls.riding = [&]{ + context.wait_for_all_requests(); + VideoSnapshot screen = console.video().snapshot(); + return screen && screen_shows_riding(*screen.frame); + }; + calls.log = [&](const std::string& message){ console.log(message, COLOR_ORANGE); }; + console.log("Bike: SELECT (MINUS) onto the Bicycle."); + if (!select_onto_bike(calls)){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, + "get_on_bike(): not on the Bicycle after SELECT twice. Is it registered?", + console + ); + } +} + Pokemon::LegMover leg_mover(const RouteStep& step, MoveSpeed speed){ if (step_speed(step, speed) == MoveSpeed::RUN && !step.surfing){ return hold_dpad_running; @@ -490,6 +542,10 @@ const char* move_speed_name(MoveSpeed speed){ return "?"; } +MoveSpeed on_foot(MoveSpeed speed){ + return speed == MoveSpeed::BIKE ? MoveSpeed::RUN : speed; +} + OverworldInterruptSet overworld_interrupts_with_repel(int max_repel_slot){ OverworldInterruptSet ret; ret.wild.emplace_back(std::make_unique()); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h index 7be9b789cb..990f7656d0 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h @@ -82,6 +82,9 @@ enum class MoveSpeed{ }; // Name of a movement speed. const char* move_speed_name(MoveSpeed speed); +// The speed walked at instead of `speed` while off the Bicycle (a door +// took the player off): a run. +MoveSpeed on_foot(MoveSpeed speed); // Walks `route` from where it was planned, at `speed`. The player must be @@ -160,6 +163,39 @@ Milliseconds tile_time(const RouteStep& step, MoveSpeed speed); // takes a step (MovementMechanics::BIKE_TAP_ONLY_TURNS). bool turns_first(const RouteStep& step, MoveSpeed speed, std::optional facing); +// Whether `step` at `speed` is walked a tap at a time, each a tile +// (count_bike_taps()): an open leg on the Bicycle, outdoors, not onto a +// warp or a spinner, and not over a map connection, where the game holds +// the player up a tile (a tap there isn't measured). Legs to a wall are +// held: the wall ends them. +bool counts_bike_taps(const RouteStep& step, MoveSpeed speed); + +// Whether, walking a route at `speed` and riding the Bicycle or not +// (`riding`), the player gets on with SELECT before `step`: a WALK where +// the Bicycle may be ridden, after a door took them off. +bool gets_on_bike(const RouteStep& step, MoveSpeed speed, bool riding); + +// The console side of select_onto_bike(): presses SELECT and waits it +// out; reads whether the player rides. +struct BikeSelectCalls{ + std::function press_select; + std::function riding; + std::function log; +}; +// Gets on the registered Bicycle with SELECT, checks the player rides, +// and presses again once if not. Returns whether they ride. +bool select_onto_bike(const BikeSelectCalls& calls); + +// Whether the player rides the Bicycle, from the screen: the overworld +// is up, and no standing player's sprite shows where the player stands +// (read_player_facing()). False under a message or a menu. +bool screen_shows_riding(const ImageViewRGB32& screen); + +// select_onto_bike() on the console: SELECT is the Switch's MINUS. +// Throws OperationFailedException if the player doesn't ride after two +// presses. +void get_on_bike(ConsoleHandle& console, ProControllerContext& context); + // The mover for `step` at `speed`: hold_dpad_running() for a run on foot // where the map allows it, hold_dpad() otherwise (B does nothing on the // water). diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index cbf6bbdbf8..ec96fcbca3 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1887,6 +1887,7 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp From 0a9bb40e98274f3ae2d34109ff28f9dee760fe7b Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 23:00:37 -0800 Subject: [PATCH 21/28] FRLG AutoStory: register the Bicycle in 00.30, ride_bike presses SELECT A new GAME step, register_bike, REGISTERs the BICYCLE from Key Items (its menu's second option) once a save. It sits at the start of 00.30's plan until 00.26's plan (frlg/stage7-celadon, #224) can take it right after the Bicycle. ride_bike now gets on with SELECT (MINUS) and checks the player rides (get_on_bike()) instead of using the bag. The golden story sim trace gains register_bike's three lines and its stub, and 00.30's later events are numbered one up. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../PokemonFRLG_AutoStoryHideout.cpp | 10 +- .../PokemonFRLG_AutoStoryItemUse.cpp | 39 +++++++- .../AutoStory/PokemonFRLG_AutoStoryItemUse.h | 15 ++- .../PokemonFRLG_AutoStoryItemUse_Tests.cpp | 4 + .../ReplayData/frlg-story-sim-trace.json | 97 ++++++++++--------- .../AutoStory/StoryData/frlg-00.30.plan.json | 9 +- 6 files changed, 118 insertions(+), 56 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp index d3eda919e7..77d1aac88c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp @@ -6,6 +6,7 @@ #include #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" #include "PokemonFRLG_AutoStoryActions.h" #include "PokemonFRLG_AutoStoryHideout.h" #include "PokemonFRLG_AutoStoryItemUse.h" @@ -132,9 +133,14 @@ std::optional run_field_game_step( if (!is_item_use_step(step)){ return std::nullopt; } - const std::string item = parse_use_item_step(step).value_or("bicycle"); + if (step.variant == RIDE_BIKE_VARIANT){ + OverworldWalkSteps::get_on_bike(console, context); + return done(); + } ConsoleItemUseActions bag(console, context, language); - const ItemUseEnd end = use_item_from_bag(console.logger(), bag, item); + const ItemUseEnd end = step.variant == REGISTER_BIKE_VARIANT + ? register_item_in_bag(console.logger(), bag, "bicycle") + : use_item_from_bag(console.logger(), bag, *parse_use_item_step(step)); if (end != ItemUseEnd::USED){ return Pokemon::outcome_failed(std::string("use_item_") + item_use_end_name(end), step.variant); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp index 59e0c00b81..5f8ecc77aa 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp @@ -52,7 +52,8 @@ std::optional parse_use_item_step(const Pokemon::StoryStep& step){ return std::string(item); } bool is_item_use_step(const Pokemon::StoryStep& step){ - return (step.variant == RIDE_BIKE_VARIANT && step.args.empty()) || parse_use_item_step(step).has_value(); + const bool bike = step.variant == RIDE_BIKE_VARIANT || step.variant == REGISTER_BIKE_VARIANT; + return (bike && step.args.empty()) || parse_use_item_step(step).has_value(); } std::optional bag_row_of(const std::vector& rows, std::string_view item){ @@ -78,7 +79,13 @@ const char* item_use_end_name(ItemUseEnd end){ return "?"; } -ItemUseEnd use_item_from_bag(Logger& logger, ItemUseActions& actions, std::string_view item){ +namespace{ + +// Opens the item's pocket, finds it and `uses` it, then back to the +// overworld. +ItemUseEnd with_bag_row( + Logger& logger, ItemUseActions& actions, std::string_view item, bool registers +){ const std::optional pocket = usable_item_pocket(item); if (!pocket){ return ItemUseEnd::UNKNOWN_ITEM; @@ -93,12 +100,25 @@ ItemUseEnd use_item_from_bag(Logger& logger, ItemUseActions& actions, std::strin actions.close_bag(); return ItemUseEnd::NONE_LEFT; } - logger.log("Bag: using the " + std::string(item) + " on row " + std::to_string(*row + 1) + "."); - actions.use_row(*row); + logger.log("Bag: " + std::string(registers ? "registering" : "using") + " the " + std::string(item) + " on row " + std::to_string(*row + 1) + "."); + if (registers){ + actions.register_row(*row); + }else{ + actions.use_row(*row); + } actions.close_bag(); return ItemUseEnd::USED; } +} // namespace + +ItemUseEnd use_item_from_bag(Logger& logger, ItemUseActions& actions, std::string_view item){ + return with_bag_row(logger, actions, item, false); +} +ItemUseEnd register_item_in_bag(Logger& logger, ItemUseActions& actions, std::string_view item){ + return with_bag_row(logger, actions, item, true); +} + ItemUseEnd renew_repel(Logger& logger, ItemUseActions& actions, std::string_view item){ if (!is_repel(item)){ return ItemUseEnd::UNKNOWN_ITEM; @@ -166,6 +186,17 @@ void ConsoleItemUseActions::use_row(size_t row){ pbf_press_button(m_context, BUTTON_A, 160ms, 1500ms); } +void ConsoleItemUseActions::register_row(size_t row){ + for (size_t c = 0; c < row; c++){ + pbf_press_dpad(m_context, DPAD_DOWN, CURSOR_TAP, CURSOR_WAIT); + } + // The item's menu: USE, REGISTER, ... Not timed on the console: the + // USE waits. + pbf_press_button(m_context, BUTTON_A, 160ms, 800ms); + pbf_press_dpad(m_context, DPAD_DOWN, CURSOR_TAP, CURSOR_WAIT); + pbf_press_button(m_context, BUTTON_A, 160ms, 1500ms); +} + void ConsoleItemUseActions::close_bag(){ // B until the overworld: past " used the REPEL.", out of the // bag and the START menu. The Escape Rope and the Bicycle leave the diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h index 74949be3d8..780a5ce463 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h @@ -41,9 +41,15 @@ inline constexpr Pokemon::StoryArgSpec USE_ITEM_ARGS[] = { {"item", Pokemon::StoryArgType::TEXT, true}, {"slot", Pokemon::StoryArgType::NUMBER, false}, }; -// The GAME step that gets on the Bicycle (the Key Items pocket's BICYCLE, -// USE). The ROUTE steps after it ride (BIKE_VARIANT). +// The GAME step that gets on the registered Bicycle with SELECT +// (OverworldWalkSteps::get_on_bike()). The ROUTE steps after it ride +// (BIKE_VARIANT). inline const std::string RIDE_BIKE_VARIANT = "ride_bike"; +// The GAME step that registers the Bicycle for SELECT: the Key Items +// pocket's BICYCLE, REGISTER, its menu's second option. Once a save: +// the registration is saved with the game, and a save made before it +// doesn't have it. +inline const std::string REGISTER_BIKE_VARIANT = "register_bike"; // The bag's pockets, in the order LEFT and RIGHT go through them. enum class BagPocket{ @@ -80,6 +86,8 @@ class ItemUseActions{ virtual std::vector read_pocket() = 0; // Moves to row `row`, USE, and through what the item says. virtual void use_row(size_t row) = 0; + // Moves to row `row` and REGISTERs it for SELECT. + virtual void register_row(size_t row) = 0; // Back to the overworld (nothing to do if the item closed the bag). virtual void close_bag() = 0; }; @@ -100,6 +108,8 @@ const char* item_use_end_name(ItemUseEnd end); // Opens the item's pocket, finds it and uses it, then back to the // overworld. ItemUseEnd use_item_from_bag(Logger& logger, ItemUseActions& actions, std::string_view item); +// The same, REGISTERing it for SELECT (the Bicycle) instead of USE. +ItemUseEnd register_item_in_bag(Logger& logger, ItemUseActions& actions, std::string_view item); // After "REPEL's effect wore off..." has closed: FRLG asks nothing, so // another of the same Repel is used if the bag has one. NONE_LEFT is no @@ -115,6 +125,7 @@ class ConsoleItemUseActions : public ItemUseActions{ virtual bool open_pocket(BagPocket pocket) override; virtual std::vector read_pocket() override; virtual void use_row(size_t row) override; + virtual void register_row(size_t row) override; virtual void close_bag() override; private: diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp index 96674e8fd6..66c2f36a92 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp @@ -36,6 +36,7 @@ class SimBag : public ItemUseActions{ std::map> pockets; bool opens = true; std::vector used; + std::vector registered; bool open = false; virtual bool open_pocket(BagPocket pocket) override{ @@ -53,6 +54,9 @@ class SimBag : public ItemUseActions{ item.count--; } } + virtual void register_row(size_t row) override{ + registered.push_back(pockets[m_pocket][row].item); + } virtual void close_bag() override{ open = false; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json index 179242eb0b..da375eaaf3 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json @@ -1201,81 +1201,84 @@ "[Checkpoint] 00.29: done, 7 run, 0 skipped", "[Checkpoint] 00.29: passed on attempt 1 (one-shot)", "begin 00.30", - "[Checkpoint] 00.30: start, 23 events", + "[Checkpoint] 00.30: start, 24 events", "[Event] 00.30/1 from_save: start", "[Event] 00.30/1 from_save: done", - "[Event] 00.30/2 east_of_mart_door: start", + "[Event] 00.30/2 register_bike: start", + "00.30/register_bike: ok at -", + "[Event] 00.30/2 register_bike: done", + "[Event] 00.30/3 east_of_mart_door: start", "00.30/east_of_mart_door: ok at LavenderTown (21, 16) facing east (tile)", - "[Event] 00.30/2 east_of_mart_door: done", - "[Event] 00.30/3 into_lavender_mart: start", + "[Event] 00.30/3 east_of_mart_door: done", + "[Event] 00.30/4 into_lavender_mart: start", "00.30/into_lavender_mart: ok at LavenderTown_Mart (4, 2) facing north (tile)", - "[Event] 00.30/3 into_lavender_mart: done", - "[Event] 00.30/4 lavender_mart_clerk: start", + "[Event] 00.30/4 into_lavender_mart: done", + "[Event] 00.30/5 lavender_mart_clerk: start", "00.30/lavender_mart_clerk: ok at LavenderTown_Mart (4, 3) facing west (tile)", - "[Event] 00.30/4 lavender_mart_clerk: done", - "[Event] 00.30/5 buy_ropes_and_super_repels: start", + "[Event] 00.30/5 lavender_mart_clerk: done", + "[Event] 00.30/6 buy_ropes_and_super_repels: start", "00.30/buy_ropes_and_super_repels: ok at LavenderTown_Mart (4, 3) facing west (tile)", - "[Event] 00.30/5 buy_ropes_and_super_repels: done", - "[Event] 00.30/6 out_of_lavender_mart: start", + "[Event] 00.30/6 buy_ropes_and_super_repels: done", + "[Event] 00.30/7 out_of_lavender_mart: start", "00.30/out_of_lavender_mart: ok at LavenderTown (20, 16) facing south (tile)", - "[Event] 00.30/6 out_of_lavender_mart: done", - "[Event] 00.30/7 ride_bike: start", + "[Event] 00.30/7 out_of_lavender_mart: done", + "[Event] 00.30/8 ride_bike: start", "00.30/ride_bike: ok at LavenderTown (20, 16) facing south (tile)", - "[Event] 00.30/7 ride_bike: done", - "[Event] 00.30/8 lavender_west_edge: start", + "[Event] 00.30/8 ride_bike: done", + "[Event] 00.30/9 lavender_west_edge: start", "00.30/lavender_west_edge: ok at LavenderTown (1, 10) facing south (tile)", - "[Event] 00.30/8 lavender_west_edge: done", - "[Event] 00.30/9 route8_east_wall: start", + "[Event] 00.30/9 lavender_west_edge: done", + "[Event] 00.30/10 route8_east_wall: start", "00.30/route8_east_wall: ok at Route8 (62, 10) facing west (tile)", - "[Event] 00.30/9 route8_east_wall: done", - "[Event] 00.30/10 east_of_rich_line: start", + "[Event] 00.30/10 route8_east_wall: done", + "[Event] 00.30/11 east_of_rich_line: start", "00.30/east_of_rich_line: ok at Route8 (57, 18) facing west (tile)", - "[Event] 00.30/10 east_of_rich_line: done", - "[Event] 00.30/11 rich_line: start", + "[Event] 00.30/11 east_of_rich_line: done", + "[Event] 00.30/12 rich_line: start", "00.30/rich_line: ok at Route8 (55, 18) facing west (tile)", - "[Event] 00.30/11 rich_line: done", - "[Event] 00.30/12 gamer_rich: start", + "[Event] 00.30/12 rich_line: done", + "[Event] 00.30/13 gamer_rich: start", "00.30/gamer_rich: ok at -", - "[Event] 00.30/12 gamer_rich: done", - "[Event] 00.30/13 west_of_glenn_column: start", + "[Event] 00.30/13 gamer_rich: done", + "[Event] 00.30/14 west_of_glenn_column: start", "00.30/west_of_glenn_column: ok at Route8 (48, 7) facing north (tile)", - "[Event] 00.30/13 west_of_glenn_column: done", - "[Event] 00.30/14 route8_row7_west_wall: start", + "[Event] 00.30/14 west_of_glenn_column: done", + "[Event] 00.30/15 route8_row7_west_wall: start", "00.30/route8_row7_west_wall: ok at Route8 (26, 7) facing west (tile)", - "[Event] 00.30/14 route8_row7_west_wall: done", - "[Event] 00.30/15 route8_row14_west_wall: start", + "[Event] 00.30/15 route8_row7_west_wall: done", + "[Event] 00.30/16 route8_row14_west_wall: start", "00.30/route8_row14_west_wall: ok at Route8 (12, 14) facing west (tile)", - "[Event] 00.30/15 route8_row14_west_wall: done", - "[Event] 00.30/16 east_of_underground_door: start", + "[Event] 00.30/16 route8_row14_west_wall: done", + "[Event] 00.30/17 east_of_underground_door: start", "00.30/east_of_underground_door: ok at Route8 (14, 5) facing north (tile)", - "[Event] 00.30/16 east_of_underground_door: done", - "[Event] 00.30/17 below_underground_door: start", + "[Event] 00.30/17 east_of_underground_door: done", + "[Event] 00.30/18 below_underground_door: start", "00.30/below_underground_door: ok at Route8 (13, 5) facing north (tile)", - "[Event] 00.30/17 below_underground_door: done", - "[Event] 00.30/18 route7_underground_exit: start", + "[Event] 00.30/18 below_underground_door: done", + "[Event] 00.30/19 route7_underground_exit: start", "00.30/route7_underground_exit: ok at Route7 (7, 15) facing south (tile)", - "[Event] 00.30/18 route7_underground_exit: done", - "[Event] 00.30/19 pidgey_grass: start", + "[Event] 00.30/19 route7_underground_exit: done", + "[Event] 00.30/20 pidgey_grass: start", "00.30/pidgey_grass: ok at Route7 (14, 6) facing east (tile)", - "[Event] 00.30/19 pidgey_grass: done", - "[Event] 00.30/20 catch_pidgey: start", + "[Event] 00.30/20 pidgey_grass: done", + "[Event] 00.30/21 catch_pidgey: start", "00.30/catch_pidgey: ok at Route7 (14, 6) (15, 6) (set)", - "[Event] 00.30/20 catch_pidgey: done", - "[Event] 00.30/21 celadon_below_center: start", + "[Event] 00.30/21 catch_pidgey: done", + "[Event] 00.30/22 celadon_below_center: start", "00.30/celadon_below_center: ok at CeladonCity (48, 12) facing north (tile)", - "[Event] 00.30/21 celadon_below_center: done", - "[Event] 00.30/22 celadon_nurse: start", + "[Event] 00.30/22 celadon_below_center: done", + "[Event] 00.30/23 celadon_nurse: start", "00.30/celadon_nurse: ok at CeladonCity (48, 12) facing north (tile)", - "[Event] 00.30/22 celadon_nurse: done", - "[Event] 00.30/23 heal_celadon: start", + "[Event] 00.30/23 celadon_nurse: done", + "[Event] 00.30/24 heal_celadon: start", "00.30/heal_celadon: ok at -", - "[Event] 00.30/23 heal_celadon: done", + "[Event] 00.30/24 heal_celadon: done", "save at CeladonCity_PokemonCenter_1F (7, 4) facing north (tile)", - "[Checkpoint] 00.30: done, 23 run, 0 skipped", + "[Checkpoint] 00.30: done, 24 run, 0 skipped", "[Checkpoint] 00.30: passed on attempt 1 (one-shot)", "[Segment] 00: 34 of 34 checkpoints one-shot", "[Segment] 00: done, 34 checkpoints", "completed", - "00.01: 2 stubbed: intro, options\n00.02: 0 stubbed\n00.03: 1 stubbed: to_the_lab\n00.04: 5 stubbed: squirtle_ball, choose_squirtle, no_nickname, rival_takes_bulbasaur, rival_battle\n00.05: 1 stubbed: parcel_from_clerk\n00.06: 2 stubbed: give_parcel, pokedex_scene\n00.07: 2 stubbed: heal_viridian, old_man_tutorial\n00.08: 1 stubbed: heal_pewter\n00.09: 3 stubbed: camper_liam, challenge_brock, brock\n00.10: 1 stubbed: shoes_from_aide\n00.11: 4 stubbed: heal_pewter, bug_catcher_colton, bug_catcher_greg, bug_catcher_james\n00.12: 1 stubbed: heal_route4\n00.12c: 1 stubbed: magikarp_to_box\n00.13: 6 stubbed: heal_route4, moon_stone_ball, found_moon_stone, rocket_grunt, super_nerd_miguel, helix_fossil\n00.14: 2 stubbed: teach_mega_kick, heal_cerulean\n00.14b: 3 stubbed: buy_potions, grind_to_lv23, heal_cerulean\n00.14c: 4 stubbed: challenge_misty, misty, heal_cerulean, teach_water_pulse\n00.15: 4 stubbed: teach_water_pulse, buy_potions, grind_for_rival, heal_cerulean\n00.15b: 2 stubbed: teach_water_pulse, rival_cerulean\n00.16: 7 stubbed: heal_cerulean, bug_catcher_cale, lass_ali, youngster_timmy, lass_reli, camper_ethan, rocket_grunt\n00.17: 4 stubbed: hiker_franklin, picnicker_kelsey, hiker_nob, lass_haley\n00.18: 7 stubbed: bill_clefairy, help_bill, bill_into_teleporter, cell_separator, bill_comes_out, thank_bill, ss_ticket\n00.19: 1 stubbed: heal_cerulean\n00.20: 3 stubbed: buy_poke_balls, catch_oddish, heal_cerulean\n00.21: 3 stubbed: rocket_grunt, camper_jeff, heal_vermilion\n00.22: 6 stubbed: show_ss_ticket, rival_ss_anne, captain, hm01_cut, ss_anne_sails, heal_vermilion\n00.23: 9 stubbed: teach_cut, teach_dig, cut_gym_tree, sailor_dwayne, engineer_baily, gentleman_tucker, trash_cans, cut_tree_out, heal_vermilion\n00.24: 4 stubbed: surge_winnable, grind_for_surge, heal_vermilion, cut_gym_tree\n00.25: 4 stubbed: challenge_lt_surge, lt_surge, cut_tree_out, heal_vermilion\n00.26: 8 stubbed: chairman, hear_story, bike_voucher, cut_cerulean_tree, talk_bike_clerk, bicycle, buy_repels, heal_cerulean\n00.27: 6 stubbed: cut_cerulean_tree, cut_route9_tree, picnicker_alicia, hiker_jeremy, bug_catcher_conner, heal_route10\n00.28: 9 stubbed: cut_route10_tree, pokemaniac_ashton, pokemaniac_winston, picnicker_martha, hiker_allen, hiker_lenny, picnicker_sofia, hiker_dudley, picnicker_dana\n00.29: 1 stubbed: heal_lavender\n00.30: 6 stubbed: buy_ropes_and_super_repels, ride_bike, gamer_rich, catch_pidgey, celadon_nurse, heal_celadon\n" + "00.01: 2 stubbed: intro, options\n00.02: 0 stubbed\n00.03: 1 stubbed: to_the_lab\n00.04: 5 stubbed: squirtle_ball, choose_squirtle, no_nickname, rival_takes_bulbasaur, rival_battle\n00.05: 1 stubbed: parcel_from_clerk\n00.06: 2 stubbed: give_parcel, pokedex_scene\n00.07: 2 stubbed: heal_viridian, old_man_tutorial\n00.08: 1 stubbed: heal_pewter\n00.09: 3 stubbed: camper_liam, challenge_brock, brock\n00.10: 1 stubbed: shoes_from_aide\n00.11: 4 stubbed: heal_pewter, bug_catcher_colton, bug_catcher_greg, bug_catcher_james\n00.12: 1 stubbed: heal_route4\n00.12c: 1 stubbed: magikarp_to_box\n00.13: 6 stubbed: heal_route4, moon_stone_ball, found_moon_stone, rocket_grunt, super_nerd_miguel, helix_fossil\n00.14: 2 stubbed: teach_mega_kick, heal_cerulean\n00.14b: 3 stubbed: buy_potions, grind_to_lv23, heal_cerulean\n00.14c: 4 stubbed: challenge_misty, misty, heal_cerulean, teach_water_pulse\n00.15: 4 stubbed: teach_water_pulse, buy_potions, grind_for_rival, heal_cerulean\n00.15b: 2 stubbed: teach_water_pulse, rival_cerulean\n00.16: 7 stubbed: heal_cerulean, bug_catcher_cale, lass_ali, youngster_timmy, lass_reli, camper_ethan, rocket_grunt\n00.17: 4 stubbed: hiker_franklin, picnicker_kelsey, hiker_nob, lass_haley\n00.18: 7 stubbed: bill_clefairy, help_bill, bill_into_teleporter, cell_separator, bill_comes_out, thank_bill, ss_ticket\n00.19: 1 stubbed: heal_cerulean\n00.20: 3 stubbed: buy_poke_balls, catch_oddish, heal_cerulean\n00.21: 3 stubbed: rocket_grunt, camper_jeff, heal_vermilion\n00.22: 6 stubbed: show_ss_ticket, rival_ss_anne, captain, hm01_cut, ss_anne_sails, heal_vermilion\n00.23: 9 stubbed: teach_cut, teach_dig, cut_gym_tree, sailor_dwayne, engineer_baily, gentleman_tucker, trash_cans, cut_tree_out, heal_vermilion\n00.24: 4 stubbed: surge_winnable, grind_for_surge, heal_vermilion, cut_gym_tree\n00.25: 4 stubbed: challenge_lt_surge, lt_surge, cut_tree_out, heal_vermilion\n00.26: 8 stubbed: chairman, hear_story, bike_voucher, cut_cerulean_tree, talk_bike_clerk, bicycle, buy_repels, heal_cerulean\n00.27: 6 stubbed: cut_cerulean_tree, cut_route9_tree, picnicker_alicia, hiker_jeremy, bug_catcher_conner, heal_route10\n00.28: 9 stubbed: cut_route10_tree, pokemaniac_ashton, pokemaniac_winston, picnicker_martha, hiker_allen, hiker_lenny, picnicker_sofia, hiker_dudley, picnicker_dana\n00.29: 1 stubbed: heal_lavender\n00.30: 7 stubbed: register_bike, buy_ropes_and_super_repels, ride_bike, gamer_rich, catch_pidgey, celadon_nurse, heal_celadon\n" ] } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json index cd34645f8a..1f5f7e7850 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData/frlg-00.30.plan.json @@ -15,6 +15,13 @@ "id": "lavender_mart", "title": "From Lavender's Center to the Mart: Escape Ropes and Super Repels", "events": [ + { + "id": "register_bike", + "kind": "game_action", + "variant": "register_bike", + "once_per_save": true, + "why": "the BICYCLE (00.26) registered for SELECT, which gets on and off in about 1 s (console B, 2026-11-09); here until 00.26's plan (frlg/stage7-celadon, #224) takes it right after the Bicycle" + }, { "id": "east_of_mart_door", "kind": "route", @@ -62,7 +69,7 @@ "id": "ride_bike", "kind": "game_action", "variant": "ride_bike", - "why": "on the BICYCLE (00.26, Key Items) for Route 8" + "why": "on the BICYCLE with SELECT (registered by register_bike) for Route 8" }, { "id": "lavender_west_edge", From 388bfff97c53444217477be2940621030ef9a250 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 23:03:41 -0800 Subject: [PATCH 22/28] FRLG walker: code health for the Bicycle taps; register_bike's trace line The sim's taps (turn, step, Bicycle) move to PokemonFRLG_OverworldSimTaps.cpp, SELECT goes through use_field(SimField::BIKE_SELECT), the counted taps are chosen in walk_and_watch(), and each new constant has its own comment. The golden trace places register_bike where 00.30 starts. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Overworld/PokemonFRLG_MovementMechanics.h | 19 +-- .../PokemonFRLG_OverworldBike_Tests.cpp | 3 +- .../PokemonFRLG_OverworldRecoveryWalk.cpp | 7 +- .../Overworld/PokemonFRLG_OverworldSim.cpp | 100 +-------------- .../Overworld/PokemonFRLG_OverworldSim.h | 12 +- .../PokemonFRLG_OverworldSimField.cpp | 2 + .../PokemonFRLG_OverworldSimTaps.cpp | 120 ++++++++++++++++++ .../ReplayData/frlg-story-sim-trace.json | 2 +- SerialPrograms/cmake/SourceFiles.cmake | 1 + 9 files changed, 147 insertions(+), 119 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h index aa9fafaa3f..1b29078224 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h @@ -65,12 +65,14 @@ inline constexpr Milliseconds BIKE_TILE_TIME(100); // hold turns the player too, and loses no tile for it. inline constexpr bool BIKE_TAP_ONLY_TURNS = false; // Chains of 40ms taps on the Bicycle (console B, 2026-11-09): one tap per -// 100ms (60ms release) moved exactly a tile each; at one per 60ms, one -// tap in 12 was dropped. Somewhere between the two the game starts -// dropping taps; the sim drops one in BIKE_FAST_TAP_DROP_EVERY of the taps -// that start sooner than this after the last one. +// 100ms (60ms release) moved exactly a tile each. inline constexpr Milliseconds BIKE_TAP_PERIOD_WORKED(100); +// At one tap per 60ms, one tap in 12 was dropped. Somewhere between that +// and BIKE_TAP_PERIOD_WORKED the game starts dropping taps; the sim drops +// taps that start sooner than this after the last one. inline constexpr Milliseconds BIKE_TAP_DROP_PERIOD(80); +// Of the taps that come sooner than BIKE_TAP_DROP_PERIOD, the sim drops +// one in this many. inline constexpr int BIKE_FAST_TAP_DROP_EVERY = 12; // With the Bicycle registered (Key Items, BICYCLE, REGISTER), SELECT gets // on and off in about 1s (console B, 2026-11-09). Going through a door @@ -117,17 +119,18 @@ inline constexpr Milliseconds STEP_TAP(66); inline constexpr Milliseconds STEP_TAP_RELEASE(700); // An open leg on the Bicycle is counted out in taps, each a tile: a 40ms -// press, released to a BIKE_TAP_PERIOD period, slower than the chains -// that worked, so none is dropped. +// press each. inline constexpr Milliseconds BIKE_TAP(40); +// The taps' period, start to start, slower than the chains that worked, +// so none is dropped. inline constexpr Milliseconds BIKE_TAP_PERIOD(120); // After a tap, how long until the screen shows whether it moved: the // tile's 100ms and video lag. The Command File Runner's tap check judged // its first tap too early, as unmoved. Not measured: a guess. inline constexpr Milliseconds BIKE_TAP_SETTLE(250); -// SELECT, pressed to get on the registered Bicycle, and the wait after -// it for the player to be up on it. +// SELECT, pressed to get on the registered Bicycle. inline constexpr Milliseconds BIKE_SELECT_PRESS(160); +// The wait after SELECT for the player to be up on the Bicycle. inline constexpr Milliseconds BIKE_SELECT_SETTLE(1200); // How long the overworld must show nothing (no box, fade or battle) after diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp index 03d1e829a4..13c094c7d0 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp @@ -114,6 +114,7 @@ UnitTestResult test_tap_chains(){ return true; } +// An open leg of `tiles` toward `direction`, ending on `end`. RouteStep open_leg(Direction direction, int tiles, MapPosition end){ RouteStep step; step.direction = direction; @@ -177,7 +178,7 @@ UnitTestResult test_select_onto_bike(){ OverworldSim sim(field(), at(10, 1), Direction::WEST, script); BikeSelectCalls calls; int presses = 0; - calls.press_select = [&]{ presses++; sim.press_select(); }; + calls.press_select = [&]{ presses++; sim.use_field(SimField::BIKE_SELECT); }; calls.riding = [&]{ return sim.state().on_bike; }; calls.log = [](const std::string&){}; const bool riding = OverworldWalkSteps::select_onto_bike(calls); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp index 08e449666c..8038545d93 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp @@ -151,9 +151,6 @@ StepWalk walk_step( const RouteStep& step, std::optional facing, const OverworldInterruptSet& interrupts, MoveSpeed speed ){ - if (counts_bike_taps(step, speed)){ - return walk_bike_taps(console, context, step, interrupts); - } StepWalk out{std::nullopt, current_time(), false, false}; switch (step.action){ case RouteAction::WALK:{ @@ -264,7 +261,9 @@ StepOutcome walk_and_watch( clear_open_message(console, context, step.wild ? interrupts.wild : interrupts.other, step); check_overworld(console, context, step); const LegRecorder recorder(console, context, interrupts, step); - const StepWalk walked = walk_step(console, context, step, facing, interrupts, speed); + const StepWalk walked = counts_bike_taps(step, speed) + ? walk_bike_taps(console, context, step, interrupts) + : walk_step(console, context, step, facing, interrupts, speed); if (walked.stopped){ return StepOutcome{.end = StepEnd::STOPPED, .walked = walked.stopped, .blocked = walked.blocked}; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp index bce5aa34b0..fc523332db 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp @@ -368,8 +368,7 @@ bool OverworldSim::after_step(const Step& step, Milliseconds pressed){ } m_cut_trees.clear(); if (m_state.on_bike){ - // A door takes the player off the Bicycle (the user's - // knowledge, not measured). + // A door takes the player off (the user's; not measured). m_state.on_bike = false; note("the warp takes the player off the Bicycle."); } @@ -613,35 +612,6 @@ void OverworldSim::hold_on(Direction direction, Milliseconds end){ press.end = end; } -bool OverworldSim::drops_bike_tap(Milliseconds start){ - const bool fast = m_last_bike_tap && start - *m_last_bike_tap < MovementMechanics::BIKE_TAP_DROP_PERIOD; - m_last_bike_tap = start; - if (!fast || ++m_fast_bike_taps % MovementMechanics::BIKE_FAST_TAP_DROP_EVERY != 0){ - return false; - } - m_state.bike_taps_dropped++; - note(std::format("a Bicycle tap is dropped, {} fast taps in.", m_fast_bike_taps)); - return true; -} - -bool OverworldSim::press_select(){ - const Milliseconds start = m_now; - if (m_state.screen != SimScreen::OVERWORLD || m_frozen || !m_script.bike_registered){ - note(m_script.bike_registered ? "SELECT does nothing off the overworld." : "SELECT does nothing: the Bicycle isn't registered."); - advance_to(start + MovementMechanics::BIKE_SELECT_PRESS); - return false; - } - if (!m_state.on_bike && !map().allow_running){ - note("SELECT: the Bicycle can't be ridden here."); - advance_to(start + MovementMechanics::BIKE_SELECT_TIME); - return false; - } - m_state.on_bike = !m_state.on_bike; - note(m_state.on_bike ? "gets on the Bicycle." : "gets off the Bicycle."); - advance_to(start + MovementMechanics::BIKE_SELECT_TIME); - return true; -} - bool OverworldSim::drops_turn_tap(){ m_turn_taps++; return m_script.dropped_turn_taps.contains(m_turn_taps); @@ -730,7 +700,6 @@ OverworldSimWalker::OverworldSimWalker(OverworldSim& sim, SimWalkerRules rules, : m_sim(sim) , m_rules(rules) , m_riding(rules.speed == MoveSpeed::BIKE) - , m_speed(rules.speed) { // In the walker's order: battles first. m_wild = { @@ -898,55 +867,7 @@ std::optional OverworldSimWalker::walk_step(const RouteStep& step, return std::nullopt; } -BikeTap OverworldSimWalker::bike_tap(const RouteStep& step){ - const MapPosition before = m_sim.state().position; - const std::vector& ints = interrupts(step); - const Milliseconds release = m_rules.bike_tap_period - BIKE_TAP; - const std::optional seen = m_sim.hold(step.direction, BIKE_TAP, release, m_rules.bike_tap_period + BIKE_TAP_SETTLE, watchers(ints)); - if (seen){ - m_log.push_back("Bike: " + ints[seen->watcher].name + " on a tap of " + to_string(step) + "."); - return BikeTap{.moved = false, .interrupt = seen->watcher}; - } - if (!(m_sim.state().position == before)){ - return BikeTap{.moved = true}; - } - return BikeTap{.moved = false, .interrupt = std::nullopt, .cleared_late = clear_late(ints, AFTER_LEG_WATCH, MAX_INTERRUPTIONS_PER_LEG) > 0}; -} - -StepOutcome OverworldSimWalker::walk_bike_taps(const RouteStep& step){ - const std::vector& ints = interrupts(step); - BikeTapCalls calls; - calls.tap = [&]{ return bike_tap(step); }; - calls.clear = [&](size_t index){ clear(ints[index]); }; - calls.log = [&](const std::string& message){ m_log.push_back(message); }; - m_leg_start = m_sim.state().time; - m_leg_dark = m_sim.state().dark; - return count_bike_taps(step, calls); -} - -// walk_and_watch(), getting on the Bicycle first where -// walk_overworld_route_with_recovery() does. -StepOutcome OverworldSimWalker::walk(const RouteStep& step, std::optional facing){ - if (m_rules.get_back_on_bike && gets_on_bike(step, m_rules.speed, m_riding)){ - BikeSelectCalls select; - select.press_select = [&]{ m_sim.press_select(); }; - select.riding = [&]{ return m_sim.state().on_bike; }; - select.log = [&](const std::string& message){ m_log.push_back(message); }; - if (!select_onto_bike(select)){ - fail("get_on_bike(): not on the Bicycle after SELECT twice. Is it registered?"); - } - m_riding = true; - facing = std::nullopt; - } - m_speed = m_riding || !m_rules.get_back_on_bike ? m_rules.speed : on_foot(m_rules.speed); - const StepOutcome outcome = walk_ridden(step, facing); - if (m_riding && step.warps){ - m_log.push_back("Bike: through the warp to " + step.end.map + ", off the Bicycle."); - m_riding = false; - } - return outcome; -} - +// walk_and_watch(). StepOutcome OverworldSimWalker::walk_ridden(const RouteStep& step, std::optional facing){ if (m_rules.battle_before_step && battle_on_its_way()){ m_log.push_back("Route: " + std::string(sim_screen_name(m_sim.state().screen)) + " before " + to_string(step) + ": watching on for a wild battle set off before it."); @@ -1063,23 +984,6 @@ RouteCalls OverworldSimWalker::calls(){ return calls; } -void sim_turn_to(OverworldSim& sim, Direction direction, bool confirm){ - for (size_t tap = 1; tap <= MAX_TURN_TAPS; tap++){ - sim.tap(direction); - const SimState& state = sim.state(); - if (!confirm || state.screen != SimScreen::OVERWORLD || state.facing == direction){ - return; - } - } -} - -void sim_step_once(OverworldSim& sim, Direction direction){ - if (sim.state().facing != direction){ - sim_turn_to(sim, direction); - } - sim.tap(direction, STEP_TAP, STEP_TAP_RELEASE); -} - std::string OverworldSimWalker::run(const std::vector& route, const RoutePlanning& planning){ try{ run_route(route, m_sim.state().facing, &planning, calls()); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h index 2f6c0c83df..62b018d870 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h @@ -250,6 +250,10 @@ enum class SimField{ // The panel of the lift the player stands in (SimScript::lift_floors): // the floor picked, the lift's door now lets out there. LIFT_FLOOR, + // Not A: SELECT, which with the Bicycle registered gets on it where + // running is allowed, or off it, in about + // MovementMechanics::BIKE_SELECT_TIME. (PokemonFRLG_OverworldSimTaps.cpp) + BIKE_SELECT, }; // Simulates overworld movement, encounters and dialogs. @@ -282,13 +286,6 @@ class OverworldSim{ Milliseconds release = MovementMechanics::TURN_SETTLE ); - // SELECT on the overworld: with the Bicycle registered, gets on it - // where running is allowed, or off it, taking about - // MovementMechanics::BIKE_SELECT_TIME. Returns whether it did. - bool press_select(); - // REGISTER on the Bicycle in the bag (SimScript::bike_registered). - void register_bike(){ m_script.bike_registered = true; } - // Watches `watchers` for `duration`, as wait_until(). std::optional watch(const std::vector& watchers, Milliseconds duration); @@ -365,6 +362,7 @@ class OverworldSim{ std::optional next_encounter(); bool rolls_encounter(int rate); bool drops_turn_tap(); + bool press_select(); // Whether the Bicycle tap starting at `start` is dropped, too soon // after the last one. bool drops_bike_tap(Milliseconds start); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp index ac198790ac..3258b390d0 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp @@ -33,6 +33,8 @@ bool OverworldSim::use_field(SimField field, size_t floor){ return push_poster_switch(); case SimField::LIFT_FLOOR: return choose_lift_floor(floor); + case SimField::BIKE_SELECT: + return press_select(); } return false; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp new file mode 100644 index 0000000000..9cff82e7b3 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp @@ -0,0 +1,120 @@ +/* Overworld Sim: taps and the Bicycle + * + * From: https://github.com/PokemonAutomation/ + * + * The walker's taps on the sim: turn taps, step taps, and on the Bicycle + * taps dropped when they come too fast, SELECT, and legs counted out a + * tap at a time. + * + */ + +#include +#include "PokemonFRLG_OverworldSim.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace MovementMechanics; +using namespace OverworldWalkSteps; + + +bool OverworldSim::drops_bike_tap(Milliseconds start){ + const bool fast = m_last_bike_tap && start - *m_last_bike_tap < MovementMechanics::BIKE_TAP_DROP_PERIOD; + m_last_bike_tap = start; + if (!fast || ++m_fast_bike_taps % MovementMechanics::BIKE_FAST_TAP_DROP_EVERY != 0){ + return false; + } + m_state.bike_taps_dropped++; + note(std::format("a Bicycle tap is dropped, {} fast taps in.", m_fast_bike_taps)); + return true; +} + +bool OverworldSim::press_select(){ + const Milliseconds start = m_now; + if (m_state.screen != SimScreen::OVERWORLD || m_frozen || !m_script.bike_registered){ + note(m_script.bike_registered ? "SELECT does nothing off the overworld." : "SELECT does nothing: the Bicycle isn't registered."); + advance_to(start + MovementMechanics::BIKE_SELECT_PRESS); + return false; + } + if (!m_state.on_bike && !map().allow_running){ + note("SELECT: the Bicycle can't be ridden here."); + advance_to(start + MovementMechanics::BIKE_SELECT_TIME); + return false; + } + m_state.on_bike = !m_state.on_bike; + note(m_state.on_bike ? "gets on the Bicycle." : "gets off the Bicycle."); + advance_to(start + MovementMechanics::BIKE_SELECT_TIME); + return true; +} + +BikeTap OverworldSimWalker::bike_tap(const RouteStep& step){ + const MapPosition before = m_sim.state().position; + const std::vector& ints = interrupts(step); + const Milliseconds release = m_rules.bike_tap_period - BIKE_TAP; + const std::optional seen = m_sim.hold(step.direction, BIKE_TAP, release, m_rules.bike_tap_period + BIKE_TAP_SETTLE, watchers(ints)); + if (seen){ + m_log.push_back("Bike: " + ints[seen->watcher].name + " on a tap of " + to_string(step) + "."); + return BikeTap{.moved = false, .interrupt = seen->watcher}; + } + if (!(m_sim.state().position == before)){ + return BikeTap{.moved = true}; + } + return BikeTap{.moved = false, .interrupt = std::nullopt, .cleared_late = clear_late(ints, AFTER_LEG_WATCH, MAX_INTERRUPTIONS_PER_LEG) > 0}; +} + +StepOutcome OverworldSimWalker::walk_bike_taps(const RouteStep& step){ + const std::vector& ints = interrupts(step); + BikeTapCalls calls; + calls.tap = [&]{ return bike_tap(step); }; + calls.clear = [&](size_t index){ clear(ints[index]); }; + calls.log = [&](const std::string& message){ m_log.push_back(message); }; + m_leg_start = m_sim.state().time; + m_leg_dark = m_sim.state().dark; + return count_bike_taps(step, calls); +} + +// walk_and_watch(), getting on the Bicycle first where +// walk_overworld_route_with_recovery() does. +StepOutcome OverworldSimWalker::walk(const RouteStep& step, std::optional facing){ + if (m_rules.get_back_on_bike && gets_on_bike(step, m_rules.speed, m_riding)){ + BikeSelectCalls select; + select.press_select = [&]{ m_sim.use_field(SimField::BIKE_SELECT); }; + select.riding = [&]{ return m_sim.state().on_bike; }; + select.log = [&](const std::string& message){ m_log.push_back(message); }; + if (!select_onto_bike(select)){ + fail("get_on_bike(): not on the Bicycle after SELECT twice. Is it registered?"); + } + m_riding = true; + facing = std::nullopt; + } + m_speed = m_riding || !m_rules.get_back_on_bike ? m_rules.speed : on_foot(m_rules.speed); + const StepOutcome outcome = walk_ridden(step, facing); + if (m_riding && step.warps){ + m_log.push_back("Bike: through the warp to " + step.end.map + ", off the Bicycle."); + m_riding = false; + } + return outcome; +} + +void sim_turn_to(OverworldSim& sim, Direction direction, bool confirm){ + for (size_t tap = 1; tap <= MAX_TURN_TAPS; tap++){ + sim.tap(direction); + const SimState& state = sim.state(); + if (!confirm || state.screen != SimScreen::OVERWORLD || state.facing == direction){ + return; + } + } +} + +void sim_step_once(OverworldSim& sim, Direction direction){ + if (sim.state().facing != direction){ + sim_turn_to(sim, direction); + } + sim.tap(direction, STEP_TAP, STEP_TAP_RELEASE); +} + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json index da375eaaf3..6f80bbffdc 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData/frlg-story-sim-trace.json @@ -1205,7 +1205,7 @@ "[Event] 00.30/1 from_save: start", "[Event] 00.30/1 from_save: done", "[Event] 00.30/2 register_bike: start", - "00.30/register_bike: ok at -", + "00.30/register_bike: ok at LavenderTown_PokemonCenter_1F (7, 4) facing north (tile)", "[Event] 00.30/2 register_bike: done", "[Event] 00.30/3 east_of_mart_door: start", "00.30/east_of_mart_door: ok at LavenderTown (21, 16) facing east (tile)", diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index ec96fcbca3..4654320160 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1886,6 +1886,7 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp From 606d4281c5e0184f38eb98b32eaaf7687dabe068 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 23:09:41 -0800 Subject: [PATCH 23/28] FRLG walker: the Bicycle tap API in its own header; linter fixes count_bike_taps() and its types move to PokemonFRLG_OverworldBikeTaps.h/.cpp, BikeSelectCalls loses its log, and the Bicycle tests' expectation is a std::string. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../PokemonFRLG_OverworldBikeTaps.cpp | 41 +++++++++++++ .../Overworld/PokemonFRLG_OverworldBikeTaps.h | 61 +++++++++++++++++++ .../PokemonFRLG_OverworldBike_Tests.cpp | 3 +- .../PokemonFRLG_OverworldRecovery.cpp | 22 +------ .../Overworld/PokemonFRLG_OverworldRecovery.h | 30 --------- .../PokemonFRLG_OverworldRecoveryWalk.cpp | 1 + .../Overworld/PokemonFRLG_OverworldSim.h | 1 + .../PokemonFRLG_OverworldSimTaps.cpp | 1 - .../Overworld/PokemonFRLG_OverworldWalk.cpp | 6 +- .../Overworld/PokemonFRLG_OverworldWalk.h | 1 - SerialPrograms/cmake/SourceFiles.cmake | 4 +- 11 files changed, 112 insertions(+), 59 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBikeTaps.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBikeTaps.h diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBikeTaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBikeTaps.cpp new file mode 100644 index 0000000000..fa2871e6d7 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBikeTaps.cpp @@ -0,0 +1,41 @@ +/* Overworld Bicycle Taps + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "PokemonFRLG_OverworldBikeTaps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +StepOutcome count_bike_taps(const RouteStep& step, const BikeTapCalls& calls){ + int moved = 0; + for (int tap = 1; tap <= step.tiles; tap++){ + const BikeTap result = calls.tap(); + if (result.interrupt || result.cleared_late){ + calls.log(std::format("Bike: tap {} of {}: interrupted after {} tile(s).", tap, step.tiles, moved)); + if (result.interrupt){ + calls.clear(*result.interrupt); + } + const int most = std::min(moved + 1, step.tiles); + return StepOutcome{.end = StepEnd::STOPPED, .walked = WalkedTiles{moved, most}}; + } + if (!result.moved){ + calls.log(std::format("Bike: tap {} of {} didn't move: blocked after {} tile(s).", tap, step.tiles, moved)); + return StepOutcome{.end = StepEnd::STOPPED, .walked = WalkedTiles{moved, moved + BLOCKED_TAP_SLACK}, .blocked = true}; + } + moved++; + } + return StepOutcome{}; +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBikeTaps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBikeTaps.h new file mode 100644 index 0000000000..c1018ce17f --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBikeTaps.h @@ -0,0 +1,61 @@ +/* Overworld Bicycle Taps + * + * From: https://github.com/PokemonAutomation/ + * + * An open leg on the Bicycle counted out a tap at a time, each a tile, + * with the console side passed in (BikeTapCalls), so the walker and the + * sim share it. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_OverworldBikeTaps_H +#define PokemonAutomation_PokemonFRLG_OverworldBikeTaps_H + +#include +#include +#include +#include +#include "PokemonFRLG_OverworldRecovery.h" +#include "PokemonFRLG_OverworldRoute.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// One tap of an open leg on the Bicycle, counted out a tile a tap: whether +// the screen moved, and which of the leg's interrupts showed, if one did. +struct BikeTap{ + bool moved = false; + std::optional interrupt; + // The tap didn't move, and then an interrupt showed, which the tap + // cleared already: a trainer who saw the player, a battle seen late. + bool cleared_late = false; +}; +// The console side of count_bike_taps(): `tap` taps once and judges it; +// `clear` clears interrupt i. +struct BikeTapCalls{ + std::function tap; + std::function clear; + std::function log; +}; +// How far a sure start may be off before a blocked tap finds the player: +// three runs of 00.30 stood 3 tiles off (console B, 2026-11-09). +constexpr int BLOCKED_TAP_SLACK = 3; + +// Walks the open leg `step` a tap at a time: on the Bicycle each tap is +// exactly a tile (MovementMechanics::BIKE_TAP). A tap that doesn't move +// the player ends the leg there, blocked, and one with an interrupt ends +// it once that is cleared. DONE once all of the leg's taps moved; else +// STOPPED, with the tiles walked: blocked, the taps that moved, and up +// to BLOCKED_TAP_SLACK more for a start that was off (run_route() keeps +// the tiles with a blocked one ahead); interrupted, the taps that moved +// and one more, which may have moved before its battle showed. +StepOutcome count_bike_taps(const RouteStep& step, const BikeTapCalls& calls); + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp index 13c094c7d0..ce660ee05b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp @@ -34,7 +34,7 @@ struct BikeInput{ UnitTestResult (*body)(); }; struct BikeExpectation{ - const char* description; + std::string description; }; struct BikeCase{ BikeInput input; @@ -180,7 +180,6 @@ UnitTestResult test_select_onto_bike(){ int presses = 0; calls.press_select = [&]{ presses++; sim.use_field(SimField::BIKE_SELECT); }; calls.riding = [&]{ return sim.state().on_bike; }; - calls.log = [](const std::string&){}; const bool riding = OverworldWalkSteps::select_onto_bike(calls); if (riding != registered || presses != (registered ? 1 : 2)){ return std::format( diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.cpp index 08492373f2..8cec6277b7 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.cpp @@ -7,6 +7,7 @@ #include #include #include "Pokemon/Navigation/Pokemon_RouteLog.h" +#include "PokemonFRLG_OverworldBikeTaps.h" #include "PokemonFRLG_OverworldReanchor.h" #include "PokemonFRLG_OverworldRecovery.h" @@ -577,27 +578,6 @@ std::optional trainer_crossing( return crossing; } -StepOutcome count_bike_taps(const RouteStep& step, const BikeTapCalls& calls){ - int moved = 0; - for (int tap = 1; tap <= step.tiles; tap++){ - const BikeTap result = calls.tap(); - if (result.interrupt || result.cleared_late){ - calls.log(std::format("Bike: tap {} of {}: interrupted after {} tile(s).", tap, step.tiles, moved)); - if (result.interrupt){ - calls.clear(*result.interrupt); - } - const int most = std::min(moved + 1, step.tiles); - return StepOutcome{.end = StepEnd::STOPPED, .walked = WalkedTiles{moved, most}}; - } - if (!result.moved){ - calls.log(std::format("Bike: tap {} of {} didn't move: blocked after {} tile(s).", tap, step.tiles, moved)); - return StepOutcome{.end = StepEnd::STOPPED, .walked = WalkedTiles{moved, moved + BLOCKED_TAP_SLACK}, .blocked = true}; - } - moved++; - } - return StepOutcome{}; -} - void run_route( const std::vector& route, std::optional facing, const RoutePlanning* planning, const RouteCalls& calls diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.h index 8744c0089f..88e18a679b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.h @@ -74,36 +74,6 @@ struct StepOutcome{ }; -// One tap of an open leg on the Bicycle, counted out a tile a tap: whether -// the screen moved, and which of the leg's interrupts showed, if one did. -struct BikeTap{ - bool moved = false; - std::optional interrupt; - // The tap didn't move, and then an interrupt showed, which the tap - // cleared already: a trainer who saw the player, a battle seen late. - bool cleared_late = false; -}; -// The console side of count_bike_taps(): `tap` taps once and judges it; -// `clear` clears interrupt i. -struct BikeTapCalls{ - std::function tap; - std::function clear; - std::function log; -}; -// How far a sure start may be off before a blocked tap finds the player: -// three runs of 00.30 stood 3 tiles off (console B, 2026-11-09). -constexpr int BLOCKED_TAP_SLACK = 3; - -// Walks the open leg `step` a tap at a time: on the Bicycle each tap is -// exactly a tile (MovementMechanics::BIKE_TAP). A tap that doesn't move -// the player ends the leg there, blocked, and one with an interrupt ends -// it once that is cleared. DONE once all of the leg's taps moved; else -// STOPPED, with the tiles walked: blocked, the taps that moved, and up -// to BLOCKED_TAP_SLACK more for a start that was off (run_route() keeps -// the tiles with a blocked one ahead); interrupted, the taps that moved -// and one more, which may have moved before its battle showed. -StepOutcome count_bike_taps(const RouteStep& step, const BikeTapCalls& calls); - // Whether, after each leg to a wall, the walker taps once more toward the // wall and logs whether the player moved (see RouteCalls). enum class WallCheck{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp index 8038545d93..26ede29f78 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp @@ -18,6 +18,7 @@ #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "VideoGames/PokemonFRLG/Inference/Dialogs/PokemonFRLG_DialogDetector.h" #include "VideoGames/PokemonFRLG/PokemonFRLG_Settings.h" +#include "PokemonFRLG_OverworldBikeTaps.h" #include "PokemonFRLG_OverworldLateBattle.h" #include "PokemonFRLG_OverworldRecoveryWalk.h" diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h index 62b018d870..c9a1b59e1e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h @@ -42,6 +42,7 @@ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "Pokemon/AutoStory/Pokemon_ScreenEvents.h" #include "PokemonFRLG_MovementMechanics.h" +#include "PokemonFRLG_OverworldBikeTaps.h" #include "PokemonFRLG_OverworldRecovery.h" #include "PokemonFRLG_OverworldRoute.h" #include "PokemonFRLG_OverworldWalk.h" diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp index 9cff82e7b3..03bde06519 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp @@ -81,7 +81,6 @@ StepOutcome OverworldSimWalker::walk(const RouteStep& step, std::optional press_select; std::function riding; - std::function log; }; // Gets on the registered Bicycle with SELECT, checks the player rides, // and presses again once if not. Returns whether they ride. diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 4654320160..b54cf6fb45 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1864,6 +1864,9 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBikeTaps.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBikeTaps.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldLateBattle.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldLateBattle.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldLateBattle_Tests.cpp @@ -1888,7 +1891,6 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp - Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp From b062392cbd32105127126a49fba154e45274ad5e Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 23:12:12 -0800 Subject: [PATCH 24/28] FRLG AutoStory: red sim test, a capture started on the Bicycle drifts out of Route 7's grass Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../PokemonFRLG_AutoStoryPaceSim_Tests.cpp | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp index 6a76612aaa..9f614cfd93 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp @@ -111,6 +111,79 @@ UnitTestResult test_capture_paced_in_trainer_sight(){ } +// A capture on the sim as ConsoleCaptureActions paces it: each search for +// a battle with its own PatchSteps, east and back, `species` read in turn. +class SimConsoleCapture : public CaptureActions{ +public: + SimConsoleCapture(Logger& logger, OverworldSim& sim, std::vector species) + : m_logger(logger), m_sim(sim), m_species(std::move(species)) + {} + virtual bool find_encounter(Direction) override{ + SimPatch pacing(m_sim, Direction::EAST, Direction::WEST); + return pace_until_battle(m_logger, pacing, PACE_MAX_STEPS); + } + virtual EncounterSeen read_encounter() override{ + const std::string species = m_read < m_species.size() ? m_species[m_read] : m_species.back(); + m_read++; + return EncounterSeen{.species = species}; + } + virtual bool flee() override{ + const bool ended = m_sim.handle_battle(SimBattleChoice::RUN) == SimBattleEnd::ENDED; + tiles.push_back(m_sim.state().position); + return ended; + } + virtual FoeHp read_foe_hp() override{ return FoeHp::UNKNOWN; } + virtual TurnEnd attack(size_t) override{ return TurnEnd::FAILED; } + virtual ThrowEnd throw_ball() override{ return ThrowEnd::CAUGHT; } + virtual bool finish_catch(CatchDestination) override{ + return m_sim.handle_battle(SimBattleChoice::FIGHT) == SimBattleEnd::ENDED; + } + // Where each battle run from left the player. + std::vector tiles; +private: + Logger& m_logger; + OverworldSim& m_sim; + std::vector m_species; + size_t m_read = 0; +}; + +// Console B, 2026-11-09 (00.30 run 5): catch_pidgey started on Route 7's +// (14, 6) on the Bicycle, ran from 4 wild battles and then paced 4 minutes +// on the flowers east of the grass (11..18, 2..6). On the Bicycle a tap +// facing another way steps too, so each turn of the pace took a tile, and +// each search began its pace afresh from where the last battle left the +// player. On the sim, from (14, 6) on the Bicycle, 6 battles, the Pidgey +// last: every battle starts on (14, 6) or (15, 6), on foot. +UnitTestResult test_capture_paced_on_bike_stays_in_grass(){ + const MapPosition patch{"Route7", {14, 6}}; + const CapturePlan plan{.species = "pidgey", .max_encounters = 30, .pace = Direction::EAST}; + SimScript script; + script.on_bike = true; + script.bike_registered = true; + for (int grass_step : {2, 5, 6, 9, 12, 13}){ + script.encounters.push_back(SimEncounter{.grass_step = grass_step}); + } + OverworldSim sim(story_maps(), patch, Direction::EAST, script); + LinesLogger logger; + SimConsoleCapture actions(logger, sim, {"rattata", "spearow", "rattata", "oddish", "meowth", "pidgey"}); + const CaptureResult result = run_capture(logger, actions, plan, 5); + if (result.end != CaptureEnd::CAUGHT || result.encounters != 6){ + return capture_message(plan, result) + joined(logger.lines) + joined(sim.state().log); + } + const MapPosition east{"Route7", {15, 6}}; + for (size_t c = 0; c < actions.tiles.size(); c++){ + if (!(actions.tiles[c] == patch) && !(actions.tiles[c] == east)){ + return std::format("Battle {} left the player on {}, off the pace's tiles.", c + 1, to_string(actions.tiles[c])) + + joined(sim.state().log); + } + } + if (sim.state().on_bike){ + return "Paced on the Bicycle." + joined(sim.state().log); + } + return true; +} + + struct AutoStoryPaceSimInput{ std::string name; std::function body; @@ -153,6 +226,15 @@ const AutoStoryPaceSimTestCase AUTO_STORY_PACE_SIM_CASES[] = { .description = "handles trainer encounter while pacing and resumes capture sequence", }, }, + { + .input = { + .name = "a capture started on the Bicycle paces on foot in the grass", + .body = test_capture_paced_on_bike_stays_in_grass, + }, + .expectation = { + .description = "every battle starts on one of the pace's two tiles, off the Bicycle", + }, + }, }; } // namespace From d2504ddaee0e079a4863aa35b5ad340566b14daa Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 23:14:16 -0800 Subject: [PATCH 25/28] FRLG AutoStory: pace for a capture or grind on foot, from where the last battle left the player Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../PokemonFRLG_AutoStoryCapture.cpp | 35 +++++++++++++++++-- .../AutoStory/PokemonFRLG_AutoStoryCapture.h | 20 +++++++++++ .../AutoStory/PokemonFRLG_AutoStoryGrind.cpp | 2 +- .../PokemonFRLG_AutoStoryPaceSim_Tests.cpp | 17 +++++---- 4 files changed, 64 insertions(+), 10 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.cpp index 877265def1..3d824b4a38 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.cpp @@ -437,6 +437,36 @@ bool pace_until_battle(Logger& logger, PatchSteps& steps, size_t max_steps){ } +bool select_off_bike(const BikeSelectCalls& calls){ + for (size_t c = 0; c < 2 && calls.riding(); c++){ + calls.press_select(); + } + return !calls.riding(); +} + +bool find_paced_encounter(Logger& logger, const BikeSelectCalls& bike, PatchSteps& steps){ + if (!select_off_bike(bike)){ + logger.log("Pacing: still on the Bicycle after SELECT twice; not pacing on it.", COLOR_RED); + return false; + } + return pace_until_battle(logger, steps, PACE_MAX_STEPS); +} + +BikeSelectCalls console_bike_select(ConsoleHandle& console, ProControllerContext& context){ + BikeSelectCalls calls; + calls.press_select = [&console, &context]{ + console.log("Pacing: SELECT (MINUS) off the Bicycle."); + pbf_press_button(context, BUTTON_MINUS, MovementMechanics::BIKE_SELECT_PRESS, MovementMechanics::BIKE_SELECT_SETTLE); + }; + calls.riding = [&console, &context]{ + context.wait_for_all_requests(); + VideoSnapshot screen = console.video().snapshot(); + return screen && screen_shows_riding(*screen.frame); + }; + return calls; +} + + ConsolePatchSteps::ConsolePatchSteps( ConsoleHandle& console, ProControllerContext& context, Direction pace, PaceSpot& spot, const PaceTrainerBattle& trainer_battle @@ -502,9 +532,8 @@ ConsoleCaptureActions::ConsoleCaptureActions( {} bool ConsoleCaptureActions::find_encounter(Direction pace){ - PaceSpot spot; - ConsolePatchSteps steps(m_console, m_context, pace, spot, m_trainer_battle); - return pace_until_battle(m_console.logger(), steps, PACE_MAX_STEPS); + ConsolePatchSteps steps(m_console, m_context, pace, m_spot, m_trainer_battle); + return find_paced_encounter(m_console.logger(), console_bike_select(m_console, m_context), steps); } EncounterSeen ConsoleCaptureActions::read_encounter(){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h index b273878c59..6b21cba411 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h @@ -38,6 +38,7 @@ #include "Pokemon/AutoStory/Pokemon_StoryEvents.h" #include "Pokemon/AutoStory/Pokemon_StorySteps.h" #include "Pokemon/Navigation/Pokemon_Grid.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" #include "PokemonFRLG_AutoStoryPcStorage.h" namespace PokemonAutomation{ @@ -249,6 +250,23 @@ class PatchSteps{ // meanwhile is fought (PatchSteps::fight_trainer()) and pacing goes on; // their battle isn't a wild one. bool pace_until_battle(Logger& logger, PatchSteps& steps, size_t max_steps); +// Before pacing: gets the player off the Bicycle with SELECT, checked, +// and once more if they still ride. On the Bicycle a tap facing another +// way steps as well as turns, so pacing's turns walk it off the patch +// (console B, 2026-11-09, 00.30 run 5: Route 7's grass). Returns whether +// the player is on foot. +bool select_off_bike(const BikeSelectCalls& calls); + +// One search for a wild battle, as a capture or a grind paces: off the +// Bicycle (select_off_bike()), then pace_until_battle(). `steps` keeps +// the tile pacing left the player on from one search to the next, so the +// pace never starts a tile out. False if no battle came, or the player +// rides on. +bool find_paced_encounter(Logger& logger, const BikeSelectCalls& bike, PatchSteps& steps); + +// select_off_bike()'s calls on the console: SELECT is the Switch's MINUS, +// and the player rides while no standing sprite shows (screen_shows_riding()). +BikeSelectCalls console_bike_select(ConsoleHandle& console, ProControllerContext& context); // Steps in a patch before pacing gives up: about 10 % of steps in grass // start a battle at Route 4's rates. @@ -318,6 +336,8 @@ class ConsoleCaptureActions : public CaptureActions{ ProControllerContext& m_context; Language m_language; PaceTrainerBattle m_trainer_battle; + // Where pacing left the player, kept from one battle to the next. + PaceSpot m_spot; }; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.cpp index 670a4dbe47..5af2285722 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.cpp @@ -349,7 +349,7 @@ bool ConsoleGrindActions::find_encounter(Direction pace){ // As ConsoleCaptureActions::find_encounter(), keeping track of the // tile: a battle starts on the tile just stepped onto. ConsolePatchSteps steps(m_console, m_context, pace, m_spot, m_trainer_battle); - return pace_until_battle(m_console.logger(), steps, PACE_MAX_STEPS); + return find_paced_encounter(m_console.logger(), console_bike_select(m_console, m_context), steps); } EncounterSeen ConsoleGrindActions::read_encounter(){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp index 9f614cfd93..34a8fb8c4e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp @@ -111,16 +111,19 @@ UnitTestResult test_capture_paced_in_trainer_sight(){ } -// A capture on the sim as ConsoleCaptureActions paces it: each search for -// a battle with its own PatchSteps, east and back, `species` read in turn. +// A capture on the sim as ConsoleCaptureActions paces it, east and back: +// find_paced_encounter() with one PatchSteps kept from battle to battle, +// `species` read in turn. class SimConsoleCapture : public CaptureActions{ public: SimConsoleCapture(Logger& logger, OverworldSim& sim, std::vector species) - : m_logger(logger), m_sim(sim), m_species(std::move(species)) - {} + : m_logger(logger), m_sim(sim), m_pacing(sim, Direction::EAST, Direction::WEST), m_species(std::move(species)) + { + m_bike.press_select = [this]{ m_sim.use_field(SimField::BIKE_SELECT); }; + m_bike.riding = [this]{ return m_sim.state().on_bike; }; + } virtual bool find_encounter(Direction) override{ - SimPatch pacing(m_sim, Direction::EAST, Direction::WEST); - return pace_until_battle(m_logger, pacing, PACE_MAX_STEPS); + return find_paced_encounter(m_logger, m_bike, m_pacing); } virtual EncounterSeen read_encounter() override{ const std::string species = m_read < m_species.size() ? m_species[m_read] : m_species.back(); @@ -143,6 +146,8 @@ class SimConsoleCapture : public CaptureActions{ private: Logger& m_logger; OverworldSim& m_sim; + SimPatch m_pacing; + BikeSelectCalls m_bike; std::vector m_species; size_t m_read = 0; }; From cbf3354299372a97d8c06a302166e197046e684a Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 23:14:41 -0800 Subject: [PATCH 26/28] FRLG AutoStory: qualify OverworldWalkSteps::BikeSelectCalls in pacing Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../AutoStory/PokemonFRLG_AutoStoryCapture.cpp | 10 +++++----- .../Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h | 8 ++++---- .../AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.cpp index 3d824b4a38..8e95418a29 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.cpp @@ -437,14 +437,14 @@ bool pace_until_battle(Logger& logger, PatchSteps& steps, size_t max_steps){ } -bool select_off_bike(const BikeSelectCalls& calls){ +bool select_off_bike(const OverworldWalkSteps::BikeSelectCalls& calls){ for (size_t c = 0; c < 2 && calls.riding(); c++){ calls.press_select(); } return !calls.riding(); } -bool find_paced_encounter(Logger& logger, const BikeSelectCalls& bike, PatchSteps& steps){ +bool find_paced_encounter(Logger& logger, const OverworldWalkSteps::BikeSelectCalls& bike, PatchSteps& steps){ if (!select_off_bike(bike)){ logger.log("Pacing: still on the Bicycle after SELECT twice; not pacing on it.", COLOR_RED); return false; @@ -452,8 +452,8 @@ bool find_paced_encounter(Logger& logger, const BikeSelectCalls& bike, PatchStep return pace_until_battle(logger, steps, PACE_MAX_STEPS); } -BikeSelectCalls console_bike_select(ConsoleHandle& console, ProControllerContext& context){ - BikeSelectCalls calls; +OverworldWalkSteps::BikeSelectCalls console_bike_select(ConsoleHandle& console, ProControllerContext& context){ + OverworldWalkSteps::BikeSelectCalls calls; calls.press_select = [&console, &context]{ console.log("Pacing: SELECT (MINUS) off the Bicycle."); pbf_press_button(context, BUTTON_MINUS, MovementMechanics::BIKE_SELECT_PRESS, MovementMechanics::BIKE_SELECT_SETTLE); @@ -461,7 +461,7 @@ BikeSelectCalls console_bike_select(ConsoleHandle& console, ProControllerContext calls.riding = [&console, &context]{ context.wait_for_all_requests(); VideoSnapshot screen = console.video().snapshot(); - return screen && screen_shows_riding(*screen.frame); + return screen && OverworldWalkSteps::screen_shows_riding(*screen.frame); }; return calls; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h index 6b21cba411..3c8745ae34 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h @@ -255,18 +255,18 @@ bool pace_until_battle(Logger& logger, PatchSteps& steps, size_t max_steps); // way steps as well as turns, so pacing's turns walk it off the patch // (console B, 2026-11-09, 00.30 run 5: Route 7's grass). Returns whether // the player is on foot. -bool select_off_bike(const BikeSelectCalls& calls); +bool select_off_bike(const OverworldWalkSteps::BikeSelectCalls& calls); // One search for a wild battle, as a capture or a grind paces: off the // Bicycle (select_off_bike()), then pace_until_battle(). `steps` keeps // the tile pacing left the player on from one search to the next, so the // pace never starts a tile out. False if no battle came, or the player // rides on. -bool find_paced_encounter(Logger& logger, const BikeSelectCalls& bike, PatchSteps& steps); +bool find_paced_encounter(Logger& logger, const OverworldWalkSteps::BikeSelectCalls& bike, PatchSteps& steps); // select_off_bike()'s calls on the console: SELECT is the Switch's MINUS, -// and the player rides while no standing sprite shows (screen_shows_riding()). -BikeSelectCalls console_bike_select(ConsoleHandle& console, ProControllerContext& context); +// and the player rides while no standing sprite shows (OverworldWalkSteps::screen_shows_riding()). +OverworldWalkSteps::BikeSelectCalls console_bike_select(ConsoleHandle& console, ProControllerContext& context); // Steps in a patch before pacing gives up: about 10 % of steps in grass // start a battle at Route 4's rates. diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp index 34a8fb8c4e..e08083594a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp @@ -147,7 +147,7 @@ class SimConsoleCapture : public CaptureActions{ Logger& m_logger; OverworldSim& m_sim; SimPatch m_pacing; - BikeSelectCalls m_bike; + OverworldWalkSteps::BikeSelectCalls m_bike; std::vector m_species; size_t m_read = 0; }; From 6c658e926814e379c68d20d812a63a4511fab000 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 23:17:29 -0800 Subject: [PATCH 27/28] FRLG AutoStory: the pacing search in its own header, AutoStoryPacing.h Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../PokemonFRLG_AutoStoryCapture.cpp | 31 +---------- .../AutoStory/PokemonFRLG_AutoStoryCapture.h | 18 ------- .../AutoStory/PokemonFRLG_AutoStoryGrind.cpp | 1 + .../PokemonFRLG_AutoStoryPaceSim_Tests.cpp | 1 + .../AutoStory/PokemonFRLG_AutoStoryPacing.cpp | 52 +++++++++++++++++++ .../AutoStory/PokemonFRLG_AutoStoryPacing.h | 46 ++++++++++++++++ SerialPrograms/cmake/SourceFiles.cmake | 2 + 7 files changed, 103 insertions(+), 48 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.h diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.cpp index 8e95418a29..5d6582a1b6 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.cpp @@ -22,6 +22,7 @@ #include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.h" #include "PokemonFRLG_AutoStoryActions.h" #include "PokemonFRLG_AutoStoryCapture.h" +#include "PokemonFRLG_AutoStoryPacing.h" #include "PokemonFRLG_AutoStoryVariantWords.h" namespace PokemonAutomation{ @@ -437,36 +438,6 @@ bool pace_until_battle(Logger& logger, PatchSteps& steps, size_t max_steps){ } -bool select_off_bike(const OverworldWalkSteps::BikeSelectCalls& calls){ - for (size_t c = 0; c < 2 && calls.riding(); c++){ - calls.press_select(); - } - return !calls.riding(); -} - -bool find_paced_encounter(Logger& logger, const OverworldWalkSteps::BikeSelectCalls& bike, PatchSteps& steps){ - if (!select_off_bike(bike)){ - logger.log("Pacing: still on the Bicycle after SELECT twice; not pacing on it.", COLOR_RED); - return false; - } - return pace_until_battle(logger, steps, PACE_MAX_STEPS); -} - -OverworldWalkSteps::BikeSelectCalls console_bike_select(ConsoleHandle& console, ProControllerContext& context){ - OverworldWalkSteps::BikeSelectCalls calls; - calls.press_select = [&console, &context]{ - console.log("Pacing: SELECT (MINUS) off the Bicycle."); - pbf_press_button(context, BUTTON_MINUS, MovementMechanics::BIKE_SELECT_PRESS, MovementMechanics::BIKE_SELECT_SETTLE); - }; - calls.riding = [&console, &context]{ - context.wait_for_all_requests(); - VideoSnapshot screen = console.video().snapshot(); - return screen && OverworldWalkSteps::screen_shows_riding(*screen.frame); - }; - return calls; -} - - ConsolePatchSteps::ConsolePatchSteps( ConsoleHandle& console, ProControllerContext& context, Direction pace, PaceSpot& spot, const PaceTrainerBattle& trainer_battle diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h index 3c8745ae34..7b692d4a67 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h @@ -38,7 +38,6 @@ #include "Pokemon/AutoStory/Pokemon_StoryEvents.h" #include "Pokemon/AutoStory/Pokemon_StorySteps.h" #include "Pokemon/Navigation/Pokemon_Grid.h" -#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" #include "PokemonFRLG_AutoStoryPcStorage.h" namespace PokemonAutomation{ @@ -250,23 +249,6 @@ class PatchSteps{ // meanwhile is fought (PatchSteps::fight_trainer()) and pacing goes on; // their battle isn't a wild one. bool pace_until_battle(Logger& logger, PatchSteps& steps, size_t max_steps); -// Before pacing: gets the player off the Bicycle with SELECT, checked, -// and once more if they still ride. On the Bicycle a tap facing another -// way steps as well as turns, so pacing's turns walk it off the patch -// (console B, 2026-11-09, 00.30 run 5: Route 7's grass). Returns whether -// the player is on foot. -bool select_off_bike(const OverworldWalkSteps::BikeSelectCalls& calls); - -// One search for a wild battle, as a capture or a grind paces: off the -// Bicycle (select_off_bike()), then pace_until_battle(). `steps` keeps -// the tile pacing left the player on from one search to the next, so the -// pace never starts a tile out. False if no battle came, or the player -// rides on. -bool find_paced_encounter(Logger& logger, const OverworldWalkSteps::BikeSelectCalls& bike, PatchSteps& steps); - -// select_off_bike()'s calls on the console: SELECT is the Switch's MINUS, -// and the player rides while no standing sprite shows (OverworldWalkSteps::screen_shows_riding()). -OverworldWalkSteps::BikeSelectCalls console_bike_select(ConsoleHandle& console, ProControllerContext& context); // Steps in a patch before pacing gives up: about 10 % of steps in grass // start a battle at Route 4's rates. diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.cpp index 5af2285722..57c103090d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.cpp @@ -10,6 +10,7 @@ #include "CommonFramework/Exceptions/OperationFailedException.h" #include "PokemonFRLG_AutoStoryActions.h" #include "PokemonFRLG_AutoStoryGrind.h" +#include "PokemonFRLG_AutoStoryPacing.h" #include "PokemonFRLG_AutoStoryVariantWords.h" namespace PokemonAutomation{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp index e08083594a..1f4d740bc8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp @@ -18,6 +18,7 @@ #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h" #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryPaceSim.h" +#include "PokemonFRLG_AutoStoryPacing.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.cpp new file mode 100644 index 0000000000..40aca3b218 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.cpp @@ -0,0 +1,52 @@ +/* AutoStory Pacing + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h" +#include "PokemonFRLG_AutoStoryPacing.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using OverworldWalkSteps::BikeSelectCalls; + + +bool select_off_bike(const BikeSelectCalls& calls){ + for (size_t c = 0; c < 2 && calls.riding(); c++){ + calls.press_select(); + } + return !calls.riding(); +} + +bool find_paced_encounter(Logger& logger, const BikeSelectCalls& bike, PatchSteps& steps){ + if (!select_off_bike(bike)){ + logger.log("Pacing: still on the Bicycle after SELECT twice; not pacing on it.", COLOR_RED); + return false; + } + return pace_until_battle(logger, steps, PACE_MAX_STEPS); +} + +BikeSelectCalls console_bike_select(ConsoleHandle& console, ProControllerContext& context){ + BikeSelectCalls calls; + calls.press_select = [&console, &context]{ + console.log("Pacing: SELECT (MINUS) off the Bicycle."); + pbf_press_button(context, BUTTON_MINUS, MovementMechanics::BIKE_SELECT_PRESS, MovementMechanics::BIKE_SELECT_SETTLE); + }; + calls.riding = [&console, &context]{ + context.wait_for_all_requests(); + VideoSnapshot screen = console.video().snapshot(); + return screen && OverworldWalkSteps::screen_shows_riding(*screen.frame); + }; + return calls; +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.h new file mode 100644 index 0000000000..c8cf49e2e2 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.h @@ -0,0 +1,46 @@ +/* AutoStory Pacing + * + * From: https://github.com/PokemonAutomation/ + * + * A capture's or grind's search for a wild battle: off the Bicycle, then + * pacing in the patch from where the last battle left the player. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryPacing_H +#define PokemonAutomation_PokemonFRLG_AutoStoryPacing_H + +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" +#include "PokemonFRLG_AutoStoryCapture.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +// Before pacing: gets the player off the Bicycle with SELECT, checked, +// and once more if they still ride. On the Bicycle a tap facing another +// way steps as well as turns, so pacing's turns walk it off the patch +// (console B, 2026-11-09, 00.30 run 5: Route 7's grass). Returns whether +// the player is on foot. +bool select_off_bike(const OverworldWalkSteps::BikeSelectCalls& calls); + +// One search for a wild battle, as a capture or a grind paces: off the +// Bicycle (select_off_bike()), then pace_until_battle(). `steps` keeps +// the tile pacing left the player on from one search to the next, so the +// pace never starts a tile out. False if no battle came, or the player +// rides on. +bool find_paced_encounter(Logger& logger, const OverworldWalkSteps::BikeSelectCalls& bike, PatchSteps& steps); + +// select_off_bike()'s calls on the console: SELECT is the Switch's MINUS, +// and the player rides while no standing sprite shows (OverworldWalkSteps::screen_shows_riding()). +OverworldWalkSteps::BikeSelectCalls console_bike_select(ConsoleHandle& console, ProControllerContext& context); + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index b54cf6fb45..3f8d707db9 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1958,6 +1958,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryParty.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryParty.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryParty_Tests.cpp From acc74a77e2cbcad87d1f1f55478c8c5eb302daad Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 23:33:22 -0800 Subject: [PATCH 28/28] PokemonFRLG: Split over-limit headers, drop char* for code health The newer code-health rules (header symbol counts, no char* / char[]) failed 41 times against frlg/story-json. Declarations move unchanged into headers by concern (Pokemon Center room, hideout lift room, bike and spinner mechanics and presses, overworld spin, sim field, overworld bike, bag, bag use, room legs, field steps, hideout lift); char* constants and parameters become std::string_view or std::string. No behavior change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Overworld/PokemonFRLG_BikeSpinMechanics.h | 68 ++++++++++++++++ .../Overworld/PokemonFRLG_BikeSpinPresses.h | 55 +++++++++++++ .../Overworld/PokemonFRLG_CodedRooms.cpp | 10 ++- .../Overworld/PokemonFRLG_CodedRooms.h | 42 ---------- .../Overworld/PokemonFRLG_HideoutLiftRoom.h | 43 ++++++++++ .../Overworld/PokemonFRLG_MovementMechanics.h | 54 ------------- .../Overworld/PokemonFRLG_OverworldBike.h | 76 +++++++++++++++++ .../PokemonFRLG_OverworldBike_Tests.cpp | 4 + .../Overworld/PokemonFRLG_OverworldMap.cpp | 19 ++--- .../Overworld/PokemonFRLG_OverworldMap.h | 39 +-------- .../PokemonFRLG_OverworldRecoveryWalk.cpp | 2 + .../Overworld/PokemonFRLG_OverworldRoute.cpp | 1 + .../PokemonFRLG_OverworldRoute_Tests.cpp | 12 +-- .../Overworld/PokemonFRLG_OverworldSim.cpp | 2 +- .../Overworld/PokemonFRLG_OverworldSim.h | 30 +------ .../PokemonFRLG_OverworldSimField.cpp | 3 + .../Overworld/PokemonFRLG_OverworldSimField.h | 48 +++++++++++ .../PokemonFRLG_OverworldSimTaps.cpp | 4 + .../PokemonFRLG_OverworldSim_Tests.cpp | 2 + .../Overworld/PokemonFRLG_OverworldSpin.h | 62 ++++++++++++++ .../PokemonFRLG_OverworldSpin_Tests.cpp | 8 +- .../Overworld/PokemonFRLG_OverworldWalk.cpp | 3 + .../Overworld/PokemonFRLG_OverworldWalk.h | 41 ---------- .../PokemonFRLG_OverworldWalk_Tests.cpp | 1 + .../Overworld/PokemonFRLG_PokemonCenterRoom.h | 52 ++++++++++++ .../AutoStory/PokemonFRLG_AutoStoryBag.h | 78 ++++++++++++++++++ .../AutoStory/PokemonFRLG_AutoStoryBagUse.h | 59 ++++++++++++++ .../PokemonFRLG_AutoStoryBattleData_Tests.cpp | 4 +- .../AutoStory/PokemonFRLG_AutoStoryEvents.h | 21 ++--- .../PokemonFRLG_AutoStoryFieldSteps.h | 54 +++++++++++++ .../PokemonFRLG_AutoStoryHideout.cpp | 5 ++ .../AutoStory/PokemonFRLG_AutoStoryHideout.h | 29 ------- .../PokemonFRLG_AutoStoryHideoutLift.h | 64 +++++++++++++++ .../PokemonFRLG_AutoStoryHideout_Tests.cpp | 6 +- .../PokemonFRLG_AutoStoryItemUse.cpp | 4 +- .../AutoStory/PokemonFRLG_AutoStoryItemUse.h | 81 +------------------ .../PokemonFRLG_AutoStoryItemUse_Tests.cpp | 5 +- .../PokemonFRLG_AutoStoryPaceSim_Tests.cpp | 2 + .../AutoStory/PokemonFRLG_AutoStoryPacing.cpp | 2 + .../AutoStory/PokemonFRLG_AutoStoryPacing.h | 1 + .../AutoStory/PokemonFRLG_AutoStoryRoomLegs.h | 44 ++++++++++ .../AutoStory/PokemonFRLG_AutoStoryRooms.cpp | 1 + .../AutoStory/PokemonFRLG_AutoStoryRooms.h | 14 ---- .../PokemonFRLG_AutoStoryRooms_Tests.cpp | 4 +- .../AutoStory/PokemonFRLG_AutoStoryRowSim.cpp | 3 + .../PokemonFRLG_AutoStoryTableSteps.cpp | 8 +- .../PokemonFRLG_AutoStoryTableSteps.h | 19 ----- ...okemonFRLG_AutoStoryVermilionGym_Tests.cpp | 1 + SerialPrograms/cmake/SourceFiles.cmake | 12 +++ 49 files changed, 818 insertions(+), 384 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_BikeSpinMechanics.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_BikeSpinPresses.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_HideoutLiftRoom.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PokemonCenterRoom.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBag.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBagUse.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFieldSteps.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideoutLift.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRoomLegs.h diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_BikeSpinMechanics.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_BikeSpinMechanics.h new file mode 100644 index 0000000000..ae9283b104 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_BikeSpinMechanics.h @@ -0,0 +1,68 @@ +/* Bicycle and Spinner Mechanics + * + * From: https://github.com/PokemonAutomation/ + * + * How FireRed and LeafGreen move the player on the Bicycle and on spinner + * tiles, as measured on the console (PokemonFRLG_MovementMechanics.h has + * walking and running). + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_BikeSpinMechanics_H +#define PokemonAutomation_PokemonFRLG_BikeSpinMechanics_H + +#include "Common/Cpp/Time.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ +namespace MovementMechanics{ + + +// Riding the Bicycle, measured on console B (2026-11-09, Lavender Town, +// east from the rock west of the Center, from rest, counted back with +// one-tile taps): holds of 450, 500, 650, 700, 900, 950, 1100, 1250, +// 1300, 1450 and 1500ms moved 5, 5, 7, 7, 9, 10, 11, 13, 13, 15 and 15 +// tiles, the same each time: a tile per 100ms, so an open leg's hold of +// (n - 1) tiles and a half moves n. 112ms a tile (a wall-to-wall time +// of the motion on screen) held "west 16 (open)" for 17 or 18 tiles on +// Route 8 (00.30 run 2). Not the decompilation's 4 frames a tile (67ms): +// the bike speeds up from rest. +inline constexpr Milliseconds BIKE_TILE_TIME(100); + +// On the Bicycle a tap never only turns the player: from rest, a 40ms tap +// moved a tile facing either way, and holds of 112, 150, 200, 250, 300, +// 336 and 400ms moved 1, 2, 2, 3, 3, 4 and 4 tiles (console B, +// 2026-11-09, Route 12's north pier). So a bike leg has no turn tap: its +// hold turns the player too, and loses no tile for it. +inline constexpr bool BIKE_TAP_ONLY_TURNS = false; + +// Chains of 40ms taps on the Bicycle (console B, 2026-11-09): one tap per +// 100ms (60ms release) moved exactly a tile each. +inline constexpr Milliseconds BIKE_TAP_PERIOD_WORKED(100); + +// At one tap per 60ms, one tap in 12 was dropped. Somewhere between that +// and BIKE_TAP_PERIOD_WORKED the game starts dropping taps; the sim drops +// taps that start sooner than this after the last one. +inline constexpr Milliseconds BIKE_TAP_DROP_PERIOD(80); + +// Of the taps that come sooner than BIKE_TAP_DROP_PERIOD, the sim drops +// one in this many. +inline constexpr int BIKE_FAST_TAP_DROP_EVERY = 12; + +// With the Bicycle registered (Key Items, BICYCLE, REGISTER), SELECT gets +// on and off in about 1s (console B, 2026-11-09). Going through a door +// takes the player off (not measured); a battle leaves them on. +inline constexpr Milliseconds BIKE_SELECT_TIME(1000); + +// A spinner tile (Rocket Hideout, Viridian Gym) slides the player a tile +// per 8 frames (pokefirered PlayerGoSpin(): MOVE_SPEED_FAST_1). Not +// measured on the console. +inline constexpr Milliseconds SPIN_TILE_TIME(133); + + +} // namespace MovementMechanics +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_BikeSpinPresses.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_BikeSpinPresses.h new file mode 100644 index 0000000000..e58d996e90 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_BikeSpinPresses.h @@ -0,0 +1,55 @@ +/* Bicycle and Spinner Presses + * + * From: https://github.com/PokemonAutomation/ + * + * The presses the walker sends on the Bicycle and after a slide, from + * PokemonFRLG_BikeSpinMechanics.h. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_BikeSpinPresses_H +#define PokemonAutomation_PokemonFRLG_BikeSpinPresses_H + +#include "Common/Cpp/Time.h" +#include "PokemonFRLG_BikeSpinMechanics.h" +#include "PokemonFRLG_MovementMechanics.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ +namespace MovementMechanics{ + + +// An open leg on the Bicycle is counted out in taps, each a tile: a 40ms +// press each. +inline constexpr Milliseconds BIKE_TAP(40); + +// The taps' period, start to start, slower than the chains that worked, +// so none is dropped. +inline constexpr Milliseconds BIKE_TAP_PERIOD(120); + +// After a tap, how long until the screen shows whether it moved: the +// tile's 100ms and video lag. The Command File Runner's tap check judged +// its first tap too early, as unmoved. Not measured: a guess. +inline constexpr Milliseconds BIKE_TAP_SETTLE(250); + +// SELECT, pressed to get on the registered Bicycle. +inline constexpr Milliseconds BIKE_SELECT_PRESS(160); + +// The wait after SELECT for the player to be up on the Bicycle. +inline constexpr Milliseconds BIKE_SELECT_SETTLE(1200); + +// The wait after a slide's last tile before the next press: the spin's +// last frames and the facing it leaves. Not measured. +inline constexpr Milliseconds SPIN_STOP_SETTLE(300); + +static_assert(BIKE_TAP_PERIOD > BIKE_TAP_PERIOD_WORKED && BIKE_TAP_PERIOD_WORKED > BIKE_TAP_DROP_PERIOD); +static_assert(BIKE_TAP < BIKE_TILE_TIME && BIKE_TAP <= LONGEST_TAP); +static_assert(BIKE_SELECT_SETTLE > BIKE_SELECT_TIME); + + +} // namespace MovementMechanics +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.cpp index 64a103cf0a..d9a0957b8c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.cpp @@ -7,6 +7,8 @@ #include #include #include "PokemonFRLG_CodedRooms.h" +#include "PokemonFRLG_HideoutLiftRoom.h" +#include "PokemonFRLG_PokemonCenterRoom.h" #include "PokemonFRLG_OverworldMaps.h" namespace PokemonAutomation{ @@ -88,7 +90,7 @@ MapObject object(std::string name, Pokemon::ObjectKind kind, const TilePos& tile // *_PokemonCenter_1F.json) all have them. The mat's warps: (6..8, 8), // the middle one MB_SOUTH_ARROW_WARP-like ("walk_south"); the stairs to // 2F at (1, 6), which isn't loaded. -const char CENTER_ROOM_JSON[] = R"({ +constexpr std::string_view CENTER_ROOM_JSON = R"({ "name": "PokemonCenter_1F", "source": {"decomp_file": "data/layouts/PokemonCenter_1F/map.bin", "revision": "ce6406d8c2eaf46d543c8a2606de0c3cb1951982", "date": "2026-11-09"}, "allow_running": false, @@ -108,7 +110,7 @@ const char CENTER_ROOM_JSON[] = R"({ // walls, 3 to 5 floor), the panel's bg event at (0, 2), and warp 1 at // (2, 5), the one MB_SOUTH_ARROW_WARP tile (warp 0 at (1, 5) has no // behaviour, so never fires). -const char LIFT_ROOM_JSON[] = R"({ +constexpr std::string_view LIFT_ROOM_JSON = R"({ "name": "RocketHideout_Elevator", "source": {"decomp_file": "data/layouts/RocketHideout_Elevator/map.bin", "revision": "ce6406d8c2eaf46d543c8a2606de0c3cb1951982", "date": "2026-11-09"}, "tiles": ["#####", "#####", "S####", ".....", ".....", ".....", "#####"], @@ -165,7 +167,7 @@ bool is_pokemon_center(std::string_view map){ } MapData pokemon_center_room(const std::string& name, const Pokemon::WarpDestination& outside){ - MapData room = parse_map(CENTER_ROOM_JSON, name); + MapData room = parse_map(std::string(CENTER_ROOM_JSON), name); room.name = name; for (MapWarp& warp : room.warps){ if (warp.y == CENTER_MAT.y){ @@ -192,7 +194,7 @@ std::optional add_pokemon_center_room(std::vector& maps, s MapData rocket_hideout_lift_room(){ - return parse_map(LIFT_ROOM_JSON, std::string(LIFT_MAP)); + return parse_map(std::string(LIFT_ROOM_JSON), std::string(LIFT_MAP)); } const std::vector& rocket_hideout_lift_doors(){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h index 348aba0492..8b8b548730 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h @@ -47,48 +47,6 @@ inline constexpr TilePos GAME_CORNER_POSTER{11, 1}; void add_coded_objects(MapData& map); -// A Pokemon Center's ground floor. pokefirered gives all of Kanto's and -// the Sevii Islands' but the Indigo Plateau's and One Island's the one -// layout, LAYOUT_POKEMON_CENTER_1F; the seven in the map lock have the -// same tiles and differ only in who stands where, none of them on the -// nurse's column. -// The mat inside the door: the player arrives on it and leaves from it, -// walking south. -inline constexpr TilePos CENTER_MAT{7, 8}; -// Below the nurse's counter, facing her (the nurse at (7, 2)). -inline constexpr TilePos CENTER_NURSE_FRONT{7, 4}; -// Whether `map` names a Pokemon Center's ground floor. -bool is_pokemon_center(std::string_view map); -// The ground floor of the Center named `name`, whose mat lets out on -// `outside` (the door tile of the town's map): its tiles, the nurse, and -// the mat's warps. No person but the nurse: who stands where differs, and -// the script keeps to the nurse's column. -MapData pokemon_center_room(const std::string& name, const Pokemon::WarpDestination& outside); -// For a sim: adds the coded room of the Center whose door is on -// `maps[town]` to `maps`, and returns its name; nothing if the town has -// no Center door. The town gets a point "
_door" on its door tile -// for the room's mat to let out on. -std::optional add_pokemon_center_room(std::vector& maps, size_t town); - - -// The Rocket Hideout's lift (pokefirered LAYOUT_ROCKET_HIDEOUT_ELEVATOR, -// 5 by 7 tiles, rows 3 to 5 walkable). -inline constexpr std::string_view LIFT_MAP = "RocketHideout_Elevator"; -// Where the floors' lift doors put the player (warp 1, an -// MB_SOUTH_ARROW_WARP tile): south from it leaves the lift. -inline constexpr TilePos LIFT_ARRIVAL{2, 5}; -// Below the panel (a bg event at (0, 2), RocketHideout_Elevator_ -// EventScript_FloorSelect), facing north. -inline constexpr TilePos LIFT_PANEL_FRONT{0, 3}; -// The lift's room. Its door is a dynamic warp: the sim takes it to the -// floor picked (SimScript::lift_floors with rocket_hideout_lift_doors()). -MapData rocket_hideout_lift_room(); -// Where the lift lets out for each floor on its panel, top first -// (setdynamicwarp in RocketHideout_Elevator's scripts): B1F (24, 25), -// B2F (28, 16), B4F (20, 23), each its floor's lift door. -const std::vector& rocket_hideout_lift_doors(); - - } // namespace PokemonFRLG } // namespace NintendoSwitch diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_HideoutLiftRoom.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_HideoutLiftRoom.h new file mode 100644 index 0000000000..30b8e80671 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_HideoutLiftRoom.h @@ -0,0 +1,43 @@ +/* Hideout Lift Room + * + * From: https://github.com/PokemonAutomation/ + * + * The Rocket Hideout's lift, coded rather than mapped + * (PokemonFRLG_CodedRooms.h). + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_HideoutLiftRoom_H +#define PokemonAutomation_PokemonFRLG_HideoutLiftRoom_H + +#include +#include "PokemonFRLG_OverworldMap.h" +#include "PokemonFRLG_OverworldRoute.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The Rocket Hideout's lift (pokefirered LAYOUT_ROCKET_HIDEOUT_ELEVATOR, +// 5 by 7 tiles, rows 3 to 5 walkable). +inline constexpr std::string_view LIFT_MAP = "RocketHideout_Elevator"; +// Where the floors' lift doors put the player (warp 1, an +// MB_SOUTH_ARROW_WARP tile): south from it leaves the lift. +inline constexpr TilePos LIFT_ARRIVAL{2, 5}; +// Below the panel (a bg event at (0, 2), RocketHideout_Elevator_ +// EventScript_FloorSelect), facing north. +inline constexpr TilePos LIFT_PANEL_FRONT{0, 3}; +// The lift's room. Its door is a dynamic warp: the sim takes it to the +// floor picked (SimScript::lift_floors with rocket_hideout_lift_doors()). +MapData rocket_hideout_lift_room(); +// Where the lift lets out for each floor on its panel, top first +// (setdynamicwarp in RocketHideout_Elevator's scripts): B1F (24, 25), +// B2F (28, 16), B4F (20, 23), each its floor's lift door. +const std::vector& rocket_hideout_lift_doors(); + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h index 1b29078224..461c103777 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h @@ -48,36 +48,6 @@ inline constexpr Milliseconds TILE_TIME(267); inline constexpr Milliseconds RUN_TILE_TIME(133); // Surfing: a tile per 8 frames. inline constexpr Milliseconds SURF_TILE_TIME(133); -// Riding the Bicycle, measured on console B (2026-11-09, Lavender Town, -// east from the rock west of the Center, from rest, counted back with -// one-tile taps): holds of 450, 500, 650, 700, 900, 950, 1100, 1250, -// 1300, 1450 and 1500ms moved 5, 5, 7, 7, 9, 10, 11, 13, 13, 15 and 15 -// tiles, the same each time: a tile per 100ms, so an open leg's hold of -// (n - 1) tiles and a half moves n. 112ms a tile (a wall-to-wall time -// of the motion on screen) held "west 16 (open)" for 17 or 18 tiles on -// Route 8 (00.30 run 2). Not the decompilation's 4 frames a tile (67ms): -// the bike speeds up from rest. -inline constexpr Milliseconds BIKE_TILE_TIME(100); -// On the Bicycle a tap never only turns the player: from rest, a 40ms tap -// moved a tile facing either way, and holds of 112, 150, 200, 250, 300, -// 336 and 400ms moved 1, 2, 2, 3, 3, 4 and 4 tiles (console B, -// 2026-11-09, Route 12's north pier). So a bike leg has no turn tap: its -// hold turns the player too, and loses no tile for it. -inline constexpr bool BIKE_TAP_ONLY_TURNS = false; -// Chains of 40ms taps on the Bicycle (console B, 2026-11-09): one tap per -// 100ms (60ms release) moved exactly a tile each. -inline constexpr Milliseconds BIKE_TAP_PERIOD_WORKED(100); -// At one tap per 60ms, one tap in 12 was dropped. Somewhere between that -// and BIKE_TAP_PERIOD_WORKED the game starts dropping taps; the sim drops -// taps that start sooner than this after the last one. -inline constexpr Milliseconds BIKE_TAP_DROP_PERIOD(80); -// Of the taps that come sooner than BIKE_TAP_DROP_PERIOD, the sim drops -// one in this many. -inline constexpr int BIKE_FAST_TAP_DROP_EVERY = 12; -// With the Bicycle registered (Key Items, BICYCLE, REGISTER), SELECT gets -// on and off in about 1s (console B, 2026-11-09). Going through a door -// takes the player off (not measured); a battle leaves them on. -inline constexpr Milliseconds BIKE_SELECT_TIME(1000); // The longest tap measured to only turn a player facing another way, and // to take exactly one step for one facing its way. inline constexpr Milliseconds LONGEST_TAP(133); @@ -92,10 +62,6 @@ inline constexpr Milliseconds STEP_TAP_SPACING_WORKED(666); // the D-pad this long. Not measured: pokefirered's fade from black is 16 // frames, then the field takes input; a tap sent at once was lost. inline constexpr Milliseconds BATTLE_EXIT_LOCKOUT(500); -// A spinner tile (Rocket Hideout, Viridian Gym) slides the player a tile -// per 8 frames (pokefirered PlayerGoSpin(): MOVE_SPEED_FAST_1). Not -// measured on the console. -inline constexpr Milliseconds SPIN_TILE_TIME(133); // The walker's presses. @@ -118,35 +84,15 @@ inline constexpr Milliseconds STEP_TAP(66); // STEP_TAP_RELEASE after it, past the lockout. inline constexpr Milliseconds STEP_TAP_RELEASE(700); -// An open leg on the Bicycle is counted out in taps, each a tile: a 40ms -// press each. -inline constexpr Milliseconds BIKE_TAP(40); -// The taps' period, start to start, slower than the chains that worked, -// so none is dropped. -inline constexpr Milliseconds BIKE_TAP_PERIOD(120); -// After a tap, how long until the screen shows whether it moved: the -// tile's 100ms and video lag. The Command File Runner's tap check judged -// its first tap too early, as unmoved. Not measured: a guess. -inline constexpr Milliseconds BIKE_TAP_SETTLE(250); -// SELECT, pressed to get on the registered Bicycle. -inline constexpr Milliseconds BIKE_SELECT_PRESS(160); -// The wait after SELECT for the player to be up on the Bicycle. -inline constexpr Milliseconds BIKE_SELECT_SETTLE(1200); // How long the overworld must show nothing (no box, fade or battle) after // a battle before the next tap: twice the lockout. inline constexpr Milliseconds AFTER_BATTLE_QUIET(1000); -// The wait after a slide's last tile before the next press: the spin's -// last frames and the facing it leaves. Not measured. -inline constexpr Milliseconds SPIN_STOP_SETTLE(300); static_assert(TURN_TAP <= LONGEST_TAP && FACE_HOLD <= LONGEST_TAP && STEP_TAP <= LONGEST_TAP); static_assert(STEP_TAP + STEP_TAP_RELEASE > STEP_TAP_SPACING_WORKED); static_assert(STEP_TAP_SPACING_WORKED > STEP_TAP_LOCKOUT); static_assert(AFTER_BATTLE_QUIET > BATTLE_EXIT_LOCKOUT); -static_assert(BIKE_TAP_PERIOD > BIKE_TAP_PERIOD_WORKED && BIKE_TAP_PERIOD_WORKED > BIKE_TAP_DROP_PERIOD); -static_assert(BIKE_TAP < BIKE_TILE_TIME && BIKE_TAP <= LONGEST_TAP); -static_assert(BIKE_SELECT_SETTLE > BIKE_SELECT_TIME); } // namespace MovementMechanics diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike.h new file mode 100644 index 0000000000..ca42c358a7 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike.h @@ -0,0 +1,76 @@ +/* Overworld Bicycle + * + * From: https://github.com/PokemonAutomation/ + * + * How the overworld walker rides the Bicycle: which legs it taps out, + * and getting on with SELECT. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_OverworldBike_H +#define PokemonAutomation_PokemonFRLG_OverworldBike_H + +#include +#include +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "PokemonFRLG_OverworldWalk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The speed walked at instead of `speed` while off the Bicycle (a door +// took the player off): a run. +MoveSpeed on_foot(MoveSpeed speed); + +namespace OverworldWalkSteps{ + +// Whether a WALK `step` at `speed` starts with a turn tap, for a player +// facing `facing`: on foot facing another way (or an unknown way), so the +// hold starts facing the right way. Never on the Bicycle, where a tap +// takes a step (MovementMechanics::BIKE_TAP_ONLY_TURNS). +bool turns_first(const RouteStep& step, MoveSpeed speed, std::optional facing); + +// Whether `step` at `speed` is walked a tap at a time, each a tile +// (count_bike_taps()): an open leg on the Bicycle, outdoors, not onto a +// warp or a spinner, and not over a map connection, where the game holds +// the player up a tile (a tap there isn't measured). Legs to a wall are +// held: the wall ends them. +bool counts_bike_taps(const RouteStep& step, MoveSpeed speed); + +// Whether, walking a route at `speed` and riding the Bicycle or not +// (`riding`), the player gets on with SELECT before `step`: a WALK where +// the Bicycle may be ridden, after a door took them off. +bool gets_on_bike(const RouteStep& step, MoveSpeed speed, bool riding); + +// The console side of select_onto_bike(): presses SELECT and waits it +// out; reads whether the player rides. +struct BikeSelectCalls{ + std::function press_select; + std::function riding; +}; + +// Gets on the registered Bicycle with SELECT, checks the player rides, +// and presses again once if not. Returns whether they ride. +bool select_onto_bike(const BikeSelectCalls& calls); + +// Whether the player rides the Bicycle, from the screen: the overworld +// is up, and no standing player's sprite shows where the player stands +// (read_player_facing()). False under a message or a menu. +bool screen_shows_riding(const ImageViewRGB32& screen); + +// select_onto_bike() on the console: SELECT is the Switch's MINUS. +// Throws OperationFailedException if the player doesn't ride after two +// presses. +void get_on_bike(ConsoleHandle& console, ProControllerContext& context); + +} // namespace OverworldWalkSteps + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp index ce660ee05b..eca8adf9da 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp @@ -14,10 +14,14 @@ #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/TestRunners/UnitTest.h" #include "PokemonFRLG_MovementMechanics.h" +#include "PokemonFRLG_BikeSpinPresses.h" +#include "PokemonFRLG_BikeSpinMechanics.h" #include "PokemonFRLG_OverworldMaps.h" #include "PokemonFRLG_OverworldRecovery.h" #include "PokemonFRLG_OverworldRoute.h" #include "PokemonFRLG_OverworldSim.h" +#include "PokemonFRLG_OverworldSimField.h" +#include "PokemonFRLG_OverworldBike.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp index 41a578fdde..997c6b4609 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.cpp @@ -8,6 +8,7 @@ #include "Common/Cpp/Exceptions.h" #include "PokemonFRLG_CodedRooms.h" #include "PokemonFRLG_OverworldMap.h" +#include "PokemonFRLG_OverworldSpin.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -231,16 +232,16 @@ StepKind classify_step_onto( return StepKind::WALK; } -const char SPIN_NORTH_REGION[] = "spin_north"; -const char SPIN_SOUTH_REGION[] = "spin_south"; -const char SPIN_WEST_REGION[] = "spin_west"; -const char SPIN_EAST_REGION[] = "spin_east"; -const char SPIN_STOP_REGION[] = "spin_stop"; - namespace{ -bool in_region(const MapData& map, const char* region, const TilePos& point){ - const auto tiles = map.regions.find(region); +// The regions a map may list spinner tiles in (PokemonFRLG_OverworldSpin.h). +constexpr std::string_view SPIN_NORTH_REGION = "spin_north"; +constexpr std::string_view SPIN_SOUTH_REGION = "spin_south"; +constexpr std::string_view SPIN_WEST_REGION = "spin_west"; +constexpr std::string_view SPIN_EAST_REGION = "spin_east"; +constexpr std::string_view SPIN_STOP_REGION = "spin_stop"; +bool in_region(const MapData& map, std::string_view region, const TilePos& point){ + const auto tiles = map.regions.find(std::string(region)); return tiles != map.regions.end() && std::find(tiles->second.begin(), tiles->second.end(), point) != tiles->second.end(); } @@ -250,7 +251,7 @@ std::optional spin_direction(const MapData& map, const TilePos& point if (const std::optional coded = coded_spin_direction(map.name, point)){ return coded; } - static const std::pair ARROWS[] = { + static const std::pair ARROWS[] = { {SPIN_NORTH_REGION, Direction::NORTH}, {SPIN_SOUTH_REGION, Direction::SOUTH}, {SPIN_WEST_REGION, Direction::WEST}, diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h index aecc3af3c0..469f17efa7 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMap.h @@ -19,6 +19,7 @@ #include #include #include +#include #include "Pokemon/Navigation/Pokemon_Grid.h" #include "Pokemon/Navigation/Pokemon_MapData.h" @@ -160,44 +161,6 @@ PlayerState after_step_onto( ); -// Spinner tiles (Rocket Hideout B2F and B3F, Viridian Gym): pokefirered's -// MB_SPIN_UP, MB_SPIN_DOWN, MB_SPIN_LEFT, MB_SPIN_RIGHT and -// MB_STOP_SPINNING. The tile glyphs can't say so: the hideout's floors -// have them coded (coded_spin_direction(), PokemonFRLG_CodedRooms.h); -// another map may list them in these regions, as it does WILD_REGION. -// "spin_north": MB_SPIN_UP. -extern const char SPIN_NORTH_REGION[]; -// "spin_south": MB_SPIN_DOWN. -extern const char SPIN_SOUTH_REGION[]; -// "spin_west": MB_SPIN_LEFT. -extern const char SPIN_WEST_REGION[]; -// "spin_east": MB_SPIN_RIGHT. -extern const char SPIN_EAST_REGION[]; -// "spin_stop": MB_STOP_SPINNING. -extern const char SPIN_STOP_REGION[]; -// The way the arrow on this tile slides the player, if it is a spinner. -std::optional spin_direction(const MapData& map, const TilePos& point); -// Whether this tile ends a slide (MB_STOP_SPINNING). -bool stops_spinning(const MapData& map, const TilePos& point); - -// Where a slide leaves the player, the way it last moved them, and how -// many tiles it took. -struct SpinSlide{ - PlayerState end; - Direction facing = Direction::NORTH; - int tiles = 0; -}; -// The slide of a player who has just stepped onto the spinner `state` -// stands on (pokefirered TryUpdatePlayerSpinDirection()): a tile at a -// time the last arrow's way, turned by each arrow on the way, until a stop -// tile, or a step the step rules refuse (a wall, a person: the game's -// collision ends the forced movement there). Nothing if `state` isn't on a -// spinner, if the arrows turn the player round in a loop, or if the slide -// crosses a warp, which isn't modelled. -std::optional spin_slide(const MapData& map, const PlayerState& state); - - - } // namespace PokemonFRLG } // namespace NintendoSwitch } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp index 26ede29f78..e7fbd0e42a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecoveryWalk.cpp @@ -21,6 +21,8 @@ #include "PokemonFRLG_OverworldBikeTaps.h" #include "PokemonFRLG_OverworldLateBattle.h" #include "PokemonFRLG_OverworldRecoveryWalk.h" +#include "PokemonFRLG_BikeSpinPresses.h" +#include "PokemonFRLG_OverworldBike.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp index d16f0ca592..2e1c573abf 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp @@ -14,6 +14,7 @@ #include "Pokemon/Navigation/Pokemon_LegPlanner.h" #include "PokemonFRLG_OverworldMaps.h" #include "PokemonFRLG_OverworldRoute.h" +#include "PokemonFRLG_OverworldSpin.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp index 0f9fcb753a..3058352ad3 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp @@ -28,7 +28,7 @@ struct RouteTestInput{ UnitTestResult (*body)(); }; struct RouteTestExpectation{ - const char* description; + std::string description; }; struct RouteTestCase{ RouteTestInput input; @@ -232,7 +232,7 @@ UnitTestResult test_mewtwo_tile_unreachable(){ UnitTestResult test_story_maps(){ const std::vector maps = overworld_maps(); - for (const char* name : { + for (const std::string name : { "PalletTown_PlayersHouse_2F", "PalletTown_ProfessorOaksLab", "Route1", "ViridianCity_Mart", "Route22", "Route2", "ViridianForest", "PewterCity", "PewterCity_Museum_1F", "PewterCity_Gym", }){ @@ -289,7 +289,7 @@ UnitTestResult test_viridian_mart_to_pewter_gym(){ } // A room with a sign on its north wall, above the third column. -const char* const TEST_SIGN_ROOM_JSON = R"({ +const std::string TEST_SIGN_ROOM_JSON = R"({ "name": "SignRoom", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, "tiles": ["##S##", "#...#", "#...#", "#...#", "#####"], "elevation": ["33333", "33333", "33333", "33333", "33333"] @@ -321,7 +321,7 @@ UnitTestResult test_no_hold_into_sign(){ } // A yard with a row of ledges across it. -const char* const TEST_LEDGE_YARD_JSON = R"({ +const std::string TEST_LEDGE_YARD_JSON = R"({ "name": "LedgeYard", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, "tiles": ["#####", "#...#", "#...#", "#vvv#", "#...#", "#####"], "elevation": ["33333", "33333", "33333", "33333", "33333", "33333"] @@ -409,14 +409,14 @@ UnitTestResult test_wild_steps(){ return true; } -const char* const TEST_ROOM_JSON = R"({ +const std::string TEST_ROOM_JSON = R"({ "name": "Room", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, "tiles": ["#####", "#...#", "#...#", "##.##"], "elevation": ["33333", "33333", "33333", "33333"], "points": {"warp_0": [2, 3]}, "warps": [{"x": 2, "y": 3, "enter": "walk_south", "to_map": "Yard", "to_point": "warp_0"}] })"; -const char* const TEST_YARD_JSON = R"({ +const std::string TEST_YARD_JSON = R"({ "name": "Yard", "source": {"decomp_file": "", "revision": "test", "date": "2026-10-31"}, "tiles": ["#####", "##D##", "#...#", "#####"], "elevation": ["33333", "33333", "33333", "33333"], diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp index fc523332db..14129e9086 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp @@ -13,6 +13,7 @@ #include "PokemonFRLG_OverworldLateBattle.h" #include "PokemonFRLG_OverworldReanchor.h" #include "PokemonFRLG_OverworldWalk.h" +#include "PokemonFRLG_OverworldBike.h" #include "PokemonFRLG_OverworldSim.h" namespace PokemonAutomation{ @@ -672,7 +673,6 @@ bool OverworldSim::close_message(){ - // // OverworldSimWalker // diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h index c9a1b59e1e..3b847ecc70 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h @@ -42,10 +42,14 @@ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "Pokemon/AutoStory/Pokemon_ScreenEvents.h" #include "PokemonFRLG_MovementMechanics.h" +#include "PokemonFRLG_BikeSpinPresses.h" +#include "PokemonFRLG_BikeSpinMechanics.h" #include "PokemonFRLG_OverworldBikeTaps.h" #include "PokemonFRLG_OverworldRecovery.h" #include "PokemonFRLG_OverworldRoute.h" +#include "PokemonFRLG_OverworldSimField.h" #include "PokemonFRLG_OverworldWalk.h" +#include "PokemonFRLG_OverworldSpin.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -236,26 +240,6 @@ enum class SimBattleEnd{ NO_BATTLE, // there was none, on screen or about to show }; -// What A does on the overworld, facing something (OverworldSim::use_field()). -enum class SimField{ - // Cuts the tree the player faces (an object named "cut_tree") with - // YES to the game's question: it is gone until the player next warps, - // as its hide flag is a FLAG_TEMP. - CUT_TREE, - // The poster the player faces (an object named "poster"): the switch - // behind it opens the stairs, which takes away the objects named - // "poster_stairs" for good (pokefirered - // CeladonCity_GameCorner_EventScript_Poster's setmetatile). The sim - // adds both to the Game Corner from code (add_coded_objects()). - POSTER_SWITCH, - // The panel of the lift the player stands in (SimScript::lift_floors): - // the floor picked, the lift's door now lets out there. - LIFT_FLOOR, - // Not A: SELECT, which with the Bicycle registered gets on it where - // running is allowed, or off it, in about - // MovementMechanics::BIKE_SELECT_TIME. (PokemonFRLG_OverworldSimTaps.cpp) - BIKE_SELECT, -}; // Simulates overworld movement, encounters and dialogs. class OverworldSim{ @@ -617,12 +601,6 @@ class OverworldSimWalker{ void add_tests_OverworldSim(UnitTestDatabase& database); // Register the tests of a warp's fade over before the hold onto it ends. void add_tests_OverworldWarpFade(UnitTestDatabase& database); -// Register the tests of spinner tiles, the lift's panel and the poster -// switch on made-up maps. -void add_tests_OverworldSpin(UnitTestDatabase& database); -// Register the tests of the Bicycle's taps and SELECT on a made-up map. -void add_tests_OverworldBike(UnitTestDatabase& database); - } // namespace PokemonFRLG diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp index 3258b390d0..dfdc41651b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp @@ -11,6 +11,9 @@ #include #include "PokemonFRLG_CodedRooms.h" #include "PokemonFRLG_OverworldSim.h" +#include "PokemonFRLG_OverworldSimField.h" +#include "PokemonFRLG_BikeSpinMechanics.h" +#include "PokemonFRLG_OverworldSpin.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.h new file mode 100644 index 0000000000..4dd405a0f6 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.h @@ -0,0 +1,48 @@ +/* Overworld Sim Field Actions + * + * From: https://github.com/PokemonAutomation/ + * + * What the overworld sim does for A (or SELECT) on the field, and the + * tests of the Bicycle's taps and SELECT in the sim. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_OverworldSimField_H +#define PokemonAutomation_PokemonFRLG_OverworldSimField_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// What A does on the overworld, facing something (OverworldSim::use_field()). +enum class SimField{ + // Cuts the tree the player faces (an object named "cut_tree") with + // YES to the game's question: it is gone until the player next warps, + // as its hide flag is a FLAG_TEMP. + CUT_TREE, + // The poster the player faces (an object named "poster"): the switch + // behind it opens the stairs, which takes away the objects named + // "poster_stairs" for good (pokefirered + // CeladonCity_GameCorner_EventScript_Poster's setmetatile). The sim + // adds both to the Game Corner from code (add_coded_objects()). + POSTER_SWITCH, + // The panel of the lift the player stands in (SimScript::lift_floors): + // the floor picked, the lift's door now lets out there. + LIFT_FLOOR, + // Not A: SELECT, which with the Bicycle registered gets on it where + // running is allowed, or off it, in about + // MovementMechanics::BIKE_SELECT_TIME. (PokemonFRLG_OverworldSimTaps.cpp) + BIKE_SELECT, +}; + +// Register the tests of the Bicycle's taps and SELECT on a made-up map. +void add_tests_OverworldBike(UnitTestDatabase& database); + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp index 03bde06519..197d4d7355 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp @@ -10,6 +10,10 @@ #include #include "PokemonFRLG_OverworldSim.h" +#include "PokemonFRLG_OverworldSimField.h" +#include "PokemonFRLG_BikeSpinMechanics.h" +#include "PokemonFRLG_BikeSpinPresses.h" +#include "PokemonFRLG_OverworldBike.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp index cfa1e6b021..25d6f4bfc4 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp @@ -24,6 +24,8 @@ #include "PokemonFRLG_OverworldLateBattle.h" #include "PokemonFRLG_OverworldMaps.h" #include "PokemonFRLG_OverworldSim.h" +#include "PokemonFRLG_OverworldSimField.h" +#include "PokemonFRLG_OverworldSpin.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin.h new file mode 100644 index 0000000000..6a4a5a9730 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin.h @@ -0,0 +1,62 @@ +/* Overworld Spinner Tiles + * + * From: https://github.com/PokemonAutomation/ + * + * Spinner tiles: which way their arrows slide the player, and where a + * slide ends. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_OverworldSpin_H +#define PokemonAutomation_PokemonFRLG_OverworldSpin_H + +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "PokemonFRLG_OverworldMap.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// Spinner tiles (Rocket Hideout B2F and B3F, Viridian Gym): pokefirered's +// MB_SPIN_UP, MB_SPIN_DOWN, MB_SPIN_LEFT, MB_SPIN_RIGHT and +// MB_STOP_SPINNING. The tile glyphs can't say so: the hideout's floors +// have them coded (coded_spin_direction(), PokemonFRLG_CodedRooms.h); +// another map may list them in these regions, as it does WILD_REGION. +// "spin_north": MB_SPIN_UP. +// "spin_south": MB_SPIN_DOWN. +// "spin_west": MB_SPIN_LEFT. +// "spin_east": MB_SPIN_RIGHT. +// "spin_stop": MB_STOP_SPINNING. +// +// The way the arrow on this tile slides the player, if it is a spinner. +std::optional spin_direction(const MapData& map, const TilePos& point); +// Whether this tile ends a slide (MB_STOP_SPINNING). +bool stops_spinning(const MapData& map, const TilePos& point); + +// Where a slide leaves the player, the way it last moved them, and how +// many tiles it took. +struct SpinSlide{ + PlayerState end; + Direction facing = Direction::NORTH; + int tiles = 0; +}; +// The slide of a player who has just stepped onto the spinner `state` +// stands on (pokefirered TryUpdatePlayerSpinDirection()): a tile at a +// time the last arrow's way, turned by each arrow on the way, until a stop +// tile, or a step the step rules refuse (a wall, a person: the game's +// collision ends the forced movement there). Nothing if `state` isn't on a +// spinner, if the arrows turn the player round in a loop, or if the slide +// crosses a warp, which isn't modelled. +std::optional spin_slide(const MapData& map, const PlayerState& state); + +// Register the tests of spinner tiles, the lift's panel and the poster +// switch on made-up maps. +void add_tests_OverworldSpin(UnitTestDatabase& database); + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp index afe24a3f8b..e17a69bda4 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp @@ -19,6 +19,8 @@ #include "PokemonFRLG_OverworldRecovery.h" #include "PokemonFRLG_OverworldRoute.h" #include "PokemonFRLG_OverworldSim.h" +#include "PokemonFRLG_OverworldSimField.h" +#include "PokemonFRLG_OverworldSpin.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -32,7 +34,7 @@ struct SpinInput{ UnitTestResult (*body)(); }; struct SpinExpectation{ - const char* description; + std::string description; }; struct SpinCase{ SpinInput input; @@ -193,7 +195,7 @@ UnitTestResult test_sim_walker_walks_slide(){ // A lift whose door, the exit mat (1, 2), lets out on the floor picked. std::vector lift_maps(){ - const char* FLOOR = R"({ + const std::string FLOOR = R"({ "name": "%s", "source": {"decomp_file": "", "revision": "test", "date": "2026-11-09"}, "tiles": ["###", "#.#", "###"] })"; @@ -203,7 +205,7 @@ std::vector lift_maps(){ "tiles": ["###", "#.#", "#.#", "###"], "warps": [{"x": 1, "y": 2, "enter": "walk_south"}] })", "Lift")); - for (const char* name : {"FloorA", "FloorB"}){ + for (const std::string name : {"FloorA", "FloorB"}){ std::string json = FLOOR; json.replace(json.find("%s"), 2, name); maps.push_back(parse_map(json, name)); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp index 2ce0c18407..104d07542f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp @@ -23,6 +23,9 @@ #include "VideoGames/PokemonFRLG/PokemonFRLG_Settings.h" #include "PokemonFRLG_PlayerFacingReader.h" #include "PokemonFRLG_OverworldWalk.h" +#include "PokemonFRLG_OverworldBike.h" +#include "PokemonFRLG_BikeSpinMechanics.h" +#include "PokemonFRLG_BikeSpinPresses.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h index 0ccb3b1bee..5666f54367 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h @@ -82,9 +82,6 @@ enum class MoveSpeed{ }; // Name of a movement speed. const char* move_speed_name(MoveSpeed speed); -// The speed walked at instead of `speed` while off the Bicycle (a door -// took the player off): a run. -MoveSpeed on_foot(MoveSpeed speed); // Walks `route` from where it was planned, at `speed`. The player must be @@ -157,43 +154,6 @@ MoveSpeed step_speed(const RouteStep& step, MoveSpeed speed); // pace whatever the speed; where running isn't allowed, the walking pace. Milliseconds tile_time(const RouteStep& step, MoveSpeed speed); -// Whether a WALK `step` at `speed` starts with a turn tap, for a player -// facing `facing`: on foot facing another way (or an unknown way), so the -// hold starts facing the right way. Never on the Bicycle, where a tap -// takes a step (MovementMechanics::BIKE_TAP_ONLY_TURNS). -bool turns_first(const RouteStep& step, MoveSpeed speed, std::optional facing); - -// Whether `step` at `speed` is walked a tap at a time, each a tile -// (count_bike_taps()): an open leg on the Bicycle, outdoors, not onto a -// warp or a spinner, and not over a map connection, where the game holds -// the player up a tile (a tap there isn't measured). Legs to a wall are -// held: the wall ends them. -bool counts_bike_taps(const RouteStep& step, MoveSpeed speed); - -// Whether, walking a route at `speed` and riding the Bicycle or not -// (`riding`), the player gets on with SELECT before `step`: a WALK where -// the Bicycle may be ridden, after a door took them off. -bool gets_on_bike(const RouteStep& step, MoveSpeed speed, bool riding); - -// The console side of select_onto_bike(): presses SELECT and waits it -// out; reads whether the player rides. -struct BikeSelectCalls{ - std::function press_select; - std::function riding; -}; -// Gets on the registered Bicycle with SELECT, checks the player rides, -// and presses again once if not. Returns whether they ride. -bool select_onto_bike(const BikeSelectCalls& calls); - -// Whether the player rides the Bicycle, from the screen: the overworld -// is up, and no standing player's sprite shows where the player stands -// (read_player_facing()). False under a message or a menu. -bool screen_shows_riding(const ImageViewRGB32& screen); - -// select_onto_bike() on the console: SELECT is the Switch's MINUS. -// Throws OperationFailedException if the player doesn't ride after two -// presses. -void get_on_bike(ConsoleHandle& console, ProControllerContext& context); // The mover for `step` at `speed`: hold_dpad_running() for a run on foot // where the map allows it, hold_dpad() otherwise (B does nothing on the @@ -299,7 +259,6 @@ void start_surf(ConsoleHandle& console, ProControllerContext& context, Direction void add_tests_OverworldWalk(UnitTestDatabase& database); - } // namespace PokemonFRLG } // namespace NintendoSwitch } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp index aa659b6101..a0f8605ca0 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk_Tests.cpp @@ -13,6 +13,7 @@ #include "PokemonFRLG_OverworldMaps.h" #include "PokemonFRLG_OverworldSim.h" #include "PokemonFRLG_OverworldWalk.h" +#include "PokemonFRLG_BikeSpinMechanics.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PokemonCenterRoom.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PokemonCenterRoom.h new file mode 100644 index 0000000000..9578c86200 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PokemonCenterRoom.h @@ -0,0 +1,52 @@ +/* Pokemon Center Room + * + * From: https://github.com/PokemonAutomation/ + * + * A Pokemon Center's ground floor, coded rather than mapped + * (PokemonFRLG_CodedRooms.h). + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_PokemonCenterRoom_H +#define PokemonAutomation_PokemonFRLG_PokemonCenterRoom_H + +#include +#include +#include +#include +#include "PokemonFRLG_OverworldMap.h" +#include "PokemonFRLG_OverworldRoute.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// A Pokemon Center's ground floor. pokefirered gives all of Kanto's and +// the Sevii Islands' but the Indigo Plateau's and One Island's the one +// layout, LAYOUT_POKEMON_CENTER_1F; the seven in the map lock have the +// same tiles and differ only in who stands where, none of them on the +// nurse's column. +// The mat inside the door: the player arrives on it and leaves from it, +// walking south. +inline constexpr TilePos CENTER_MAT{7, 8}; +// Below the nurse's counter, facing her (the nurse at (7, 2)). +inline constexpr TilePos CENTER_NURSE_FRONT{7, 4}; +// Whether `map` names a Pokemon Center's ground floor. +bool is_pokemon_center(std::string_view map); +// The ground floor of the Center named `name`, whose mat lets out on +// `outside` (the door tile of the town's map): its tiles, the nurse, and +// the mat's warps. No person but the nurse: who stands where differs, and +// the script keeps to the nurse's column. +MapData pokemon_center_room(const std::string& name, const Pokemon::WarpDestination& outside); +// For a sim: adds the coded room of the Center whose door is on +// `maps[town]` to `maps`, and returns its name; nothing if the town has +// no Center door. The town gets a point "
_door" on its door tile +// for the room's mat to let out on. +std::optional add_pokemon_center_room(std::vector& maps, size_t town); + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBag.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBag.h new file mode 100644 index 0000000000..9bd80334fb --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBag.h @@ -0,0 +1,78 @@ +/* AutoStory Bag + * + * From: https://github.com/PokemonAutomation/ + * + * The bag as the story's item steps drive it: its pockets, the rows of + * items, and the actions on it, the console's or a test's. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryBag_H +#define PokemonAutomation_PokemonFRLG_AutoStoryBag_H + +#include +#include +#include +#include +#include +#include "VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_ItemListReader.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The bag's pockets, in the order LEFT and RIGHT go through them. +enum class BagPocket{ + ITEMS, + KEY_ITEMS, + POKE_BALLS, +}; + +// The pocket of an item the story uses from the bag: repel, super-repel, +// max-repel, escape-rope (Items) or bicycle (Key Items); nothing for +// another. +std::optional usable_item_pocket(std::string_view item); + +// Whether `item` is a Repel of any kind. +bool is_repel(std::string_view item); + +// The row of `item` in `rows` (read_bag_pocket()'s, from the top), if it +// has one left: a count of -1 (unread) counts as some. +std::optional bag_row_of(const std::vector& rows, std::string_view item); + +// The bag, as use_item_from_bag() drives it: the console's, or a test's. +class ItemUseActions{ +public: + virtual ~ItemUseActions() = default; + // From the overworld, opens the bag on `pocket`, scrolled to its top. + virtual bool open_pocket(BagPocket pocket) = 0; + // The open pocket's rows, from the top. + virtual std::vector read_pocket() = 0; + // Moves to row `row`, USE, and through what the item says. + virtual void use_row(size_t row) = 0; + // Moves to row `row` and REGISTERs it for SELECT. + virtual void register_row(size_t row) = 0; + // Back to the overworld (nothing to do if the item closed the bag). + virtual void close_bag() = 0; +}; + +// How use_item_from_bag() ended. +enum class ItemUseEnd{ + USED, + // The pocket has none left (or the name isn't read). + NONE_LEFT, + // The bag didn't open. + NO_BAG, + // Not an item the story uses from the bag. + UNKNOWN_ITEM, +}; + +// "used", "none_left": the end's name in logs and failures. +std::string item_use_end_name(ItemUseEnd end); + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBagUse.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBagUse.h new file mode 100644 index 0000000000..802f5c0724 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBagUse.h @@ -0,0 +1,59 @@ +/* AutoStory Bag Use + * + * From: https://github.com/PokemonAutomation/ + * + * Using and registering items from the bag, and renewing a Repel, on + * the console or a test's bag. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryBagUse_H +#define PokemonAutomation_PokemonFRLG_AutoStoryBagUse_H + +#include +#include +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "CommonFramework/Language.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "PokemonFRLG_AutoStoryBag.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// Opens the item's pocket, finds it and uses it, then back to the +// overworld. +ItemUseEnd use_item_from_bag(Logger& logger, ItemUseActions& actions, std::string_view item); + +// The same, REGISTERing it for SELECT (the Bicycle) instead of USE. +ItemUseEnd register_item_in_bag(Logger& logger, ItemUseActions& actions, std::string_view item); + +// After "REPEL's effect wore off..." has closed: FRLG asks nothing, so +// another of the same Repel is used if the bag has one. NONE_LEFT is no +// failure: the walk goes on without. +ItemUseEnd renew_repel(Logger& logger, ItemUseActions& actions, std::string_view item); + +// The console's bag. +class ConsoleItemUseActions : public ItemUseActions{ +public: + ConsoleItemUseActions(ConsoleHandle& console, ProControllerContext& context, Language language); + + virtual bool open_pocket(BagPocket pocket) override; + virtual std::vector read_pocket() override; + virtual void use_row(size_t row) override; + virtual void register_row(size_t row) override; + virtual void close_bag() override; + +private: + ConsoleHandle& m_console; + ProControllerContext& m_context; + Language m_language; +}; + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData_Tests.cpp index f9e53a5779..4b4a442605 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData_Tests.cpp @@ -145,8 +145,8 @@ UnitTestResult test_story_trainers(){ // party. UnitTestResult test_hideout_trainers(){ struct Expected{ - const char* map; - const char* battle; + std::string map; + std::string battle; std::vector party; }; const Expected EXPECTED[] = { diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h index 4ecbd72ab7..ee7cb5dced 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h @@ -44,11 +44,11 @@ namespace NintendoSwitch{ namespace PokemonFRLG{ -// Story leg definition imported from Pokemon AutoStory. +// Story leg definition imported from Pokemon AutoStory. using Pokemon::StoryLeg; -// Story step definition imported from Pokemon AutoStory. +// Story step definition imported from Pokemon AutoStory. using Pokemon::StoryStep; -// Story step kind classification imported from Pokemon AutoStory. +// Story step kind classification imported from Pokemon AutoStory. using Pokemon::StoryStepKind; // Where the player stands and faces: the one-tile poses of FRLG's table @@ -63,17 +63,13 @@ struct StoryPose{ // The variant of a ROUTE or LEGS step that runs, with B held: the player // has the Running Shoes (00.10 on). inline const std::string RUN_VARIANT = "run"; -// The variant of a ROUTE or LEGS step ridden on the Bicycle, mounted by a -// RIDE_BIKE_VARIANT step before it (PokemonFRLG_AutoStoryItemUse.h). -// Indoors, where the game doesn't let the player ride, it walks. -inline const std::string BIKE_VARIANT = "bike"; // The variant of a STEP into a trainer's line: a tile at a time, at most // TRAINER_LINE_TILES, until the trainer sees the player // (step_until_seen()). The battle after it starts on the trainer's row // whether the walk before ended on its tile or a tile short. inline const std::string TRAINER_LINE_VARIANT = "trainer_line"; -// Maximum tiles to step into a trainer's line of sight. +// Maximum tiles to step into a trainer's line of sight. inline constexpr int TRAINER_LINE_TILES = 3; // The variant of a STEP into the line of a trainer who looks around @@ -181,7 +177,7 @@ const std::vector& story_maps(); // City's harbor). const RouteCosts& story_route_costs(); -// Converts a StoryPose to a general navigation Pose. +// Converts a StoryPose to a general navigation Pose. Pokemon::Pose to_pose(const StoryPose& pose); // `pose` as a StoryPose: nothing unless it is on one tile. std::optional story_pose(const Pokemon::Pose& pose); @@ -349,8 +345,7 @@ Pokemon::Checkpoint build_checkpoint(const StoryCheckpoint& data, StoryExecutor& Pokemon::Segment make_story_segment(const std::vector& checkpoints, StoryExecutor& executor); - -} // namespace PokemonFRLG -} // namespace NintendoSwitch -} // namespace PokemonAutomation +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFieldSteps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFieldSteps.h new file mode 100644 index 0000000000..7712e128e1 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFieldSteps.h @@ -0,0 +1,54 @@ +/* AutoStory Field Steps + * + * From: https://github.com/PokemonAutomation/ + * + * Table constructors for the GAME steps on the field: the hideout's + * switch and lift, Pokemon Centers, and items from the bag. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryFieldSteps_H +#define PokemonAutomation_PokemonFRLG_AutoStoryFieldSteps_H + +#include +#include +#include +#include "PokemonFRLG_AutoStoryTableSteps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ +namespace StoryTable{ + + +// Pushes the switch behind the Game Corner's poster the player faces +// (POSTER_SWITCH_VARIANT). +StoryStep poster_switch(std::string name, std::string why); + +// Rides the Rocket Hideout's lift from the floor it stands at to `to` +// (LIFT_FLOOR_VARIANT): "B1F", "B2F", "B4F". The player starts below +// `from`'s lift door, facing north, and ends on `to`'s, facing south. +StoryStep lift_floor(std::string name, std::string_view from, std::string_view to, std::string why); + +// Into the Pokemon Center `center` (a map name) from below its door, +// facing north, to below the nurse's counter (CENTER_ENTER_VARIANT): the +// same script in every Center, with no map of its rooms. +StoryStep center_enter(std::string name, const std::string& center, std::string why); + +// Out of the Center from below the nurse's counter (CENTER_EXIT_VARIANT) +// to `below_door`, the tile below its door outside, facing south. +StoryStep center_exit(std::string name, Pokemon::Pose below_door, std::string why); + +// Uses `item` from the bag on the overworld (USE_ITEM_VARIANT): a +// Repel, or an Escape Rope, which leaves the player on `pose_after`. +StoryStep use_item(std::string name, std::string_view item, std::string why, std::optional pose_after = std::nullopt); + +// Gets on the Bicycle (RIDE_BIKE_VARIANT). +StoryStep ride_bike(std::string name, std::string why); + + +} // namespace StoryTable +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp index 77d1aac88c..7c648a746c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp @@ -7,10 +7,15 @@ #include #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike.h" #include "PokemonFRLG_AutoStoryActions.h" #include "PokemonFRLG_AutoStoryHideout.h" +#include "PokemonFRLG_AutoStoryHideoutLift.h" #include "PokemonFRLG_AutoStoryItemUse.h" +#include "PokemonFRLG_AutoStoryBagUse.h" +#include "PokemonFRLG_AutoStoryBag.h" #include "PokemonFRLG_AutoStoryRooms.h" +#include "PokemonFRLG_AutoStoryRoomLegs.h" #include "PokemonFRLG_AutoStoryVariantWords.h" namespace PokemonAutomation{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h index 475c5cc9cd..f343157956 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h @@ -50,38 +50,10 @@ inline constexpr Pokemon::StoryArgSpec LIFT_FLOOR_ARGS[] = { {"to", Pokemon::StoryArgType::TEXT, true}, }; -// The lift panel's menu, top first (pokefirered -// sMultichoiceList_RocketHideoutElevator): B1F, B2F, B4F, EXIT. -const std::vector& rocket_hideout_lift_menu(); - -// A ride: the menu rows the cursor starts on and is moved to. -struct LiftRide{ - size_t from = 0; - size_t to = 0; -}; -// The ride a lift_floor step names, or nothing if it is not one, or names -// a floor the panel hasn't. -std::optional parse_lift_floor_step(const Pokemon::StoryStep& step); - -// Presses of the D-pad on a menu: which way, how many. -struct MenuPresses{ - Pokemon::Direction direction = Pokemon::Direction::SOUTH; - size_t presses = 0; -}; -// The fewest presses that move the cursor from `ride.from` to `ride.to` -// on a menu of `entries` rows. A multichoice of more than three rows -// wraps round (pokefirered DrawVerticalMultichoiceMenu()). -MenuPresses lift_menu_presses(const LiftRide& ride, size_t entries); // Whether `step` is one of this file's steps. bool is_hideout_step(const Pokemon::StoryStep& step); -// On the console: A on the panel, the floor picked, the ride waited out -// back to the overworld, the player still in the lift. -void pick_lift_floor(ConsoleHandle& console, ProControllerContext& context, const LiftRide& ride); -// On the console, the whole ride: into the lift from below the door, -// the floor picked, out onto the floor's lift door. -void ride_lift(ConsoleHandle& console, ProControllerContext& context, const LiftRide& ride); // On the console: A on the poster, its message closed back to the // overworld. void push_poster_switch(ConsoleHandle& console, ProControllerContext& context); @@ -99,7 +71,6 @@ std::optional run_field_game_step( void add_tests_AutoStoryHideout(UnitTestDatabase& database); - } // namespace PokemonFRLG } // namespace NintendoSwitch } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideoutLift.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideoutLift.h new file mode 100644 index 0000000000..0f782cae71 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideoutLift.h @@ -0,0 +1,64 @@ +/* AutoStory Hideout Lift + * + * From: https://github.com/PokemonAutomation/ + * + * Riding the Rocket Hideout's lift: its panel's menu, the presses that + * pick a floor, and the ride on the console. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryHideoutLift_H +#define PokemonAutomation_PokemonFRLG_AutoStoryHideoutLift_H + +#include +#include +#include +#include +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "Pokemon/AutoStory/Pokemon_StorySteps.h" +#include "Pokemon/Navigation/Pokemon_Grid.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The lift panel's menu, top first (pokefirered +// sMultichoiceList_RocketHideoutElevator): B1F, B2F, B4F, EXIT. +const std::vector& rocket_hideout_lift_menu(); + +// A ride: the menu rows the cursor starts on and is moved to. +struct LiftRide{ + size_t from = 0; + size_t to = 0; +}; + +// The ride a lift_floor step names, or nothing if it is not one, or names +// a floor the panel hasn't. +std::optional parse_lift_floor_step(const Pokemon::StoryStep& step); + +// Presses of the D-pad on a menu: which way, how many. +struct MenuPresses{ + Pokemon::Direction direction = Pokemon::Direction::SOUTH; + size_t presses = 0; +}; + +// The fewest presses that move the cursor from `ride.from` to `ride.to` +// on a menu of `entries` rows. A multichoice of more than three rows +// wraps round (pokefirered DrawVerticalMultichoiceMenu()). +MenuPresses lift_menu_presses(const LiftRide& ride, size_t entries); + +// On the console: A on the panel, the floor picked, the ride waited out +// back to the overworld, the player still in the lift. +void pick_lift_floor(ConsoleHandle& console, ProControllerContext& context, const LiftRide& ride); + +// On the console, the whole ride: into the lift from below the door, +// the floor picked, out onto the floor's lift door. +void ride_lift(ConsoleHandle& console, ProControllerContext& context, const LiftRide& ride); + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp index 0df832e8fb..e84ba87ea8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp @@ -9,13 +9,17 @@ #include #include "Common/Cpp/TestRunners/UnitTest.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_HideoutLiftRoom.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h" #include "PokemonFRLG_AutoStoryHideout.h" +#include "PokemonFRLG_AutoStoryHideoutLift.h" #include "PokemonFRLG_AutoStoryRowSim.h" #include "PokemonFRLG_AutoStoryTableSteps.h" +#include "PokemonFRLG_AutoStoryFieldSteps.h" #include "PokemonFRLG_AutoStoryVariantWords.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -213,7 +217,7 @@ struct AutoStoryHideoutBodyInput{ UnitTestResult (*body)(); }; struct AutoStoryHideoutBodyExpectation{ - const char* description; + std::string description; }; struct AutoStoryHideoutBodyCase{ AutoStoryHideoutBodyInput input; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp index 5f8ecc77aa..d1b7ecb16d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp @@ -10,6 +10,8 @@ #include "PokemonFRLG_AutoStoryActions.h" #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryItemUse.h" +#include "PokemonFRLG_AutoStoryBagUse.h" +#include "PokemonFRLG_AutoStoryBag.h" #include "PokemonFRLG_AutoStoryVariantWords.h" namespace PokemonAutomation{ @@ -65,7 +67,7 @@ std::optional bag_row_of(const std::vector& rows, std::string_vi return std::nullopt; } -const char* item_use_end_name(ItemUseEnd end){ +std::string item_use_end_name(ItemUseEnd end){ switch (end){ case ItemUseEnd::USED: return "used"; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h index 780a5ce463..f19b161c35 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h @@ -45,25 +45,16 @@ inline constexpr Pokemon::StoryArgSpec USE_ITEM_ARGS[] = { // (OverworldWalkSteps::get_on_bike()). The ROUTE steps after it ride // (BIKE_VARIANT). inline const std::string RIDE_BIKE_VARIANT = "ride_bike"; +// The variant of a ROUTE or LEGS step ridden on the Bicycle, mounted by a +// RIDE_BIKE_VARIANT step before it. +// Indoors, where the game doesn't let the player ride, it walks. +inline const std::string BIKE_VARIANT = "bike"; // The GAME step that registers the Bicycle for SELECT: the Key Items // pocket's BICYCLE, REGISTER, its menu's second option. Once a save: // the registration is saved with the game, and a save made before it // doesn't have it. inline const std::string REGISTER_BIKE_VARIANT = "register_bike"; -// The bag's pockets, in the order LEFT and RIGHT go through them. -enum class BagPocket{ - ITEMS, - KEY_ITEMS, - POKE_BALLS, -}; - -// The pocket of an item the story uses from the bag: repel, super-repel, -// max-repel, escape-rope (Items) or bicycle (Key Items); nothing for -// another. -std::optional usable_item_pocket(std::string_view item); -// Whether `item` is a Repel of any kind. -bool is_repel(std::string_view item); // The item a use_item step uses, or nothing if it isn't one, the item // isn't one the story uses from the bag, or it names a slot. @@ -71,75 +62,11 @@ std::optional parse_use_item_step(const Pokemon::StoryStep& step); // Whether `step` is one of this file's steps. bool is_item_use_step(const Pokemon::StoryStep& step); -// The row of `item` in `rows` (read_bag_pocket()'s, from the top), if it -// has one left: a count of -1 (unread) counts as some. -std::optional bag_row_of(const std::vector& rows, std::string_view item); - - -// The bag, as use_item_from_bag() drives it: the console's, or a test's. -class ItemUseActions{ -public: - virtual ~ItemUseActions() = default; - // From the overworld, opens the bag on `pocket`, scrolled to its top. - virtual bool open_pocket(BagPocket pocket) = 0; - // The open pocket's rows, from the top. - virtual std::vector read_pocket() = 0; - // Moves to row `row`, USE, and through what the item says. - virtual void use_row(size_t row) = 0; - // Moves to row `row` and REGISTERs it for SELECT. - virtual void register_row(size_t row) = 0; - // Back to the overworld (nothing to do if the item closed the bag). - virtual void close_bag() = 0; -}; - -// How use_item_from_bag() ended. -enum class ItemUseEnd{ - USED, - // The pocket has none left (or the name isn't read). - NONE_LEFT, - // The bag didn't open. - NO_BAG, - // Not an item the story uses from the bag. - UNKNOWN_ITEM, -}; -// "used", "none_left": the end's name in logs and failures. -const char* item_use_end_name(ItemUseEnd end); - -// Opens the item's pocket, finds it and uses it, then back to the -// overworld. -ItemUseEnd use_item_from_bag(Logger& logger, ItemUseActions& actions, std::string_view item); -// The same, REGISTERing it for SELECT (the Bicycle) instead of USE. -ItemUseEnd register_item_in_bag(Logger& logger, ItemUseActions& actions, std::string_view item); - -// After "REPEL's effect wore off..." has closed: FRLG asks nothing, so -// another of the same Repel is used if the bag has one. NONE_LEFT is no -// failure: the walk goes on without. -ItemUseEnd renew_repel(Logger& logger, ItemUseActions& actions, std::string_view item); - - -// The console's bag. -class ConsoleItemUseActions : public ItemUseActions{ -public: - ConsoleItemUseActions(ConsoleHandle& console, ProControllerContext& context, Language language); - - virtual bool open_pocket(BagPocket pocket) override; - virtual std::vector read_pocket() override; - virtual void use_row(size_t row) override; - virtual void register_row(size_t row) override; - virtual void close_bag() override; - -private: - ConsoleHandle& m_console; - ProControllerContext& m_context; - Language m_language; -}; - // Registers the item use tests. void add_tests_AutoStoryItemUse(UnitTestDatabase& database); - } // namespace PokemonFRLG } // namespace NintendoSwitch } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp index 66c2f36a92..82900c7f7a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp @@ -14,9 +14,12 @@ #include "Common/Cpp/Logging/AbstractLogger.h" #include "Common/Cpp/TestRunners/UnitTest.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_BikeSpinMechanics.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryItemUse.h" +#include "PokemonFRLG_AutoStoryBagUse.h" +#include "PokemonFRLG_AutoStoryBag.h" #include "PokemonFRLG_AutoStoryVariantWords.h" namespace PokemonAutomation{ @@ -216,7 +219,7 @@ struct AutoStoryItemUseBodyInput{ UnitTestResult (*body)(); }; struct AutoStoryItemUseBodyExpectation{ - const char* description; + std::string description; }; struct AutoStoryItemUseBodyCase{ AutoStoryItemUseBodyInput input; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp index 1f4d740bc8..a796111168 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPaceSim_Tests.cpp @@ -19,6 +19,8 @@ #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryPaceSim.h" #include "PokemonFRLG_AutoStoryPacing.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.cpp index 40aca3b218..fff4f60541 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.cpp @@ -7,7 +7,9 @@ #include "CommonFramework/VideoPipeline/VideoFeed.h" #include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_BikeSpinPresses.h" #include "PokemonFRLG_AutoStoryPacing.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.h index c8cf49e2e2..7c36a7674a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryPacing.h @@ -14,6 +14,7 @@ #include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" #include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike.h" #include "PokemonFRLG_AutoStoryCapture.h" namespace PokemonAutomation{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRoomLegs.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRoomLegs.h new file mode 100644 index 0000000000..aa8fc199a9 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRoomLegs.h @@ -0,0 +1,44 @@ +/* AutoStory Room Legs + * + * From: https://github.com/PokemonAutomation/ + * + * The scripted legs through a Pokemon Center and the Rocket Hideout's + * lift. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryRoomLegs_H +#define PokemonAutomation_PokemonFRLG_AutoStoryRoomLegs_H + +#include +#include +#include "PokemonFRLG_AutoStoryRooms.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// Into a Center: north through the door, north 4 to the nurse. +const std::vector& center_enter_legs(); + +// Out of a Center: south 4 to the mat, south through the door. +const std::vector& center_exit_legs(); + +// Into the hideout's lift, from below a floor's lift door facing north: +// north through the door onto LIFT_ARRIVAL, north 2, west 2 to below the +// panel (LIFT_PANEL_FRONT), facing it. +const std::vector& lift_in_legs(); + +// Out of the lift, from below the panel: east 2, south 2 onto the arrow +// warp, south through it, onto the floor picked's lift door. +const std::vector& lift_out_legs(); + +// The legs of the Center's step `variant`; empty if it isn't one. +const std::vector& center_legs(std::string_view variant); + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.cpp index 52de05543b..e6dbf9cb22 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.cpp @@ -7,6 +7,7 @@ #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" #include "PokemonFRLG_AutoStoryActions.h" #include "PokemonFRLG_AutoStoryRooms.h" +#include "PokemonFRLG_AutoStoryRoomLegs.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.h index ee5b3d9f91..3cb36443b0 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.h @@ -47,19 +47,6 @@ struct RoomLeg{ int tiles = 0; bool warps = false; }; -// Into a Center: north through the door, north 4 to the nurse. -const std::vector& center_enter_legs(); -// Out of a Center: south 4 to the mat, south through the door. -const std::vector& center_exit_legs(); -// Into the hideout's lift, from below a floor's lift door facing north: -// north through the door onto LIFT_ARRIVAL, north 2, west 2 to below the -// panel (LIFT_PANEL_FRONT), facing it. -const std::vector& lift_in_legs(); -// Out of the lift, from below the panel: east 2, south 2 onto the arrow -// warp, south through it, onto the floor picked's lift door. -const std::vector& lift_out_legs(); -// The legs of the Center's step `variant`; empty if it isn't one. -const std::vector& center_legs(std::string_view variant); // On the console: the legs, a step tap a tile (step_once()), each warp's // fade waited out. `facing` is the way the player faces at the start, if @@ -74,7 +61,6 @@ void walk_room_legs( void add_tests_AutoStoryRooms(UnitTestDatabase& database); - } // namespace PokemonFRLG } // namespace NintendoSwitch } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms_Tests.cpp index 00884dc0b3..a314f95df8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms_Tests.cpp @@ -8,11 +8,13 @@ #include #include "Common/Cpp/TestRunners/UnitTest.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PokemonCenterRoom.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h" #include "PokemonFRLG_AutoStoryRooms.h" #include "PokemonFRLG_AutoStoryRowSim.h" #include "PokemonFRLG_AutoStoryTableSteps.h" +#include "PokemonFRLG_AutoStoryFieldSteps.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -62,7 +64,7 @@ std::string in_and_out(const std::string& town, const std::string& center){ const MapPosition below_door{town, door.next(Direction::SOUTH)}; OverworldSim sim(maps, below_door, Direction::NORTH); StoryPose at{town, below_door.point, Direction::NORTH}; - std::string failure = row_step_on_sim(sim, at, StoryTable::center_enter("to_nurse", center.c_str(), "test"), "test"); + std::string failure = row_step_on_sim(sim, at, StoryTable::center_enter("to_nurse", center, "test"), "test"); if (!failure.empty()){ return failure; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp index e8e33c6b4a..91ce25068e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRowSim.cpp @@ -18,8 +18,11 @@ #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h" #include "PokemonFRLG_AutoStoryHideout.h" +#include "PokemonFRLG_AutoStoryHideoutLift.h" #include "PokemonFRLG_AutoStoryRooms.h" +#include "PokemonFRLG_AutoStoryRoomLegs.h" #include "PokemonFRLG_AutoStoryRowSim.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp index d9815af8dc..28daae9c8f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.cpp @@ -5,10 +5,14 @@ */ #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_HideoutLiftRoom.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PokemonCenterRoom.h" #include "PokemonFRLG_AutoStoryHideout.h" +#include "PokemonFRLG_AutoStoryHideoutLift.h" #include "PokemonFRLG_AutoStoryItemUse.h" #include "PokemonFRLG_AutoStoryRooms.h" #include "PokemonFRLG_AutoStoryTableSteps.h" +#include "PokemonFRLG_AutoStoryFieldSteps.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -175,10 +179,10 @@ StoryStep lift_floor(std::string name, std::string_view from, std::string_view t return step; } -StoryStep center_enter(std::string name, const char* center, std::string why){ +StoryStep center_enter(std::string name, const std::string& center, std::string why){ StoryStep step = make(StoryStepKind::GAME, std::move(name), std::move(why)); step.variant = CENTER_ENTER_VARIANT; - step.pose_after = at(center, CENTER_NURSE_FRONT.x, CENTER_NURSE_FRONT.y, Direction::NORTH); + step.pose_after = at(center.c_str(), CENTER_NURSE_FRONT.x, CENTER_NURSE_FRONT.y, Direction::NORTH); return step; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h index f562000d58..00d2012393 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTableSteps.h @@ -183,25 +183,6 @@ StoryStep options_step(); // Cuts the tree the player faces (CUT_TREE_VARIANT) and steps onto its // tile, `tree`, facing the way the step goes. StoryStep cut_tree(std::string name, Pokemon::Pose tree, std::string why); -// Pushes the switch behind the Game Corner's poster the player faces -// (POSTER_SWITCH_VARIANT). -StoryStep poster_switch(std::string name, std::string why); -// Rides the Rocket Hideout's lift from the floor it stands at to `to` -// (LIFT_FLOOR_VARIANT): "B1F", "B2F", "B4F". The player starts below -// `from`'s lift door, facing north, and ends on `to`'s, facing south. -StoryStep lift_floor(std::string name, std::string_view from, std::string_view to, std::string why); -// Into the Pokemon Center `center` (a map name) from below its door, -// facing north, to below the nurse's counter (CENTER_ENTER_VARIANT): the -// same script in every Center, with no map of its rooms. -StoryStep center_enter(std::string name, const char* center, std::string why); -// Out of the Center from below the nurse's counter (CENTER_EXIT_VARIANT) -// to `below_door`, the tile below its door outside, facing south. -StoryStep center_exit(std::string name, Pokemon::Pose below_door, std::string why); -// Uses `item` from the bag on the overworld (USE_ITEM_VARIANT): a -// Repel, or an Escape Rope, which leaves the player on `pose_after`. -StoryStep use_item(std::string name, std::string_view item, std::string why, std::optional pose_after = std::nullopt); -// Gets on the Bicycle (RIDE_BIKE_VARIANT). -StoryStep ride_bike(std::string name, std::string why); } // namespace StoryTable diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryVermilionGym_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryVermilionGym_Tests.cpp index 2f70ad11b1..17a9b016b8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryVermilionGym_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryVermilionGym_Tests.cpp @@ -25,6 +25,7 @@ #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.h" #include "PokemonFRLG_AutoStoryDamage.h" #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryGrind.h" diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 3f8d707db9..25b636266e 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1861,9 +1861,13 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Inference/Sounds/PokemonFRLG_CatchFanfareDetector.h Source/VideoGames/PokemonFRLG/Inference/Sounds/PokemonFRLG_ShinySoundDetector.cpp Source/VideoGames/PokemonFRLG/Inference/Sounds/PokemonFRLG_ShinySoundDetector.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_BikeSpinMechanics.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_BikeSpinPresses.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_CodedRooms.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_HideoutLiftRoom.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_MovementMechanics.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBikeTaps.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBikeTaps.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldBike_Tests.cpp @@ -1889,9 +1893,11 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimTaps.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSimField.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin.h Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSpin_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWalk.h @@ -1899,6 +1905,7 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldWarpFade_Tests.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.cpp Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PlayerFacingReader.h + Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_PokemonCenterRoom.h Source/VideoGames/PokemonFRLG/PokemonFRLG_Navigation.cpp Source/VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h Source/VideoGames/PokemonFRLG/PokemonFRLG_Panels.cpp @@ -1914,6 +1921,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryAbilities_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryActions.h + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBag.h + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBagUse.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattle.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattle.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData.cpp @@ -1935,6 +1944,7 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryDamage.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFieldSteps.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFoeTracker.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryFoeTracker.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryGrind.cpp @@ -1943,6 +1953,7 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideout_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryHideoutLift.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItemUse_Tests.cpp @@ -1974,6 +1985,7 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngLoop_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRoomLegs.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRooms_Tests.cpp