Enforce Dupes Clause and Shiny Clause rules

Dupes Clause greys out Pokemon in the encounter modal whose evolution
family has already been caught, preventing duplicate selections. Shiny
Clause adds a dedicated Shiny Box and lets shiny catches bypass the
one-per-route lock via a new is_shiny column on encounters and a
/pokemon/families endpoint that computes evolution family groups.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 21:08:25 +01:00
parent 7b7945246d
commit ad1eb0524c
15 changed files with 599 additions and 54 deletions

View File

@@ -12,6 +12,7 @@ from app.schemas.pokemon import (
BulkImportItem,
BulkImportResult,
EvolutionResponse,
FamiliesResponse,
PaginatedPokemonResponse,
PokemonCreate,
PokemonResponse,
@@ -109,6 +110,44 @@ async def create_pokemon(
return pokemon
@router.get("/pokemon/families", response_model=FamiliesResponse)
async def get_pokemon_families(
session: AsyncSession = Depends(get_session),
):
"""Return evolution families as connected components of Pokemon IDs."""
from collections import deque
result = await session.execute(select(Evolution))
evolutions = result.scalars().all()
# Build undirected adjacency list
adj: dict[int, set[int]] = {}
for evo in evolutions:
adj.setdefault(evo.from_pokemon_id, set()).add(evo.to_pokemon_id)
adj.setdefault(evo.to_pokemon_id, set()).add(evo.from_pokemon_id)
# BFS to find connected components
visited: set[int] = set()
families: list[list[int]] = []
for node in adj:
if node in visited:
continue
component: list[int] = []
queue = deque([node])
while queue:
current = queue.popleft()
if current in visited:
continue
visited.add(current)
component.append(current)
for neighbor in adj.get(current, set()):
if neighbor not in visited:
queue.append(neighbor)
families.append(sorted(component))
return FamiliesResponse(families=families)
@router.get("/pokemon/{pokemon_id}", response_model=PokemonResponse)
async def get_pokemon(
pokemon_id: int, session: AsyncSession = Depends(get_session)