Files
nuzlocke-tracker/tools/import-pokedb/import_pokedb/mappings.py
Julian Tabel df7ea64b9e Add reference data mappings and auto-download for PokeDB import tool
Add mappings module with pokemon form, location area, encounter method,
and version mappings. Auto-download PokeDB JSON exports from CDN on
first run, caching in .pokedb_cache/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 10:02:57 +01:00

385 lines
13 KiB
Python

"""Reference data mappings: PokeDB identifiers → seed format values."""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
from typing import Any
from .loader import PokeDBData
# ---------------------------------------------------------------------------
# Encounter method mapping
# ---------------------------------------------------------------------------
# PokeDB encounter_method_identifier → our simplified method name.
# Keys can be exact matches or prefix patterns (ending with *).
ENCOUNTER_METHOD_MAP: dict[str, str] = {
# Walking / grass / cave
"walking-tall-grass": "walk",
"walking-long-grass": "walk",
"walking-cave": "walk",
"walking-bridge": "walk",
"walking-building": "walk",
"walking-sand": "walk",
"walking-snow": "walk",
"walking-rough-terrain": "walk",
"walking-marsh": "walk",
"walking-puddle": "walk",
"walking-flower-field": "walk",
"walking-ice": "walk",
"walking-forest": "walk",
"walking-snowfield": "walk",
"dark-grass": "walk",
"shaking-grass": "walk",
"rustling-grass": "walk",
"yellow-flowers": "walk",
"red-flowers": "walk",
"purple-flowers": "walk",
# Surfing
"surfing": "surf",
"surfing-ocean": "surf",
"surfing-puddle": "surf",
"surfing-rapids": "surf",
"surfing-underwater": "surf",
"rippling-water": "surf",
# Fishing
"fishing-old-rod": "old-rod",
"fishing-good-rod": "good-rod",
"fishing-super-rod": "super-rod",
"fishing": "fishing",
"fishing-special": "fishing",
# Rock smash
"rock-smash": "rock-smash",
# Headbutt
"headbutt-low": "headbutt",
"headbutt-normal": "headbutt",
"headbutt-high": "headbutt",
# Gift / special acquisition
"npc-gift": "gift",
"egg": "gift",
"revive": "gift",
"fossil": "gift",
# Trade
"npc-trade": "trade",
# Overworld / symbol encounters (Gen 8+)
"symbol-encounter": "walk",
"wanderer": "walk",
"flying": "walk",
# Static / fixed
"fixed-encounter": "static",
"static-encounter": "static",
"legendary-encounter": "static",
"interactable": "static",
# Special methods
"swarm": "swarm",
"poke-radar": "pokeradar",
"dual-slot-mode": "dual-slot",
"honey-tree": "honey",
"trophy-garden": "walk",
"great-marsh": "walk",
"cave-spot": "walk",
"bubble-spot": "surf",
"sand-spot": "walk",
"horde": "walk",
"sos-encounter": "walk",
"ambush": "walk",
# Seaweed / diving
"diving": "surf",
"seaweed": "surf",
# Raids
"max-raid": "raid",
"dynamax-adventure": "raid",
"tera-raid": "raid",
# Misc
"roaming": "roaming",
"safari-zone": "walk",
"bug-contest": "walk",
}
# Prefix-based fallbacks for methods not explicitly listed above.
_METHOD_PREFIX_MAP: list[tuple[str, str]] = [
("walking-", "walk"),
("surfing-", "surf"),
("fishing-", "fishing"),
("headbutt-", "headbutt"),
("flying-", "walk"),
]
def map_encounter_method(method_identifier: str) -> str | None:
"""Map a PokeDB encounter method to our simplified method name.
Returns None if the method is unrecognized.
"""
if method_identifier in ENCOUNTER_METHOD_MAP:
return ENCOUNTER_METHOD_MAP[method_identifier]
for prefix, mapped in _METHOD_PREFIX_MAP:
if method_identifier.startswith(prefix):
return mapped
return None
# ---------------------------------------------------------------------------
# Version mapping
# ---------------------------------------------------------------------------
# PokeDB version identifiers that differ from our game slugs.
# Most are 1:1, these handle exceptions.
_VERSION_OVERRIDES: dict[str, str] = {
"lets-go-pikachu": "lets-go-pikachu",
"lets-go-eevee": "lets-go-eevee",
}
def build_version_map(
pokedb: PokeDBData,
version_groups: dict[str, Any],
) -> dict[str, str]:
"""Build a mapping from PokeDB version_identifier → our game slug.
Returns the mapping dict. Logs warnings for unmapped versions.
"""
# Collect all our known game slugs
our_slugs: set[str] = set()
for vg in version_groups.values():
for slug in vg.get("versions", []):
our_slugs.add(slug)
# Collect all PokeDB version identifiers
pokedb_versions: set[str] = set()
for v in pokedb.versions:
identifier = v.get("identifier", "")
if identifier:
pokedb_versions.add(identifier)
mapping: dict[str, str] = {}
for pdb_ver in sorted(pokedb_versions):
if pdb_ver in _VERSION_OVERRIDES:
mapping[pdb_ver] = _VERSION_OVERRIDES[pdb_ver]
elif pdb_ver in our_slugs:
mapping[pdb_ver] = pdb_ver
# else: PokeDB version not in our version_groups (expected for some)
# Report our games that have no PokeDB mapping
mapped_slugs = set(mapping.values())
unmapped_ours = our_slugs - mapped_slugs
if unmapped_ours:
print(f" Versions in our config with no PokeDB match: {sorted(unmapped_ours)}")
return mapping
# ---------------------------------------------------------------------------
# Pokemon form mapping
# ---------------------------------------------------------------------------
def _normalize_slug(identifier: str) -> str:
"""Normalize a PokeDB pokemon_form_identifier to a PokeAPI-style slug.
PokeDB uses "pidgey-default" for base forms — strip the "-default" suffix.
Non-default forms like "rattata-alola" are already PokeAPI-style slugs.
"""
if identifier.endswith("-default"):
return identifier[: -len("-default")]
return identifier
def _name_to_slug(name: str) -> str:
"""Convert a display name to a PokeAPI-style slug.
"Bulbasaur""bulbasaur"
"Mr. Mime""mr-mime"
"Farfetch'd""farfetchd"
"Nidoran♀""nidoran-f"
"Nidoran♂""nidoran-m"
"Flabébé""flabebe"
"Type: Null""type-null"
"""
s = name.lower()
s = s.replace("", "-f").replace("", "-m")
s = s.replace("'", "").replace("'", "").replace(".", "").replace(":", "")
s = s.replace("é", "e").replace("É", "e")
s = s.replace(" ", "-")
# Collapse multiple hyphens
s = re.sub(r"-+", "-", s)
return s.strip("-")
def _name_to_form_slug(name: str) -> str | None:
"""Convert a display name with form suffix to a PokeAPI-style slug.
"Rattata (Alola)""rattata-alola"
"Basculin (Blue Striped)""basculin-blue-striped"
"Deoxys Normal""deoxys-normal" (space-separated variant)
"""
# Try parenthesized form: "Base (Suffix)"
m = re.match(r"^(.+?)\s*\((.+)\)$", name)
if m:
base = _name_to_slug(m.group(1))
suffix = _name_to_slug(m.group(2))
return f"{base}-{suffix}"
# Try space-separated form: "Deoxys Normal"
parts = name.split()
if len(parts) >= 2:
return _name_to_slug(name)
return None
class PokemonMapper:
"""Maps PokeDB pokemon_form_identifier → (pokeapi_id, display_name)."""
def __init__(self, pokemon_json_path: Path, pokedb: PokeDBData) -> None:
# Build slug → (pokeapi_id, name) from existing pokemon.json
self._slug_to_info: dict[str, tuple[int, str]] = {}
self._id_to_info: dict[int, tuple[int, str]] = {} # pokeapi_id → (national_dex, name)
self._unmapped: set[str] = set()
if pokemon_json_path.exists():
with open(pokemon_json_path) as f:
pokemon_list = json.load(f)
for p in pokemon_list:
pid = p["pokeapi_id"]
name = p["name"]
ndex = p["national_dex"]
self._id_to_info[pid] = (ndex, name)
# Index by base slug (from pokeapi_id for base forms)
slug = _name_to_slug(name)
self._slug_to_info[slug] = (pid, name)
# Also index by form slug if it has a form suffix
form_slug = _name_to_form_slug(name)
if form_slug and form_slug != slug:
self._slug_to_info[form_slug] = (pid, name)
# Build index from PokeDB pokemon_forms.json if it has useful fields
self._pokedb_form_index: dict[str, dict] = {}
for form in pokedb.pokemon_forms:
identifier = form.get("identifier", "")
if identifier:
self._pokedb_form_index[identifier] = form
def lookup(self, pokemon_form_identifier: str | None) -> tuple[int, str] | None:
"""Look up a PokeDB pokemon_form_identifier.
Returns (pokeapi_id, display_name) or None if unmapped.
"""
if not pokemon_form_identifier:
return None
slug = _normalize_slug(pokemon_form_identifier)
# Direct slug match
if slug in self._slug_to_info:
return self._slug_to_info[slug]
# Try the PokeDB form record for a pokemon_id field
form_record = self._pokedb_form_index.get(pokemon_form_identifier, {})
pokemon_id = form_record.get("pokemon_id")
if pokemon_id and pokemon_id in self._id_to_info:
ndex, name = self._id_to_info[pokemon_id]
# Cache for future lookups
self._slug_to_info[slug] = (pokemon_id, name)
return (pokemon_id, name)
# Track unmapped
if pokemon_form_identifier not in self._unmapped:
self._unmapped.add(pokemon_form_identifier)
return None
def report_unmapped(self) -> None:
"""Print warnings for any unmapped identifiers."""
if self._unmapped:
print(
f"\nWarning: {len(self._unmapped)} unmapped pokemon form identifiers:",
file=sys.stderr,
)
for ident in sorted(self._unmapped):
print(f" - {ident}", file=sys.stderr)
# ---------------------------------------------------------------------------
# Location area mapping
# ---------------------------------------------------------------------------
# Region prefixes to strip from location identifiers (matching Go tool behavior).
_REGION_PREFIXES = [
"kanto-", "johto-", "hoenn-", "sinnoh-",
"unova-", "kalos-", "alola-", "galar-",
"hisui-", "paldea-",
]
def _identifier_to_name(identifier: str) -> str:
"""Convert a hyphenated identifier to a Title Case display name.
"route-01-kanto""Route 01 Kanto" (region stripping done separately)
"viridian-forest""Viridian Forest"
"""
return identifier.replace("-", " ").title()
class LocationMapper:
"""Maps PokeDB location_area_identifier → (location_name, area_suffix)."""
def __init__(self, pokedb: PokeDBData) -> None:
# Build location_area_identifier → location_identifier lookup
self._area_to_location: dict[str, str] = {}
# location_identifier → location display name
self._location_names: dict[str, str] = {}
# location_area_identifier → area display name
self._area_names: dict[str, str] = {}
# Index locations
for loc in pokedb.locations:
identifier = loc.get("identifier", "")
name = loc.get("name", "")
if identifier:
self._location_names[identifier] = name if name else self._clean_location_name(identifier)
# Index location areas
for area in pokedb.location_areas:
area_id = area.get("identifier", "")
loc_id = area.get("location_identifier", "")
area_name = area.get("name", "")
if area_id:
self._area_to_location[area_id] = loc_id
self._area_names[area_id] = area_name if area_name else ""
@staticmethod
def _clean_location_name(identifier: str) -> str:
"""Clean a location identifier into a display name."""
name = identifier
for prefix in _REGION_PREFIXES:
if name.startswith(prefix):
name = name[len(prefix):]
break
return _identifier_to_name(name)
def get_location_name(self, location_area_identifier: str) -> str:
"""Get the display name for a location area's parent location."""
loc_id = self._area_to_location.get(location_area_identifier, "")
if loc_id and loc_id in self._location_names:
return self._location_names[loc_id]
# Fallback: derive from the area identifier itself
return self._clean_location_name(location_area_identifier)
def get_area_name(self, location_area_identifier: str) -> str:
"""Get the display name for a specific location area."""
return self._area_names.get(location_area_identifier, "")
def get_location_identifier(self, location_area_identifier: str) -> str:
"""Get the parent location identifier for a location area."""
return self._area_to_location.get(location_area_identifier, "")