Add seed JSON output (per-game, games.json, pokemon.json)

Wire output module into CLI pipeline: route ordering, special encounter
merging, and JSON writing for per-game encounters, global games list,
and pokemon list with types and sprite paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Julian Tabel
2026-02-11 10:59:56 +01:00
parent 29b954726a
commit df55233c62
4 changed files with 281 additions and 10 deletions

View File

@@ -18,6 +18,7 @@ from pathlib import Path
from .loader import load_pokedb_data, load_seed_config
from .mappings import PokemonMapper, LocationMapper, build_version_map, map_encounter_method
from .output import sort_routes, merge_special_encounters, write_game_json, write_games_json, write_pokemon_json
from .processing import filter_encounters_for_game, process_encounters, build_routes
from .sprites import download_sprites
@@ -157,6 +158,8 @@ def main(argv: list[str] | None = None) -> None:
continue
games_to_process.append((vg_key, slug, generation))
all_encountered_form_ids: set[str] = set()
for vg_key, game_slug, generation in games_to_process:
print(f"\n--- {game_slug} ---")
@@ -168,6 +171,12 @@ def main(argv: list[str] | None = None) -> None:
print(f" Raw encounters: {len(game_encounters)}")
# Track encountered form IDs for pokemon.json
for e in game_encounters:
fid = e.get("pokemon_form_identifier")
if fid:
all_encountered_form_ids.add(fid)
# Process into grouped encounters
encounters_by_area = process_encounters(
game_encounters, generation, pokemon_mapper, location_mapper,
@@ -177,6 +186,12 @@ def main(argv: list[str] | None = None) -> None:
# Build route hierarchy
routes = build_routes(encounters_by_area, location_mapper)
# Merge special encounters (starters, gifts, fossils)
routes = merge_special_encounters(routes, config, vg_key, pokemon_mapper)
# Sort routes by game progression order
routes = sort_routes(routes, config, vg_key)
# Stats
total_routes = sum(1 + len(r.children) for r in routes)
total_enc = sum(
@@ -186,13 +201,21 @@ def main(argv: list[str] | None = None) -> None:
print(f" Routes: {total_routes}")
print(f" Encounter entries: {total_enc}")
# Write per-game JSON
write_game_json(routes, output_dir, game_slug)
# Download sprites for all encountered pokemon
print("\nDownloading sprites...")
sprites_dir = output_dir / "sprites"
sprite_map = download_sprites(pokemon_mapper, form_ids_in_encounters, sprites_dir)
sprite_map = download_sprites(pokemon_mapper, all_encountered_form_ids, sprites_dir)
print(f" Sprite map covers {len(sprite_map)} forms")
print("\nProcessing complete. Output not yet written (subtask gkcy).")
# Write global JSON files
print("\nWriting global data files...")
write_games_json(config, output_dir)
write_pokemon_json(pokemon_mapper, all_encountered_form_ids, sprite_map, output_dir)
print("\nDone!")
if __name__ == "__main__":

View File

@@ -464,6 +464,16 @@ class PokemonMapper:
return None
def lookup_by_id(self, pokeapi_id: int) -> tuple[int, str] | None:
"""Look up a pokemon by its pokeapi_id.
Returns (pokeapi_id, display_name) or None if not found.
"""
if pokeapi_id in self._id_to_info:
_, name = self._id_to_info[pokeapi_id]
return (pokeapi_id, name)
return None
def get_sprite_url(self, pokemon_form_identifier: str | None) -> str | None:
"""Get the PokeDB CDN sprite URL (100x100 medium) for a form identifier."""
if not pokemon_form_identifier:

View File

@@ -0,0 +1,238 @@
"""Output seed JSON files in the existing format."""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
from .loader import SeedConfig
from .mappings import PokemonMapper
from .models import Encounter, Route
from .sprites import sprite_path_for_pokemon
# ---------------------------------------------------------------------------
# Route ordering
# ---------------------------------------------------------------------------
def _get_route_order(config: SeedConfig, vg_key: str) -> list[str] | None:
"""Get the route order list for a version group, resolving aliases."""
return config.route_order.get(vg_key)
def _route_sort_key(name: str, order_list: list[str]) -> tuple[int, str]:
"""Compute sort key for a route name against an ordered list."""
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)
def sort_routes(routes: list[Route], config: SeedConfig, vg_key: str) -> list[Route]:
"""Sort routes by game progression order and assign sequential order values."""
order_list = _get_route_order(config, vg_key)
if order_list:
routes.sort(key=lambda r: _route_sort_key(r.name, order_list))
else:
print(f" Warning: No route order for {vg_key}, using default order", file=sys.stderr)
# Assign sequential order values
order = 1
for route in routes:
route.order = order
order += 1
for child in route.children:
child.order = order
order += 1
return routes
# ---------------------------------------------------------------------------
# Special encounters
# ---------------------------------------------------------------------------
def _get_special_encounters(
config: SeedConfig,
vg_key: str,
) -> dict[str, list[dict[str, Any]]] | None:
"""Get special encounters for a version group, resolving aliases."""
if not config.special_encounters:
return None
encounters = config.special_encounters.get("encounters", {})
aliases = config.special_encounters.get("aliases", {})
if vg_key in encounters:
return encounters[vg_key]
if vg_key in aliases:
alias_target = aliases[vg_key]
if alias_target in encounters:
return encounters[alias_target]
return None
def merge_special_encounters(
routes: list[Route],
config: SeedConfig,
vg_key: str,
pokemon_mapper: PokemonMapper | None = None,
) -> list[Route]:
"""Merge special encounters (starters, gifts, fossils) into routes."""
special_data = _get_special_encounters(config, vg_key)
if not special_data:
return routes
# Build lookup: route name → route (including children)
route_map: dict[str, Route] = {}
for route in routes:
route_map[route.name] = route
for child in route.children:
route_map[child.name] = child
for location_name, encounter_dicts in special_data.items():
encounters = []
for e in encounter_dicts:
# Look up proper display name from pokemon mapper if available
pokemon_name = e["pokemon_name"]
if pokemon_mapper:
info = pokemon_mapper.lookup_by_id(e["pokeapi_id"])
if info:
pokemon_name = info[1]
encounters.append(Encounter(
pokeapi_id=e["pokeapi_id"],
pokemon_name=pokemon_name,
method=e["method"],
encounter_rate=e["encounter_rate"],
min_level=e["min_level"],
max_level=e["max_level"],
))
if location_name in route_map:
route_map[location_name].encounters.extend(encounters)
else:
new_route = Route(name=location_name, order=0, encounters=encounters)
routes.append(new_route)
route_map[location_name] = new_route
return routes
# ---------------------------------------------------------------------------
# JSON output
# ---------------------------------------------------------------------------
def write_json(path: Path, data: Any) -> None:
"""Write data as formatted JSON with trailing newline."""
content = json.dumps(data, indent=2, ensure_ascii=False)
path.write_text(content + "\n", encoding="utf-8")
print(f" -> {path}")
def write_game_json(routes: list[Route], output_dir: Path, game_slug: str) -> None:
"""Write a per-game route/encounter JSON file."""
data = [r.to_dict() for r in routes]
write_json(output_dir / f"{game_slug}.json", data)
def write_games_json(config: SeedConfig, output_dir: Path) -> None:
"""Write games.json from version_groups config."""
games = []
for vg_info in config.version_groups.values():
for game_info in vg_info.get("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(output_dir / "games.json", games)
print(f" Wrote {len(games)} games")
def write_pokemon_json(
pokemon_mapper: PokemonMapper,
encountered_form_ids: set[str],
sprite_map: dict[str, str],
output_dir: Path,
) -> None:
"""Write pokemon.json with all pokemon referenced in encounters."""
seen_ids: set[int] = set()
pokemon_list: list[dict[str, Any]] = []
for form_id in sorted(encountered_form_ids):
info = pokemon_mapper.lookup(form_id)
if info is None:
continue
pokeapi_id, name = info
if pokeapi_id in seen_ids:
continue
seen_ids.add(pokeapi_id)
# Get additional data from PokeDB form record
form_data = pokemon_mapper.get_form_data(form_id)
national_dex = form_data.get("ndex_id", pokeapi_id) if form_data else pokeapi_id
# Types from PokeDB form data
types = _extract_types(form_data) if form_data else []
# Sprite URL
sprite_url: str | None = None
if form_id in sprite_map:
sprite_url = sprite_path_for_pokemon(pokeapi_id)
pokemon_list.append({
"pokeapi_id": pokeapi_id,
"national_dex": national_dex,
"name": name,
"types": types,
"sprite_url": sprite_url,
})
# Sort by pokeapi_id
pokemon_list.sort(key=lambda p: p["pokeapi_id"])
write_json(output_dir / "pokemon.json", pokemon_list)
print(f" Wrote {len(pokemon_list)} pokemon")
# PokeDB type IDs → type names (from PokeDB's type system)
_TYPE_ID_MAP: dict[int, str] = {
1: "normal",
2: "fighting",
3: "flying",
4: "poison",
5: "ground",
6: "rock",
7: "bug",
8: "ghost",
9: "steel",
10: "fire",
11: "water",
12: "grass",
13: "electric",
14: "psychic",
15: "ice",
16: "dragon",
17: "dark",
18: "fairy",
}
def _extract_types(form_data: dict[str, Any]) -> list[str]:
"""Extract type names from a PokeDB form record."""
types = []
type1_id = form_data.get("type_1_id")
type2_id = form_data.get("type_2_id")
if type1_id and type1_id in _TYPE_ID_MAP:
types.append(_TYPE_ID_MAP[type1_id])
if type2_id and type2_id in _TYPE_ID_MAP:
types.append(_TYPE_ID_MAP[type2_id])
return types