Add all Gen 1-9 games with colors to seed data

- Add 37 games from Gen 1-9 (Red/Blue through Scarlet/Violet)
- Add color field to Game model matching box art/branding
- Add migration for games.color column
- Update fetch_pokeapi.py to fetch all games and output colors
- Update seed loader to upsert game colors
- Update frontend Game type to include color field

Games without PokeAPI encounter data (ORAS, Let's Go, Sword/Shield,
BDSP, Legends Arceus, Scarlet/Violet) have location structure but
empty encounter tables.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Julian Tabel
2026-02-06 11:46:10 +01:00
parent 5406e5a386
commit f7f5417b6b
41 changed files with 130695 additions and 32 deletions

View File

@@ -10,13 +10,39 @@ parent: nuzlocke-tracker-f5ob
Routes are currently in alphabetical order from PokeAPI. Update the order field in each game's JSON seed file to reflect actual game progression (e.g., Pallet Town → Route 1 → Viridian City → Route 2 → ...).
## Details
- 646 routes total across 5 games
## Automated Approach: Bulbapedia Walkthrough Scraping
We already have all location/encounter data from PokeAPI. The missing piece is progression order, which can be extracted from Bulbapedia walkthroughs.
### Data Source
Every game has a Bulbapedia walkthrough with routes listed in progression order:
- https://bulbapedia.bulbagarden.net/wiki/Walkthrough:Pokémon_FireRed_and_LeafGreen
- https://bulbapedia.bulbagarden.net/wiki/Walkthrough:Pokémon_Emerald
- https://bulbapedia.bulbagarden.net/wiki/Walkthrough:Pokémon_HeartGold_and_SoulSilver
- (and so on for all games through Gen 8)
### Implementation Plan
1. **Scrape walkthrough TOCs** - Parse the section headings from each game's walkthrough page to get route order
2. **Normalize names** - Map Bulbapedia location names to PokeAPI location names (handle differences like "Route 1" vs "Kanto Route 1")
3. **Generate ordering** - Create a JSON mapping of `{game: {location_name: order_number}}`
4. **Update fetch_pokeapi.py** - Apply ordering when generating seed data
### Benefits
- Automatable for all games (Gen 1-8)
- Bulbapedia walkthroughs are community-maintained and accurate
- Scales as we add more games
- Only needs to run once per game (or when walkthroughs update)
### Considerations
- Gen 9 (Scarlet/Violet) is open-world so ordering is less meaningful
- Some games have branching paths - may need to pick a canonical order
- Name matching between Bulbapedia and PokeAPI may need fuzzy matching
## Current Scope
- FireRed and LeafGreen share the same route progression (Kanto)
- HeartGold and SoulSilver share the same route progression (Johto + Kanto)
- Emerald has its own progression (Hoenn)
- So effectively 3 unique orderings to define
- After updating JSON files, re-run the seed: `podman compose exec -e PYTHONUNBUFFERED=1 -w /app/src api python -m app.seeds`
- So effectively 3 unique orderings to define for current games
## Files
- `backend/src/app/seeds/data/firered.json`

View File

