211 lines
6.3 KiB
Python
211 lines
6.3 KiB
Python
# 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()
|