edh-elo/app/routers/games.py
Jack Jackson e4ea529fbe Cosmetic and final-basic functionality
At this point it _should_ be just about usable for folks to poke-around
in, though ugly as sin.
2024-06-09 09:45:56 -07:00

156 lines
4.9 KiB
Python

import json
import logging
from functional import seq
from typing import List, Mapping
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.routers.decks import list_decks
from app.sql import models
from .players import list_players
from ..elo import rerank
from ..sql import crud, schemas
from ..sql.database import get_db
from ..templates import jinja_templates
api_router = APIRouter(prefix="/game", tags=["game"])
html_router = APIRouter(
prefix="/game", include_in_schema=False, default_response_class=HTMLResponse
)
LOGGER = logging.getLogger(__name__)
########
# API Routes
########
@api_router.post("/", response_model=schemas.Game, status_code=201)
def create_game(game: schemas.GameCreate, db: Session = Depends(get_db)):
created_game = crud.create_game(db=db, game=game)
# Update ELO scores
last_score = (
db.query(models.EloScore).order_by(models.EloScore.after_game_id.desc()).first()
)
if last_score:
last_scored_game_id = last_score.after_game_id
else:
last_scored_game_id = 0
if created_game.id != last_scored_game_id + 1:
# TODO - better error reporting?
LOGGER.error(
f"Created a game with id {created_game.id}, which is not after the last-scored-game-id {last_scored_game_id}. ELO calculation paused."
)
return created_game
deck_ids = [id for id in [getattr(game, f"deck_id_{n+1}") for n in range(6)] if id]
deck_scores_before_this_game = [
crud.get_latest_score_for_deck(db, deck_id) for deck_id in deck_ids
]
new_scores = rerank(
deck_scores_before_this_game, deck_ids.index(game.winning_deck_id)
)
for score, deck_id in zip(new_scores, deck_ids):
db.add(
models.EloScore(after_game_id=created_game.id, deck_id=deck_id, score=score)
)
db.commit()
return created_game
@api_router.get("/list", response_model=list[schemas.Game])
def list_games(skip: int = 0, limit: int = 100, db=Depends(get_db)):
return crud.get_games(db, skip=skip, limit=limit)
@api_router.get("/{game_id}", response_model=schemas.Game)
def read_game(game_id: int, db=Depends(get_db)):
db_game = crud.get_game_by_id(db, game_id)
if db_game is None:
raise HTTPException(status_code=404, detail="Game not found")
return db_game
@api_router.delete("/{game_id}", status_code=204)
def delete_game(game_id: str, db=Depends(get_db)):
crud.delete_game_by_id(db, int(game_id))
########
# HTML Routes
########
@html_router.get("/create", response_class=HTMLResponse)
def game_create_html(request: Request, db=Depends(get_db)):
players = list_players(db=db)
win_types = db.query(models.WinType).all()
return jinja_templates.TemplateResponse(
request,
"games/create.html",
{
"players": players,
# `json.dumps` is necessary because otherwise
# the keys are surrounded with single-quotes,
# on which JavaScript's `JSON.parse` will choke.
"player_decks": json.dumps(
{
str(player.id): [
{key: getattr(deck, key) for key in ["id", "name"]}
for deck in player.decks
]
for player in players
}
),
"win_types": win_types,
},
)
# TODO - pagination
@html_router.get("/list")
def games_html(request: Request, db=Depends(get_db)):
games = list_games(db=db)
# TODO - a more "data-intensive application" implementation would fetch only the decks involved in the games for
# this page
decks = list_decks(db=db, limit=-1)
decks_by_id = {deck.id: deck for deck in decks}
game_names = {game.id: _build_game_deck_names(game, decks_by_id) for game in games}
return jinja_templates.TemplateResponse(
request,
"games/list.html",
{"games": games, "decks_by_id": decks_by_id, "game_names": game_names},
)
def _build_game_deck_names(
game: models.Game, decks_by_id: Mapping[int, models.Deck]
) -> List[str]:
return (
seq(range(6))
.map(lambda i: i + 1)
.map(lambda i: f"deck_id_{i}")
.map(lambda key: getattr(game, key))
.filter(lambda x: x)
.map(lambda deck_id: decks_by_id[deck_id])
.map(lambda deck: {"owner": deck.owner.name, "name": deck.name, "id": deck.id})
)
# This must be after the static-path routes, lest it take priority over them
@html_router.get("/{game_id}")
def game_html(request: Request, game_id: str, db=Depends(get_db)):
game = read_game(game_id, db)
decks = list_decks(db=db, limit=-1)
decks_by_id = {deck.id: deck for deck in decks}
game_deck_names = _build_game_deck_names(game, decks_by_id)
return jinja_templates.TemplateResponse(
request, "games/detail.html", {"game": game, "game_deck_names": game_deck_names}
)