@@ -0,0 +1,29 @@
"""add color field to games
Revision ID: d4e5f6a7b8c9
Revises: c3d4e5f6a7b8
Create Date: 2026-02-06 14:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'd4e5f6a7b8c9'
down_revision: Union[str, Sequence[str], None] = 'c3d4e5f6a7b8'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.add_column(
'games',
sa.Column('color', sa.String(7), nullable=True),
)
def downgrade() -> None:
op.drop_column('games', 'color')

View File

@@ -14,6 +14,7 @@ class Game(Base):
region: Mapped[str] = mapped_column(String(50))
box_art_url: Mapped[str | None] = mapped_column(String(500))
release_year: Mapped[int | None] = mapped_column(SmallInteger)
color: Mapped[str | None] = mapped_column(String(7)) # Hex color e.g. #FF0000
routes: Mapped[list["Route"]] = relationship(back_populates="game")
runs: Mapped[list["NuzlockeRun"]] = relationship(back_populates="game")

View File

@@ -17,6 +17,7 @@ class GameResponse(CamelModel):
region: str
box_art_url: str | None
release_year: int | None
color: str | None
class RouteWithChildrenResponse(RouteResponse):
@@ -37,6 +38,7 @@ class GameCreate(CamelModel):
region: str
box_art_url: str | None = None
release_year: int | None = None
color: str | None = None
class GameUpdate(CamelModel):
@@ -46,6 +48,7 @@ class GameUpdate(CamelModel):
region: str | None = None
box_art_url: str | None = None
release_year: int | None = None
color: str | None = None
class RouteCreate(CamelModel):

View File

@@ -0,0 +1 @@
[]

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

@@ -0,0 +1 @@
[]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,37 +1,298 @@
[
{
"name": "Pokemon FireRed",
"slug": "firered",
"generation": 3,
"name": "Pokemon Red",
"slug": "red",
"generation": 1,
"region": "kanto",
"release_year": 2004
"release_year": 1996,
"color": "#FF1111"
},
{
"name": "Pokemon LeafGreen",
"slug": "leafgreen",
"generation": 3,
"name": "Pokemon Blue",
"slug": "blue",
"generation": 1,
"region": "kanto",
"release_year": 2004
"release_year": 1996,
"color": "#1111FF"
},
{
"name": "Pokemon Yellow",
"slug": "yellow",
"generation": 1,
"region": "kanto",
"release_year": 1998,
"color": "#FFD733"
},
{
"name": "Pokemon Gold",
"slug": "gold",
"generation": 2,
"region": "johto",
"release_year": 1999,
"color": "#DAA520"
},
{
"name": "Pokemon Silver",
"slug": "silver",
"generation": 2,
"region": "johto",
"release_year": 1999,
"color": "#C0C0C0"
},
{
"name": "Pokemon Crystal",
"slug": "crystal",
"generation": 2,
"region": "johto",
"release_year": 2000,
"color": "#4FD9FF"
},
{
"name": "Pokemon Ruby",
"slug": "ruby",
"generation": 3,
"region": "hoenn",
"release_year": 2002,
"color": "#A00000"
},
{
"name": "Pokemon Sapphire",
"slug": "sapphire",
"generation": 3,
"region": "hoenn",
"release_year": 2002,
"color": "#0000A0"
},
{
"name": "Pokemon Emerald",
"slug": "emerald",
"generation": 3,
"region": "hoenn",
"release_year": 2005
"release_year": 2005,
"color": "#00A000"
},
{
"name": "Pokemon FireRed",
"slug": "firered",
"generation": 3,
"region": "kanto",
"release_year": 2004,
"color": "#FF7327"
},
{
"name": "Pokemon LeafGreen",
"slug": "leafgreen",
"generation": 3,
"region": "kanto",
"release_year": 2004,
"color": "#00DD00"
},
{
"name": "Pokemon Diamond",
"slug": "diamond",
"generation": 4,
"region": "sinnoh",
"release_year": 2006,
"color": "#AAAAFF"
},
{
"name": "Pokemon Pearl",
"slug": "pearl",
"generation": 4,
"region": "sinnoh",
"release_year": 2006,
"color": "#FFAAAA"
},
{
"name": "Pokemon Platinum",
"slug": "platinum",
"generation": 4,
"region": "sinnoh",
"release_year": 2008,
"color": "#999999"
},
{
"name": "Pokemon HeartGold",
"slug": "heartgold",
"generation": 4,
"region": "johto",
"release_year": 2010
"release_year": 2010,
"color": "#B69E00"
},
{
"name": "Pokemon SoulSilver",
"slug": "soulsilver",
"generation": 4,
"region": "johto",
"release_year": 2010
"release_year": 2010,
"color": "#C0C0E0"
},
{
"name": "Pokemon Black",
"slug": "black",
"generation": 5,
"region": "unova",
"release_year": 2010,
"color": "#444444"
},
{
"name": "Pokemon White",
"slug": "white",
"generation": 5,
"region": "unova",
"release_year": 2010,
"color": "#E1E1E1"
},
{
"name": "Pokemon Black 2",
"slug": "black-2",
"generation": 5,
"region": "unova",
"release_year": 2012,
"color": "#424B50"
},
{
"name": "Pokemon White 2",
"slug": "white-2",
"generation": 5,
"region": "unova",
"release_year": 2012,
"color": "#E3CED0"
},
{
"name": "Pokemon X",
"slug": "x",
"generation": 6,
"region": "kalos",
"release_year": 2013,
"color": "#025DA6"
},
{
"name": "Pokemon Y",
"slug": "y",
"generation": 6,
"region": "kalos",
"release_year": 2013,
"color": "#EA1A3E"
},
{
"name": "Pokemon Omega Ruby",
"slug": "omega-ruby",
"generation": 6,
"region": "hoenn",
"release_year": 2014,
"color": "#CF3025"
},
{
"name": "Pokemon Alpha Sapphire",
"slug": "alpha-sapphire",
"generation": 6,
"region": "hoenn",
"release_year": 2014,
"color": "#26649C"
},
{
"name": "Pokemon Sun",
"slug": "sun",
"generation": 7,
"region": "alola",
"release_year": 2016,
"color": "#F1912B"
},
{
"name": "Pokemon Moon",
"slug": "moon",
"generation": 7,
"region": "alola",
"release_year": 2016,
"color": "#5599CA"
},
{
"name": "Pokemon Ultra Sun",
"slug": "ultra-sun",
"generation": 7,
"region": "alola",
"release_year": 2017,
"color": "#E95B2B"
},
{
"name": "Pokemon Ultra Moon",
"slug": "ultra-moon",
"generation": 7,
"region": "alola",
"release_year": 2017,
"color": "#204E8C"
},
{
"name": "Pokemon Let's Go Pikachu",
"slug": "lets-go-pikachu",
"generation": 7,
"region": "kanto",
"release_year": 2018,
"color": "#F5DA00"
},
{
"name": "Pokemon Let's Go Eevee",
"slug": "lets-go-eevee",
"generation": 7,
"region": "kanto",
"release_year": 2018,
"color": "#D4924B"
},
{
"name": "Pokemon Sword",
"slug": "sword",
"generation": 8,
"region": "galar",
"release_year": 2019,
"color": "#00D4E7"
},
{
"name": "Pokemon Shield",
"slug": "shield",
"generation": 8,
"region": "galar",
"release_year": 2019,
"color": "#EF3B6E"
},
{
"name": "Pokemon Brilliant Diamond",
"slug": "brilliant-diamond",
"generation": 8,
"region": "sinnoh",
"release_year": 2021,
"color": "#44BAE5"
},
{
"name": "Pokemon Shining Pearl",
"slug": "shining-pearl",
"generation": 8,
"region": "sinnoh",
"release_year": 2021,
"color": "#E18AAA"
},
{
"name": "Pokemon Legends: Arceus",
"slug": "legends-arceus",
"generation": 8,
"region": "hisui",
"release_year": 2022,
"color": "#36597B"
},
{
"name": "Pokemon Scarlet",
"slug": "scarlet",
"generation": 9,
"region": "paldea",
"release_year": 2022,
"color": "#F93C3C"
},
{
"name": "Pokemon Violet",
"slug": "violet",
"generation": 9,
"region": "paldea",
"release_year": 2022,
"color": "#A96EEC"
}
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1 @@
[]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
[]

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

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1 @@
[]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
[]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
[]

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

@@ -32,23 +32,98 @@ def extract_id(url: str) -> int:
return int(url.rstrip("/").split("/")[-1])
# Game definitions
# Game definitions - Gen 1 through Gen 8
VERSION_GROUPS = {
"firered-leafgreen": {
"versions": ["firered", "leafgreen"],
"generation": 3,
# === Generation 1 ===
"red-blue": {
"versions": ["red", "blue"],
"generation": 1,
"region": "kanto",
"region_id": 1,
"games": {
"firered": {
"name": "Pokemon FireRed",
"slug": "firered",
"release_year": 2004,
"red": {
"name": "Pokemon Red",
"slug": "red",
"release_year": 1996,
"color": "#FF1111",
},
"leafgreen": {
"name": "Pokemon LeafGreen",
"slug": "leafgreen",
"release_year": 2004,
"blue": {
"name": "Pokemon Blue",
"slug": "blue",
"release_year": 1996,
"color": "#1111FF",
},
},
},
"yellow": {
"versions": ["yellow"],
"generation": 1,
"region": "kanto",
"region_id": 1,
"games": {
"yellow": {
"name": "Pokemon Yellow",
"slug": "yellow",
"release_year": 1998,
"color": "#FFD733",
},
},
},
# === Generation 2 ===
"gold-silver": {
"versions": ["gold", "silver"],
"generation": 2,
"region": "johto",
"region_id": 2,
"extra_regions": [1], # Kanto post-game
"games": {
"gold": {
"name": "Pokemon Gold",
"slug": "gold",
"release_year": 1999,
"color": "#DAA520",
},
"silver": {
"name": "Pokemon Silver",
"slug": "silver",
"release_year": 1999,
"color": "#C0C0C0",
},
},
},
"crystal": {
"versions": ["crystal"],
"generation": 2,
"region": "johto",
"region_id": 2,
"extra_regions": [1], # Kanto post-game
"games": {
"crystal": {
"name": "Pokemon Crystal",
"slug": "crystal",
"release_year": 2000,
"color": "#4FD9FF",
},
},
},
# === Generation 3 ===
"ruby-sapphire": {
"versions": ["ruby", "sapphire"],
"generation": 3,
"region": "hoenn",
"region_id": 3,
"games": {
"ruby": {
"name": "Pokemon Ruby",
"slug": "ruby",
"release_year": 2002,
"color": "#A00000",
},
"sapphire": {
"name": "Pokemon Sapphire",
"slug": "sapphire",
"release_year": 2002,
"color": "#0000A0",
},
},
},
@@ -62,6 +137,62 @@ VERSION_GROUPS = {
"name": "Pokemon Emerald",
"slug": "emerald",
"release_year": 2005,
"color": "#00A000",
},
},
},
"firered-leafgreen": {
"versions": ["firered", "leafgreen"],
"generation": 3,
"region": "kanto",
"region_id": 1,
"games": {
"firered": {
"name": "Pokemon FireRed",
"slug": "firered",
"release_year": 2004,
"color": "#FF7327",
},
"leafgreen": {
"name": "Pokemon LeafGreen",
"slug": "leafgreen",
"release_year": 2004,
"color": "#00DD00",
},
},
},
# === Generation 4 ===
"diamond-pearl": {
"versions": ["diamond", "pearl"],
"generation": 4,
"region": "sinnoh",
"region_id": 4,
"games": {
"diamond": {
"name": "Pokemon Diamond",
"slug": "diamond",
"release_year": 2006,
"color": "#AAAAFF",
},
"pearl": {
"name": "Pokemon Pearl",
"slug": "pearl",
"release_year": 2006,
"color": "#FFAAAA",
},
},
},
"platinum": {
"versions": ["platinum"],
"generation": 4,
"region": "sinnoh",
"region_id": 4,
"games": {
"platinum": {
"name": "Pokemon Platinum",
"slug": "platinum",
"release_year": 2008,
"color": "#999999",
},
},
},
@@ -70,16 +201,238 @@ VERSION_GROUPS = {
"generation": 4,
"region": "johto",
"region_id": 2,
"extra_regions": [1], # Kanto post-game
"games": {
"heartgold": {
"name": "Pokemon HeartGold",
"slug": "heartgold",
"release_year": 2010,
"color": "#B69E00",
},
"soulsilver": {
"name": "Pokemon SoulSilver",
"slug": "soulsilver",
"release_year": 2010,
"color": "#C0C0E0",
},
},
},
# === Generation 5 ===
"black-white": {
"versions": ["black", "white"],
"generation": 5,
"region": "unova",
"region_id": 5,
"games": {
"black": {
"name": "Pokemon Black",
"slug": "black",
"release_year": 2010,
"color": "#444444",
},
"white": {
"name": "Pokemon White",
"slug": "white",
"release_year": 2010,
"color": "#E1E1E1",
},
},
},
"black-2-white-2": {
"versions": ["black-2", "white-2"],
"generation": 5,
"region": "unova",
"region_id": 5,
"games": {
"black-2": {
"name": "Pokemon Black 2",
"slug": "black-2",
"release_year": 2012,
"color": "#424B50",
},
"white-2": {
"name": "Pokemon White 2",
"slug": "white-2",
"release_year": 2012,
"color": "#E3CED0",
},
},
},
# === Generation 6 ===
"x-y": {
"versions": ["x", "y"],
"generation": 6,
"region": "kalos",
"region_id": 6,
"games": {
"x": {
"name": "Pokemon X",
"slug": "x",
"release_year": 2013,
"color": "#025DA6",
},
"y": {
"name": "Pokemon Y",
"slug": "y",
"release_year": 2013,
"color": "#EA1A3E",
},
},
},
"omega-ruby-alpha-sapphire": {
"versions": ["omega-ruby", "alpha-sapphire"],
"generation": 6,
"region": "hoenn",
"region_id": 3,
"games": {
"omega-ruby": {
"name": "Pokemon Omega Ruby",
"slug": "omega-ruby",
"release_year": 2014,
"color": "#CF3025",
},
"alpha-sapphire": {
"name": "Pokemon Alpha Sapphire",
"slug": "alpha-sapphire",
"release_year": 2014,
"color": "#26649C",
},
},
},
# === Generation 7 ===
"sun-moon": {
"versions": ["sun", "moon"],
"generation": 7,
"region": "alola",
"region_id": 7,
"games": {
"sun": {
"name": "Pokemon Sun",
"slug": "sun",
"release_year": 2016,
"color": "#F1912B",
},
"moon": {
"name": "Pokemon Moon",
"slug": "moon",
"release_year": 2016,
"color": "#5599CA",
},
},
},
"ultra-sun-ultra-moon": {
"versions": ["ultra-sun", "ultra-moon"],
"generation": 7,
"region": "alola",
"region_id": 7,
"games": {
"ultra-sun": {
"name": "Pokemon Ultra Sun",
"slug": "ultra-sun",
"release_year": 2017,
"color": "#E95B2B",
},
"ultra-moon": {
"name": "Pokemon Ultra Moon",
"slug": "ultra-moon",
"release_year": 2017,
"color": "#204E8C",
},
},
},
"lets-go": {
"versions": ["lets-go-pikachu", "lets-go-eevee"],
"generation": 7,
"region": "kanto",
"region_id": 1,
"games": {
"lets-go-pikachu": {
"name": "Pokemon Let's Go Pikachu",
"slug": "lets-go-pikachu",
"release_year": 2018,
"color": "#F5DA00",
},
"lets-go-eevee": {
"name": "Pokemon Let's Go Eevee",
"slug": "lets-go-eevee",
"release_year": 2018,
"color": "#D4924B",
},
},
},
# === Generation 8 ===
"sword-shield": {
"versions": ["sword", "shield"],
"generation": 8,
"region": "galar",
"region_id": 8,
"games": {
"sword": {
"name": "Pokemon Sword",
"slug": "sword",
"release_year": 2019,
"color": "#00D4E7",
},
"shield": {
"name": "Pokemon Shield",
"slug": "shield",
"release_year": 2019,
"color": "#EF3B6E",
},
},
},
"brilliant-diamond-shining-pearl": {
"versions": ["brilliant-diamond", "shining-pearl"],
"generation": 8,
"region": "sinnoh",
"region_id": 4,
"games": {
"brilliant-diamond": {
"name": "Pokemon Brilliant Diamond",
"slug": "brilliant-diamond",
"release_year": 2021,
"color": "#44BAE5",
},
"shining-pearl": {
"name": "Pokemon Shining Pearl",
"slug": "shining-pearl",
"release_year": 2021,
"color": "#E18AAA",
},
},
},
"legends-arceus": {
"versions": ["legends-arceus"],
"generation": 8,
"region": "hisui",
"region_id": 9,
"games": {
"legends-arceus": {
"name": "Pokemon Legends: Arceus",
"slug": "legends-arceus",
"release_year": 2022,
"color": "#36597B",
},
},
},
# === Generation 9 ===
"scarlet-violet": {
"versions": ["scarlet", "violet"],
"generation": 9,
"region": "paldea",
"region_id": 10,
"games": {
"scarlet": {
"name": "Pokemon Scarlet",
"slug": "scarlet",
"release_year": 2022,
"color": "#F93C3C",
},
"violet": {
"name": "Pokemon Violet",
"slug": "violet",
"release_year": 2022,
"color": "#A96EEC",
},
},
},
@@ -197,10 +550,10 @@ def process_version(version_name: str, vg_info: dict) -> list[dict]:
region = load_resource("region", vg_info["region_id"])
location_refs = list(region["locations"])
# For HGSS, also include Kanto locations
if version_name in ("heartgold", "soulsilver"):
kanto = load_resource("region", 1)
location_refs = location_refs + list(kanto["locations"])
# Include extra regions (e.g., Kanto for Johto games)
for extra_region_id in vg_info.get("extra_regions", []):
extra_region = load_resource("region", extra_region_id)
location_refs = location_refs + list(extra_region["locations"])
print(f" Found {len(location_refs)} locations")
@@ -510,6 +863,7 @@ def main():
"generation": vg_info["generation"],
"region": vg_info["region"],
"release_year": game_info["release_year"],
"color": game_info.get("color"),
})
write_json("games.json", games)

View File

@@ -20,6 +20,7 @@ async def upsert_games(session: AsyncSession, games: list[dict]) -> dict[str, in
generation=game["generation"],
region=game["region"],
release_year=game.get("release_year"),
color=game.get("color"),
).on_conflict_do_update(
index_elements=["slug"],
set_={
@@ -27,6 +28,7 @@ async def upsert_games(session: AsyncSession, games: list[dict]) -> dict[str, in
"generation": game["generation"],
"region": game["region"],
"release_year": game.get("release_year"),
"color": game.get("color"),
},
)
await session.execute(stmt)

View File

@@ -21,7 +21,27 @@ from app.seeds.loader import (
DATA_DIR = Path(__file__).parent / "data"
GAME_FILES = ["firered", "leafgreen", "emerald", "heartgold", "soulsilver"]
# All Gen 1-9 games
GAME_FILES = [
# Gen 1
"red", "blue", "yellow",
# Gen 2
"gold", "silver", "crystal",
# Gen 3
"ruby", "sapphire", "emerald", "firered", "leafgreen",
# Gen 4
"diamond", "pearl", "platinum", "heartgold", "soulsilver",
# Gen 5
"black", "white", "black-2", "white-2",
# Gen 6
"x", "y", "omega-ruby", "alpha-sapphire",
# Gen 7
"sun", "moon", "ultra-sun", "ultra-moon", "lets-go-pikachu", "lets-go-eevee",
# Gen 8
"sword", "shield", "brilliant-diamond", "shining-pearl", "legends-arceus",
# Gen 9
"scarlet", "violet",
]
def load_json(filename: str) -> list[dict]:

View File

@@ -6,6 +6,7 @@ export interface Game {
region: string
boxArtUrl: string | null
releaseYear: number | null
color: string | null
}
export interface Route {