- FastAPI backend with PostgreSQL + Redis - 4 core features: Registration, Courts, Matchmaking, Tournament - Double elimination tournament with bracket visualization - ELO-based matchmaking (Stage 1 Open, Stage 2 Skill-Based) - Real-time WebSocket updates - TV Screen display for courts - Vue 3 + Tailwind CSS frontend - Seed data: 12 players, 4 courts, active matches, tournament in progress - Docker compose stack with Nginx reverse proxy
161 lines
5.6 KiB
Python
161 lines
5.6 KiB
Python
from fastapi import APIRouter, Depends, HTTPException
|
|
from sqlalchemy.orm import Session
|
|
from app.database import get_db
|
|
from app.models.match import Match, MatchPlayer, MatchStage, MatchStatus, MatchType
|
|
from app.models.player import Player
|
|
from app.services import matchmaking
|
|
from pydantic import BaseModel
|
|
from typing import Optional, List
|
|
|
|
router = APIRouter(prefix="/matches", tags=["matches"])
|
|
|
|
|
|
class CreateMatchRequest(BaseModel):
|
|
title: str
|
|
stage: str = "open"
|
|
match_type: str = "doubles"
|
|
creator_player_id: int
|
|
elo_tolerance: float = 200
|
|
|
|
|
|
class JoinMatchRequest(BaseModel):
|
|
player_id: int
|
|
team: int
|
|
|
|
|
|
class ScoreUpdateRequest(BaseModel):
|
|
team1_score: int
|
|
team2_score: int
|
|
|
|
|
|
def match_to_dict(match: Match, db: Session) -> dict:
|
|
players = db.query(MatchPlayer).filter(MatchPlayer.match_id == match.id).all()
|
|
team1 = []
|
|
team2 = []
|
|
for mp in players:
|
|
p = db.query(Player).filter(Player.id == mp.player_id).first()
|
|
if p:
|
|
pdata = {
|
|
"id": p.id,
|
|
"name": p.name,
|
|
"elo": round(p.elo_rating, 1),
|
|
"avatar_color": p.avatar_color,
|
|
"elo_change": mp.elo_change,
|
|
}
|
|
if mp.team == 1:
|
|
team1.append(pdata)
|
|
else:
|
|
team2.append(pdata)
|
|
|
|
return {
|
|
"id": match.id,
|
|
"title": match.title,
|
|
"stage": match.stage.value,
|
|
"match_type": match.match_type.value,
|
|
"status": match.status.value,
|
|
"team1": team1,
|
|
"team2": team2,
|
|
"team1_score": match.team1_score,
|
|
"team2_score": match.team2_score,
|
|
"max_players": match.max_players,
|
|
"current_players": len(players),
|
|
"min_elo": match.min_elo,
|
|
"max_elo": match.max_elo,
|
|
"court_id": match.court_id,
|
|
"court_name": match.court.name if match.court else None,
|
|
"started_at": match.started_at.isoformat() if match.started_at else None,
|
|
"ended_at": match.ended_at.isoformat() if match.ended_at else None,
|
|
"created_at": match.created_at.isoformat() if match.created_at else None,
|
|
}
|
|
|
|
|
|
@router.get("/")
|
|
def get_matches(status: Optional[str] = None, stage: Optional[str] = None, db: Session = Depends(get_db)):
|
|
query = db.query(Match)
|
|
if status:
|
|
query = query.filter(Match.status == status)
|
|
if stage:
|
|
query = query.filter(Match.stage == stage)
|
|
matches = query.order_by(Match.created_at.desc()).limit(50).all()
|
|
return [match_to_dict(m, db) for m in matches]
|
|
|
|
|
|
@router.get("/lobby")
|
|
def get_lobby(db: Session = Depends(get_db)):
|
|
matches = matchmaking.get_lobby_matches(db)
|
|
return [match_to_dict(m, db) for m in matches]
|
|
|
|
|
|
@router.get("/active")
|
|
def get_active_matches(db: Session = Depends(get_db)):
|
|
matches = db.query(Match).filter(Match.status == MatchStatus.IN_PROGRESS).all()
|
|
return [match_to_dict(m, db) for m in matches]
|
|
|
|
|
|
@router.get("/{match_id}")
|
|
def get_match(match_id: int, db: Session = Depends(get_db)):
|
|
match = db.query(Match).filter(Match.id == match_id).first()
|
|
if not match:
|
|
raise HTTPException(status_code=404, detail="Match not found")
|
|
return match_to_dict(match, db)
|
|
|
|
|
|
@router.post("/")
|
|
def create_match(req: CreateMatchRequest, db: Session = Depends(get_db)):
|
|
player = db.query(Player).filter(Player.id == req.creator_player_id).first()
|
|
if not player:
|
|
raise HTTPException(status_code=404, detail="Player not found")
|
|
|
|
match_type = MatchType.DOUBLES if req.match_type == "doubles" else MatchType.SINGLES
|
|
|
|
if req.stage == "open":
|
|
match = matchmaking.create_open_match(db, req.title, match_type)
|
|
elif req.stage == "skill_based":
|
|
match = matchmaking.create_skill_match(db, req.title, player.elo_rating, match_type, req.elo_tolerance)
|
|
else:
|
|
raise HTTPException(status_code=400, detail="Invalid stage. Use 'open' or 'skill_based'")
|
|
|
|
# Auto-join creator to team 1
|
|
matchmaking.join_match(db, match.id, player.id, 1)
|
|
return match_to_dict(match, db)
|
|
|
|
|
|
@router.post("/{match_id}/join")
|
|
def join_match(match_id: int, req: JoinMatchRequest, db: Session = Depends(get_db)):
|
|
result = matchmaking.join_match(db, match_id, req.player_id, req.team)
|
|
if not result:
|
|
raise HTTPException(status_code=400, detail="Cannot join match (full, ELO constraint, or already joined)")
|
|
|
|
match = db.query(Match).filter(Match.id == match_id).first()
|
|
return match_to_dict(match, db)
|
|
|
|
|
|
@router.post("/{match_id}/start")
|
|
def start_match(match_id: int, db: Session = Depends(get_db)):
|
|
match = matchmaking.start_match(db, match_id)
|
|
if not match:
|
|
raise HTTPException(status_code=400, detail="Cannot start match (not enough players or already started)")
|
|
return match_to_dict(match, db)
|
|
|
|
|
|
@router.post("/{match_id}/score")
|
|
def update_score(match_id: int, req: ScoreUpdateRequest, db: Session = Depends(get_db)):
|
|
match = db.query(Match).filter(Match.id == match_id).first()
|
|
if not match:
|
|
raise HTTPException(status_code=404, detail="Match not found")
|
|
if match.status != MatchStatus.IN_PROGRESS:
|
|
raise HTTPException(status_code=400, detail="Match not in progress")
|
|
|
|
match.team1_score = req.team1_score
|
|
match.team2_score = req.team2_score
|
|
db.commit()
|
|
return match_to_dict(match, db)
|
|
|
|
|
|
@router.post("/{match_id}/complete")
|
|
def complete_match(match_id: int, req: ScoreUpdateRequest, db: Session = Depends(get_db)):
|
|
match = matchmaking.complete_match(db, match_id, req.team1_score, req.team2_score)
|
|
if not match:
|
|
raise HTTPException(status_code=400, detail="Cannot complete match")
|
|
return match_to_dict(match, db)
|