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:
@@ -0,0 +1,29 @@
|
||||
"""add is_shiny to encounters
|
||||
|
||||
Revision ID: b1c2d3e4f5a6
|
||||
Revises: f6a7b8c9d0e1
|
||||
Create Date: 2026-02-07 18:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'b1c2d3e4f5a6'
|
||||
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(
|
||||
'encounters',
|
||||
sa.Column('is_shiny', sa.Boolean(), nullable=False, server_default=sa.text('false')),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('encounters', 'is_shiny')
|
||||
@@ -50,8 +50,12 @@ async def create_encounter(
|
||||
detail="Cannot create encounter on a parent route. Use a child route instead.",
|
||||
)
|
||||
|
||||
# Shiny clause: shiny encounters bypass the route-lock check
|
||||
shiny_clause_on = run.rules.get("shinyClause", True) if run.rules else True
|
||||
skip_route_lock = data.is_shiny and shiny_clause_on
|
||||
|
||||
# If this route has a parent, check if sibling already has an encounter
|
||||
if route.parent_route_id is not None:
|
||||
if route.parent_route_id is not None and not skip_route_lock:
|
||||
# Get all sibling routes (routes with same parent, including this one)
|
||||
siblings_result = await session.execute(
|
||||
select(Route).where(Route.parent_route_id == route.parent_route_id)
|
||||
@@ -99,6 +103,7 @@ async def create_encounter(
|
||||
nickname=data.nickname,
|
||||
status=data.status,
|
||||
catch_level=data.catch_level,
|
||||
is_shiny=data.is_shiny,
|
||||
)
|
||||
session.add(encounter)
|
||||
await session.commit()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, SmallInteger, String, func
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, SmallInteger, String, func, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.database import Base
|
||||
@@ -21,6 +21,7 @@ class Encounter(Base):
|
||||
current_pokemon_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("pokemon.id"), index=True
|
||||
)
|
||||
is_shiny: Mapped[bool] = mapped_column(Boolean, default=False, server_default=text("false"))
|
||||
caught_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ class EncounterCreate(CamelModel):
|
||||
nickname: str | None = None
|
||||
status: str
|
||||
catch_level: int | None = None
|
||||
is_shiny: bool = False
|
||||
|
||||
|
||||
class EncounterUpdate(CamelModel):
|
||||
@@ -32,6 +33,7 @@ class EncounterResponse(CamelModel):
|
||||
catch_level: int | None
|
||||
faint_level: int | None
|
||||
death_cause: str | None
|
||||
is_shiny: bool
|
||||
caught_at: datetime
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,10 @@ class EvolutionResponse(CamelModel):
|
||||
region: str | None
|
||||
|
||||
|
||||
class FamiliesResponse(CamelModel):
|
||||
families: list[list[int]]
|
||||
|
||||
|
||||
class RouteEncounterResponse(CamelModel):
|
||||
id: int
|
||||
route_id: int
|
||||
|
||||
Reference in New Issue
Block a user