🏓 Initial ServeSync demo build
- 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
This commit is contained in:
160
backend/app/api/matches.py
Normal file
160
backend/app/api/matches.py
Normal file
@@ -0,0 +1,160 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user