2026-02-07 13:12:56 +01:00
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
2026-02-05 15:09:05 +01:00
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Response
|
2026-02-09 09:47:28 +01:00
|
|
|
from sqlalchemy import func, select
|
2026-02-05 15:09:05 +01:00
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
from sqlalchemy.orm import joinedload, selectinload
|
|
|
|
|
|
|
|
|
|
from app.core.database import get_session
|
|
|
|
|
from app.models.encounter import Encounter
|
|
|
|
|
from app.models.game import Game
|
2026-02-09 09:47:28 +01:00
|
|
|
from app.models.genlocke import Genlocke, GenlockeLeg
|
2026-02-05 15:09:05 +01:00
|
|
|
from app.models.nuzlocke_run import NuzlockeRun
|
2026-02-09 09:47:28 +01:00
|
|
|
from app.schemas.run import RunCreate, RunDetailResponse, RunGenlockeContext, RunResponse, RunUpdate
|
2026-02-05 15:09:05 +01:00
|
|
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("", response_model=RunResponse, status_code=201)
|
|
|
|
|
async def create_run(
|
|
|
|
|
data: RunCreate, session: AsyncSession = Depends(get_session)
|
|
|
|
|
):
|
|
|
|
|
# Validate game exists
|
|
|
|
|
game = await session.get(Game, data.game_id)
|
|
|
|
|
if game is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Game not found")
|
|
|
|
|
|
|
|
|
|
run = NuzlockeRun(
|
|
|
|
|
game_id=data.game_id,
|
|
|
|
|
name=data.name,
|
|
|
|
|
status="active",
|
|
|
|
|
rules=data.rules,
|
|
|
|
|
)
|
|
|
|
|
session.add(run)
|
|
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(run)
|
|
|
|
|
return run
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("", response_model=list[RunResponse])
|
|
|
|
|
async def list_runs(session: AsyncSession = Depends(get_session)):
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
select(NuzlockeRun).order_by(NuzlockeRun.started_at.desc())
|
|
|
|
|
)
|
|
|
|
|
return result.scalars().all()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/{run_id}", response_model=RunDetailResponse)
|
|
|
|
|
async def get_run(run_id: int, session: AsyncSession = Depends(get_session)):
|
|
|
|
|
result = await session.execute(
|
|
|
|
|
select(NuzlockeRun)
|
|
|
|
|
.where(NuzlockeRun.id == run_id)
|
|
|
|
|
.options(
|
|
|
|
|
joinedload(NuzlockeRun.game),
|
|
|
|
|
selectinload(NuzlockeRun.encounters)
|
|
|
|
|
.joinedload(Encounter.pokemon),
|
|
|
|
|
selectinload(NuzlockeRun.encounters)
|
2026-02-05 19:26:49 +01:00
|
|
|
.joinedload(Encounter.current_pokemon),
|
|
|
|
|
selectinload(NuzlockeRun.encounters)
|
2026-02-05 15:09:05 +01:00
|
|
|
.joinedload(Encounter.route),
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
run = result.scalar_one_or_none()
|
|
|
|
|
if run is None:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Run not found")
|
2026-02-09 09:47:28 +01:00
|
|
|
|
|
|
|
|
# Check if this run belongs to a genlocke
|
|
|
|
|
genlocke_context = None
|
|
|
|
|
leg_result = await session.execute(
|
|
|
|
|
select(GenlockeLeg)
|
|
|
|
|
.where(GenlockeLeg.run_id == run_id)
|
|
|
|
|
.options(joinedload(GenlockeLeg.genlocke))
|
|
|
|
|
)
|
|
|
|
|
leg = leg_result.scalar_one_or_none()
|
|
|
|
|
if leg:
|
|
|
|
|
total_legs_result = await session.execute(
|
|
|
|
|
select(func.count())
|
|
|
|
|
.select_from(GenlockeLeg)
|
|
|
|
|
.where(GenlockeLeg.genlocke_id == leg.genlocke_id)
|
|
|
|
|
)
|
|
|
|
|
total_legs = total_legs_result.scalar_one()
|
|
|
|
|
genlocke_context = RunGenlockeContext(
|
|
|
|
|
genlocke_id=leg.genlocke_id,
|
|
|
|
|
genlocke_name=leg.genlocke.name,
|
|
|
|
|
leg_order=leg.leg_order,
|
|
|
|
|
total_legs=total_legs,
|
|
|
|
|
is_final_leg=leg.leg_order == total_legs,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
response = RunDetailResponse.model_validate(run)
|
|
|
|
|
response.genlocke = genlocke_context
|
|
|
|
|
return response
|
2026-02-05 15:09:05 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.patch("/{run_id}", response_model=RunResponse)
|
|
|
|
|
async def update_run(
|
|
|
|
|
run_id: int,
|
|
|
|
|
data: RunUpdate,
|
|
|
|
|
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")
|
|
|
|
|
|
|
|
|
|
update_data = data.model_dump(exclude_unset=True)
|
2026-02-07 13:12:56 +01:00
|
|
|
|
|
|
|
|
# Auto-set completed_at when ending a run
|
|
|
|
|
if "status" in update_data and update_data["status"] in ("completed", "failed"):
|
|
|
|
|
if run.status != "active":
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=400, detail="Only active runs can be ended"
|
|
|
|
|
)
|
|
|
|
|
update_data["completed_at"] = datetime.now(timezone.utc)
|
|
|
|
|
|
2026-02-05 15:09:05 +01:00
|
|
|
for field, value in update_data.items():
|
|
|
|
|
setattr(run, field, value)
|
|
|
|
|
|
2026-02-09 09:47:28 +01:00
|
|
|
# Genlocke side effects when run status changes
|
|
|
|
|
if "status" in update_data and update_data["status"] in ("completed", "failed"):
|
|
|
|
|
leg_result = await session.execute(
|
|
|
|
|
select(GenlockeLeg)
|
|
|
|
|
.where(GenlockeLeg.run_id == run_id)
|
|
|
|
|
.options(joinedload(GenlockeLeg.genlocke))
|
|
|
|
|
)
|
|
|
|
|
leg = leg_result.scalar_one_or_none()
|
|
|
|
|
if leg:
|
|
|
|
|
genlocke = leg.genlocke
|
|
|
|
|
if update_data["status"] == "failed":
|
|
|
|
|
genlocke.status = "failed"
|
|
|
|
|
elif update_data["status"] == "completed":
|
|
|
|
|
total_legs_result = await session.execute(
|
|
|
|
|
select(func.count())
|
|
|
|
|
.select_from(GenlockeLeg)
|
|
|
|
|
.where(GenlockeLeg.genlocke_id == genlocke.id)
|
|
|
|
|
)
|
|
|
|
|
total_legs = total_legs_result.scalar_one()
|
|
|
|
|
if leg.leg_order == total_legs:
|
|
|
|
|
genlocke.status = "completed"
|
|
|
|
|
|
2026-02-05 15:09:05 +01:00
|
|
|
await session.commit()
|
|
|
|
|
await session.refresh(run)
|
|
|
|
|
return run
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.delete("/{run_id}", status_code=204)
|
|
|
|
|
async def delete_run(
|
|
|
|
|
run_id: int, 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")
|
|
|
|
|
|
|
|
|
|
# Delete associated encounters first
|
|
|
|
|
encounters = await session.execute(
|
|
|
|
|
select(Encounter).where(Encounter.run_id == run_id)
|
|
|
|
|
)
|
|
|
|
|
for enc in encounters.scalars():
|
|
|
|
|
await session.delete(enc)
|
|
|
|
|
|
|
|
|
|
await session.delete(run)
|
|
|
|
|
await session.commit()
|
|
|
|
|
return Response(status_code=204)
|