From aaaeb2146e438dcc40c60240fb26e6745e09831d Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Mon, 9 Feb 2026 09:06:15 +0100 Subject: [PATCH] Add game category and region metadata for genlocke presets Add `category` field (original/remake/enhanced/sequel/spinoff) to the Game model and tag all 38 games. Create regions.json with generation mapping, ordering, and genlocke preset defaults per region. Add GET /games/by-region endpoint returning games grouped by region. Co-Authored-By: Claude Opus 4.6 --- ...ather-generation-metadata-games-regions.md | 24 ++++-- .../a1b2c3d4e5f8_add_category_to_games.py | 29 +++++++ backend/src/app/api/games.py | 37 +++++++++ backend/src/app/models/game.py | 1 + backend/src/app/schemas/game.py | 16 ++++ backend/src/app/seeds/data/games.json | 38 +++++++++ backend/src/app/seeds/data/regions.json | 83 +++++++++++++++++++ backend/src/app/seeds/loader.py | 2 + frontend/src/types/game.ts | 16 ++++ 9 files changed, 237 insertions(+), 9 deletions(-) create mode 100644 backend/src/app/alembic/versions/a1b2c3d4e5f8_add_category_to_games.py create mode 100644 backend/src/app/seeds/data/regions.json diff --git a/.beans/nuzlocke-tracker-glh8--gather-generation-metadata-games-regions.md b/.beans/nuzlocke-tracker-glh8--gather-generation-metadata-games-regions.md index 7611959..80227af 100644 --- a/.beans/nuzlocke-tracker-glh8--gather-generation-metadata-games-regions.md +++ b/.beans/nuzlocke-tracker-glh8--gather-generation-metadata-games-regions.md @@ -1,11 +1,11 @@ --- # nuzlocke-tracker-glh8 title: Gather generation metadata (games, regions) -status: todo +status: in-progress type: task priority: normal created_at: 2026-02-08T19:20:49Z -updated_at: 2026-02-09T07:45:21Z +updated_at: 2026-02-09T08:05:52Z parent: nuzlocke-tracker-25mh blocking: - nuzlocke-tracker-kz5g @@ -27,11 +27,17 @@ Collect and store metadata about each Pokemon generation to support genlocke fea - However, a dedicated generations reference would be useful for UI purposes (showing all generations even if not all games are seeded yet) - Check if `backend/src/app/seeds/data/generations.json` already exists or if this needs to be created from scratch +## Decisions +- Legends games (Hisui, Lumiose) are excluded from genlocke presets — available via Custom only +- Black 2/White 2 are grouped with Black/White in the same Unova slot (category: sequel) +- Normal Genlocke defaults use best mainline remake: FireRed, HeartGold, Alpha Sapphire, Platinum, Ultra Sun +- Metadata stored as `category` field on Game model + standalone `regions.json` seed file + ## Checklist -- [ ] Define the generation-to-region mapping (Gen 1 = Kanto, Gen 2 = Johto, ..., Gen 9 = Paldea) -- [ ] Determine how to group games by region (use `region` field on existing Game model, or create a dedicated lookup) -- [ ] Create a `generations.json` seed file (or equivalent) with: generation number, region name, region order, and which games belong to each region -- [ ] Categorize each game as "original", "remake", or "enhanced" so presets can filter appropriately -- [ ] Define which game is the "default" pick per region for the Normal Genlocke preset (e.g., FireRed for Kanto, HeartGold for Johto) -- [ ] Add an API endpoint or extend the games endpoint to return games grouped by region with generation metadata -- [ ] Verify all seeded games are correctly tagged with their region \ No newline at end of file +- [x] Define the generation-to-region mapping (Gen 1 = Kanto, Gen 2 = Johto, ..., Gen 9 = Paldea) +- [x] Determine how to group games by region (use `region` field on existing Game model, or create a dedicated lookup) +- [x] Create a `regions.json` seed file (or equivalent) with: generation number, region name, region order, and which games belong to each region +- [x] Categorize each game as "original", "remake", "enhanced", "sequel", or "spinoff" so presets can filter appropriately +- [x] Define which game is the "default" pick per region for the Normal Genlocke preset (e.g., FireRed for Kanto, HeartGold for Johto) +- [x] Add an API endpoint or extend the games endpoint to return games grouped by region with generation metadata +- [x] Verify all seeded games are correctly tagged with their region \ No newline at end of file diff --git a/backend/src/app/alembic/versions/a1b2c3d4e5f8_add_category_to_games.py b/backend/src/app/alembic/versions/a1b2c3d4e5f8_add_category_to_games.py new file mode 100644 index 0000000..17d6b5b --- /dev/null +++ b/backend/src/app/alembic/versions/a1b2c3d4e5f8_add_category_to_games.py @@ -0,0 +1,29 @@ +"""add category to games + +Revision ID: a1b2c3d4e5f8 +Revises: f6a7b8c9d0e1 +Create Date: 2026-02-09 12:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a1b2c3d4e5f8' +down_revision: Union[str, Sequence[str], None] = 'f6a7b8c9d0e1' +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('category', sa.String(20), nullable=True), + ) + + +def downgrade() -> None: + op.drop_column('games', 'category') diff --git a/backend/src/app/api/games.py b/backend/src/app/api/games.py index eacbe30..eddcf18 100644 --- a/backend/src/app/api/games.py +++ b/backend/src/app/api/games.py @@ -1,3 +1,6 @@ +import json +from pathlib import Path + from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import delete, select from sqlalchemy.ext.asyncio import AsyncSession @@ -14,6 +17,7 @@ from app.schemas.game import ( GameDetailResponse, GameResponse, GameUpdate, + RegionResponse, RouteCreate, RouteReorderRequest, RouteResponse, @@ -46,6 +50,38 @@ async def list_games(session: AsyncSession = Depends(get_session)): return result.scalars().all() +@router.get("/by-region", response_model=list[RegionResponse]) +async def list_games_by_region(session: AsyncSession = Depends(get_session)): + """Return games grouped by region with generation metadata and genlocke preset defaults.""" + regions_path = Path(__file__).parent.parent / "seeds" / "data" / "regions.json" + with open(regions_path) as f: + regions_data = json.load(f) + + result = await session.execute(select(Game).order_by(Game.release_year, Game.name)) + all_games = result.scalars().all() + + games_by_region: dict[str, list[Game]] = {} + for game in all_games: + games_by_region.setdefault(game.region, []).append(game) + + response = [] + for region in regions_data: + region_games = games_by_region.get(region["name"], []) + defaults = region["genlocke_defaults"] + response.append({ + "name": region["name"], + "generation": region["generation"], + "order": region["order"], + "genlocke_defaults": { + "true_genlocke": defaults["true"], + "normal_genlocke": defaults["normal"], + }, + "games": region_games, + }) + + return response + + @router.get("/{game_id}", response_model=GameDetailResponse) async def get_game(game_id: int, session: AsyncSession = Depends(get_session)): game = await _get_game_or_404(session, game_id) @@ -66,6 +102,7 @@ async def get_game(game_id: int, session: AsyncSession = Depends(get_session)): "slug": game.slug, "generation": game.generation, "region": game.region, + "category": game.category, "box_art_url": game.box_art_url, "release_year": game.release_year, "color": game.color, diff --git a/backend/src/app/models/game.py b/backend/src/app/models/game.py index a137d05..f25536f 100644 --- a/backend/src/app/models/game.py +++ b/backend/src/app/models/game.py @@ -12,6 +12,7 @@ class Game(Base): slug: Mapped[str] = mapped_column(String(100), unique=True) generation: Mapped[int] = mapped_column(SmallInteger) region: Mapped[str] = mapped_column(String(50)) + category: Mapped[str | None] = mapped_column(String(20)) # original, remake, enhanced, sequel, spinoff 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 diff --git a/backend/src/app/schemas/game.py b/backend/src/app/schemas/game.py index ab12974..c3442cf 100644 --- a/backend/src/app/schemas/game.py +++ b/backend/src/app/schemas/game.py @@ -17,6 +17,7 @@ class GameResponse(CamelModel): slug: str generation: int region: str + category: str | None = None box_art_url: str | None release_year: int | None color: str | None @@ -39,6 +40,7 @@ class GameCreate(CamelModel): slug: str generation: int region: str + category: str | None = None box_art_url: str | None = None release_year: int | None = None color: str | None = None @@ -49,11 +51,25 @@ class GameUpdate(CamelModel): slug: str | None = None generation: int | None = None region: str | None = None + category: str | None = None box_art_url: str | None = None release_year: int | None = None color: str | None = None +class GenlockeDefaultsResponse(CamelModel): + true_genlocke: str # game slug for true genlocke default + normal_genlocke: str # game slug for normal genlocke default + + +class RegionResponse(CamelModel): + name: str + generation: int + order: int + genlocke_defaults: GenlockeDefaultsResponse + games: list[GameResponse] = [] + + class RouteCreate(CamelModel): name: str order: int diff --git a/backend/src/app/seeds/data/games.json b/backend/src/app/seeds/data/games.json index 8881d5e..c1abe45 100644 --- a/backend/src/app/seeds/data/games.json +++ b/backend/src/app/seeds/data/games.json @@ -4,6 +4,7 @@ "slug": "alpha-sapphire", "generation": 6, "region": "hoenn", + "category": "remake", "release_year": 2014, "color": "#26649C" }, @@ -12,6 +13,7 @@ "slug": "black", "generation": 5, "region": "unova", + "category": "original", "release_year": 2010, "color": "#444444" }, @@ -20,6 +22,7 @@ "slug": "black-2", "generation": 5, "region": "unova", + "category": "sequel", "release_year": 2012, "color": "#424B50" }, @@ -28,6 +31,7 @@ "slug": "blue", "generation": 1, "region": "kanto", + "category": "original", "release_year": 1996, "color": "#1111FF" }, @@ -36,6 +40,7 @@ "slug": "brilliant-diamond", "generation": 8, "region": "sinnoh", + "category": "remake", "release_year": 2021, "color": "#44BAE5" }, @@ -44,6 +49,7 @@ "slug": "crystal", "generation": 2, "region": "johto", + "category": "enhanced", "release_year": 2000, "color": "#4FD9FF" }, @@ -52,6 +58,7 @@ "slug": "diamond", "generation": 4, "region": "sinnoh", + "category": "original", "release_year": 2006, "color": "#AAAAFF" }, @@ -60,6 +67,7 @@ "slug": "emerald", "generation": 3, "region": "hoenn", + "category": "enhanced", "release_year": 2005, "color": "#00A000" }, @@ -68,6 +76,7 @@ "slug": "firered", "generation": 3, "region": "kanto", + "category": "remake", "release_year": 2004, "color": "#FF7327" }, @@ -76,6 +85,7 @@ "slug": "gold", "generation": 2, "region": "johto", + "category": "original", "release_year": 1999, "color": "#DAA520" }, @@ -84,6 +94,7 @@ "slug": "heartgold", "generation": 4, "region": "johto", + "category": "remake", "release_year": 2010, "color": "#B69E00" }, @@ -92,6 +103,7 @@ "slug": "leafgreen", "generation": 3, "region": "kanto", + "category": "remake", "release_year": 2004, "color": "#00DD00" }, @@ -100,6 +112,7 @@ "slug": "legends-arceus", "generation": 8, "region": "hisui", + "category": "spinoff", "release_year": 2022, "color": "#36597B" }, @@ -108,6 +121,7 @@ "slug": "legends-z-a", "generation": 9, "region": "lumiose", + "category": "spinoff", "release_year": 2025, "color": "#3A7BDB" }, @@ -116,6 +130,7 @@ "slug": "lets-go-eevee", "generation": 7, "region": "kanto", + "category": "remake", "release_year": 2018, "color": "#D4924B" }, @@ -124,6 +139,7 @@ "slug": "lets-go-pikachu", "generation": 7, "region": "kanto", + "category": "remake", "release_year": 2018, "color": "#F5DA00" }, @@ -132,6 +148,7 @@ "slug": "moon", "generation": 7, "region": "alola", + "category": "original", "release_year": 2016, "color": "#5599CA" }, @@ -140,6 +157,7 @@ "slug": "omega-ruby", "generation": 6, "region": "hoenn", + "category": "remake", "release_year": 2014, "color": "#CF3025" }, @@ -148,6 +166,7 @@ "slug": "pearl", "generation": 4, "region": "sinnoh", + "category": "original", "release_year": 2006, "color": "#FFAAAA" }, @@ -156,6 +175,7 @@ "slug": "platinum", "generation": 4, "region": "sinnoh", + "category": "enhanced", "release_year": 2008, "color": "#999999" }, @@ -164,6 +184,7 @@ "slug": "red", "generation": 1, "region": "kanto", + "category": "original", "release_year": 1996, "color": "#FF1111" }, @@ -172,6 +193,7 @@ "slug": "ruby", "generation": 3, "region": "hoenn", + "category": "original", "release_year": 2002, "color": "#A00000" }, @@ -180,6 +202,7 @@ "slug": "sapphire", "generation": 3, "region": "hoenn", + "category": "original", "release_year": 2002, "color": "#0000A0" }, @@ -188,6 +211,7 @@ "slug": "scarlet", "generation": 9, "region": "paldea", + "category": "original", "release_year": 2022, "color": "#F93C3C" }, @@ -196,6 +220,7 @@ "slug": "shield", "generation": 8, "region": "galar", + "category": "original", "release_year": 2019, "color": "#EF3B6E" }, @@ -204,6 +229,7 @@ "slug": "shining-pearl", "generation": 8, "region": "sinnoh", + "category": "remake", "release_year": 2021, "color": "#E18AAA" }, @@ -212,6 +238,7 @@ "slug": "silver", "generation": 2, "region": "johto", + "category": "original", "release_year": 1999, "color": "#C0C0C0" }, @@ -220,6 +247,7 @@ "slug": "soulsilver", "generation": 4, "region": "johto", + "category": "remake", "release_year": 2010, "color": "#C0C0E0" }, @@ -228,6 +256,7 @@ "slug": "sun", "generation": 7, "region": "alola", + "category": "original", "release_year": 2016, "color": "#F1912B" }, @@ -236,6 +265,7 @@ "slug": "sword", "generation": 8, "region": "galar", + "category": "original", "release_year": 2019, "color": "#00D4E7" }, @@ -244,6 +274,7 @@ "slug": "ultra-moon", "generation": 7, "region": "alola", + "category": "enhanced", "release_year": 2017, "color": "#204E8C" }, @@ -252,6 +283,7 @@ "slug": "ultra-sun", "generation": 7, "region": "alola", + "category": "enhanced", "release_year": 2017, "color": "#E95B2B" }, @@ -260,6 +292,7 @@ "slug": "violet", "generation": 9, "region": "paldea", + "category": "original", "release_year": 2022, "color": "#A96EEC" }, @@ -268,6 +301,7 @@ "slug": "white", "generation": 5, "region": "unova", + "category": "original", "release_year": 2010, "color": "#E1E1E1" }, @@ -276,6 +310,7 @@ "slug": "white-2", "generation": 5, "region": "unova", + "category": "sequel", "release_year": 2012, "color": "#E3CED0" }, @@ -284,6 +319,7 @@ "slug": "x", "generation": 6, "region": "kalos", + "category": "original", "release_year": 2013, "color": "#025DA6" }, @@ -292,6 +328,7 @@ "slug": "y", "generation": 6, "region": "kalos", + "category": "original", "release_year": 2013, "color": "#EA1A3E" }, @@ -300,6 +337,7 @@ "slug": "yellow", "generation": 1, "region": "kanto", + "category": "enhanced", "release_year": 1998, "color": "#FFD733" } diff --git a/backend/src/app/seeds/data/regions.json b/backend/src/app/seeds/data/regions.json new file mode 100644 index 0000000..4e1d3e9 --- /dev/null +++ b/backend/src/app/seeds/data/regions.json @@ -0,0 +1,83 @@ +[ + { + "name": "kanto", + "generation": 1, + "order": 1, + "genlocke_defaults": { + "true": "red", + "normal": "firered" + } + }, + { + "name": "johto", + "generation": 2, + "order": 2, + "genlocke_defaults": { + "true": "gold", + "normal": "heartgold" + } + }, + { + "name": "hoenn", + "generation": 3, + "order": 3, + "genlocke_defaults": { + "true": "ruby", + "normal": "alpha-sapphire" + } + }, + { + "name": "sinnoh", + "generation": 4, + "order": 4, + "genlocke_defaults": { + "true": "diamond", + "normal": "platinum" + } + }, + { + "name": "unova", + "generation": 5, + "order": 5, + "genlocke_defaults": { + "true": "black", + "normal": "black" + } + }, + { + "name": "kalos", + "generation": 6, + "order": 6, + "genlocke_defaults": { + "true": "x", + "normal": "x" + } + }, + { + "name": "alola", + "generation": 7, + "order": 7, + "genlocke_defaults": { + "true": "sun", + "normal": "ultra-sun" + } + }, + { + "name": "galar", + "generation": 8, + "order": 8, + "genlocke_defaults": { + "true": "sword", + "normal": "sword" + } + }, + { + "name": "paldea", + "generation": 9, + "order": 9, + "genlocke_defaults": { + "true": "scarlet", + "normal": "scarlet" + } + } +] diff --git a/backend/src/app/seeds/loader.py b/backend/src/app/seeds/loader.py index acfae14..af7cc79 100644 --- a/backend/src/app/seeds/loader.py +++ b/backend/src/app/seeds/loader.py @@ -51,6 +51,7 @@ async def upsert_games( "slug": game["slug"], "generation": game["generation"], "region": game["region"], + "category": game.get("category"), "release_year": game.get("release_year"), "color": game.get("color"), } @@ -58,6 +59,7 @@ async def upsert_games( "name": game["name"], "generation": game["generation"], "region": game["region"], + "category": game.get("category"), "release_year": game.get("release_year"), "color": game.get("color"), } diff --git a/frontend/src/types/game.ts b/frontend/src/types/game.ts index b726432..347a31d 100644 --- a/frontend/src/types/game.ts +++ b/frontend/src/types/game.ts @@ -1,15 +1,31 @@ +export type GameCategory = 'original' | 'remake' | 'enhanced' | 'sequel' | 'spinoff' + export interface Game { id: number name: string slug: string generation: number region: string + category: GameCategory | null boxArtUrl: string | null releaseYear: number | null color: string | null versionGroupId: number | null } +export interface GenlockeDefaults { + trueGenlocke: string // game slug + normalGenlocke: string // game slug +} + +export interface Region { + name: string + generation: number + order: number + genlockeDefaults: GenlockeDefaults + games: Game[] +} + export interface Route { id: number name: string