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 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 19:44:10 +01:00
parent 0bf628157f
commit 66ad41cf1c
3 changed files with 90 additions and 696 deletions

View File

@@ -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()

View File

@@ -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"
}
}

View File

@@ -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"]