edh-elo/app/routers/suggest.py
2024-09-04 21:49:25 -07:00

146 lines
4.6 KiB
Python

import logging
from collections import defaultdict
from dataclasses import dataclass
from itertools import product
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from sqlalchemy.sql.expression import func
from typing import Callable, List, Mapping, Optional
from app.routers import players
from app.sql import models
from ..sql.database import get_db
from ..templates import jinja_templates
api_router = APIRouter(prefix="/suggest", tags=["suggest"])
html_router = APIRouter(
prefix="/suggest", include_in_schema=False, default_response_class=HTMLResponse
)
LOGGER = logging.getLogger(__name__)
@api_router.get("/matchup")
def suggest_game(
player_ids: Optional[str] = None,
excluded_decks: Optional[str] = None,
strategy_id: Optional[str] = None,
db=Depends(get_db),
):
if not strategy_id:
strategy_id = "smallest_score_spread"
if strategy_id not in STRATEGIES:
raise HTTPException(
status_code=400,
detail=f"Cannot generate a matchup recommendation with strategy {strategy_id}. Available options: {STRATEGIES.keys()}",
)
# TODO - this should be an error, surely!
player_ids_as_list = list(map(int, player_ids.split(","))) if player_ids else []
excluded_decks_as_list = (
list(map(int, excluded_decks.split(","))) if excluded_decks else []
)
LOGGER.info(f"{player_ids=}")
LOGGER.info(f"{excluded_decks=}")
return STRATEGIES[strategy_id].strategy(
player_ids_as_list, excluded_decks_as_list, db
)
def smallest_score_spread_strategy(
player_ids: List[int], excluded_decks: List[int], db: Session
):
row_number_column = (
func.row_number()
.over(
partition_by=[models.Deck.name],
order_by=models.EloScore.id.desc(),
)
.label("row_number")
)
sub_query = (
db.query(
models.Deck.id, models.Deck.name, models.Player.name, models.EloScore.score
)
.join(models.Player, models.Deck.owner_id == models.Player.id)
.join(models.EloScore, models.Deck.id == models.EloScore.deck_id)
.filter(models.Deck.owner_id.in_(player_ids))
.filter(models.Deck.id.not_in(excluded_decks))
.add_column(row_number_column)
)
sub_query = sub_query.subquery()
query = db.query(sub_query).filter(sub_query.c.row_number == 1)
results = query.all()
decks_by_player = defaultdict(list)
for row in results:
LOGGER.info(f"Processing {row}")
decks_by_player[row[2]].append(
{"id": row[0], "name": row[1], "latest_score": row[3]}
)
# Tried using `heapq` for this logic but it keps producing duplicates :shrug:
matchups = []
for info_tuple in product(*decks_by_player.values()):
scores = [info["latest_score"] for info in info_tuple]
score_spread = max([abs(x - y) for (x, y) in product(scores, repeat=2)])
matchup = Matchup(
# Oof, there's _definitely_ a better way of doing this...
participants=list(
zip(decks_by_player.keys(), [info["name"] for info in info_tuple])
),
score=100 - score_spread,
description=f"Score spread of {score_spread}, between {max(scores)} and {min(scores)}",
)
matchups.append((score_spread, matchup))
matchups.sort(key=lambda tup: tup[0])
return [item[1] for item in matchups[:5]]
# `order=True` so that tuples containing these can be put into a heap
@dataclass(order=True)
class Matchup:
# `participants` is a List of dictionaries containing `player_name` and `deck_name`
participants: List[Mapping]
score: int
description: str
@dataclass
class StrategyDefinition:
strategy_id: str
friendly_name: str
# Parameters are:
# `List[int]` of player_ids
# `List[int]` of excluded_decks
# `Session`, a db-connection
strategy: Callable[[List[int], List[int], Session], List[Matchup]]
description: Optional[str] = None
STRATEGIES: Mapping[str, StrategyDefinition] = {
strat_def.strategy_id: strat_def
for strat_def in [
StrategyDefinition(
strategy_id="smallest_score_spread",
friendly_name="Smallest Score Spread",
strategy=smallest_score_spread_strategy,
)
]
}
@html_router.get("/matchup")
def suggest_matchup_html(request: Request, db=Depends(get_db)):
return jinja_templates.TemplateResponse(
request,
"suggest/matchup.html",
{"players": players.list_players(db=db), "strategies": STRATEGIES},
)