From e324559476f0618cc901f8c89f38b501b231f7e3 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Wed, 11 Feb 2026 21:36:50 +0100 Subject: [PATCH 1/5] Add naming scheme selection to run configuration Add a nullable naming_scheme column to NuzlockeRun so users can pick a themed word category for nickname suggestions. Includes Alembic migration, updated Pydantic schemas, a GET /runs/naming-categories endpoint backed by a cached dictionary loader, and frontend dropdowns in both the NewRun creation flow and the RunDashboard for mid-run changes. Co-Authored-By: Claude Opus 4.6 --- ...ame-suggestions-into-encounter-registra.md | 15 +++++-- ...cker-c6ly--build-name-suggestion-engine.md | 20 ++++++--- .../nuzlocke-tracker-igl3--name-generation.md | 12 +++++- ...g-scheme-selection-to-run-configuration.md | 26 ++++++++---- ...-ueyy--create-name-dictionary-data-file.md | 23 +++++++---- ...c9d1_add_naming_scheme_to_nuzlocke_runs.py | 29 +++++++++++++ backend/src/app/api/runs.py | 7 ++++ backend/src/app/models/nuzlocke_run.py | 1 + backend/src/app/schemas/run.py | 3 ++ backend/src/app/services/naming.py | 18 ++++++++ frontend/src/api/runs.ts | 4 ++ frontend/src/hooks/useRuns.ts | 10 ++++- frontend/src/pages/NewRun.tsx | 41 ++++++++++++++++++- frontend/src/pages/RunDashboard.tsx | 34 ++++++++++++++- frontend/src/types/game.ts | 3 ++ 15 files changed, 215 insertions(+), 31 deletions(-) create mode 100644 backend/src/app/alembic/versions/e5f6a7b8c9d1_add_naming_scheme_to_nuzlocke_runs.py create mode 100644 backend/src/app/services/naming.py diff --git a/.beans/nuzlocke-tracker-bi4e--integrate-name-suggestions-into-encounter-registra.md b/.beans/nuzlocke-tracker-bi4e--integrate-name-suggestions-into-encounter-registra.md index 53dcacd..899ded3 100644 --- a/.beans/nuzlocke-tracker-bi4e--integrate-name-suggestions-into-encounter-registra.md +++ b/.beans/nuzlocke-tracker-bi4e--integrate-name-suggestions-into-encounter-registra.md @@ -3,8 +3,9 @@ title: Integrate name suggestions into encounter registration UI status: todo type: task +priority: normal created_at: 2026-02-11T15:56:44Z -updated_at: 2026-02-11T15:56:44Z +updated_at: 2026-02-11T20:23:40Z parent: nuzlocke-tracker-igl3 --- @@ -12,17 +13,23 @@ Show name suggestions in the encounter registration flow so users can pick a nic ## Requirements -- When a user clicks an encounter slot and registers a new Pokemon, display 5-10 name suggestions below/near the nickname input +- When a user registers a new Pokemon encounter, display 5-10 name suggestions below/near the nickname input - Each suggestion is a clickable chip/button that fills in the nickname field - Include a "regenerate" button to get a fresh batch of suggestions - Only show suggestions if the run has a naming scheme selected - The nickname input should still be editable for manual entry +## Implementation Notes + +- **Data fetching**: Call `GET /api/v1/runs/{run_id}/name-suggestions?count=10` to get suggestions from the backend. +- **Regeneration**: Each call to the endpoint returns a fresh random batch (backend handles exclusion of used names). +- **No dictionary data in frontend**: All suggestion logic lives in the backend. + ## Checklist - [ ] Add a name suggestions component (chips/buttons with regenerate) - [ ] Integrate the component into the encounter registration modal/form -- [ ] Wire up the name suggestion engine to the component +- [ ] Wire up the backend API endpoint to the component via React Query - [ ] Ensure clicking a suggestion populates the nickname field -- [ ] Ensure regenerate fetches a new batch without repeating prior suggestions +- [ ] Ensure regenerate fetches a new batch from the API - [ ] Hide suggestions gracefully if no naming scheme is set on the run \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-c6ly--build-name-suggestion-engine.md b/.beans/nuzlocke-tracker-c6ly--build-name-suggestion-engine.md index 4a6d948..7529b85 100644 --- a/.beans/nuzlocke-tracker-c6ly--build-name-suggestion-engine.md +++ b/.beans/nuzlocke-tracker-c6ly--build-name-suggestion-engine.md @@ -5,24 +5,32 @@ status: todo type: task priority: normal created_at: 2026-02-11T15:56:44Z -updated_at: 2026-02-11T15:56:48Z +updated_at: 2026-02-11T20:23:35Z parent: nuzlocke-tracker-igl3 blocking: - nuzlocke-tracker-bi4e --- -Build the core logic that picks random name suggestions from the dictionary based on a selected naming scheme (category). +Build the backend service and API endpoint that picks random name suggestions from the dictionary based on a selected naming scheme. ## Requirements -- Given a category and a list of already-used names in the run, return 5-10 unique suggestions -- Suggestions must not include names already assigned to other Pokemon in the same run +- Given a category and a run ID, return 5-10 unique suggestions +- The engine queries the run's existing encounter nicknames and excludes them from suggestions - Support regeneration (return a fresh batch, avoiding previously shown suggestions where possible) - Handle edge case where category is nearly exhausted gracefully (return fewer suggestions) +## Implementation Notes + +- **Backend service**: A Python module that loads the dictionary JSON, filters by category, excludes used names, and picks random suggestions. +- **API endpoint**: `GET /api/v1/runs/{run_id}/name-suggestions?count=10` — reads the run's `naming_scheme`, fetches encounter nicknames, returns suggestions. +- **No new DB tables needed**: Used names come from `Encounter.nickname` on the run's encounters. +- **Caching**: Load the dictionary once and cache in memory (it's static data). + ## Checklist -- [ ] Create a service/utility module for name suggestion logic +- [ ] Create a service module for name suggestion logic (e.g. `services/name_suggestions.py`) +- [ ] Implement dictionary loading with in-memory caching - [ ] Implement random selection from a category with exclusion of used names -- [ ] Implement regeneration that avoids repeating previous suggestions +- [ ] Add API endpoint for fetching suggestions - [ ] Add unit tests for the suggestion logic \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-igl3--name-generation.md b/.beans/nuzlocke-tracker-igl3--name-generation.md index 072a2a5..7397b49 100644 --- a/.beans/nuzlocke-tracker-igl3--name-generation.md +++ b/.beans/nuzlocke-tracker-igl3--name-generation.md @@ -5,14 +5,22 @@ status: todo type: epic priority: normal created_at: 2026-02-05T13:45:15Z -updated_at: 2026-02-11T15:57:27Z +updated_at: 2026-02-11T20:23:14Z --- Implement a dictionary-based nickname generation system for Nuzlocke runs. Instead of using an LLM API to generate names on the fly, provide a static dictionary of words categorised by theme. A word can belong to multiple categories, making it usable across different naming schemes. +## Architecture Decisions + +- **Dictionary storage**: Static JSON file in `backend/src/app/seeds/data/`, alongside other seed data. Not exposed to frontend directly. +- **Dictionary format**: Category-keyed structure (`{ "mythology": ["Apollo", ...], "space": ["Nova", ...] }`) for fast lookup by naming scheme. Words may appear in multiple categories. +- **Suggestion logic**: Backend service with API endpoint. Frontend calls the backend to get suggestions. +- **Used-name tracking**: No new storage needed. The existing `Encounter.nickname` field already tracks assigned names. The suggestion engine queries encounter nicknames for the current run and excludes them. +- **Naming scheme per run**: Dedicated nullable `naming_scheme` column on `NuzlockeRun` (not in the `rules` JSONB). + ## Approach -- **Static dictionary**: A local data file (JSON) containing words tagged with categories (e.g. mythology, food, space, nature, warriors, music, etc.) +- **Static dictionary**: A local data file (JSON) containing words organised by category (e.g. mythology, food, space, nature, warriors, music, etc.) - **~150-200 words per category**: A typical Nuzlocke has ~100 encounters, so this provides ample variety without repetition. - **Name suggestion UX**: When registering a new encounter, the user is shown 5-10 suggested names from their chosen naming scheme. They can click one to select it, or regenerate for a fresh batch. - **Naming scheme selection**: Users pick a naming scheme (category) per run, either at run creation or in run settings. diff --git a/.beans/nuzlocke-tracker-m86o--add-naming-scheme-selection-to-run-configuration.md b/.beans/nuzlocke-tracker-m86o--add-naming-scheme-selection-to-run-configuration.md index 47d6d35..dda5241 100644 --- a/.beans/nuzlocke-tracker-m86o--add-naming-scheme-selection-to-run-configuration.md +++ b/.beans/nuzlocke-tracker-m86o--add-naming-scheme-selection-to-run-configuration.md @@ -1,11 +1,11 @@ --- # nuzlocke-tracker-m86o title: Add naming scheme selection to run configuration -status: todo +status: in-progress type: task priority: normal created_at: 2026-02-11T15:56:44Z -updated_at: 2026-02-11T15:56:48Z +updated_at: 2026-02-11T20:35:59Z parent: nuzlocke-tracker-igl3 blocking: - nuzlocke-tracker-bi4e @@ -15,14 +15,24 @@ Allow users to select a naming scheme (category) for their Nuzlocke run. ## Requirements -- Add a `namingScheme` field to the run model/settings (optional, nullable — user may not want auto-naming) +- Add a `naming_scheme` column to the NuzlockeRun model (nullable string — user may not want auto-naming) - Provide a dropdown/selector in run creation and run settings where the user can pick a category -- List available categories dynamically from the dictionary data +- List available categories dynamically by querying a backend endpoint that reads the dictionary file - Allow changing the naming scheme mid-run +## Implementation Notes + +- **Storage**: Dedicated nullable `naming_scheme` column on `NuzlockeRun` (not in the `rules` JSONB). This is a first-class run setting. +- **Migration**: Add an Alembic migration for the new column. +- **API**: Add/update the run creation and update endpoints to accept `namingScheme`. +- **Categories endpoint**: Add a GET endpoint that returns the list of available category names from the dictionary file, so the frontend can populate the dropdown. + ## Checklist -- [ ] Add `namingScheme` field to the NuzlockeRun type/model -- [ ] Add naming scheme selector to run creation UI -- [ ] Add naming scheme selector to run settings UI -- [ ] Persist the selected naming scheme with the run data \ No newline at end of file +- [x] Add `naming_scheme` nullable column to NuzlockeRun model +- [x] Create Alembic migration for the new column +- [x] Update run Pydantic schemas to include `namingScheme` +- [x] Update run creation and update endpoints to persist the naming scheme +- [x] Add GET endpoint to list available naming categories from the dictionary +- [x] Add naming scheme selector to run creation UI +- [x] Add naming scheme selector to run settings UI \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-ueyy--create-name-dictionary-data-file.md b/.beans/nuzlocke-tracker-ueyy--create-name-dictionary-data-file.md index 98d66ba..c03721d 100644 --- a/.beans/nuzlocke-tracker-ueyy--create-name-dictionary-data-file.md +++ b/.beans/nuzlocke-tracker-ueyy--create-name-dictionary-data-file.md @@ -5,25 +5,34 @@ status: todo type: task priority: normal created_at: 2026-02-11T15:56:26Z -updated_at: 2026-02-11T15:56:48Z +updated_at: 2026-02-11T20:23:29Z parent: nuzlocke-tracker-igl3 blocking: - nuzlocke-tracker-c6ly --- -Create a JSON data file containing themed words for nickname generation. +Create a JSON data file containing themed words for nickname generation, stored in the backend alongside other seed data. ## Requirements -- Each word entry has: `word` (string) and `categories` (string array) +- Store at `backend/src/app/seeds/data/name_dictionary.json` +- Category-keyed structure for fast lookup: + ```json + { + "mythology": ["Apollo", "Athena", "Loki", ...], + "space": ["Apollo", "Nova", "Nebula", ...], + "food": ["Basil", "Sage", "Pepper", ...] + } + ``` +- Words may appear in multiple categories - Categories should include themes like: mythology, food, space, nature, warriors, music, literature, gems, ocean, weather, etc. - Target 150-200 words per category -- Words can belong to multiple categories - Words should be short, punchy, and suitable as Pokemon nicknames (ideally 1-2 words, max ~12 characters) +- This file is NOT seeded into the database — it is read directly by the backend service at runtime ## Checklist -- [ ] Define the data schema / TypeScript type for dictionary entries -- [ ] Create the JSON data file with initial categories +- [ ] Create `backend/src/app/seeds/data/name_dictionary.json` with the category-keyed structure - [ ] Populate each category with 150-200 words -- [ ] Validate no duplicates exist within the file \ No newline at end of file +- [ ] Validate no duplicates exist within a single category +- [ ] Add a utility function to load the dictionary from disk (with caching) \ No newline at end of file diff --git a/backend/src/app/alembic/versions/e5f6a7b8c9d1_add_naming_scheme_to_nuzlocke_runs.py b/backend/src/app/alembic/versions/e5f6a7b8c9d1_add_naming_scheme_to_nuzlocke_runs.py new file mode 100644 index 0000000..7ab0eda --- /dev/null +++ b/backend/src/app/alembic/versions/e5f6a7b8c9d1_add_naming_scheme_to_nuzlocke_runs.py @@ -0,0 +1,29 @@ +"""add naming_scheme to nuzlocke_runs + +Revision ID: e5f6a7b8c9d1 +Revises: d4e5f6a7b9c0 +Create Date: 2026-02-11 12:00:00.000000 + +""" + +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "e5f6a7b8c9d1" +down_revision: str | Sequence[str] | None = "d4e5f6a7b9c0" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column( + "nuzlocke_runs", + sa.Column("naming_scheme", sa.String(50), nullable=True), + ) + + +def downgrade() -> None: + op.drop_column("nuzlocke_runs", "naming_scheme") diff --git a/backend/src/app/api/runs.py b/backend/src/app/api/runs.py index 5db7d65..a63e296 100644 --- a/backend/src/app/api/runs.py +++ b/backend/src/app/api/runs.py @@ -19,10 +19,16 @@ from app.schemas.run import ( RunResponse, RunUpdate, ) +from app.services.naming import get_naming_categories router = APIRouter() +@router.get("/naming-categories", response_model=list[str]) +async def list_naming_categories(): + return get_naming_categories() + + @router.post("", response_model=RunResponse, status_code=201) async def create_run(data: RunCreate, session: AsyncSession = Depends(get_session)): # Validate game exists @@ -35,6 +41,7 @@ async def create_run(data: RunCreate, session: AsyncSession = Depends(get_sessio name=data.name, status="active", rules=data.rules, + naming_scheme=data.naming_scheme, ) session.add(run) await session.commit() diff --git a/backend/src/app/models/nuzlocke_run.py b/backend/src/app/models/nuzlocke_run.py index 01269a4..795eff4 100644 --- a/backend/src/app/models/nuzlocke_run.py +++ b/backend/src/app/models/nuzlocke_run.py @@ -22,6 +22,7 @@ class NuzlockeRun(Base): ) completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) hof_encounter_ids: Mapped[list[int] | None] = mapped_column(JSONB, default=None) + naming_scheme: Mapped[str | None] = mapped_column(String(50), nullable=True) game: Mapped["Game"] = relationship(back_populates="runs") encounters: Mapped[list["Encounter"]] = relationship(back_populates="run") diff --git a/backend/src/app/schemas/run.py b/backend/src/app/schemas/run.py index 32f7431..aa1abff 100644 --- a/backend/src/app/schemas/run.py +++ b/backend/src/app/schemas/run.py @@ -9,6 +9,7 @@ class RunCreate(CamelModel): game_id: int name: str rules: dict = {} + naming_scheme: str | None = None class RunUpdate(CamelModel): @@ -16,6 +17,7 @@ class RunUpdate(CamelModel): status: str | None = None rules: dict | None = None hof_encounter_ids: list[int] | None = None + naming_scheme: str | None = None class RunResponse(CamelModel): @@ -25,6 +27,7 @@ class RunResponse(CamelModel): status: str rules: dict hof_encounter_ids: list[int] | None = None + naming_scheme: str | None = None started_at: datetime completed_at: datetime | None diff --git a/backend/src/app/services/naming.py b/backend/src/app/services/naming.py new file mode 100644 index 0000000..42d9305 --- /dev/null +++ b/backend/src/app/services/naming.py @@ -0,0 +1,18 @@ +import json +from functools import lru_cache +from pathlib import Path + +DICTIONARY_PATH = Path(__file__).resolve().parents[1] / "seeds" / "data" / "name_dictionary.json" + + +@lru_cache(maxsize=1) +def _load_dictionary() -> dict[str, list[str]]: + if not DICTIONARY_PATH.exists(): + return {} + with open(DICTIONARY_PATH) as f: + return json.load(f) + + +def get_naming_categories() -> list[str]: + """Return sorted list of available naming category names.""" + return sorted(_load_dictionary().keys()) diff --git a/frontend/src/api/runs.ts b/frontend/src/api/runs.ts index 280c47f..2a7e4d4 100644 --- a/frontend/src/api/runs.ts +++ b/frontend/src/api/runs.ts @@ -28,3 +28,7 @@ export function updateRun( export function deleteRun(id: number): Promise { return api.del(`/runs/${id}`) } + +export function getNamingCategories(): Promise { + return api.get('/runs/naming-categories') +} diff --git a/frontend/src/hooks/useRuns.ts b/frontend/src/hooks/useRuns.ts index 68fb31e..7843523 100644 --- a/frontend/src/hooks/useRuns.ts +++ b/frontend/src/hooks/useRuns.ts @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { getRuns, getRun, createRun, updateRun, deleteRun } from '../api/runs' +import { getRuns, getRun, createRun, updateRun, deleteRun, getNamingCategories } from '../api/runs' import type { CreateRunInput, UpdateRunInput } from '../types/game' export function useRuns() { @@ -51,3 +51,11 @@ export function useDeleteRun() { }, }) } + +export function useNamingCategories() { + return useQuery({ + queryKey: ['naming-categories'], + queryFn: getNamingCategories, + staleTime: Infinity, + }) +} diff --git a/frontend/src/pages/NewRun.tsx b/frontend/src/pages/NewRun.tsx index 90e7fd9..2ddeac3 100644 --- a/frontend/src/pages/NewRun.tsx +++ b/frontend/src/pages/NewRun.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' import { GameGrid, RulesConfiguration, StepIndicator } from '../components' import { useGames, useGameRoutes } from '../hooks/useGames' -import { useCreateRun, useRuns } from '../hooks/useRuns' +import { useCreateRun, useRuns, useNamingCategories } from '../hooks/useRuns' import type { Game, NuzlockeRules } from '../types' import { DEFAULT_RULES } from '../types' import { RULE_DEFINITIONS } from '../types/rules' @@ -14,11 +14,13 @@ export function NewRun() { const { data: games, isLoading, error } = useGames() const { data: runs } = useRuns() const createRun = useCreateRun() + const { data: namingCategories } = useNamingCategories() const [step, setStep] = useState(1) const [selectedGame, setSelectedGame] = useState(null) const [rules, setRules] = useState(DEFAULT_RULES) const [runName, setRunName] = useState('') + const [namingScheme, setNamingScheme] = useState(null) const { data: routes } = useGameRoutes(selectedGame?.id ?? null) const hiddenRules = useMemo(() => { @@ -44,7 +46,7 @@ export function NewRun() { const handleCreate = () => { if (!selectedGame) return createRun.mutate( - { gameId: selectedGame.id, name: runName, rules }, + { gameId: selectedGame.id, name: runName, rules, namingScheme }, { onSuccess: (data) => navigate(`/runs/${data.id}`) }, ) } @@ -180,6 +182,33 @@ export function NewRun() { /> + {namingCategories && namingCategories.length > 0 && ( +
+ + +

