Files
brackets/backend/app/logic.py
T

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()