From 66ad41cf1cb4e75b7cafac623f4708efcc8df4f3 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sat, 7 Feb 2026 19:44:10 +0100 Subject: [PATCH] Replace Python fetch scripts with static special_encounters.json Remove fetch_pokeapi.py and special_encounters.py (now handled by the Go tool) and add special_encounters.json as the new config source. Co-Authored-By: Claude Opus 4.6 --- backend/src/app/seeds/fetch_pokeapi.py | 600 ------------------ backend/src/app/seeds/special_encounters.json | 90 +++ backend/src/app/seeds/special_encounters.py | 96 --- 3 files changed, 90 insertions(+), 696 deletions(-) delete mode 100644 backend/src/app/seeds/fetch_pokeapi.py create mode 100644 backend/src/app/seeds/special_encounters.json delete mode 100644 backend/src/app/seeds/special_encounters.py diff --git a/backend/src/app/seeds/fetch_pokeapi.py b/backend/src/app/seeds/fetch_pokeapi.py deleted file mode 100644 index c6e3d7d..0000000 --- a/backend/src/app/seeds/fetch_pokeapi.py +++ /dev/null @@ -1,600 +0,0 @@ -"""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 - -from app.seeds.special_encounters import SPECIAL_ENCOUNTERS - -REPO_ROOT = Path(__file__).parents[4] # backend/src/app/seeds -> repo root -POKEAPI_DIR = REPO_ROOT / "data" / "pokeapi" / "data" / "api" / "v2" -SEEDS_DIR = Path(__file__).parent -DATA_DIR = SEEDS_DIR / "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]) - - -def _load_version_groups() -> dict: - with open(SEEDS_DIR / "version_groups.json") as f: - return json.load(f) - - -def _load_route_order() -> dict[str, list[str]]: - with open(SEEDS_DIR / "route_order.json") as f: - data = json.load(f) - routes = dict(data["routes"]) - for alias, target in data.get("aliases", {}).items(): - routes[alias] = routes[target] - return routes - - -VERSION_GROUPS = _load_version_groups() -ROUTE_ORDER: dict[str, list[str]] = _load_route_order() - -# Encounter methods to include (excludes gift, legendary-only, etc.) -INCLUDED_METHODS = { - "walk", - "surf", - "old-rod", - "good-rod", - "super-rod", - "rock-smash", - "headbutt", -} - -# Collect all pokemon PokeAPI IDs across games -all_pokeapi_ids: 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, - "pokeapi_id": 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["pokeapi_id"], enc["method"]) - if key not in agg: - agg[key] = { - "pokeapi_id": enc["pokeapi_id"], - "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 merge_special_encounters(routes: list[dict], special_data: dict[str, list[dict]]) -> list[dict]: - """Merge special encounters into existing routes or create new ones.""" - # Build lookup: route name -> route dict (including children) - route_map: dict[str, dict] = {} - for r in routes: - route_map[r["name"]] = r - for child in r.get("children", []): - route_map[child["name"]] = child - - for location_name, encounters in special_data.items(): - for enc in encounters: - all_pokeapi_ids.add(enc["pokeapi_id"]) - - if location_name in route_map: - route_map[location_name]["encounters"].extend(encounters) - else: - new_route = {"name": location_name, "order": 0, "encounters": encounters} - routes.append(new_route) - route_map[location_name] = new_route - - return routes - - -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_pokeapi_ids.add(enc["pokeapi_id"]) - 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_pokeapi_ids.add(enc["pokeapi_id"]) - 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_pokeapi_ids.add(enc["pokeapi_id"]) - routes.append({ - "name": display_name, - "order": 0, - "encounters": aggregated, - }) - - # Merge special encounters (starters, gifts, fossils) - special_data = SPECIAL_ENCOUNTERS.get(vg_key, {}) - if special_data: - routes = merge_special_encounters(routes, special_data) - - # 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 format_form_name(poke_data: dict) -> str: - """Convert a PokeAPI pokemon form entry to a clean display name. - - e.g. 'rattata-alola' (species: 'rattata') -> 'Rattata (Alola)' - """ - species_name = poke_data["species"]["name"] - full_name = poke_data["name"] - if full_name.startswith(species_name + "-"): - form_suffix = full_name[len(species_name) + 1:] - base = species_name.title().replace("-", " ") - suffix = form_suffix.title().replace("-", " ") - return f"{base} ({suffix})" - return full_name.title().replace("-", " ") - - -def fetch_all_pokemon() -> list[dict]: - """Fetch all Pokemon species + encountered forms from the local PokeAPI data.""" - pokemon_dir = POKEAPI_DIR / "pokemon-species" - - # Get all base species IDs - all_species = [] - for entry in pokemon_dir.iterdir(): - if entry.is_dir() and entry.name.isdigit(): - dex = int(entry.name) - if dex < 10000: - all_species.append(dex) - - # Also include form IDs that appear in encounter data - form_ids = sorted(d for d in all_pokeapi_ids if d >= 10000) - - all_species.sort() - print(f"\n--- Fetching {len(all_species)} Pokemon species + {len(form_ids)} forms ---") - - pokemon_list = [] - - # Fetch base species - 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({ - "pokeapi_id": dex, - "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)} species") - - # Fetch forms (from pokemon endpoint, with form-aware naming) - for form_dex in form_ids: - poke = load_resource("pokemon", form_dex) - types = [t["type"]["name"] for t in poke["types"]] - species_id = extract_id(poke["species"]["url"]) - pokemon_list.append({ - "pokeapi_id": form_dex, - "national_dex": species_id, - "name": format_form_name(poke), - "types": types, - "sprite_url": f"https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{form_dex}.png", - }) - - if form_ids: - print(f" Fetched {len(form_ids)} forms") - - return sorted(pokemon_list, key=lambda x: (x["national_dex"], x["pokeapi_id"])) - - -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_pokeapi_id": from_dex, - "to_pokeapi_id": 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 - # Skip form IDs (>= 10000) — they don't have pokemon-species entries - chain_ids: set[int] = set() - dex_sorted = sorted(d for d in seeded_dex if d < 10000) - - 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_pokeapi_id"], p["to_pokeapi_id"], 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_pokeapi_id"], x["to_pokeapi_id"])) - - -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_pokeapi_id"] == removal["from_dex"] - and e["to_pokeapi_id"] == removal["to_dex"]) - ] - - # Add entries - for addition in overrides.get("add", []): - evolutions.append({ - "from_pokeapi_id": addition["from_dex"], - "to_pokeapi_id": 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_pokeapi_id"] == mod["from_dex"] - and e["to_pokeapi_id"] == mod["to_dex"]): - for key, value in mod.get("set", {}).items(): - e[key] = value - - # Re-sort - evolutions.sort(key=lambda x: (x["from_pokeapi_id"], x["to_pokeapi_id"])) - 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 PokeAPI IDs for evolution filtering - all_seeded_dex = {p["pokeapi_id"] 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() diff --git a/backend/src/app/seeds/special_encounters.json b/backend/src/app/seeds/special_encounters.json new file mode 100644 index 0000000..04b47cc --- /dev/null +++ b/backend/src/app/seeds/special_encounters.json @@ -0,0 +1,90 @@ +{ + "encounters": { + "firered-leafgreen": { + "Starter": [ + {"pokeapi_id": 1, "pokemon_name": "bulbasaur", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, + {"pokeapi_id": 4, "pokemon_name": "charmander", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, + {"pokeapi_id": 7, "pokemon_name": "squirtle", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5} + ], + "Route 4": [ + {"pokeapi_id": 129, "pokemon_name": "magikarp", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5} + ], + "Celadon City": [ + {"pokeapi_id": 133, "pokemon_name": "eevee", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25} + ], + "Saffron City": [ + {"pokeapi_id": 131, "pokemon_name": "lapras", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25}, + {"pokeapi_id": 106, "pokemon_name": "hitmonlee", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25}, + {"pokeapi_id": 107, "pokemon_name": "hitmonchan", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25} + ], + "Cinnabar Island": [ + {"pokeapi_id": 138, "pokemon_name": "omanyte", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5}, + {"pokeapi_id": 140, "pokemon_name": "kabuto", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5}, + {"pokeapi_id": 142, "pokemon_name": "aerodactyl", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5} + ], + "Water Labyrinth": [ + {"pokeapi_id": 175, "pokemon_name": "togepi", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5} + ] + }, + "heartgold-soulsilver": { + "Starter": [ + {"pokeapi_id": 152, "pokemon_name": "chikorita", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, + {"pokeapi_id": 155, "pokemon_name": "cyndaquil", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, + {"pokeapi_id": 158, "pokemon_name": "totodile", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5} + ], + "Violet City": [ + {"pokeapi_id": 175, "pokemon_name": "togepi", "method": "gift", "encounter_rate": 100, "min_level": 1, "max_level": 1} + ], + "Goldenrod City": [ + {"pokeapi_id": 133, "pokemon_name": "eevee", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5} + ], + "Cianwood City": [ + {"pokeapi_id": 213, "pokemon_name": "shuckle", "method": "gift", "encounter_rate": 100, "min_level": 20, "max_level": 20} + ], + "Mt Mortar": [ + {"pokeapi_id": 236, "pokemon_name": "tyrogue", "method": "gift", "encounter_rate": 100, "min_level": 10, "max_level": 10} + ], + "Dragons Den": [ + {"pokeapi_id": 147, "pokemon_name": "dratini", "method": "gift", "encounter_rate": 100, "min_level": 15, "max_level": 15} + ], + "Pewter City": [ + {"pokeapi_id": 138, "pokemon_name": "omanyte", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, + {"pokeapi_id": 140, "pokemon_name": "kabuto", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, + {"pokeapi_id": 142, "pokemon_name": "aerodactyl", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, + {"pokeapi_id": 345, "pokemon_name": "lileep", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, + {"pokeapi_id": 347, "pokemon_name": "anorith", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, + {"pokeapi_id": 408, "pokemon_name": "cranidos", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, + {"pokeapi_id": 410, "pokemon_name": "shieldon", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20} + ] + }, + "emerald": { + "Starter": [ + {"pokeapi_id": 252, "pokemon_name": "treecko", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, + {"pokeapi_id": 255, "pokemon_name": "torchic", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, + {"pokeapi_id": 258, "pokemon_name": "mudkip", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5} + ], + "Route 119": [ + {"pokeapi_id": 351, "pokemon_name": "castform", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25} + ], + "Lavaridge Town": [ + {"pokeapi_id": 360, "pokemon_name": "wynaut", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5} + ], + "Mossdeep City": [ + {"pokeapi_id": 374, "pokemon_name": "beldum", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5} + ], + "Rustboro City": [ + {"pokeapi_id": 345, "pokemon_name": "lileep", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, + {"pokeapi_id": 347, "pokemon_name": "anorith", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20} + ] + } + }, + "aliases": { + "red-blue": "firered-leafgreen", + "yellow": "firered-leafgreen", + "lets-go": "firered-leafgreen", + "gold-silver": "heartgold-soulsilver", + "crystal": "heartgold-soulsilver", + "ruby-sapphire": "emerald", + "omega-ruby-alpha-sapphire": "emerald" + } +} diff --git a/backend/src/app/seeds/special_encounters.py b/backend/src/app/seeds/special_encounters.py deleted file mode 100644 index 219fae7..0000000 --- a/backend/src/app/seeds/special_encounters.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Special encounter data not available from PokeAPI wild encounter tables. - -Includes starters, gifts, fossils, and other guaranteed encounters. -Keyed by version group name (same keys as VERSION_GROUPS in fetch_pokeapi.py). -Each value maps route display names to lists of encounter dicts using the -same format as aggregated PokeAPI encounters. -""" - -SPECIAL_ENCOUNTERS: dict[str, dict[str, list[dict]]] = { - "firered-leafgreen": { - "Starter": [ - {"pokeapi_id": 1, "pokemon_name": "bulbasaur", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - {"pokeapi_id": 4, "pokemon_name": "charmander", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - {"pokeapi_id": 7, "pokemon_name": "squirtle", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - ], - "Route 4": [ - {"pokeapi_id": 129, "pokemon_name": "magikarp", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - ], - "Celadon City": [ - {"pokeapi_id": 133, "pokemon_name": "eevee", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25}, - ], - "Saffron City": [ - {"pokeapi_id": 131, "pokemon_name": "lapras", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25}, - {"pokeapi_id": 106, "pokemon_name": "hitmonlee", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25}, - {"pokeapi_id": 107, "pokemon_name": "hitmonchan", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25}, - ], - "Cinnabar Island": [ - {"pokeapi_id": 138, "pokemon_name": "omanyte", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - {"pokeapi_id": 140, "pokemon_name": "kabuto", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - {"pokeapi_id": 142, "pokemon_name": "aerodactyl", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - ], - "Water Labyrinth": [ - {"pokeapi_id": 175, "pokemon_name": "togepi", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - ], - }, - "heartgold-soulsilver": { - "Starter": [ - {"pokeapi_id": 152, "pokemon_name": "chikorita", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - {"pokeapi_id": 155, "pokemon_name": "cyndaquil", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - {"pokeapi_id": 158, "pokemon_name": "totodile", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - ], - "Violet City": [ - {"pokeapi_id": 175, "pokemon_name": "togepi", "method": "gift", "encounter_rate": 100, "min_level": 1, "max_level": 1}, - ], - "Goldenrod City": [ - {"pokeapi_id": 133, "pokemon_name": "eevee", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - ], - "Cianwood City": [ - {"pokeapi_id": 213, "pokemon_name": "shuckle", "method": "gift", "encounter_rate": 100, "min_level": 20, "max_level": 20}, - ], - "Mt Mortar": [ - {"pokeapi_id": 236, "pokemon_name": "tyrogue", "method": "gift", "encounter_rate": 100, "min_level": 10, "max_level": 10}, - ], - "Dragons Den": [ - {"pokeapi_id": 147, "pokemon_name": "dratini", "method": "gift", "encounter_rate": 100, "min_level": 15, "max_level": 15}, - ], - "Pewter City": [ - {"pokeapi_id": 138, "pokemon_name": "omanyte", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, - {"pokeapi_id": 140, "pokemon_name": "kabuto", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, - {"pokeapi_id": 142, "pokemon_name": "aerodactyl", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, - {"pokeapi_id": 345, "pokemon_name": "lileep", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, - {"pokeapi_id": 347, "pokemon_name": "anorith", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, - {"pokeapi_id": 408, "pokemon_name": "cranidos", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, - {"pokeapi_id": 410, "pokemon_name": "shieldon", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, - ], - }, - "emerald": { - "Starter": [ - {"pokeapi_id": 252, "pokemon_name": "treecko", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - {"pokeapi_id": 255, "pokemon_name": "torchic", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - {"pokeapi_id": 258, "pokemon_name": "mudkip", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - ], - "Route 119": [ - {"pokeapi_id": 351, "pokemon_name": "castform", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25}, - ], - "Lavaridge Town": [ - {"pokeapi_id": 360, "pokemon_name": "wynaut", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - ], - "Mossdeep City": [ - {"pokeapi_id": 374, "pokemon_name": "beldum", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5}, - ], - "Rustboro City": [ - {"pokeapi_id": 345, "pokemon_name": "lileep", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, - {"pokeapi_id": 347, "pokemon_name": "anorith", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20}, - ], - }, -} - -# Aliases — version groups sharing the same special encounter data -SPECIAL_ENCOUNTERS["red-blue"] = SPECIAL_ENCOUNTERS["firered-leafgreen"] -SPECIAL_ENCOUNTERS["yellow"] = SPECIAL_ENCOUNTERS["firered-leafgreen"] -SPECIAL_ENCOUNTERS["lets-go"] = SPECIAL_ENCOUNTERS["firered-leafgreen"] -SPECIAL_ENCOUNTERS["gold-silver"] = SPECIAL_ENCOUNTERS["heartgold-soulsilver"] -SPECIAL_ENCOUNTERS["crystal"] = SPECIAL_ENCOUNTERS["heartgold-soulsilver"] -SPECIAL_ENCOUNTERS["ruby-sapphire"] = SPECIAL_ENCOUNTERS["emerald"] -SPECIAL_ENCOUNTERS["omega-ruby-alpha-sapphire"] = SPECIAL_ENCOUNTERS["emerald"]