+ Get nickname suggestions from a themed word list when catching Pokemon. +

+
+ )} +

Summary @@ -203,6 +232,14 @@ export function NewRun() { {enabledRuleCount} of {totalRuleCount} enabled

+
+
Naming Scheme
+
+ {namingScheme + ? namingScheme.charAt(0).toUpperCase() + namingScheme.slice(1) + : 'None'} +
+
diff --git a/frontend/src/pages/RunDashboard.tsx b/frontend/src/pages/RunDashboard.tsx index fcda00b..b328840 100644 --- a/frontend/src/pages/RunDashboard.tsx +++ b/frontend/src/pages/RunDashboard.tsx @@ -1,6 +1,6 @@ import { useMemo, useState } from 'react' import { useParams, Link } from 'react-router-dom' -import { useRun, useUpdateRun } from '../hooks/useRuns' +import { useRun, useUpdateRun, useNamingCategories } from '../hooks/useRuns' import { useGameRoutes } from '../hooks/useGames' import { useCreateEncounter, useUpdateEncounter } from '../hooks/useEncounters' import { StatCard, PokemonCard, RuleBadges, StatusChangeModal, EndRunModal } from '../components' @@ -51,6 +51,7 @@ export function RunDashboard() { const createEncounter = useCreateEncounter(runIdNum) const updateEncounter = useUpdateEncounter(runIdNum) const updateRun = useUpdateRun(runIdNum) + const { data: namingCategories } = useNamingCategories() const [selectedEncounter, setSelectedEncounter] = useState(null) const [showEndRun, setShowEndRun] = useState(false) @@ -197,6 +198,37 @@ export function RunDashboard() { + {/* Naming Scheme */} + {namingCategories && namingCategories.length > 0 && ( +
+

+ Naming Scheme +

+ {isActive ? ( + + ) : ( + + {run.namingScheme + ? run.namingScheme.charAt(0).toUpperCase() + run.namingScheme.slice(1) + : 'None'} + + )} +
+ )} + {/* Active Team */}
diff --git a/frontend/src/types/game.ts b/frontend/src/types/game.ts index 8937bf5..87ad1ef 100644 --- a/frontend/src/types/game.ts +++ b/frontend/src/types/game.ts @@ -90,6 +90,7 @@ export interface NuzlockeRun { status: RunStatus rules: NuzlockeRules hofEncounterIds: number[] | null + namingScheme: string | null startedAt: string completedAt: string | null } @@ -132,6 +133,7 @@ export interface CreateRunInput { gameId: number name: string rules?: NuzlockeRules + namingScheme?: string | null } export interface UpdateRunInput { @@ -139,6 +141,7 @@ export interface UpdateRunInput { status?: RunStatus rules?: NuzlockeRules hofEncounterIds?: number[] + namingScheme?: string | null } export interface CreateEncounterInput { From 15283ede9126fce4f9b207aac8bce439c47a5dad Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Wed, 11 Feb 2026 21:42:33 +0100 Subject: [PATCH 2/5] Add name dictionary with 10 themed categories Create name_dictionary.json with 175 words each across mythology, food, space, nature, warriors, music, literature, gems, ocean, and weather categories. Words are short (<= 12 chars), title-cased, and suitable as Pokemon nicknames. No intra-category duplicates; cross-category overlap is intentional. Co-Authored-By: Claude Opus 4.6 --- .../nuzlocke-tracker-igl3--name-generation.md | 6 +- ...-ueyy--create-name-dictionary-data-file.md | 12 +- .../src/app/seeds/data/name_dictionary.json | 372 ++++++++++++++++++ 3 files changed, 381 insertions(+), 9 deletions(-) create mode 100644 backend/src/app/seeds/data/name_dictionary.json diff --git a/.beans/nuzlocke-tracker-igl3--name-generation.md b/.beans/nuzlocke-tracker-igl3--name-generation.md index 7397b49..a4d739c 100644 --- a/.beans/nuzlocke-tracker-igl3--name-generation.md +++ b/.beans/nuzlocke-tracker-igl3--name-generation.md @@ -5,7 +5,7 @@ status: todo type: epic priority: normal created_at: 2026-02-05T13:45:15Z -updated_at: 2026-02-11T20:23:14Z +updated_at: 2026-02-11T20:41:56Z --- Implement a dictionary-based nickname generation system for Nuzlocke runs. Instead of using an LLM API to generate names on the fly, provide a static dictionary of words categorised by theme. A word can belong to multiple categories, making it usable across different naming schemes. @@ -27,8 +27,8 @@ Implement a dictionary-based nickname generation system for Nuzlocke runs. Inste ## Success Criteria -- [ ] Word dictionary data file exists with multiple categories, each containing 150-200 words +- [x] Word dictionary data file exists with multiple categories, each containing 150-200 words - [ ] Name suggestion engine picks random names from the selected category, avoiding duplicates already used in the run - [ ] Encounter registration UI shows 5-10 clickable name suggestions - [ ] User can regenerate suggestions if none fit -- [ ] User can select a naming scheme per run \ No newline at end of file +- [x] User can select a naming scheme per run \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-ueyy--create-name-dictionary-data-file.md b/.beans/nuzlocke-tracker-ueyy--create-name-dictionary-data-file.md index c03721d..63a1299 100644 --- a/.beans/nuzlocke-tracker-ueyy--create-name-dictionary-data-file.md +++ b/.beans/nuzlocke-tracker-ueyy--create-name-dictionary-data-file.md @@ -1,11 +1,11 @@ --- # nuzlocke-tracker-ueyy title: Create name dictionary data file -status: todo +status: completed type: task priority: normal created_at: 2026-02-11T15:56:26Z -updated_at: 2026-02-11T20:23:29Z +updated_at: 2026-02-11T20:42:16Z parent: nuzlocke-tracker-igl3 blocking: - nuzlocke-tracker-c6ly @@ -32,7 +32,7 @@ Create a JSON data file containing themed words for nickname generation, stored ## Checklist -- [ ] Create `backend/src/app/seeds/data/name_dictionary.json` with the category-keyed structure -- [ ] Populate each category with 150-200 words -- [ ] Validate no duplicates exist within a single category -- [ ] Add a utility function to load the dictionary from disk (with caching) \ No newline at end of file +- [x] Create `backend/src/app/seeds/data/name_dictionary.json` with the category-keyed structure +- [x] Populate each category with 150-200 words +- [x] Validate no duplicates exist within a single category +- [x] Add a utility function to load the dictionary from disk (with caching) \ No newline at end of file diff --git a/backend/src/app/seeds/data/name_dictionary.json b/backend/src/app/seeds/data/name_dictionary.json new file mode 100644 index 0000000..a07e755 --- /dev/null +++ b/backend/src/app/seeds/data/name_dictionary.json @@ -0,0 +1,372 @@ +{ + "mythology": [ + "Zeus", "Hera", "Apollo", "Athena", "Ares", + "Hermes", "Artemis", "Hades", "Poseidon", "Demeter", + "Hephaestus", "Aphrodite", "Dionysus", "Persephone", "Eros", + "Nike", "Helios", "Selene", "Nyx", "Chronos", + "Atlas", "Titan", "Hydra", "Cerberus", "Minotaur", + "Medusa", "Pegasus", "Phoenix", "Chimera", "Cyclops", + "Icarus", "Orpheus", "Perseus", "Achilles", "Odysseus", + "Ajax", "Hector", "Paris", "Helen", "Circe", + "Calypso", "Siren", "Muse", "Oracle", "Sphinx", + "Styx", "Elysium", "Olympus", "Tartarus", "Nemesis", + "Odin", "Thor", "Loki", "Freya", "Tyr", + "Baldur", "Fenrir", "Jormungandr", "Ragnarok", "Valkyrie", + "Mjolnir", "Yggdrasil", "Bifrost", "Valhalla", "Asgard", + "Midgard", "Norns", "Huginn", "Muninn", "Sleipnir", + "Ra", "Osiris", "Isis", "Anubis", "Thoth", + "Bastet", "Sekhmet", "Sobek", "Horus", "Seth", + "Nephthys", "Ammit", "Scarab", "Apophis", "Khepri", + "Amaterasu", "Susanoo", "Tsukuyomi", "Izanagi", "Izanami", + "Raijin", "Fujin", "Inari", "Kitsune", "Tengu", + "Oni", "Tanuki", "Yokai", "Kami", "Byakko", + "Vishnu", "Shiva", "Brahma", "Kali", "Ganesh", + "Lakshmi", "Hanuman", "Indra", "Agni", "Surya", + "Garuda", "Naga", "Deva", "Asura", "Dharma", + "Karma", "Mantra", "Lotus", "Chakra", "Maya", + "Brigid", "Morrigan", "Dagda", "Lugh", "Danu", + "Cernunnos", "Oberon", "Titania", "Merlin", "Avalon", + "Excalibur", "Druid", "Banshee", "Sidhe", "Fomorian", + "Leviathan", "Golem", "Djinn", "Ifrit", "Typhon", + "Griffin", "Kraken", "Wyrm", "Drake", "Wyvern", + "Basilisk", "Manticore", "Harpy", "Gorgon", "Triton", + "Prometheus", "Pandora", "Daphne", "Echo", "Narcissus", + "Midas", "Theseus", "Orion", "Castor", "Pollux", + "Zephyr", "Boreas", "Aether", "Chaos", "Gaia", + "Uranus", "Rhea", "Hyperion", "Themis", "Mnemosyne", + "Clio", "Fury", "Fate", "Moira", "Clotho" + ], + "food": [ + "Basil", "Sage", "Thyme", "Rosemary", "Saffron", + "Ginger", "Pepper", "Cinnamon", "Nutmeg", "Clove", + "Paprika", "Wasabi", "Cumin", "Fennel", "Dill", + "Anise", "Cardamom", "Turmeric", "Coriander", "Oregano", + "Tarragon", "Chive", "Parsley", "Mint", "Vanilla", + "Mango", "Kiwi", "Peach", "Cherry", "Plum", + "Fig", "Lemon", "Lime", "Melon", "Berry", + "Olive", "Cocoa", "Mocha", "Latte", "Chai", + "Matcha", "Espresso", "Cider", "Mead", "Porter", + "Stout", "Sake", "Roux", "Bisque", "Broth", + "Frappe", "Sorbet", "Gelato", "Truffle", "Praline", + "Nougat", "Toffee", "Caramel", "Fudge", "Brioche", + "Croissant", "Baguette", "Pretzel", "Dumpling", "Gyoza", + "Mochi", "Tempura", "Ramen", "Udon", "Soba", + "Pesto", "Salsa", "Kimchi", "Naan", "Tofu", + "Panko", "Miso", "Tahini", "Hummus", "Falafel", + "Burrito", "Taco", "Nacho", "Churro", "Crepe", + "Waffle", "Scone", "Crumpet", "Biscuit", "Cobbler", + "Crisp", "Tart", "Torte", "Mousse", "Souffle", + "Quiche", "Gratin", "Fondue", "Risotto", "Gnocchi", + "Ravioli", "Penne", "Orzo", "Pilaf", "Paella", + "Tagine", "Curry", "Satay", "Bulgogi", "Teriyaki", + "Ponzu", "Sriracha", "Harissa", "Chutney", "Relish", + "Pickle", "Caper", "Anchovy", "Prawn", "Scallop", + "Brisket", "Bison", "Quinoa", "Barley", "Millet", + "Almond", "Pecan", "Walnut", "Cashew", "Pistachio", + "Hazel", "Acai", "Guava", "Lychee", "Papaya", + "Tamarind", "Kumquat", "Clementine", "Nectar", "Honey", + "Maple", "Agave", "Molasses", "Butter", "Cream", + "Brie", "Gouda", "Cheddar", "Ricotta", "Paneer", + "Toasted", "Smoked", "Braised", "Seared", "Glazed", + "Roasted", "Grilled", "Poached", "Zest", "Tang", + "Crunch", "Morsel", "Nibble", "Drizzle", "Dash", + "Pinch", "Sprig", "Garnish", "Feast", "Brunch", + "Savory", "Umami", "Tangy", "Citrus", "Jambon" + ], + "space": [ + "Nova", "Nebula", "Quasar", "Pulsar", "Cosmos", + "Comet", "Meteor", "Astral", "Stellar", "Solar", + "Lunar", "Orbit", "Eclipse", "Galaxy", "Vortex", + "Zenith", "Nadir", "Apogee", "Perigee", "Epoch", + "Apollo", "Gemini", "Mercury", "Venus", "Mars", + "Jupiter", "Saturn", "Neptune", "Uranus", "Pluto", + "Titan", "Europa", "Io", "Callisto", "Ganymede", + "Triton", "Charon", "Phobos", "Deimos", "Ceres", + "Juno", "Vesta", "Pallas", "Eris", "Sedna", + "Sirius", "Rigel", "Vega", "Altair", "Deneb", + "Polaris", "Canopus", "Betelgeuse", "Antares", "Aldebaran", + "Spica", "Arcturus", "Capella", "Procyon", "Achernar", + "Castor", "Pollux", "Regulus", "Fomalhaut", "Mira", + "Bellatrix", "Mintaka", "Alnilam", "Alnitak", "Saiph", + "Orion", "Lyra", "Draco", "Hydra", "Corvus", + "Aquila", "Cygnus", "Phoenix", "Andromeda", "Cassiopeia", + "Perseus", "Centauri", "Pegasus", "Scorpius", "Leo", + "Aries", "Virgo", "Libra", "Pisces", "Taurus", + "Photon", "Neutron", "Proton", "Plasma", "Flux", + "Prism", "Spectrum", "Horizon", "Corona", "Halo", + "Aurora", "Solstice", "Equinox", "Parallax", "Parsec", + "Lightyear", "Warp", "Void", "Rift", "Abyss", + "Singularity", "Binary", "Cluster", "Remnant", "Dwarf", + "Giant", "Supernova", "Stardust", "Starfall", "Skyfire", + "Sunspot", "Flare", "Radiance", "Luminous", "Beacon", + "Voyager", "Pioneer", "Sputnik", "Hubble", "Kepler", + "Galileo", "Cassini", "Rosetta", "Artemis", "Chandra", + "Solaris", "Nebular", "Asteria", "Celestia", "Astra", + "Solis", "Lumen", "Ignis", "Ember", "Blaze", + "Spark", "Glimmer", "Shimmer", "Twilight", "Dusk", + "Dawn", "Crescent", "Waning", "Waxing", "Umbra", + "Penumbra", "Perihelion", "Aphelion", "Azimuth", "Meridian", + "Helix", "Spiral", "Ring", "Arc", "Bolt", + "Surge", "Pulse", "Wave", "Drift", "Phase", + "Xenon", "Neon", "Argon", "Helium", "Lithium" + ], + "nature": [ + "Willow", "Cedar", "Birch", "Aspen", "Maple", + "Rowan", "Alder", "Elm", "Oak", "Pine", + "Spruce", "Juniper", "Laurel", "Ivy", "Fern", + "Moss", "Lichen", "Thistle", "Clover", "Briar", + "Thorn", "Reed", "Sage", "Basil", "Lotus", + "Orchid", "Dahlia", "Iris", "Lily", "Violet", + "Jasmine", "Heather", "Daisy", "Poppy", "Aster", + "Azalea", "Magnolia", "Wisteria", "Camellia", "Marigold", + "Hawk", "Falcon", "Eagle", "Osprey", "Heron", + "Crane", "Wren", "Finch", "Lark", "Robin", + "Sparrow", "Raven", "Crow", "Jay", "Dove", + "Swift", "Owl", "Kestrel", "Harrier", "Condor", + "Wolf", "Fox", "Bear", "Lynx", "Puma", + "Jaguar", "Panther", "Stag", "Elk", "Moose", + "Bison", "Badger", "Otter", "Mink", "Ermine", + "Viper", "Cobra", "Adder", "Gecko", "Newt", + "Coral", "Pearl", "Amber", "Flint", "Slate", + "Granite", "Basalt", "Quartz", "Mica", "Clay", + "Dune", "Mesa", "Butte", "Gorge", "Canyon", + "Ridge", "Summit", "Crest", "Peak", "Cliff", + "Bluff", "Dale", "Glen", "Vale", "Hollow", + "Meadow", "Prairie", "Tundra", "Taiga", "Savanna", + "Grove", "Copse", "Thicket", "Canopy", "Glade", + "Brook", "Creek", "Rivulet", "Spring", "Rapids", + "Cascade", "Torrent", "Eddy", "Delta", "Marsh", + "Bog", "Fen", "Mire", "Lagoon", "Oasis", + "Frost", "Dew", "Mist", "Haze", "Fog", + "Breeze", "Gust", "Gale", "Squall", "Tempest", + "Aurora", "Solstice", "Equinox", "Eclipse", "Dawn", + "Dusk", "Twilight", "Ember", "Spark", "Flame", + "Petal", "Bloom", "Blossom", "Sprout", "Seedling", + "Sapling", "Root", "Bark", "Sap", "Nectar", + "Pollen", "Spore", "Frond", "Tendril", "Vine", + "Bramble", "Nettle", "Sorrel", "Yarrow", "Tansy", + "Mushroom", "Fungi", "Truffle", "Morel", "Chanterelle" + ], + "warriors": [ + "Spartan", "Samurai", "Viking", "Shogun", "Ronin", + "Gladiator", "Centurion", "Legionary", "Tribune", "Praetor", + "Knight", "Paladin", "Templar", "Crusader", "Sentinel", + "Warden", "Marshal", "Captain", "General", "Admiral", + "Vanguard", "Raider", "Berserker", "Valkyrie", "Einherjar", + "Ninja", "Shinobi", "Kenshi", "Bushido", "Daimyo", + "Mongol", "Khan", "Sultan", "Shah", "Pharaoh", + "Legatus", "Consul", "Imperator", "Caesar", "Augustus", + "Hussar", "Cossack", "Janissary", "Mamluk", "Saracen", + "Apache", "Comanche", "Mohawk", "Zulu", "Aztec", + "Mayan", "Incan", "Sparrow", "Hawk", "Falcon", + "Talon", "Fang", "Claw", "Tusk", "Horn", + "Blade", "Saber", "Rapier", "Cutlass", "Katana", + "Claymore", "Scimitar", "Halberd", "Glaive", "Pike", + "Lance", "Javelin", "Spear", "Trident", "Mace", + "Flail", "Hammer", "Maul", "Axe", "Tomahawk", + "Dagger", "Stiletto", "Dirk", "Kunai", "Shuriken", + "Bow", "Longbow", "Crossbow", "Bolt", "Arrow", + "Quiver", "Shield", "Buckler", "Aegis", "Bulwark", + "Rampart", "Bastion", "Citadel", "Fortress", "Garrison", + "Helm", "Visor", "Gauntlet", "Bracer", "Greave", + "Cuirass", "Brigand", "Chainmail", "Plate", "Scale", + "Banner", "Standard", "Crest", "Sigil", "Herald", + "Siege", "Assault", "Charge", "Rally", "Flanker", + "Scout", "Ranger", "Sniper", "Archer", "Lancer", + "Dragoon", "Cavalry", "Infantry", "Legion", "Phalanx", + "Cohort", "Regiment", "Battalion", "Brigade", "Platoon", + "Striker", "Brawler", "Duelist", "Champion", "Conqueror", + "Warlord", "Overlord", "Chieftain", "Thane", "Jarl", + "Reaver", "Marauder", "Pillager", "Ravager", "Slayer", + "Titan", "Colossus", "Juggernaut", "Goliath", "Ajax", + "Hector", "Achilles", "Leonidas", "Hannibal", "Attila", + "Genghis", "Alexander", "Boudicca", "Shaka", "Saladin", + "Agincourt", "Thermopylae", "Troy", "Carthage", "Masada", + "Valor", "Honor", "Glory", "Fury", "Wrath" + ], + "music": [ + "Tempo", "Rhythm", "Melody", "Harmony", "Chord", + "Riff", "Solo", "Verse", "Chorus", "Bridge", + "Cadence", "Crescendo", "Forte", "Piano", "Allegro", + "Adagio", "Vivace", "Presto", "Staccato", "Legato", + "Vibrato", "Tremolo", "Glissando", "Arpeggio", "Pizzicato", + "Fermata", "Sforzando", "Rubato", "Ostinato", "Coda", + "Treble", "Bass", "Alto", "Tenor", "Soprano", + "Baritone", "Falsetto", "Contralto", "Aria", "Duet", + "Sonata", "Fugue", "Prelude", "Nocturne", "Etude", + "Opus", "Requiem", "Ballad", "Anthem", "Hymn", + "Lyric", "Serenade", "Caprice", "Fantasia", "Overture", + "Minuet", "Waltz", "Bolero", "Tango", "Mambo", + "Samba", "Rumba", "Salsa", "Swing", "Bebop", + "Funk", "Soul", "Blues", "Jazz", "Gospel", + "Grunge", "Punk", "Metal", "Rock", "Indie", + "Techno", "Trance", "House", "Dubstep", "Ambient", + "Synth", "Drone", "Fuzz", "Wah", "Reverb", + "Echo", "Loop", "Beat", "Drop", "Groove", + "Fiddle", "Banjo", "Lute", "Harp", "Lyre", + "Viola", "Cello", "Oboe", "Flute", "Fife", + "Bugle", "Cornet", "Trumpet", "Trombone", "Tuba", + "Sax", "Clarinet", "Bassoon", "Piccolo", "Organ", + "Cymbal", "Gong", "Chime", "Bell", "Snare", + "Tambourine", "Bongo", "Conga", "Djembe", "Tabla", + "Sitar", "Shamisen", "Koto", "Guzheng", "Erhu", + "Raga", "Mantra", "Chant", "Dirge", "Elegy", + "Refrain", "Motif", "Theme", "Encore", "Finale", + "Pitch", "Tone", "Note", "Scale", "Octave", + "Sharp", "Flat", "Major", "Minor", "Modal", + "Acoustic", "Electric", "Muted", "Resonant", "Harmonic", + "Sonic", "Stereo", "Mono", "Vinyl", "Track", + "Album", "Mixtape", "Jam", "Gig", "Venue", + "Stage", "Amp", "Speaker", "Mic", "Reed", + "Bow", "Pick", "Slide", "Hammer", "Mallet", + "Timpani", "Marimba", "Celesta", "Dulcimer", "Zither" + ], + "literature": [ + "Prose", "Verse", "Stanza", "Sonnet", "Haiku", + "Limerick", "Ballad", "Ode", "Epic", "Saga", + "Fable", "Myth", "Legend", "Tale", "Lore", + "Tome", "Codex", "Scroll", "Grimoire", "Almanac", + "Quill", "Ink", "Vellum", "Parchment", "Folio", + "Preface", "Prologue", "Epilogue", "Chapter", "Canto", + "Epitaph", "Elegy", "Dirge", "Psalm", "Hymn", + "Allegory", "Parable", "Satire", "Parody", "Irony", + "Motif", "Theme", "Trope", "Genre", "Canon", + "Muse", "Oracle", "Bard", "Scribe", "Sage", + "Hamlet", "Othello", "Prospero", "Oberon", "Titania", + "Puck", "Ariel", "Caliban", "Portia", "Cordelia", + "Macbeth", "Banquo", "Lear", "Juliet", "Romeo", + "Brutus", "Cassius", "Shylock", "Mercutio", "Falstaff", + "Gatsby", "Atticus", "Scout", "Holden", "Pip", + "Huck", "Ishmael", "Ahab", "Queequeg", "Dorian", + "Darcy", "Heathcliff", "Rochester", "Eyre", "Estella", + "Cosette", "Valjean", "Quixote", "Sancho", "Dulcinea", + "Odysseus", "Aeneas", "Beowulf", "Gilgamesh", "Roland", + "Percival", "Galahad", "Lancelot", "Gawain", "Merlin", + "Scheherazade", "Sinbad", "Aladdin", "Mowgli", "Baloo", + "Nemo", "Moreau", "Jekyll", "Hyde", "Dracula", + "Renfield", "Harker", "Shelley", "Bronte", "Austen", + "Poe", "Frost", "Wilde", "Twain", "Dickens", + "Tolstoy", "Kafka", "Orwell", "Hemingway", "Faulkner", + "Woolf", "Plath", "Dumas", "Hugo", "Verne", + "Cipher", "Riddle", "Enigma", "Rune", "Glyph", + "Sigil", "Symbol", "Token", "Emblem", "Crest", + "Aria", "Lyric", "Rhyme", "Meter", "Cadence", + "Pathos", "Ethos", "Logos", "Hubris", "Nemesis", + "Catharsis", "Climax", "Zenith", "Nadir", "Pivot", + "Twist", "Reverie", "Whimsy", "Satyr", "Nymph", + "Sprite", "Wraith", "Phantom", "Specter", "Shade", + "Vestige", "Relic", "Echo", "Mirage", "Revenant", + "Requiem", "Opus", "Magnum", "Novella", "Lexicon" + ], + "gems": [ + "Ruby", "Sapphire", "Emerald", "Diamond", "Topaz", + "Amethyst", "Opal", "Pearl", "Garnet", "Peridot", + "Onyx", "Jade", "Agate", "Jasper", "Quartz", + "Citrine", "Zircon", "Beryl", "Spinel", "Tanzanite", + "Tourmaline", "Morganite", "Kunzite", "Iolite", "Larimar", + "Sunstone", "Moonstone", "Bloodstone", "Lodestone", "Heliodor", + "Chrysolite", "Alexandrite", "Andalusite", "Labradorite", "Amazonite", + "Rhodonite", "Sodalite", "Fluorite", "Calcite", "Pyrite", + "Galena", "Bauxite", "Magnetite", "Hematite", "Malachite", + "Azurite", "Turquoise", "Lapis", "Lazuli", "Carnelian", + "Sardonyx", "Chalcedony", "Aventurine", "Obsidian", "Granite", + "Marble", "Basalt", "Pumice", "Slate", "Flint", + "Amber", "Coral", "Ivory", "Jet", "Nacre", + "Gold", "Silver", "Platinum", "Copper", "Bronze", + "Iron", "Steel", "Titanium", "Cobalt", "Nickel", + "Chrome", "Zinc", "Tin", "Lead", "Brass", + "Pewter", "Electrum", "Palladium", "Rhodium", "Iridium", + "Osmium", "Ruthenium", "Tungsten", "Bismuth", "Lithium", + "Cesium", "Radium", "Gallium", "Indium", "Thallium", + "Strontium", "Barium", "Cadmium", "Antimony", "Tellurium", + "Selenite", "Celestite", "Sphalerite", "Kyanite", "Prehnite", + "Apatite", "Danburite", "Howlite", "Lepidolite", "Sugilite", + "Charoite", "Seraphinite", "Moldavite", "Tektite", "Shungite", + "Crystal", "Prism", "Facet", "Carat", "Luster", + "Brilliance", "Clarity", "Sparkle", "Shimmer", "Glimmer", + "Gleam", "Sheen", "Polish", "Ore", "Vein", + "Nugget", "Ingot", "Alloy", "Forge", "Smelt", + "Temper", "Anneal", "Refine", "Crucible", "Slag", + "Geode", "Druzy", "Cabochon", "Cameo", "Inlay", + "Filigree", "Gilt", "Patina", "Verdigris", "Tarnish", + "Shard", "Sliver", "Splinter", "Fragment", "Chip", + "Matrix", "Stratum", "Seam", "Deposit", "Quarry", + "Cavern", "Grotto", "Trove", "Cache", "Vault", + "Crown", "Diadem", "Tiara", "Circlet", "Coronet", + "Amulet", "Talisman", "Pendant", "Brooch", "Signet" + ], + "ocean": [ + "Tide", "Wave", "Surf", "Swell", "Crest", + "Trough", "Breaker", "Ripple", "Current", "Drift", + "Riptide", "Undertow", "Maelstrom", "Whirlpool", "Eddy", + "Tsunami", "Surge", "Deluge", "Torrent", "Cascade", + "Coral", "Reef", "Atoll", "Lagoon", "Shoal", + "Sandbar", "Islet", "Archipelago", "Fjord", "Inlet", + "Cove", "Bay", "Gulf", "Sound", "Strait", + "Channel", "Harbor", "Marina", "Pier", "Wharf", + "Dock", "Jetty", "Quay", "Beacon", "Lighthouse", + "Anchor", "Helm", "Rudder", "Keel", "Hull", + "Bow", "Stern", "Port", "Mast", "Sail", + "Rigging", "Galley", "Frigate", "Sloop", "Schooner", + "Clipper", "Brigantine", "Corsair", "Buccaneer", "Mariner", + "Skipper", "Bosun", "Navigator", "Helmsman", "Admiral", + "Commodore", "Captain", "Privateer", "Seafarer", "Voyager", + "Kraken", "Leviathan", "Siren", "Selkie", "Nereid", + "Triton", "Poseidon", "Neptune", "Calypso", "Charybdis", + "Scylla", "Davy", "Locker", "Nautilus", "Nemo", + "Whale", "Orca", "Dolphin", "Narwhal", "Porpoise", + "Shark", "Barracuda", "Marlin", "Sailfish", "Swordfish", + "Manta", "Stingray", "Moray", "Grouper", "Snapper", + "Tuna", "Mackerel", "Herring", "Sardine", "Anchovy", + "Squid", "Octopus", "Ammonite", "Cuttlefish", "Jellyfish", + "Anemone", "Urchin", "Starfish", "Seahorse", "Conch", + "Abalone", "Clam", "Oyster", "Mussel", "Scallop", + "Lobster", "Crab", "Shrimp", "Barnacle", "Kelp", + "Seaweed", "Plankton", "Algae", "Brine", "Foam", + "Spray", "Spume", "Mist", "Salt", "Pearl", + "Trench", "Abyss", "Fathom", "Depth", "Benthic", + "Pelagic", "Abyssal", "Hadal", "Tidal", "Littoral", + "Estuary", "Delta", "Mangrove", "Marsh", "Wetland", + "Driftwood", "Flotsam", "Jetsam", "Wreck", "Salvage", + "Bounty", "Plunder", "Treasure", "Doubloon", "Cutlass", + "Cannon", "Broadside", "Mainsail", "Spinnaker", "Jib", + "Bowsprit", "Gangway", "Porthole", "Bulkhead", "Bilge" + ], + "weather": [ + "Storm", "Tempest", "Squall", "Gale", "Gust", + "Breeze", "Zephyr", "Draft", "Whirlwind", "Tornado", + "Cyclone", "Typhoon", "Hurricane", "Monsoon", "Sirocco", + "Mistral", "Chinook", "Foehn", "Harmattan", "Tramontane", + "Thunder", "Lightning", "Bolt", "Flash", "Strike", + "Rumble", "Crack", "Boom", "Clap", "Roar", + "Rain", "Drizzle", "Shower", "Downpour", "Deluge", + "Torrent", "Cloudburst", "Sprinkle", "Mizzle", "Sleet", + "Snow", "Blizzard", "Flurry", "Frost", "Ice", + "Hail", "Rime", "Hoarfrost", "Glaze", "Verglas", + "Fog", "Mist", "Haze", "Smog", "Murk", + "Overcast", "Gloom", "Shadow", "Shade", "Dusk", + "Cloud", "Cumulus", "Stratus", "Cirrus", "Nimbus", + "Cumulonimbus", "Alto", "Wisp", "Veil", "Canopy", + "Anvil", "Billow", "Plume", "Column", "Front", + "Ridge", "Trough", "Vortex", "Eye", "Funnel", + "Twister", "Spout", "Surge", "Flood", "Flicker", + "Cascade", "Gush", "Swell", "Crest", "Tide", + "Drought", "Arid", "Parch", "Scorch", "Blaze", + "Ember", "Kindle", "Sear", "Wither", "Dustbowl", + "Heat", "Warmth", "Balmy", "Swelter", "Muggy", + "Humid", "Sultry", "Tropic", "Equator", "Solstice", + "Equinox", "Aurora", "Borealis", "Mirage", "Shimmer", + "Glint", "Gleam", "Radiance", "Glow", "Halo", + "Rainbow", "Prism", "Spectrum", "Arc", "Bow", + "Dew", "Droplet", "Puddle", "Rivulet", "Stream", + "Thaw", "Melt", "Freeze", "Chill", "Nip", + "Bite", "Brisk", "Crisp", "Cool", "Cold", + "Frigid", "Glacial", "Polar", "Arctic", "Tundra", + "Permafrost", "Icecap", "Avalanche", "Snowdrift", "Whiteout", + "Barometer", "Pressure", "Isobar", "Celsius", "Kelvin", + "Climate", "Season", "Forecast", "Outlook", "Pattern", + "Windchill", "Dewpoint", "Updraft", "Downdraft", "Jetstream", + "Tradewind", "Doldrums", "Lull", "Calm", "Serene", + "Clear", "Sunny", "Bright", "Radiant", "Blazing" + ] +} From 2ac6c23577a39a740cf2d6de851c45e113536f89 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Wed, 11 Feb 2026 21:45:04 +0100 Subject: [PATCH 3/5] Add name suggestion engine with API endpoint and tests Expand services/naming.py with suggest_names() that picks random words from a category while excluding nicknames already used in the run. Add GET /runs/{run_id}/name-suggestions?count=10 endpoint that reads the run's naming_scheme and returns filtered suggestions. Includes 12 unit tests covering selection, exclusion, exhaustion, and cross-category independence. Co-Authored-By: Claude Opus 4.6 --- ...cker-c6ly--build-name-suggestion-engine.md | 14 ++-- .../nuzlocke-tracker-igl3--name-generation.md | 4 +- backend/src/app/api/runs.py | 27 +++++- backend/src/app/services/naming.py | 27 ++++++ backend/tests/test_naming.py | 84 +++++++++++++++++++ 5 files changed, 146 insertions(+), 10 deletions(-) create mode 100644 backend/tests/test_naming.py diff --git a/.beans/nuzlocke-tracker-c6ly--build-name-suggestion-engine.md b/.beans/nuzlocke-tracker-c6ly--build-name-suggestion-engine.md index 7529b85..e4f8ed6 100644 --- a/.beans/nuzlocke-tracker-c6ly--build-name-suggestion-engine.md +++ b/.beans/nuzlocke-tracker-c6ly--build-name-suggestion-engine.md @@ -1,11 +1,11 @@ --- # nuzlocke-tracker-c6ly title: Build name suggestion engine -status: todo +status: completed type: task priority: normal created_at: 2026-02-11T15:56:44Z -updated_at: 2026-02-11T20:23:35Z +updated_at: 2026-02-11T20:44:27Z parent: nuzlocke-tracker-igl3 blocking: - nuzlocke-tracker-bi4e @@ -29,8 +29,8 @@ Build the backend service and API endpoint that picks random name suggestions fr ## Checklist -- [ ] Create a service module for name suggestion logic (e.g. `services/name_suggestions.py`) -- [ ] Implement dictionary loading with in-memory caching -- [ ] Implement random selection from a category with exclusion of used names -- [ ] Add API endpoint for fetching suggestions -- [ ] Add unit tests for the suggestion logic \ No newline at end of file +- [x] Create a service module for name suggestion logic (e.g. `services/name_suggestions.py`) +- [x] Implement dictionary loading with in-memory caching +- [x] Implement random selection from a category with exclusion of used names +- [x] Add API endpoint for fetching suggestions +- [x] Add unit tests for the suggestion logic \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-igl3--name-generation.md b/.beans/nuzlocke-tracker-igl3--name-generation.md index a4d739c..a6c3cff 100644 --- a/.beans/nuzlocke-tracker-igl3--name-generation.md +++ b/.beans/nuzlocke-tracker-igl3--name-generation.md @@ -5,7 +5,7 @@ status: todo type: epic priority: normal created_at: 2026-02-05T13:45:15Z -updated_at: 2026-02-11T20:41:56Z +updated_at: 2026-02-11T20:44:23Z --- Implement a dictionary-based nickname generation system for Nuzlocke runs. Instead of using an LLM API to generate names on the fly, provide a static dictionary of words categorised by theme. A word can belong to multiple categories, making it usable across different naming schemes. @@ -28,7 +28,7 @@ Implement a dictionary-based nickname generation system for Nuzlocke runs. Inste ## Success Criteria - [x] Word dictionary data file exists with multiple categories, each containing 150-200 words -- [ ] Name suggestion engine picks random names from the selected category, avoiding duplicates already used in the run +- [x] Name suggestion engine picks random names from the selected category, avoiding duplicates already used in the run - [ ] Encounter registration UI shows 5-10 clickable name suggestions - [ ] User can regenerate suggestions if none fit - [x] User can select a naming scheme per run \ No newline at end of file diff --git a/backend/src/app/api/runs.py b/backend/src/app/api/runs.py index a63e296..69ccea0 100644 --- a/backend/src/app/api/runs.py +++ b/backend/src/app/api/runs.py @@ -19,7 +19,7 @@ from app.schemas.run import ( RunResponse, RunUpdate, ) -from app.services.naming import get_naming_categories +from app.services.naming import get_naming_categories, suggest_names router = APIRouter() @@ -29,6 +29,31 @@ async def list_naming_categories(): return get_naming_categories() +@router.get("/{run_id}/name-suggestions", response_model=list[str]) +async def get_name_suggestions( + run_id: int, + count: int = 10, + session: AsyncSession = Depends(get_session), +): + run = await session.get(NuzlockeRun, run_id) + if run is None: + raise HTTPException(status_code=404, detail="Run not found") + + if not run.naming_scheme: + return [] + + # Collect nicknames already used in this run + result = await session.execute( + select(Encounter.nickname).where( + Encounter.run_id == run_id, + Encounter.nickname.isnot(None), + ) + ) + used_names = {row[0] for row in result} + + return suggest_names(run.naming_scheme, used_names, count) + + @router.post("", response_model=RunResponse, status_code=201) async def create_run(data: RunCreate, session: AsyncSession = Depends(get_session)): # Validate game exists diff --git a/backend/src/app/services/naming.py b/backend/src/app/services/naming.py index 42d9305..83a0533 100644 --- a/backend/src/app/services/naming.py +++ b/backend/src/app/services/naming.py @@ -1,4 +1,5 @@ import json +import random from functools import lru_cache from pathlib import Path @@ -16,3 +17,29 @@ def _load_dictionary() -> dict[str, list[str]]: def get_naming_categories() -> list[str]: """Return sorted list of available naming category names.""" return sorted(_load_dictionary().keys()) + + +def get_words_for_category(category: str) -> list[str]: + """Return the word list for a category, or empty list if not found.""" + return _load_dictionary().get(category, []) + + +def suggest_names( + category: str, + used_names: set[str], + count: int = 10, +) -> list[str]: + """Pick random name suggestions from a category, excluding used names. + + Returns up to `count` names. If the category is nearly exhausted, + returns fewer. + """ + words = get_words_for_category(category) + if not words: + return [] + + available = [w for w in words if w not in used_names] + if not available: + return [] + + return random.sample(available, min(count, len(available))) diff --git a/backend/tests/test_naming.py b/backend/tests/test_naming.py new file mode 100644 index 0000000..df60250 --- /dev/null +++ b/backend/tests/test_naming.py @@ -0,0 +1,84 @@ +from unittest.mock import patch + +import pytest + +from app.services.naming import ( + get_naming_categories, + get_words_for_category, + suggest_names, +) + +MOCK_DICTIONARY = { + "mythology": ["Apollo", "Athena", "Loki", "Thor", "Zeus"], + "food": ["Basil", "Sage", "Pepper", "Saffron", "Mango"], + "space": ["Apollo", "Nova", "Nebula", "Comet", "Vega"], +} + + +@pytest.fixture(autouse=True) +def _mock_dictionary(): + with patch("app.services.naming._load_dictionary", return_value=MOCK_DICTIONARY): + yield + + +class TestGetNamingCategories: + def test_returns_sorted_categories(self): + result = get_naming_categories() + assert result == ["food", "mythology", "space"] + + def test_returns_empty_for_empty_dictionary(self): + with patch("app.services.naming._load_dictionary", return_value={}): + assert get_naming_categories() == [] + + +class TestGetWordsForCategory: + def test_returns_words_for_valid_category(self): + result = get_words_for_category("mythology") + assert result == ["Apollo", "Athena", "Loki", "Thor", "Zeus"] + + def test_returns_empty_for_unknown_category(self): + assert get_words_for_category("nonexistent") == [] + + +class TestSuggestNames: + def test_returns_requested_count(self): + result = suggest_names("mythology", set(), count=3) + assert len(result) == 3 + assert all(name in MOCK_DICTIONARY["mythology"] for name in result) + + def test_excludes_used_names(self): + used = {"Apollo", "Athena", "Loki"} + result = suggest_names("mythology", used, count=10) + assert set(result) <= {"Thor", "Zeus"} + assert not set(result) & used + + def test_returns_fewer_when_category_nearly_exhausted(self): + used = {"Apollo", "Athena", "Loki", "Thor"} + result = suggest_names("mythology", used, count=10) + assert result == ["Zeus"] + + def test_returns_empty_when_category_fully_exhausted(self): + used = {"Apollo", "Athena", "Loki", "Thor", "Zeus"} + result = suggest_names("mythology", used, count=10) + assert result == [] + + def test_returns_empty_for_unknown_category(self): + result = suggest_names("nonexistent", set(), count=10) + assert result == [] + + def test_no_duplicates_in_suggestions(self): + result = suggest_names("mythology", set(), count=5) + assert len(result) == len(set(result)) + + def test_default_count_is_ten(self): + # food has 5 words, so we should get all 5 + result = suggest_names("food", set()) + assert len(result) == 5 + + def test_cross_category_names_handled_independently(self): + # "Apollo" used in mythology shouldn't affect space + used = {"Apollo"} + mythology_result = suggest_names("mythology", used, count=10) + space_result = suggest_names("space", used, count=10) + assert "Apollo" not in mythology_result + assert "Apollo" not in space_result From 39d18c241e512249de7c38f6fd748fe3ea28d1df Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Wed, 11 Feb 2026 21:48:29 +0100 Subject: [PATCH 4/5] Integrate name suggestions into encounter registration UI Add clickable suggestion chips below the nickname input in the encounter modal. Chips are fetched from GET /runs/{id}/name-suggestions via React Query, shown only when a naming scheme is set. Clicking a chip fills in the nickname; a regenerate button fetches a fresh random batch. Completes the Name Generation epic. Co-Authored-By: Claude Opus 4.6 --- ...ame-suggestions-into-encounter-registra.md | 16 +++---- .../nuzlocke-tracker-igl3--name-generation.md | 8 ++-- frontend/src/api/runs.ts | 4 ++ frontend/src/components/EncounterModal.tsx | 42 +++++++++++++++++++ frontend/src/hooks/useRuns.ts | 10 ++++- frontend/src/pages/RunEncounters.tsx | 2 + 6 files changed, 69 insertions(+), 13 deletions(-) diff --git a/.beans/nuzlocke-tracker-bi4e--integrate-name-suggestions-into-encounter-registra.md b/.beans/nuzlocke-tracker-bi4e--integrate-name-suggestions-into-encounter-registra.md index 899ded3..9b9e6ce 100644 --- a/.beans/nuzlocke-tracker-bi4e--integrate-name-suggestions-into-encounter-registra.md +++ b/.beans/nuzlocke-tracker-bi4e--integrate-name-suggestions-into-encounter-registra.md @@ -1,11 +1,11 @@ --- # nuzlocke-tracker-bi4e title: Integrate name suggestions into encounter registration UI -status: todo +status: completed type: task priority: normal created_at: 2026-02-11T15:56:44Z -updated_at: 2026-02-11T20:23:40Z +updated_at: 2026-02-11T20:48:02Z parent: nuzlocke-tracker-igl3 --- @@ -27,9 +27,9 @@ Show name suggestions in the encounter registration flow so users can pick a nic ## Checklist -- [ ] Add a name suggestions component (chips/buttons with regenerate) -- [ ] Integrate the component into the encounter registration modal/form -- [ ] Wire up the backend API endpoint to the component via React Query -- [ ] Ensure clicking a suggestion populates the nickname field -- [ ] Ensure regenerate fetches a new batch from the API -- [ ] Hide suggestions gracefully if no naming scheme is set on the run \ No newline at end of file +- [x] Add a name suggestions component (chips/buttons with regenerate) +- [x] Integrate the component into the encounter registration modal/form +- [x] Wire up the backend API endpoint to the component via React Query +- [x] Ensure clicking a suggestion populates the nickname field +- [x] Ensure regenerate fetches a new batch from the API +- [x] Hide suggestions gracefully if no naming scheme is set on the run \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-igl3--name-generation.md b/.beans/nuzlocke-tracker-igl3--name-generation.md index a6c3cff..b29cc27 100644 --- a/.beans/nuzlocke-tracker-igl3--name-generation.md +++ b/.beans/nuzlocke-tracker-igl3--name-generation.md @@ -1,11 +1,11 @@ --- # nuzlocke-tracker-igl3 title: Name Generation -status: todo +status: completed type: epic priority: normal created_at: 2026-02-05T13:45:15Z -updated_at: 2026-02-11T20:44:23Z +updated_at: 2026-02-11T20:48:02Z --- Implement a dictionary-based nickname generation system for Nuzlocke runs. Instead of using an LLM API to generate names on the fly, provide a static dictionary of words categorised by theme. A word can belong to multiple categories, making it usable across different naming schemes. @@ -29,6 +29,6 @@ Implement a dictionary-based nickname generation system for Nuzlocke runs. Inste - [x] Word dictionary data file exists with multiple categories, each containing 150-200 words - [x] Name suggestion engine picks random names from the selected category, avoiding duplicates already used in the run -- [ ] Encounter registration UI shows 5-10 clickable name suggestions -- [ ] User can regenerate suggestions if none fit +- [x] Encounter registration UI shows 5-10 clickable name suggestions +- [x] User can regenerate suggestions if none fit - [x] User can select a naming scheme per run \ No newline at end of file diff --git a/frontend/src/api/runs.ts b/frontend/src/api/runs.ts index 2a7e4d4..1757bcb 100644 --- a/frontend/src/api/runs.ts +++ b/frontend/src/api/runs.ts @@ -32,3 +32,7 @@ export function deleteRun(id: number): Promise { export function getNamingCategories(): Promise { return api.get('/runs/naming-categories') } + +export function getNameSuggestions(runId: number, count = 10): Promise { + return api.get(`/runs/${runId}/name-suggestions?count=${count}`) +} diff --git a/frontend/src/components/EncounterModal.tsx b/frontend/src/components/EncounterModal.tsx index cd8d3a8..6c38032 100644 --- a/frontend/src/components/EncounterModal.tsx +++ b/frontend/src/components/EncounterModal.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useMemo } from 'react' import { useRoutePokemon } from '../hooks/useGames' +import { useNameSuggestions } from '../hooks/useRuns' import { EncounterMethodBadge, getMethodLabel, @@ -15,6 +16,8 @@ import type { interface EncounterModalProps { route: Route gameId: number + runId: number + namingScheme?: string | null existing?: EncounterDetail dupedPokemonIds?: Set retiredPokemonIds?: Set @@ -92,6 +95,8 @@ function pickRandomPokemon( export function EncounterModal({ route, gameId, + runId, + namingScheme, existing, dupedPokemonIds, retiredPokemonIds, @@ -120,6 +125,10 @@ export function EncounterModal({ const isEditing = !!existing + const showSuggestions = !!namingScheme && status === 'caught' && !isEditing + const { data: suggestions, refetch: regenerate, isFetching: loadingSuggestions } = + useNameSuggestions(showSuggestions ? runId : null) + // Pre-select pokemon when editing useEffect(() => { if (existing && routePokemon) { @@ -380,6 +389,39 @@ export function EncounterModal({ placeholder="Give it a name..." className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500" /> + {showSuggestions && suggestions && suggestions.length > 0 && ( +
+
+ + Suggestions ({namingScheme}) + + +
+
+ {suggestions.map((name) => ( + + ))} +
+
+ )}
)} diff --git a/frontend/src/hooks/useRuns.ts b/frontend/src/hooks/useRuns.ts index 7843523..8e65126 100644 --- a/frontend/src/hooks/useRuns.ts +++ b/frontend/src/hooks/useRuns.ts @@ -1,6 +1,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' -import { getRuns, getRun, createRun, updateRun, deleteRun, getNamingCategories } from '../api/runs' +import { getRuns, getRun, createRun, updateRun, deleteRun, getNamingCategories, getNameSuggestions } from '../api/runs' import type { CreateRunInput, UpdateRunInput } from '../types/game' export function useRuns() { @@ -59,3 +59,11 @@ export function useNamingCategories() { staleTime: Infinity, }) } + +export function useNameSuggestions(runId: number | null) { + return useQuery({ + queryKey: ['name-suggestions', runId], + queryFn: () => getNameSuggestions(runId!), + enabled: runId !== null, + }) +} diff --git a/frontend/src/pages/RunEncounters.tsx b/frontend/src/pages/RunEncounters.tsx index ec50e65..7b07b43 100644 --- a/frontend/src/pages/RunEncounters.tsx +++ b/frontend/src/pages/RunEncounters.tsx @@ -1431,6 +1431,8 @@ export function RunEncounters() { Date: Wed, 11 Feb 2026 21:51:35 +0100 Subject: [PATCH 5/5] Add merge migration; COmmit bean changes --- ...d-boss-data-to-seed-files-for-all-games.md | 11 ++++++++ ...g-scheme-selection-to-run-configuration.md | 4 +-- ...eparate-seedinit-container-after-pokedb.md | 4 +-- ...ca323_merge_naming_scheme_and_genlocke_.py | 28 +++++++++++++++++++ 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 .beans/nuzlocke-tracker-l272--add-boss-data-to-seed-files-for-all-games.md create mode 100644 backend/src/app/alembic/versions/e5f70a1ca323_merge_naming_scheme_and_genlocke_.py diff --git a/.beans/nuzlocke-tracker-l272--add-boss-data-to-seed-files-for-all-games.md b/.beans/nuzlocke-tracker-l272--add-boss-data-to-seed-files-for-all-games.md new file mode 100644 index 0000000..7431416 --- /dev/null +++ b/.beans/nuzlocke-tracker-l272--add-boss-data-to-seed-files-for-all-games.md @@ -0,0 +1,11 @@ +--- +# nuzlocke-tracker-l272 +title: Add boss data to seed files for all games +status: completed +type: feature +priority: normal +created_at: 2026-02-11T20:23:20Z +updated_at: 2026-02-11T20:31:27Z +--- + +Add gym leaders, Elite Four, champions and equivalents for all remaining games. Add kahuna and totem boss types for Alola games. Create 11 new seed files, complete 2 existing ones, and update frontend types/UI for new boss types. \ No newline at end of file diff --git a/.beans/nuzlocke-tracker-m86o--add-naming-scheme-selection-to-run-configuration.md b/.beans/nuzlocke-tracker-m86o--add-naming-scheme-selection-to-run-configuration.md index dda5241..80dfd86 100644 --- a/.beans/nuzlocke-tracker-m86o--add-naming-scheme-selection-to-run-configuration.md +++ b/.beans/nuzlocke-tracker-m86o--add-naming-scheme-selection-to-run-configuration.md @@ -1,11 +1,11 @@ --- # nuzlocke-tracker-m86o title: Add naming scheme selection to run configuration -status: in-progress +status: completed type: task priority: normal created_at: 2026-02-11T15:56:44Z -updated_at: 2026-02-11T20:35:59Z +updated_at: 2026-02-11T20:37:00Z parent: nuzlocke-tracker-igl3 blocking: - nuzlocke-tracker-bi4e diff --git a/.beans/nuzlocke-tracker-spx3--evaluate-separate-seedinit-container-after-pokedb.md b/.beans/nuzlocke-tracker-spx3--evaluate-separate-seedinit-container-after-pokedb.md index 11cd381..b86d2ec 100644 --- a/.beans/nuzlocke-tracker-spx3--evaluate-separate-seedinit-container-after-pokedb.md +++ b/.beans/nuzlocke-tracker-spx3--evaluate-separate-seedinit-container-after-pokedb.md @@ -1,11 +1,11 @@ --- # nuzlocke-tracker-spx3 title: Evaluate separate seed/init container after PokeDB import -status: draft +status: scrapped type: task priority: low created_at: 2026-02-10T14:30:57Z -updated_at: 2026-02-10T14:30:57Z +updated_at: 2026-02-11T20:15:43Z --- After the PokeDB.org data import (beans-bs05) is complete, evaluate whether the seed data has grown enough to justify splitting seeding into a separate init container. diff --git a/backend/src/app/alembic/versions/e5f70a1ca323_merge_naming_scheme_and_genlocke_.py b/backend/src/app/alembic/versions/e5f70a1ca323_merge_naming_scheme_and_genlocke_.py new file mode 100644 index 0000000..0d02cc6 --- /dev/null +++ b/backend/src/app/alembic/versions/e5f70a1ca323_merge_naming_scheme_and_genlocke_.py @@ -0,0 +1,28 @@ +"""merge naming_scheme and genlocke_transfers + +Revision ID: e5f70a1ca323 +Revises: e5f6a7b8c9d1, e5f6a7b9c0d1 +Create Date: 2026-02-11 21:49:29.942841 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e5f70a1ca323' +down_revision: Union[str, Sequence[str], None] = ('e5f6a7b8c9d1', 'e5f6a7b9c0d1') +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + pass + + +def downgrade() -> None: + """Downgrade schema.""" + pass