#!/usr/bin/env python3 """Merge per-condition encounter rates from a fresh import into curated seed files. Usage: # From repo root (requires PokeDB cache): python tools/merge-conditions.py --game heartgold # Process all games that have conditions: python tools/merge-conditions.py --all # Dry run (print what would change, don't write): python tools/merge-conditions.py --game heartgold --dry-run """ from __future__ import annotations import argparse import json import sys from pathlib import Path # Add tools/import-pokedb to sys.path so we can import the library REPO_ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(REPO_ROOT / "tools" / "import-pokedb")) from import_pokedb.loader import load_pokedb_data, load_seed_config # noqa: E402 from import_pokedb.mappings import ( # noqa: E402 LocationMapper, PokemonMapper, build_version_map, ) from import_pokedb.processing import ( # noqa: E402 build_routes, filter_den_routes, filter_encounters_for_game, process_encounters, ) from import_pokedb.output import merge_special_encounters, sort_routes # noqa: E402 SEEDS_DIR = REPO_ROOT / "backend" / "src" / "app" / "seeds" DATA_DIR = SEEDS_DIR / "data" # Games that have per-condition encounter rates CONDITION_GAMES: dict[str, str] = { # Gen 2: morning/day/night "gold": "gold-silver", "silver": "gold-silver", "crystal": "crystal", # Gen 4: morning/day/night "heartgold": "heartgold-soulsilver", "soulsilver": "heartgold-soulsilver", "diamond": "diamond-pearl", "pearl": "diamond-pearl", "platinum": "platinum", "brilliant-diamond": "brilliant-diamond-shining-pearl", "shining-pearl": "brilliant-diamond-shining-pearl", # Gen 5: spring/summer/autumn/winter "black": "black-white", "white": "black-white", "black-2": "black-2-white-2", "white-2": "black-2-white-2", # Gen 6: horde encounters "x": "x-y", "y": "x-y", # Gen 7: day/night + SOS "sun": "sun-moon", "moon": "sun-moon", "ultra-sun": "ultra-sun-ultra-moon", "ultra-moon": "ultra-sun-ultra-moon", # Gen 8: weather "sword": "sword-shield", "shield": "sword-shield", } def normalize_route_name(name: str) -> str: """Normalize a route name for fuzzy matching.""" return name.lower().strip() def build_fresh_lookup( game_slug: str, vg_key: str, generation: int, pokedb: object, config: object, pokemon_mapper: PokemonMapper, location_mapper: LocationMapper, ) -> dict[str, dict[tuple[int, str], dict[str, int]]]: """Run the import pipeline and build a conditions lookup. Returns: {normalized_route_name: {(pokeapi_id, method): conditions_dict}} """ game_encounters = filter_encounters_for_game( pokedb.encounters, game_slug ) if not game_encounters: return {} encounters_by_area = process_encounters( game_encounters, generation, pokemon_mapper, location_mapper ) routes = build_routes(encounters_by_area, location_mapper) if vg_key == "sword-shield": routes = filter_den_routes(routes) routes = merge_special_encounters( routes, config, vg_key, pokemon_mapper ) routes = sort_routes(routes, config, vg_key) lookup: dict[str, dict[tuple[int, str], dict[str, int]]] = {} def index_route(route): key = normalize_route_name(route.name) enc_map: dict[tuple[int, str], dict[str, int]] = {} for enc in route.encounters: if enc.conditions: enc_map[(enc.pokeapi_id, enc.method)] = enc.conditions if enc_map: lookup[key] = enc_map for route in routes: index_route(route) for child in route.children: index_route(child) return lookup def merge_conditions_into_seed( seed_data: list[dict], lookup: dict[str, dict[tuple[int, str], dict[str, int]]], game_slug: str, dry_run: bool = False, ) -> tuple[list[dict], int]: """Merge conditions from lookup into seed data, return (updated_data, count).""" merged_count = 0 def process_route(route: dict) -> None: nonlocal merged_count route_key = normalize_route_name(route["name"]) route_lookup = lookup.get(route_key) if route_lookup is None: return for enc in route.get("encounters", []): key = (enc["pokeapi_id"], enc["method"]) conditions = route_lookup.get(key) if conditions: if dry_run: print( f" {route['name']}: " f"{enc.get('pokemon_name', '?')} ({enc['method']}) " f"-> {conditions}" ) enc["conditions"] = conditions enc["encounter_rate"] = None merged_count += 1 for child in route.get("children", []): process_route(child) for route in seed_data: process_route(route) return seed_data, merged_count def process_game( game_slug: str, pokedb, config, pokemon_mapper: PokemonMapper, location_mapper: LocationMapper, version_map: dict[str, str], dry_run: bool = False, ) -> int: """Process a single game. Returns number of encounters merged.""" vg_key = CONDITION_GAMES.get(game_slug) if vg_key is None: print(f" Skipping {game_slug}: not a condition game") return 0 # Find generation vg_info = config.version_groups.get(vg_key) if vg_info is None: print(f" Warning: version group '{vg_key}' not found") return 0 generation = vg_info.get("generation", 0) # Build fresh import lookup lookup = build_fresh_lookup( game_slug, vg_key, generation, pokedb, config, pokemon_mapper, location_mapper, ) if not lookup: print(" No conditions found in fresh import") return 0 total_conditions = sum(len(v) for v in lookup.values()) print( f" Fresh import: {len(lookup)} routes with conditions, " f"{total_conditions} encounter+condition pairs" ) # Load existing seed file seed_path = DATA_DIR / f"{game_slug}.json" if not seed_path.exists(): print(f" Warning: seed file not found: {seed_path}") return 0 with open(seed_path) as f: seed_data = json.load(f) # Merge updated_data, merged_count = merge_conditions_into_seed( seed_data, lookup, game_slug, dry_run=dry_run ) if merged_count == 0: print(" No encounters matched for merging") return 0 print(f" Merged conditions into {merged_count} encounters") if not dry_run: with open(seed_path, "w") as f: json.dump(updated_data, f, indent=2, ensure_ascii=False) f.write("\n") print(f" Wrote {seed_path}") return merged_count def main() -> None: parser = argparse.ArgumentParser( description="Merge per-condition encounter rates into seed files." ) parser.add_argument( "--game", type=str, help="Process a specific game slug" ) parser.add_argument( "--all", action="store_true", help="Process all games with conditions", ) parser.add_argument( "--dry-run", action="store_true", help="Print what would change without writing files", ) parser.add_argument( "--pokedb-dir", type=Path, default=None, help="Path to PokeDB data directory", ) args = parser.parse_args() if not args.game and not args.all: parser.error("Specify --game SLUG or --all") pokedb_dir = args.pokedb_dir or (SEEDS_DIR / ".pokedb_cache") print(f"PokeDB data: {pokedb_dir}") print(f"Seed data: {DATA_DIR}") print() # Load PokeDB data pokedb = load_pokedb_data(pokedb_dir) print(pokedb.summary()) print() # Load seed config config = load_seed_config(SEEDS_DIR) print(f"Loaded {len(config.version_groups)} version groups") print() # Build mappings pokemon_json = DATA_DIR / "pokemon.json" pokemon_mapper = PokemonMapper(pokemon_json, pokedb) location_mapper = LocationMapper(pokedb) version_map = build_version_map(pokedb, config.version_groups) # Determine games to process if args.game: games = [args.game] else: games = list(CONDITION_GAMES.keys()) total_merged = 0 for game_slug in games: print(f"\n--- {game_slug} ---") count = process_game( game_slug, pokedb, config, pokemon_mapper, location_mapper, version_map, dry_run=args.dry_run, ) total_merged += count print(f"\nTotal: {total_merged} encounters updated across {len(games)} games") if args.dry_run: print("(dry run — no files written)") if __name__ == "__main__": main()