Co-authored-by: Julian Tabel <juliantabel.jt@gmail.com> Co-committed-by: Julian Tabel <juliantabel.jt@gmail.com>
323 lines
8.9 KiB
Python
323 lines
8.9 KiB
Python
#!/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()
|