Separate PokeAPI ID from national dex for correct form identification

Pokemon forms (e.g., Alolan Rattata) had their PokeAPI ID (10091) stored as
national_dex, causing them to display incorrectly. This renames the unique
identifier to pokeapi_id and adds a real national_dex field shared between
forms and their base species, so Alolan Rattata correctly shows as #19.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 14:55:06 +01:00
parent cb027e5215
commit d168d99bba
46 changed files with 23459 additions and 22289 deletions

View File

@@ -0,0 +1,53 @@
"""rename national_dex to pokeapi_id, add real national_dex
Revision ID: e5f6a7b8c9d0
Revises: d4e5f6a7b8c9
Create Date: 2026-02-07 10:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'e5f6a7b8c9d0'
down_revision: Union[str, Sequence[str], None] = 'd4e5f6a7b8c9'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# Rename national_dex -> pokeapi_id and widen to Integer
op.alter_column(
'pokemon', 'national_dex',
new_column_name='pokeapi_id',
type_=sa.Integer(),
existing_type=sa.SmallInteger(),
existing_nullable=False,
)
# Add real national_dex column (shared between forms and base species)
op.add_column(
'pokemon',
sa.Column('national_dex', sa.SmallInteger(), nullable=False, server_default='0'),
)
# Populate national_dex = pokeapi_id for all existing rows
# (correct for base species; forms will be fixed by re-seeding)
op.execute('UPDATE pokemon SET national_dex = pokeapi_id')
# Remove the default now that all rows are populated
op.alter_column('pokemon', 'national_dex', server_default=None)
def downgrade() -> None:
op.drop_column('pokemon', 'national_dex')
op.alter_column(
'pokemon', 'pokeapi_id',
new_column_name='national_dex',
type_=sa.SmallInteger(),
existing_type=sa.Integer(),
existing_nullable=False,
)

View File

@@ -43,7 +43,7 @@ async def list_pokemon(
total = (await session.execute(count_query)).scalar() or 0
# Get paginated items
items_query = base_query.order_by(Pokemon.national_dex).offset(offset).limit(limit)
items_query = base_query.order_by(Pokemon.national_dex, Pokemon.name).offset(offset).limit(limit)
result = await session.execute(items_query)
items = result.scalars().all()
@@ -67,11 +67,12 @@ async def bulk_import_pokemon(
for item in items:
try:
existing = await session.execute(
select(Pokemon).where(Pokemon.national_dex == item.national_dex)
select(Pokemon).where(Pokemon.pokeapi_id == item.pokeapi_id)
)
pokemon = existing.scalar_one_or_none()
if pokemon is not None:
pokemon.national_dex = item.national_dex
pokemon.name = item.name
pokemon.types = item.types
if item.sprite_url is not None:
@@ -82,7 +83,7 @@ async def bulk_import_pokemon(
session.add(pokemon)
created += 1
except Exception as e:
errors.append(f"Dex #{item.national_dex} ({item.name}): {e}")
errors.append(f"PokeAPI #{item.pokeapi_id} ({item.name}): {e}")
await session.commit()
return BulkImportResult(created=created, updated=updated, errors=errors)
@@ -93,12 +94,12 @@ async def create_pokemon(
data: PokemonCreate, session: AsyncSession = Depends(get_session)
):
existing = await session.execute(
select(Pokemon).where(Pokemon.national_dex == data.national_dex)
select(Pokemon).where(Pokemon.pokeapi_id == data.pokeapi_id)
)
if existing.scalar_one_or_none() is not None:
raise HTTPException(
status_code=409,
detail=f"Pokemon with national dex #{data.national_dex} already exists",
detail=f"Pokemon with PokeAPI ID #{data.pokeapi_id} already exists",
)
pokemon = Pokemon(**data.model_dump())
@@ -145,17 +146,17 @@ async def update_pokemon(
raise HTTPException(status_code=404, detail="Pokemon not found")
update_data = data.model_dump(exclude_unset=True)
if "national_dex" in update_data:
if "pokeapi_id" in update_data:
existing = await session.execute(
select(Pokemon).where(
Pokemon.national_dex == update_data["national_dex"],
Pokemon.pokeapi_id == update_data["pokeapi_id"],
Pokemon.id != pokemon_id,
)
)
if existing.scalar_one_or_none() is not None:
raise HTTPException(
status_code=409,
detail=f"Pokemon with national dex #{update_data['national_dex']} already exists",
detail=f"Pokemon with PokeAPI ID #{update_data['pokeapi_id']} already exists",
)
for field, value in update_data.items():

View File

@@ -1,4 +1,4 @@
from sqlalchemy import SmallInteger, String
from sqlalchemy import Integer, SmallInteger, String
from sqlalchemy.dialects.postgresql import ARRAY
from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -9,7 +9,8 @@ class Pokemon(Base):
__tablename__ = "pokemon"
id: Mapped[int] = mapped_column(primary_key=True)
national_dex: Mapped[int] = mapped_column(SmallInteger, unique=True)
pokeapi_id: Mapped[int] = mapped_column(Integer, unique=True)
national_dex: Mapped[int] = mapped_column(SmallInteger)
name: Mapped[str] = mapped_column(String(50))
types: Mapped[list[str]] = mapped_column(ARRAY(String(20)))
sprite_url: Mapped[str | None] = mapped_column(String(500))
@@ -22,4 +23,4 @@ class Pokemon(Base):
)
def __repr__(self) -> str:
return f"<Pokemon(id={self.id}, name='{self.name}', dex={self.national_dex})>"
return f"<Pokemon(id={self.id}, name='{self.name}', pokeapi_id={self.pokeapi_id}, dex={self.national_dex})>"

View File

@@ -5,6 +5,7 @@ from app.schemas.base import CamelModel
class PokemonResponse(CamelModel):
id: int
pokeapi_id: int
national_dex: int
name: str
types: list[str]
@@ -47,6 +48,7 @@ class RouteEncounterDetailResponse(RouteEncounterResponse):
class PokemonCreate(CamelModel):
pokeapi_id: int
national_dex: int
name: str
types: list[str]
@@ -54,6 +56,7 @@ class PokemonCreate(CamelModel):
class PokemonUpdate(CamelModel):
pokeapi_id: int | None = None
national_dex: int | None = None
name: str | None = None
types: list[str] | None = None
@@ -76,6 +79,7 @@ class RouteEncounterUpdate(CamelModel):
class BulkImportItem(BaseModel):
pokeapi_id: int
national_dex: int
name: str
types: list[str]

View File

@@ -4,7 +4,7 @@
"order": 1,
"encounters": [
{
"national_dex": 252,
"pokeapi_id": 252,
"pokemon_name": "treecko",
"method": "starter",
"encounter_rate": 100,
@@ -12,7 +12,7 @@
"max_level": 5
},
{
"national_dex": 255,
"pokeapi_id": 255,
"pokemon_name": "torchic",
"method": "starter",
"encounter_rate": 100,
@@ -20,7 +20,7 @@
"max_level": 5
},
{
"national_dex": 258,
"pokeapi_id": 258,
"pokemon_name": "mudkip",
"method": "starter",
"encounter_rate": 100,
@@ -34,7 +34,7 @@
"order": 2,
"encounters": [
{
"national_dex": 345,
"pokeapi_id": 345,
"pokemon_name": "lileep",
"method": "fossil",
"encounter_rate": 100,
@@ -42,7 +42,7 @@
"max_level": 20
},
{
"national_dex": 347,
"pokeapi_id": 347,
"pokemon_name": "anorith",
"method": "fossil",
"encounter_rate": 100,
@@ -56,7 +56,7 @@
"order": 3,
"encounters": [
{
"national_dex": 360,
"pokeapi_id": 360,
"pokemon_name": "wynaut",
"method": "gift",
"encounter_rate": 100,
@@ -70,7 +70,7 @@
"order": 4,
"encounters": [
{
"national_dex": 351,
"pokeapi_id": 351,
"pokemon_name": "castform",
"method": "gift",
"encounter_rate": 100,
@@ -84,7 +84,7 @@
"order": 5,
"encounters": [
{
"national_dex": 374,
"pokeapi_id": 374,
"pokemon_name": "beldum",
"method": "gift",
"encounter_rate": 100,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"order": 1,
"encounters": [
{
"national_dex": 1,
"pokeapi_id": 1,
"pokemon_name": "bulbasaur",
"method": "starter",
"encounter_rate": 100,
@@ -12,7 +12,7 @@
"max_level": 5
},
{
"national_dex": 4,
"pokeapi_id": 4,
"pokemon_name": "charmander",
"method": "starter",
"encounter_rate": 100,
@@ -20,7 +20,7 @@
"max_level": 5
},
{
"national_dex": 7,
"pokeapi_id": 7,
"pokemon_name": "squirtle",
"method": "starter",
"encounter_rate": 100,
@@ -34,7 +34,7 @@
"order": 2,
"encounters": [
{
"national_dex": 129,
"pokeapi_id": 129,
"pokemon_name": "magikarp",
"method": "gift",
"encounter_rate": 100,
@@ -48,7 +48,7 @@
"order": 3,
"encounters": [
{
"national_dex": 133,
"pokeapi_id": 133,
"pokemon_name": "eevee",
"method": "gift",
"encounter_rate": 100,
@@ -62,7 +62,7 @@
"order": 4,
"encounters": [
{
"national_dex": 131,
"pokeapi_id": 131,
"pokemon_name": "lapras",
"method": "gift",
"encounter_rate": 100,
@@ -70,7 +70,7 @@
"max_level": 25
},
{
"national_dex": 106,
"pokeapi_id": 106,
"pokemon_name": "hitmonlee",
"method": "gift",
"encounter_rate": 100,
@@ -78,7 +78,7 @@
"max_level": 25
},
{
"national_dex": 107,
"pokeapi_id": 107,
"pokemon_name": "hitmonchan",
"method": "gift",
"encounter_rate": 100,
@@ -92,7 +92,7 @@
"order": 5,
"encounters": [
{
"national_dex": 138,
"pokeapi_id": 138,
"pokemon_name": "omanyte",
"method": "fossil",
"encounter_rate": 100,
@@ -100,7 +100,7 @@
"max_level": 5
},
{
"national_dex": 140,
"pokeapi_id": 140,
"pokemon_name": "kabuto",
"method": "fossil",
"encounter_rate": 100,
@@ -108,7 +108,7 @@
"max_level": 5
},
{
"national_dex": 142,
"pokeapi_id": 142,
"pokemon_name": "aerodactyl",
"method": "fossil",
"encounter_rate": 100,
@@ -122,7 +122,7 @@
"order": 6,
"encounters": [
{
"national_dex": 175,
"pokeapi_id": 175,
"pokemon_name": "togepi",
"method": "gift",
"encounter_rate": 100,

View File

@@ -4,7 +4,7 @@
"order": 1,
"encounters": [
{
"national_dex": 1,
"pokeapi_id": 1,
"pokemon_name": "bulbasaur",
"method": "starter",
"encounter_rate": 100,
@@ -12,7 +12,7 @@
"max_level": 5
},
{
"national_dex": 4,
"pokeapi_id": 4,
"pokemon_name": "charmander",
"method": "starter",
"encounter_rate": 100,
@@ -20,7 +20,7 @@
"max_level": 5
},
{
"national_dex": 7,
"pokeapi_id": 7,
"pokemon_name": "squirtle",
"method": "starter",
"encounter_rate": 100,
@@ -34,7 +34,7 @@
"order": 2,
"encounters": [
{
"national_dex": 129,
"pokeapi_id": 129,
"pokemon_name": "magikarp",
"method": "gift",
"encounter_rate": 100,
@@ -48,7 +48,7 @@
"order": 3,
"encounters": [
{
"national_dex": 133,
"pokeapi_id": 133,
"pokemon_name": "eevee",
"method": "gift",
"encounter_rate": 100,
@@ -62,7 +62,7 @@
"order": 4,
"encounters": [
{
"national_dex": 131,
"pokeapi_id": 131,
"pokemon_name": "lapras",
"method": "gift",
"encounter_rate": 100,
@@ -70,7 +70,7 @@
"max_level": 25
},
{
"national_dex": 106,
"pokeapi_id": 106,
"pokemon_name": "hitmonlee",
"method": "gift",
"encounter_rate": 100,
@@ -78,7 +78,7 @@
"max_level": 25
},
{
"national_dex": 107,
"pokeapi_id": 107,
"pokemon_name": "hitmonchan",
"method": "gift",
"encounter_rate": 100,
@@ -92,7 +92,7 @@
"order": 5,
"encounters": [
{
"national_dex": 138,
"pokeapi_id": 138,
"pokemon_name": "omanyte",
"method": "fossil",
"encounter_rate": 100,
@@ -100,7 +100,7 @@
"max_level": 5
},
{
"national_dex": 140,
"pokeapi_id": 140,
"pokemon_name": "kabuto",
"method": "fossil",
"encounter_rate": 100,
@@ -108,7 +108,7 @@
"max_level": 5
},
{
"national_dex": 142,
"pokeapi_id": 142,
"pokemon_name": "aerodactyl",
"method": "fossil",
"encounter_rate": 100,
@@ -122,7 +122,7 @@
"order": 6,
"encounters": [
{
"national_dex": 175,
"pokeapi_id": 175,
"pokemon_name": "togepi",
"method": "gift",
"encounter_rate": 100,

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@
"order": 1,
"encounters": [
{
"national_dex": 252,
"pokeapi_id": 252,
"pokemon_name": "treecko",
"method": "starter",
"encounter_rate": 100,
@@ -12,7 +12,7 @@
"max_level": 5
},
{
"national_dex": 255,
"pokeapi_id": 255,
"pokemon_name": "torchic",
"method": "starter",
"encounter_rate": 100,
@@ -20,7 +20,7 @@
"max_level": 5
},
{
"national_dex": 258,
"pokeapi_id": 258,
"pokemon_name": "mudkip",
"method": "starter",
"encounter_rate": 100,
@@ -34,7 +34,7 @@
"order": 2,
"encounters": [
{
"national_dex": 345,
"pokeapi_id": 345,
"pokemon_name": "lileep",
"method": "fossil",
"encounter_rate": 100,
@@ -42,7 +42,7 @@
"max_level": 20
},
{
"national_dex": 347,
"pokeapi_id": 347,
"pokemon_name": "anorith",
"method": "fossil",
"encounter_rate": 100,
@@ -56,7 +56,7 @@
"order": 3,
"encounters": [
{
"national_dex": 360,
"pokeapi_id": 360,
"pokemon_name": "wynaut",
"method": "gift",
"encounter_rate": 100,
@@ -70,7 +70,7 @@
"order": 4,
"encounters": [
{
"national_dex": 351,
"pokeapi_id": 351,
"pokemon_name": "castform",
"method": "gift",
"encounter_rate": 100,
@@ -84,7 +84,7 @@
"order": 5,
"encounters": [
{
"national_dex": 374,
"pokeapi_id": 374,
"pokemon_name": "beldum",
"method": "gift",
"encounter_rate": 100,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -63,8 +63,8 @@ INCLUDED_METHODS = {
"headbutt",
}
# Collect all pokemon dex numbers across games
all_pokemon_dex: set[int] = set()
# Collect all pokemon PokeAPI IDs across games
all_pokeapi_ids: set[int] = set()
def clean_location_name(name: str) -> str:
@@ -137,7 +137,7 @@ def get_encounters_for_area(area_id: int, version_name: str) -> list[dict]:
encounters.append({
"pokemon_name": pokemon_name,
"national_dex": dex_num,
"pokeapi_id": dex_num,
"method": method,
"chance": enc["chance"],
"min_level": enc["min_level"],
@@ -152,10 +152,10 @@ def aggregate_encounters(raw_encounters: list[dict]) -> list[dict]:
agg: dict[tuple[int, str], dict] = {}
for enc in raw_encounters:
key = (enc["national_dex"], enc["method"])
key = (enc["pokeapi_id"], enc["method"])
if key not in agg:
agg[key] = {
"national_dex": enc["national_dex"],
"pokeapi_id": enc["pokeapi_id"],
"pokemon_name": enc["pokemon_name"],
"method": enc["method"],
"encounter_rate": 0,
@@ -184,7 +184,7 @@ def merge_special_encounters(routes: list[dict], special_data: dict[str, list[di
for location_name, encounters in special_data.items():
for enc in encounters:
all_pokemon_dex.add(enc["national_dex"])
all_pokeapi_ids.add(enc["pokeapi_id"])
if location_name in route_map:
route_map[location_name]["encounters"].extend(encounters)
@@ -252,7 +252,7 @@ def process_version(version_name: str, vg_info: dict, vg_key: str) -> list[dict]
if aggregated:
route_name = f"{display_name} ({area_suffix})"
for enc in aggregated:
all_pokemon_dex.add(enc["national_dex"])
all_pokeapi_ids.add(enc["pokeapi_id"])
child_routes.append({
"name": route_name,
"order": 0,
@@ -275,7 +275,7 @@ def process_version(version_name: str, vg_info: dict, vg_key: str) -> list[dict]
if aggregated:
route_name = f"{display_name} ({area_suffix})"
for enc in aggregated:
all_pokemon_dex.add(enc["national_dex"])
all_pokeapi_ids.add(enc["pokeapi_id"])
routes.append({
"name": route_name,
"order": 0,
@@ -287,7 +287,7 @@ def process_version(version_name: str, vg_info: dict, vg_key: str) -> list[dict]
aggregated = aggregate_encounters(all_encounters)
if aggregated:
for enc in aggregated:
all_pokemon_dex.add(enc["national_dex"])
all_pokeapi_ids.add(enc["pokeapi_id"])
routes.append({
"name": display_name,
"order": 0,
@@ -351,7 +351,7 @@ def fetch_all_pokemon() -> list[dict]:
all_species.append(dex)
# Also include form IDs that appear in encounter data
form_ids = sorted(d for d in all_pokemon_dex if d >= 10000)
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 ---")
@@ -363,6 +363,7 @@ def fetch_all_pokemon() -> list[dict]:
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,
@@ -375,8 +376,10 @@ def fetch_all_pokemon() -> list[dict]:
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({
"national_dex": form_dex,
"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",
@@ -385,7 +388,7 @@ def fetch_all_pokemon() -> list[dict]:
if form_ids:
print(f" Fetched {len(form_ids)} forms")
return sorted(pokemon_list, key=lambda x: x["national_dex"])
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]:
@@ -442,8 +445,8 @@ def flatten_evolution_chain(chain: dict, seeded_dex: set[int]) -> list[dict]:
if from_dex in seeded_dex and to_dex in seeded_dex:
pairs.append({
"from_national_dex": from_dex,
"to_national_dex": to_dex,
"from_pokeapi_id": from_dex,
"to_pokeapi_id": to_dex,
"trigger": trigger,
"min_level": min_level,
"item": item,
@@ -484,13 +487,13 @@ def fetch_evolution_data(seeded_dex: set[int]) -> list[dict]:
chain = load_resource("evolution-chain", chain_id)
pairs = flatten_evolution_chain(chain["chain"], seeded_dex)
for p in pairs:
key = (p["from_national_dex"], p["to_national_dex"], p["trigger"])
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_national_dex"], x["to_national_dex"]))
return sorted(all_pairs, key=lambda x: (x["from_pokeapi_id"], x["to_pokeapi_id"]))
def apply_evolution_overrides(evolutions: list[dict]) -> None:
@@ -506,15 +509,15 @@ def apply_evolution_overrides(evolutions: list[dict]) -> None:
for removal in overrides.get("remove", []):
evolutions[:] = [
e for e in evolutions
if not (e["from_national_dex"] == removal["from_dex"]
and e["to_national_dex"] == removal["to_dex"])
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_national_dex": addition["from_dex"],
"to_national_dex": addition["to_dex"],
"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"),
@@ -525,13 +528,13 @@ def apply_evolution_overrides(evolutions: list[dict]) -> None:
# Modify entries
for mod in overrides.get("modify", []):
for e in evolutions:
if (e["from_national_dex"] == mod["from_dex"]
and e["to_national_dex"] == mod["to_dex"]):
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_national_dex"], x["to_national_dex"]))
evolutions.sort(key=lambda x: (x["from_pokeapi_id"], x["to_pokeapi_id"]))
print(f" Applied overrides: {len(evolutions)} pairs after overrides")
@@ -580,8 +583,8 @@ def main():
write_json("pokemon.json", pokemon)
print(f"\nWrote {len(pokemon)} Pokemon to pokemon.json")
# Build set of all seeded dex numbers for evolution filtering
all_seeded_dex = {p["national_dex"] for p in pokemon}
# 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)

View File

@@ -40,16 +40,18 @@ async def upsert_games(session: AsyncSession, games: list[dict]) -> dict[str, in
async def upsert_pokemon(session: AsyncSession, pokemon_list: list[dict]) -> dict[int, int]:
"""Upsert pokemon records, return {national_dex: id} mapping."""
"""Upsert pokemon records, return {pokeapi_id: id} mapping."""
for poke in pokemon_list:
stmt = insert(Pokemon).values(
pokeapi_id=poke["pokeapi_id"],
national_dex=poke["national_dex"],
name=poke["name"],
types=poke["types"],
sprite_url=poke.get("sprite_url"),
).on_conflict_do_update(
index_elements=["national_dex"],
index_elements=["pokeapi_id"],
set_={
"national_dex": poke["national_dex"],
"name": poke["name"],
"types": poke["types"],
"sprite_url": poke.get("sprite_url"),
@@ -59,8 +61,8 @@ async def upsert_pokemon(session: AsyncSession, pokemon_list: list[dict]) -> dic
await session.flush()
result = await session.execute(select(Pokemon.national_dex, Pokemon.id))
return {row.national_dex: row.id for row in result}
result = await session.execute(select(Pokemon.pokeapi_id, Pokemon.id))
return {row.pokeapi_id: row.id for row in result}
async def upsert_routes(
@@ -131,9 +133,9 @@ async def upsert_route_encounters(
"""Upsert encounters for a route, return count of upserted rows."""
count = 0
for enc in encounters:
pokemon_id = dex_to_id.get(enc["national_dex"])
pokemon_id = dex_to_id.get(enc["pokeapi_id"])
if pokemon_id is None:
print(f" Warning: no pokemon_id for dex {enc['national_dex']}")
print(f" Warning: no pokemon_id for pokeapi_id {enc['pokeapi_id']}")
continue
stmt = insert(RouteEncounter).values(
@@ -169,8 +171,8 @@ async def upsert_evolutions(
count = 0
for evo in evolutions:
from_id = dex_to_id.get(evo["from_national_dex"])
to_id = dex_to_id.get(evo["to_national_dex"])
from_id = dex_to_id.get(evo["from_pokeapi_id"])
to_id = dex_to_id.get(evo["to_pokeapi_id"])
if from_id is None or to_id is None:
continue

View File

@@ -9,79 +9,79 @@ same format as aggregated PokeAPI encounters.
SPECIAL_ENCOUNTERS: dict[str, dict[str, list[dict]]] = {
"firered-leafgreen": {
"Starter": [
{"national_dex": 1, "pokemon_name": "bulbasaur", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"national_dex": 4, "pokemon_name": "charmander", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"national_dex": 7, "pokemon_name": "squirtle", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"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": [
{"national_dex": 129, "pokemon_name": "magikarp", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"pokeapi_id": 129, "pokemon_name": "magikarp", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
],
"Celadon City": [
{"national_dex": 133, "pokemon_name": "eevee", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
{"pokeapi_id": 133, "pokemon_name": "eevee", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
],
"Saffron City": [
{"national_dex": 131, "pokemon_name": "lapras", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
{"national_dex": 106, "pokemon_name": "hitmonlee", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
{"national_dex": 107, "pokemon_name": "hitmonchan", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
{"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": [
{"national_dex": 138, "pokemon_name": "omanyte", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"national_dex": 140, "pokemon_name": "kabuto", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"national_dex": 142, "pokemon_name": "aerodactyl", "method": "fossil", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"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": [
{"national_dex": 175, "pokemon_name": "togepi", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"pokeapi_id": 175, "pokemon_name": "togepi", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
],
},
"heartgold-soulsilver": {
"Starter": [
{"national_dex": 152, "pokemon_name": "chikorita", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"national_dex": 155, "pokemon_name": "cyndaquil", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"national_dex": 158, "pokemon_name": "totodile", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"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": [
{"national_dex": 175, "pokemon_name": "togepi", "method": "gift", "encounter_rate": 100, "min_level": 1, "max_level": 1},
{"pokeapi_id": 175, "pokemon_name": "togepi", "method": "gift", "encounter_rate": 100, "min_level": 1, "max_level": 1},
],
"Goldenrod City": [
{"national_dex": 133, "pokemon_name": "eevee", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"pokeapi_id": 133, "pokemon_name": "eevee", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
],
"Cianwood City": [
{"national_dex": 213, "pokemon_name": "shuckle", "method": "gift", "encounter_rate": 100, "min_level": 20, "max_level": 20},
{"pokeapi_id": 213, "pokemon_name": "shuckle", "method": "gift", "encounter_rate": 100, "min_level": 20, "max_level": 20},
],
"Mt Mortar": [
{"national_dex": 236, "pokemon_name": "tyrogue", "method": "gift", "encounter_rate": 100, "min_level": 10, "max_level": 10},
{"pokeapi_id": 236, "pokemon_name": "tyrogue", "method": "gift", "encounter_rate": 100, "min_level": 10, "max_level": 10},
],
"Dragons Den": [
{"national_dex": 147, "pokemon_name": "dratini", "method": "gift", "encounter_rate": 100, "min_level": 15, "max_level": 15},
{"pokeapi_id": 147, "pokemon_name": "dratini", "method": "gift", "encounter_rate": 100, "min_level": 15, "max_level": 15},
],
"Pewter City": [
{"national_dex": 138, "pokemon_name": "omanyte", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
{"national_dex": 140, "pokemon_name": "kabuto", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
{"national_dex": 142, "pokemon_name": "aerodactyl", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
{"national_dex": 345, "pokemon_name": "lileep", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
{"national_dex": 347, "pokemon_name": "anorith", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
{"national_dex": 408, "pokemon_name": "cranidos", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
{"national_dex": 410, "pokemon_name": "shieldon", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
{"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": [
{"national_dex": 252, "pokemon_name": "treecko", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"national_dex": 255, "pokemon_name": "torchic", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"national_dex": 258, "pokemon_name": "mudkip", "method": "starter", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"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": [
{"national_dex": 351, "pokemon_name": "castform", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
{"pokeapi_id": 351, "pokemon_name": "castform", "method": "gift", "encounter_rate": 100, "min_level": 25, "max_level": 25},
],
"Lavaridge Town": [
{"national_dex": 360, "pokemon_name": "wynaut", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"pokeapi_id": 360, "pokemon_name": "wynaut", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
],
"Mossdeep City": [
{"national_dex": 374, "pokemon_name": "beldum", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
{"pokeapi_id": 374, "pokemon_name": "beldum", "method": "gift", "encounter_rate": 100, "min_level": 5, "max_level": 5},
],
"Rustboro City": [
{"national_dex": 345, "pokemon_name": "lileep", "method": "fossil", "encounter_rate": 100, "min_level": 20, "max_level": 20},
{"national_dex": 347, "pokemon_name": "anorith", "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},
],
},
}