"""Matchmaking Engine for ServeSync""" from sqlalchemy.orm import Session from app.models.match import Match, MatchPlayer, MatchStage, MatchStatus, MatchType from app.models.court import Court from app.models.player import Player from app.models.booking import Booking, BookingStatus from app.services.elo import calculate_elo_change, get_elo_range, is_valid_match from datetime import datetime, timedelta from typing import Optional, List def create_open_match(db: Session, title: str, match_type: MatchType = MatchType.DOUBLES) -> Match: """Stage 1: Create an open match lobby - no restrictions""" max_players = 4 if match_type == MatchType.DOUBLES else 2 match = Match( stage=MatchStage.OPEN, match_type=match_type, status=MatchStatus.LOBBY, title=title, max_players=max_players, ) db.add(match) db.commit() db.refresh(match) return match def create_skill_match( db: Session, title: str, creator_elo: float, match_type: MatchType = MatchType.DOUBLES, tolerance: float = 200, ) -> Match: """Stage 2: Create a skill-based match with ELO constraints""" min_elo, max_elo = get_elo_range(creator_elo, tolerance) max_players = 4 if match_type == MatchType.DOUBLES else 2 match = Match( stage=MatchStage.SKILL_BASED, match_type=match_type, status=MatchStatus.LOBBY, title=title, max_players=max_players, min_elo=min_elo, max_elo=max_elo, ) db.add(match) db.commit() db.refresh(match) return match def join_match(db: Session, match_id: int, player_id: int, team: int) -> Optional[MatchPlayer]: """Add a player to a match lobby""" match = db.query(Match).filter(Match.id == match_id).first() if not match or match.status != MatchStatus.LOBBY: return None player = db.query(Player).filter(Player.id == player_id).first() if not player: return None # Check player count current_players = db.query(MatchPlayer).filter(MatchPlayer.match_id == match_id).count() if current_players >= match.max_players: return None # Check if already joined existing = db.query(MatchPlayer).filter( MatchPlayer.match_id == match_id, MatchPlayer.player_id == player_id ).first() if existing: return existing # For skill-based matches, check ELO constraint if match.stage == MatchStage.SKILL_BASED: if match.min_elo and match.max_elo: if not is_valid_match(player.elo_rating, match.min_elo, match.max_elo): return None mp = MatchPlayer( match_id=match_id, player_id=player_id, team=team, elo_before=player.elo_rating, ) db.add(mp) db.commit() db.refresh(mp) return mp def start_match(db: Session, match_id: int) -> Optional[Match]: """Start a match - auto-assign court if available""" match = db.query(Match).filter(Match.id == match_id).first() if not match: return None players = db.query(MatchPlayer).filter(MatchPlayer.match_id == match_id).count() if players < match.max_players: return None # Auto-assign an available court if not match.court_id: court = find_available_court(db) if court: match.court_id = court.id # Create booking now = datetime.utcnow() booking = Booking( player_id=db.query(MatchPlayer).filter(MatchPlayer.match_id == match_id).first().player_id, court_id=court.id, start_time=now, end_time=now + timedelta(hours=1), duration_hours=1.0, total_cost=court.hourly_rate, status=BookingStatus.IN_PROGRESS, is_match_booking=True, ) db.add(booking) match.status = MatchStatus.IN_PROGRESS match.started_at = datetime.utcnow() db.commit() db.refresh(match) return match def find_available_court(db: Session) -> Optional[Court]: """Find an available court not currently in use""" from sqlalchemy import and_ busy_court_ids = db.query(Match.court_id).filter( Match.status == MatchStatus.IN_PROGRESS, Match.court_id.isnot(None) ).all() busy_ids = [c[0] for c in busy_court_ids] court = db.query(Court).filter( Court.is_active == True, Court.id.notin_(busy_ids) ).first() return court def complete_match(db: Session, match_id: int, team1_score: int, team2_score: int) -> Optional[Match]: """Complete a match and update ELO ratings""" match = db.query(Match).filter(Match.id == match_id).first() if not match or match.status != MatchStatus.IN_PROGRESS: return None match.status = MatchStatus.COMPLETED match.team1_score = team1_score match.team2_score = team2_score match.ended_at = datetime.utcnow() winner_team = 1 if team1_score > team2_score else 2 # Get players team1_players = db.query(MatchPlayer).filter( MatchPlayer.match_id == match_id, MatchPlayer.team == 1 ).all() team2_players = db.query(MatchPlayer).filter( MatchPlayer.match_id == match_id, MatchPlayer.team == 2 ).all() # Calculate average ELO team1_avg = sum(p.elo_before or p.player.elo_rating for p in team1_players) / len(team1_players) team2_avg = sum(p.elo_before or p.player.elo_rating for p in team2_players) / len(team2_players) if winner_team == 1: w_change, l_change = calculate_elo_change(team1_avg, team2_avg) else: l_change, w_change = calculate_elo_change(team2_avg, team1_avg) w_change, l_change = -l_change, -w_change # Update player ELOs for mp in team1_players: is_winner = (winner_team == 1) change = w_change if is_winner else l_change mp.is_winner = is_winner mp.elo_change = change mp.elo_after = (mp.elo_before or mp.player.elo_rating) + change mp.player.elo_rating += change if is_winner: mp.player.wins += 1 else: mp.player.losses += 1 mp.player.total_matches += 1 for mp in team2_players: is_winner = (winner_team == 2) change = w_change if is_winner else l_change mp.is_winner = is_winner mp.elo_change = change mp.elo_after = (mp.elo_before or mp.player.elo_rating) + change mp.player.elo_rating += change if is_winner: mp.player.wins += 1 else: mp.player.losses += 1 mp.player.total_matches += 1 db.commit() db.refresh(match) return match def get_lobby_matches(db: Session, stage: Optional[MatchStage] = None) -> List[Match]: """Get all lobby matches, optionally filtered by stage""" query = db.query(Match).filter(Match.status == MatchStatus.LOBBY) if stage: query = query.filter(Match.stage == stage) return query.order_by(Match.created_at.desc()).all()