"""Fetch game data from local PokeAPI submodule and write static JSON seed files. Reads from the PokeAPI/api-data git submodule at data/pokeapi/ — no network access or container needed. Only uses Python stdlib. Usage: python -m app.seeds.fetch_pokeapi Requires the submodule to be initialized: git submodule update --init """ import json import re import sys from pathlib import Path REPO_ROOT = Path(__file__).parents[4] # backend/src/app/seeds -> repo root POKEAPI_DIR = REPO_ROOT / "data" / "pokeapi" / "data" / "api" / "v2" DATA_DIR = Path(__file__).parent / "data" def load_resource(endpoint: str, resource_id: int) -> dict: """Load a PokeAPI resource from the local submodule data.""" path = POKEAPI_DIR / endpoint / str(resource_id) / "index.json" with open(path) as f: return json.load(f) def extract_id(url: str) -> int: """Extract the numeric ID from a PokeAPI URL (absolute or relative).""" return int(url.rstrip("/").split("/")[-1]) # Game definitions - Gen 1 through Gen 8 VERSION_GROUPS = { # === Generation 1 === "red-blue": { "versions": ["red", "blue"], "generation": 1, "region": "kanto", "region_id": 1, "games": { "red": { "name": "Pokemon Red", "slug": "red", "release_year": 1996, "color": "#FF1111", }, "blue": { "name": "Pokemon Blue", "slug": "blue", "release_year": 1996, "color": "#1111FF", }, }, }, "yellow": { "versions": ["yellow"], "generation": 1, "region": "kanto", "region_id": 1, "games": { "yellow": { "name": "Pokemon Yellow", "slug": "yellow", "release_year": 1998, "color": "#FFD733", }, }, }, # === Generation 2 === "gold-silver": { "versions": ["gold", "silver"], "generation": 2, "region": "johto", "region_id": 2, "extra_regions": [1], # Kanto post-game "games": { "gold": { "name": "Pokemon Gold", "slug": "gold", "release_year": 1999, "color": "#DAA520", }, "silver": { "name": "Pokemon Silver", "slug": "silver", "release_year": 1999, "color": "#C0C0C0", }, }, }, "crystal": { "versions": ["crystal"], "generation": 2, "region": "johto", "region_id": 2, "extra_regions": [1], # Kanto post-game "games": { "crystal": { "name": "Pokemon Crystal", "slug": "crystal", "release_year": 2000, "color": "#4FD9FF", }, }, }, # === Generation 3 === "ruby-sapphire": { "versions": ["ruby", "sapphire"], "generation": 3, "region": "hoenn", "region_id": 3, "games": { "ruby": { "name": "Pokemon Ruby", "slug": "ruby", "release_year": 2002, "color": "#A00000", }, "sapphire": { "name": "Pokemon Sapphire", "slug": "sapphire", "release_year": 2002, "color": "#0000A0", }, }, }, "emerald": { "versions": ["emerald"], "generation": 3, "region": "hoenn", "region_id": 3, "games": { "emerald": { "name": "Pokemon Emerald", "slug": "emerald", "release_year": 2005, "color": "#00A000", }, }, }, "firered-leafgreen": { "versions": ["firered", "leafgreen"], "generation": 3, "region": "kanto", "region_id": 1, "games": { "firered": { "name": "Pokemon FireRed", "slug": "firered", "release_year": 2004, "color": "#FF7327", }, "leafgreen": { "name": "Pokemon LeafGreen", "slug": "leafgreen", "release_year": 2004, "color": "#00DD00", }, }, }, # === Generation 4 === "diamond-pearl": { "versions": ["diamond", "pearl"], "generation": 4, "region": "sinnoh", "region_id": 4, "games": { "diamond": { "name": "Pokemon Diamond", "slug": "diamond", "release_year": 2006, "color": "#AAAAFF", }, "pearl": { "name": "Pokemon Pearl", "slug": "pearl", "release_year": 2006, "color": "#FFAAAA", }, }, }, "platinum": { "versions": ["platinum"], "generation": 4, "region": "sinnoh", "region_id": 4, "games": { "platinum": { "name": "Pokemon Platinum", "slug": "platinum", "release_year": 2008, "color": "#999999", }, }, }, "heartgold-soulsilver": { "versions": ["heartgold", "soulsilver"], "generation": 4, "region": "johto", "region_id": 2, "extra_regions": [1], # Kanto post-game "games": { "heartgold": { "name": "Pokemon HeartGold", "slug": "heartgold", "release_year": 2010, "color": "#B69E00", }, "soulsilver": { "name": "Pokemon SoulSilver", "slug": "soulsilver", "release_year": 2010, "color": "#C0C0E0", }, }, }, # === Generation 5 === "black-white": { "versions": ["black", "white"], "generation": 5, "region": "unova", "region_id": 5, "games": { "black": { "name": "Pokemon Black", "slug": "black", "release_year": 2010, "color": "#444444", }, "white": { "name": "Pokemon White", "slug": "white", "release_year": 2010, "color": "#E1E1E1", }, }, }, "black-2-white-2": { "versions": ["black-2", "white-2"], "generation": 5, "region": "unova", "region_id": 5, "games": { "black-2": { "name": "Pokemon Black 2", "slug": "black-2", "release_year": 2012, "color": "#424B50", }, "white-2": { "name": "Pokemon White 2", "slug": "white-2", "release_year": 2012, "color": "#E3CED0", }, }, }, # === Generation 6 === "x-y": { "versions": ["x", "y"], "generation": 6, "region": "kalos", "region_id": 6, "games": { "x": { "name": "Pokemon X", "slug": "x", "release_year": 2013, "color": "#025DA6", }, "y": { "name": "Pokemon Y", "slug": "y", "release_year": 2013, "color": "#EA1A3E", }, }, }, "omega-ruby-alpha-sapphire": { "versions": ["omega-ruby", "alpha-sapphire"], "generation": 6, "region": "hoenn", "region_id": 3, "games": { "omega-ruby": { "name": "Pokemon Omega Ruby", "slug": "omega-ruby", "release_year": 2014, "color": "#CF3025", }, "alpha-sapphire": { "name": "Pokemon Alpha Sapphire", "slug": "alpha-sapphire", "release_year": 2014, "color": "#26649C", }, }, }, # === Generation 7 === "sun-moon": { "versions": ["sun", "moon"], "generation": 7, "region": "alola", "region_id": 7, "games": { "sun": { "name": "Pokemon Sun", "slug": "sun", "release_year": 2016, "color": "#F1912B", }, "moon": { "name": "Pokemon Moon", "slug": "moon", "release_year": 2016, "color": "#5599CA", }, }, }, "ultra-sun-ultra-moon": { "versions": ["ultra-sun", "ultra-moon"], "generation": 7, "region": "alola", "region_id": 7, "games": { "ultra-sun": { "name": "Pokemon Ultra Sun", "slug": "ultra-sun", "release_year": 2017, "color": "#E95B2B", }, "ultra-moon": { "name": "Pokemon Ultra Moon", "slug": "ultra-moon", "release_year": 2017, "color": "#204E8C", }, }, }, "lets-go": { "versions": ["lets-go-pikachu", "lets-go-eevee"], "generation": 7, "region": "kanto", "region_id": 1, "games": { "lets-go-pikachu": { "name": "Pokemon Let's Go Pikachu", "slug": "lets-go-pikachu", "release_year": 2018, "color": "#F5DA00", }, "lets-go-eevee": { "name": "Pokemon Let's Go Eevee", "slug": "lets-go-eevee", "release_year": 2018, "color": "#D4924B", }, }, }, # === Generation 8 === "sword-shield": { "versions": ["sword", "shield"], "generation": 8, "region": "galar", "region_id": 8, "games": { "sword": { "name": "Pokemon Sword", "slug": "sword", "release_year": 2019, "color": "#00D4E7", }, "shield": { "name": "Pokemon Shield", "slug": "shield", "release_year": 2019, "color": "#EF3B6E", }, }, }, "brilliant-diamond-shining-pearl": { "versions": ["brilliant-diamond", "shining-pearl"], "generation": 8, "region": "sinnoh", "region_id": 4, "games": { "brilliant-diamond": { "name": "Pokemon Brilliant Diamond", "slug": "brilliant-diamond", "release_year": 2021, "color": "#44BAE5", }, "shining-pearl": { "name": "Pokemon Shining Pearl", "slug": "shining-pearl", "release_year": 2021, "color": "#E18AAA", }, }, }, "legends-arceus": { "versions": ["legends-arceus"], "generation": 8, "region": "hisui", "region_id": 9, "games": { "legends-arceus": { "name": "Pokemon Legends: Arceus", "slug": "legends-arceus", "release_year": 2022, "color": "#36597B", }, }, }, # === Generation 9 === "scarlet-violet": { "versions": ["scarlet", "violet"], "generation": 9, "region": "paldea", "region_id": 10, "games": { "scarlet": { "name": "Pokemon Scarlet", "slug": "scarlet", "release_year": 2022, "color": "#F93C3C", }, "violet": { "name": "Pokemon Violet", "slug": "violet", "release_year": 2022, "color": "#A96EEC", }, }, }, } # Encounter methods to include (excludes gift, legendary-only, etc.) INCLUDED_METHODS = { "walk", "surf", "old-rod", "good-rod", "super-rod", "rock-smash", "headbutt", } # Route progression order by version group. # Keys are version group names from VERSION_GROUPS. Names must match # clean_location_name() output. Routes not listed fall to the end alphabetically. ROUTE_ORDER: dict[str, list[str]] = { "firered-leafgreen": [ # Main Kanto progression "Pallet Town", "Route 1", "Viridian City", "Route 22", "Route 2", "Viridian Forest", "Route 3", "Mt Moon", "Route 4", "Cerulean City", "Route 24", "Route 25", "Route 5", "Route 6", "Vermilion City", "Ss Anne", "Route 11", "Digletts Cave", "Route 9", "Route 10", "Rock Tunnel", "Power Plant", "Route 8", "Route 7", "Celadon City", "Route 16", "Route 17", "Route 18", "Fuchsia City", "Safari Zone", "Route 15", "Route 14", "Route 13", "Route 12", "Pokemon Tower", "Sea Route 19", "Sea Route 20", "Seafoam Islands", "Cinnabar Island", "Pokemon Mansion", "Sea Route 21", "Route 23", "Victory Road 2", "Cerulean Cave", # Sevii Islands "One Island", "Kindle Road", "Treasure Beach", "Mt Ember", "Cape Brink", "Berry Forest", "Bond Bridge", "Three Isle Port", "Four Island", "Icefall Cave", "Resort Gorgeous", "Water Labyrinth", "Five Island", "Five Isle Meadow", "Memorial Pillar", "Outcast Island", "Green Path", "Water Path", "Ruin Valley", "Lost Cave", "Pattern Bush", "Trainer Tower", "Canyon Entrance", "Sevault Canyon", "Tanoby Ruins", "Monean Chamber", "Liptoo Chamber", "Weepth Chamber", "Dilford Chamber", "Scufib Chamber", "Rixy Chamber", "Viapos Chamber", "Altering Cave", ], "heartgold-soulsilver": [ # Johto "New Bark Town", "Route 29", "Cherrygrove City", "Route 30", "Route 31", "Dark Cave", "Violet City", "Sprout Tower", "Route 32", "Ruins Of Alph", "Union Cave", "Route 33", "Slowpoke Well", "Route 34", "Ilex Forest", "National Park", "Route 35", "Route 36", "Route 37", "Ecruteak City", "Burned Tower", "Bell Tower", "Route 38", "Route 39", "Olivine City", "Sea Route 40", "Sea Route 41", "Cianwood City", "Route 42", "Mt Mortar", "Lake Of Rage", "Route 43", "Route 44", "Ice Path", "Blackthorn City", "Dragons Den", "Route 45", "Route 46", "Route 47", "Route 48", "Safari Zone", "Whirl Islands", "Tohjo Falls", # Kanto post-game "Route 27", "Route 26", "Victory Road 1", "Route 28", "Mt Silver", "Pallet Town", "Route 1", "Viridian City", "Viridian Forest", "Route 2", "Route 3", "Mt Moon", "Route 4", "Cerulean City", "Route 24", "Route 25", "Cerulean Cave", "Route 5", "Route 6", "Vermilion City", "Route 7", "Route 8", "Route 9", "Route 10", "Rock Tunnel", "Route 11", "Digletts Cave", "Route 12", "Route 13", "Route 14", "Route 15", "Fuchsia City", "Route 16", "Route 17", "Route 18", "Celadon City", "Sea Route 19", "Sea Route 20", "Seafoam Islands", "Cinnabar Island", "Sea Route 21", "Route 22", # Misc "Unknown All Poliwag", "Unknown All Rattata", "Unknown All Bugs", ], "emerald": [ "Route 101", "Oldale Town", "Route 103", "Route 102", "Petalburg City", "Route 104", "Petalburg Woods", "Rusturf Tunnel", "Route 116", "Route 105", "Route 106", "Dewford Town", "Granite Cave", "Route 107", "Route 108", "Route 109", "Slateport City", "Route 110", "New Mauville", "Route 117", "Route 111", "Mirage Tower", "Route 112", "Fiery Path", "Jagged Pass", "Route 113", "Route 114", "Meteor Falls", "Route 115", "Route 118", "Route 119", "Route 120", "Route 121", "Safari Zone", "Lilycove City", "Route 122", "Mt Pyre", "Route 123", "Magma Hideout", "Route 124", "Mossdeep City", "Route 125", "Shoal Cave", "Route 126", "Sootopolis City", "Cave Of Origin", "Route 127", "Route 128", "Seafloor Cavern", "Route 129", "Route 130", "Route 131", "Pacifidlog Town", "Route 132", "Route 133", "Route 134", "Ever Grande City", "Victory Road", "Sky Pillar", "Abandoned Ship", "Desert Underpass", "Artisan Cave", "Altering Cave", ], } # Aliases — version groups sharing same route progression ROUTE_ORDER["red-blue"] = ROUTE_ORDER["firered-leafgreen"] ROUTE_ORDER["yellow"] = ROUTE_ORDER["firered-leafgreen"] ROUTE_ORDER["lets-go"] = ROUTE_ORDER["firered-leafgreen"] ROUTE_ORDER["gold-silver"] = ROUTE_ORDER["heartgold-soulsilver"] ROUTE_ORDER["crystal"] = ROUTE_ORDER["heartgold-soulsilver"] ROUTE_ORDER["ruby-sapphire"] = ROUTE_ORDER["emerald"] ROUTE_ORDER["omega-ruby-alpha-sapphire"] = ROUTE_ORDER["emerald"] # Collect all pokemon dex numbers across games all_pokemon_dex: set[int] = set() def clean_location_name(name: str) -> str: """Convert PokeAPI location slug to a clean display name. e.g. 'kanto-route-1' -> 'Route 1' 'pallet-town' -> 'Pallet Town' """ for prefix in [ "kanto-", "johto-", "hoenn-", "sinnoh-", "unova-", "kalos-", "alola-", "galar-", ]: if name.startswith(prefix): name = name[len(prefix):] break name = name.replace("-", " ").title() name = re.sub(r"Route (\d+)", r"Route \1", name) return name def clean_area_name(area_name: str, location_name: str) -> str | None: """Extract meaningful area suffix, or None if it's the default area.""" if area_name.startswith(location_name): suffix = area_name[len(location_name):].strip("-").strip() if not suffix or suffix == "area": return None return suffix.replace("-", " ").title() return area_name.replace("-", " ").title() def sort_routes_by_progression(routes: list[dict], vg_key: str) -> list[dict]: """Sort routes by game progression order for the given version group. Routes not in the ordering list fall to the end, sorted alphabetically. Uses starts-with matching so 'Route 2' matches 'Route 2 (South Towards ...)'. """ order_list = ROUTE_ORDER.get(vg_key) if not order_list: return routes def sort_key(route: dict) -> tuple[int, str]: name = route["name"] for i, ordered_name in enumerate(order_list): if name == ordered_name or name.startswith(ordered_name + " ("): return (i, name) return (len(order_list), name) # Unmatched → end, alphabetical return sorted(routes, key=sort_key) def get_encounters_for_area(area_id: int, version_name: str) -> list[dict]: """Get encounter data for a location area, filtered by version.""" area = load_resource("location-area", area_id) encounters = [] for pe in area["pokemon_encounters"]: pokemon_url = pe["pokemon"]["url"] dex_num = extract_id(pokemon_url) pokemon_name = pe["pokemon"]["name"] for vd in pe["version_details"]: if vd["version"]["name"] != version_name: continue for enc in vd["encounter_details"]: method = enc["method"]["name"] if method not in INCLUDED_METHODS: continue encounters.append({ "pokemon_name": pokemon_name, "national_dex": dex_num, "method": method, "chance": enc["chance"], "min_level": enc["min_level"], "max_level": enc["max_level"], }) return encounters def aggregate_encounters(raw_encounters: list[dict]) -> list[dict]: """Aggregate encounter rates by pokemon + method (sum chances across level ranges).""" agg: dict[tuple[int, str], dict] = {} for enc in raw_encounters: key = (enc["national_dex"], enc["method"]) if key not in agg: agg[key] = { "national_dex": enc["national_dex"], "pokemon_name": enc["pokemon_name"], "method": enc["method"], "encounter_rate": 0, "min_level": enc["min_level"], "max_level": enc["max_level"], } agg[key]["encounter_rate"] += enc["chance"] agg[key]["min_level"] = min(agg[key]["min_level"], enc["min_level"]) agg[key]["max_level"] = max(agg[key]["max_level"], enc["max_level"]) result = list(agg.values()) for r in result: r["encounter_rate"] = min(r["encounter_rate"], 100) return sorted(result, key=lambda x: (-x["encounter_rate"], x["pokemon_name"])) def process_version(version_name: str, vg_info: dict, vg_key: str) -> list[dict]: """Process all locations for a specific game version. Creates hierarchical route structure where locations with multiple areas become parent routes with child routes for each area. """ print(f"\n--- Processing {version_name} ---") region = load_resource("region", vg_info["region_id"]) location_refs = list(region["locations"]) # Include extra regions (e.g., Kanto for Johto games) for extra_region_id in vg_info.get("extra_regions", []): extra_region = load_resource("region", extra_region_id) location_refs = location_refs + list(extra_region["locations"]) print(f" Found {len(location_refs)} locations") routes = [] for loc_ref in location_refs: loc_name = loc_ref["name"] loc_id = extract_id(loc_ref["url"]) display_name = clean_location_name(loc_name) location = load_resource("location", loc_id) areas = location["areas"] if not areas: continue all_encounters: list[dict] = [] area_specific: dict[str, list[dict]] = {} for area_ref in areas: area_id = extract_id(area_ref["url"]) area_slug = area_ref["name"] area_suffix = clean_area_name(area_slug, loc_name) encounters = get_encounters_for_area(area_id, version_name) if not encounters: continue if area_suffix and len(areas) > 1: area_specific[area_suffix] = encounters else: all_encounters.extend(encounters) # If we have multiple area-specific encounters, create a parent route # with child routes for each area (hierarchical grouping) if area_specific and len(area_specific) > 1: child_routes = [] for area_suffix, area_encs in area_specific.items(): aggregated = aggregate_encounters(area_encs) if aggregated: route_name = f"{display_name} ({area_suffix})" for enc in aggregated: all_pokemon_dex.add(enc["national_dex"]) child_routes.append({ "name": route_name, "order": 0, "encounters": aggregated, }) # Only add parent if we have child routes if child_routes: routes.append({ "name": display_name, "order": 0, "encounters": [], # Parent routes have no encounters "children": child_routes, }) elif area_specific: # Only one area-specific route - don't create parent/child, just use suffix for area_suffix, area_encs in area_specific.items(): aggregated = aggregate_encounters(area_encs) if aggregated: route_name = f"{display_name} ({area_suffix})" for enc in aggregated: all_pokemon_dex.add(enc["national_dex"]) routes.append({ "name": route_name, "order": 0, "encounters": aggregated, }) # Non-area-specific encounters (or single area without suffix) if all_encounters: aggregated = aggregate_encounters(all_encounters) if aggregated: for enc in aggregated: all_pokemon_dex.add(enc["national_dex"]) routes.append({ "name": display_name, "order": 0, "encounters": aggregated, }) # Sort routes by game progression order routes = sort_routes_by_progression(routes, vg_key) # Reassign sequential order values order = 1 for route in routes: route["order"] = order order += 1 for child in route.get("children", []): child["order"] = order order += 1 # Count routes including children total_routes = sum(1 + len(r.get("children", [])) for r in routes) print(f" Routes with encounters: {total_routes}") total_enc = sum( len(r["encounters"]) + sum(len(c["encounters"]) for c in r.get("children", [])) for r in routes ) print(f" Total encounter entries: {total_enc}") return routes def fetch_all_pokemon() -> list[dict]: """Fetch all Pokemon species from the local PokeAPI data.""" pokemon_dir = POKEAPI_DIR / "pokemon-species" # Get all species IDs (directories with numeric names, excluding forms 10000+) all_species = [] for entry in pokemon_dir.iterdir(): if entry.is_dir() and entry.name.isdigit(): dex = int(entry.name) if dex < 10000: # Exclude alternate forms all_species.append(dex) all_species.sort() print(f"\n--- Fetching {len(all_species)} Pokemon species ---") pokemon_list = [] for i, dex in enumerate(all_species, 1): poke = load_resource("pokemon", dex) types = [t["type"]["name"] for t in poke["types"]] pokemon_list.append({ "national_dex": dex, "name": poke["name"].title().replace("-", " "), "types": types, "sprite_url": f"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{dex}.png", }) if i % 100 == 0 or i == len(all_species): print(f" Fetched {i}/{len(all_species)}") return sorted(pokemon_list, key=lambda x: x["national_dex"]) def flatten_evolution_chain(chain: dict, seeded_dex: set[int]) -> list[dict]: """Recursively flatten a PokeAPI evolution chain into (from, to) pairs.""" pairs = [] from_dex = int(chain["species"]["url"].rstrip("/").split("/")[-1]) for evo in chain.get("evolves_to", []): to_dex = int(evo["species"]["url"].rstrip("/").split("/")[-1]) for detail in evo["evolution_details"]: trigger = detail["trigger"]["name"] min_level = detail.get("min_level") item = detail.get("item") if item: item = item["name"] held_item = detail.get("held_item") if held_item: held_item = held_item["name"] # Collect other conditions as a string conditions = [] if detail.get("min_happiness"): conditions.append(f"happiness >= {detail['min_happiness']}") if detail.get("min_affection"): conditions.append(f"affection >= {detail['min_affection']}") if detail.get("min_beauty"): conditions.append(f"beauty >= {detail['min_beauty']}") if detail.get("time_of_day"): conditions.append(detail["time_of_day"]) if detail.get("known_move"): conditions.append(f"knows {detail['known_move']['name']}") if detail.get("known_move_type"): conditions.append(f"knows {detail['known_move_type']['name']}-type move") if detail.get("location"): conditions.append(f"at {detail['location']['name']}") if detail.get("party_species"): conditions.append(f"with {detail['party_species']['name']} in party") if detail.get("party_type"): conditions.append(f"with {detail['party_type']['name']}-type in party") if detail.get("gender") is not None: conditions.append("female" if detail["gender"] == 1 else "male") if detail.get("needs_overworld_rain"): conditions.append("raining") if detail.get("turn_upside_down"): conditions.append("turn upside down") if detail.get("trade_species"): conditions.append(f"trade for {detail['trade_species']['name']}") if detail.get("relative_physical_stats") is not None: stat_map = {1: "atk > def", -1: "atk < def", 0: "atk = def"} conditions.append(stat_map.get(detail["relative_physical_stats"], "")) condition = ", ".join(conditions) if conditions else None if from_dex in seeded_dex and to_dex in seeded_dex: pairs.append({ "from_national_dex": from_dex, "to_national_dex": to_dex, "trigger": trigger, "min_level": min_level, "item": item, "held_item": held_item, "condition": condition, }) # Recurse into further evolutions pairs.extend(flatten_evolution_chain(evo, seeded_dex)) return pairs def fetch_evolution_data(seeded_dex: set[int]) -> list[dict]: """Fetch evolution chains from local PokeAPI data for all seeded pokemon.""" print(f"\n--- Fetching evolution chains ---") # First, get the evolution chain URL for each pokemon species chain_ids: set[int] = set() dex_sorted = sorted(seeded_dex) for i, dex in enumerate(dex_sorted, 1): species = load_resource("pokemon-species", dex) chain_url = species["evolution_chain"]["url"] chain_id = extract_id(chain_url) chain_ids.add(chain_id) if i % 50 == 0 or i == len(dex_sorted): print(f" Species fetched: {i}/{len(dex_sorted)}") print(f" Found {len(chain_ids)} unique evolution chains") # Fetch each chain and flatten all_pairs: list[dict] = [] seen: set[tuple[int, int, str]] = set() for chain_id in sorted(chain_ids): chain = load_resource("evolution-chain", chain_id) pairs = flatten_evolution_chain(chain["chain"], seeded_dex) for p in pairs: key = (p["from_national_dex"], p["to_national_dex"], p["trigger"]) if key not in seen: seen.add(key) all_pairs.append(p) print(f" Total evolution pairs: {len(all_pairs)}") return sorted(all_pairs, key=lambda x: (x["from_national_dex"], x["to_national_dex"])) def apply_evolution_overrides(evolutions: list[dict]) -> None: """Apply overrides from evolution_overrides.json if it exists.""" overrides_path = DATA_DIR / "evolution_overrides.json" if not overrides_path.exists(): return with open(overrides_path) as f: overrides = json.load(f) # Remove entries for removal in overrides.get("remove", []): evolutions[:] = [ e for e in evolutions if not (e["from_national_dex"] == removal["from_dex"] and e["to_national_dex"] == removal["to_dex"]) ] # Add entries for addition in overrides.get("add", []): evolutions.append({ "from_national_dex": addition["from_dex"], "to_national_dex": addition["to_dex"], "trigger": addition.get("trigger", "level-up"), "min_level": addition.get("min_level"), "item": addition.get("item"), "held_item": addition.get("held_item"), "condition": addition.get("condition"), }) # Modify entries for mod in overrides.get("modify", []): for e in evolutions: if (e["from_national_dex"] == mod["from_dex"] and e["to_national_dex"] == mod["to_dex"]): for key, value in mod.get("set", {}).items(): e[key] = value # Re-sort evolutions.sort(key=lambda x: (x["from_national_dex"], x["to_national_dex"])) print(f" Applied overrides: {len(evolutions)} pairs after overrides") def write_json(filename: str, data): path = DATA_DIR / filename with open(path, "w") as f: json.dump(data, f, indent=2) print(f" -> {path}") def main(): if not POKEAPI_DIR.is_dir(): print( f"Error: PokeAPI data not found at {POKEAPI_DIR}\n" "Initialize the submodule with: git submodule update --init", file=sys.stderr, ) sys.exit(1) DATA_DIR.mkdir(parents=True, exist_ok=True) # Build games.json games = [] for vg_info in VERSION_GROUPS.values(): for game_info in vg_info["games"].values(): games.append({ "name": game_info["name"], "slug": game_info["slug"], "generation": vg_info["generation"], "region": vg_info["region"], "release_year": game_info["release_year"], "color": game_info.get("color"), }) write_json("games.json", games) print(f"Wrote {len(games)} games to games.json") # Process each version for vg_key, vg_info in VERSION_GROUPS.items(): for ver_name in vg_info["versions"]: routes = process_version(ver_name, vg_info, vg_key) write_json(f"{ver_name}.json", routes) # Fetch all Pokemon species pokemon = fetch_all_pokemon() write_json("pokemon.json", pokemon) print(f"\nWrote {len(pokemon)} Pokemon to pokemon.json") # Build set of all seeded dex numbers for evolution filtering all_seeded_dex = {p["national_dex"] for p in pokemon} # Fetch evolution chains for all seeded Pokemon evolutions = fetch_evolution_data(all_seeded_dex) apply_evolution_overrides(evolutions) write_json("evolutions.json", evolutions) print(f"\nWrote {len(evolutions)} evolution pairs to evolutions.json") print("\nDone! JSON files written to seeds/data/") print("Review route ordering and curate as needed.") if __name__ == "__main__": main()