# backend/app/logic.py from collections import defaultdict, deque from datetime import timedelta from uuid import uuid4 from sqlalchemy.orm import Session from . import models from .constants import MatchStatus, TournamentTypes from .core.brackets import BracketGenerator def generate_bracket(db: Session, t: models.Tournament): teams = t.teams if not teams: return gen = BracketGenerator() is_double = t.type == TournamentTypes.DOUBLE abstract_matches = gen.generate(len(teams), double_elimination=is_double) id_map = {m.id: str(uuid4()) for m in abstract_matches} def resolve_target(match_node): curr = match_node while curr and curr.is_bye: curr = curr.next_win return curr db_matches = [] friendly_counter = 1 for m in abstract_matches: real_win = resolve_target(m.next_win) real_loss = resolve_target(m.next_loss) initial_status = MatchStatus.SCHEDULED p1_id = None p2_id = None p1_seed = m.teams[0] p2_seed = m.teams[1] if p1_seed and p1_seed <= len(teams): p1_id = teams[p1_seed - 1].id if p2_seed and p2_seed <= len(teams): p2_id = teams[p2_seed - 1].id if p1_id and p2_id: initial_status = MatchStatus.PENDING new_match = models.Match( id=id_map[m.id], tournament_id=t.id, match_number=friendly_counter, bracket_type=m.bracket_type, round_number=m.round_number, status=initial_status, p1_team_id=p1_id, p2_team_id=p2_id, winner_next_match_id=id_map[real_win.id] if real_win else None, loser_next_match_id=id_map[real_loss.id] if real_loss else None, ) db_matches.append(new_match) friendly_counter += 1 db.add_all(db_matches) db.flush() def update_schedule_times(db: Session, t: models.Tournament): if not t.courts: return matches = t.matches if not matches: return # 1. Build Dependency Graph adj = defaultdict(list) in_degree = {m.id: 0 for m in matches} match_map = {m.id: m for m in matches} for m in matches: if m.id not in adj: adj[m.id] = [] if m.winner_next_match_id and m.winner_next_match_id in match_map: adj[m.id].append(m.winner_next_match_id) in_degree[m.winner_next_match_id] += 1 if m.loser_next_match_id and m.loser_next_match_id in match_map: adj[m.id].append(m.loser_next_match_id) in_degree[m.loser_next_match_id] += 1 # 2. Initialize constraints # FIX: Ensure tournament_start is naive (no timezone) to match SQLite DB datetimes tournament_start = t.timestamp if tournament_start.tzinfo is not None: tournament_start = tournament_start.replace(tzinfo=None) match_earliest_start = {m.id: tournament_start for m in matches} court_timers = {c.id: tournament_start for c in t.courts} # 3. Topological Sort queue = deque([m.id for m in matches if in_degree[m.id] == 0]) # Sort initial batch to prioritize logical order initial_order = sorted( list(queue), key=lambda mid: (match_map[mid].bracket_type, match_map[mid].match_number), ) queue = deque(initial_order) while queue: current_id = queue.popleft() match = match_map[current_id] # FIX: Ensure DB start_time is treated as naive m_start = match.start_time if m_start and m_start.tzinfo is not None: m_start = m_start.replace(tzinfo=None) if match.status == MatchStatus.FINISHED: # If finished, propagate actual time if not m_start: m_start = tournament_start actual_end = m_start + timedelta(minutes=t.duration) else: # Schedule: Must wait for dependencies (min_start) AND court availability min_start = match_earliest_start[current_id] best_court_id = min( court_timers, key=lambda k: max(court_timers[k], min_start) ) scheduled_start = max(court_timers[best_court_id], min_start) match.court_id = best_court_id match.start_time = scheduled_start actual_end = scheduled_start + timedelta(minutes=t.duration) court_timers[best_court_id] = actual_end for child_id in adj[current_id]: if actual_end > match_earliest_start[child_id]: match_earliest_start[child_id] = actual_end in_degree[child_id] -= 1 if in_degree[child_id] == 0: queue.append(child_id) db.commit() def advance_winner(db: Session, match: models.Match, winner_id: int): if not winner_id: return loser_id = match.p1_team_id if match.p1_team_id != winner_id else match.p2_team_id def update_next_match(next_match, team_id): if not next_match: return if not next_match.p1_team_id: next_match.p1_team_id = team_id elif not next_match.p2_team_id: next_match.p2_team_id = team_id if ( next_match.p1_team_id and next_match.p2_team_id and next_match.status == MatchStatus.SCHEDULED ): next_match.status = MatchStatus.PENDING db.add(next_match) update_next_match(match.winner_next_match, winner_id) if match.loser_next_match and loser_id: update_next_match(match.loser_next_match, loser_id) db.commit() def undo_advancement(db: Session, match: models.Match): if not match.winner_team_id: return winner_id = match.winner_team_id loser_id = match.p1_team_id if match.p1_team_id != winner_id else match.p2_team_id def clear_from_next(next_match, team_id): if not next_match: return if next_match.p1_team_id == team_id: next_match.p1_team_id = None elif next_match.p2_team_id == team_id: next_match.p2_team_id = None if next_match.status == MatchStatus.PENDING: next_match.status = MatchStatus.SCHEDULED db.add(next_match) clear_from_next(match.winner_next_match, winner_id) if match.loser_next_match and loser_id: clear_from_next(match.loser_next_match, loser_id) db.commit()