Files
brackets/backend/app/logic.py
T

241 lines
7.4 KiB
Python

# backend/app/logic.py
from collections import defaultdict
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, is_winner_path):
if is_winner_path:
curr = match_node.next_win
slot = getattr(match_node, "next_win_slot", None)
else:
curr = match_node.next_loss
slot = getattr(match_node, "next_loss_slot", None)
while curr and getattr(curr, "is_bye", False):
slot = getattr(curr, "next_win_slot", None)
curr = curr.next_win
return curr, slot
db_matches = []
friendly_counter = 1
for m in abstract_matches:
real_win, win_slot = resolve_target(m, True)
real_loss, loss_slot = resolve_target(m, False)
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,
winner_next_match_slot=win_slot if real_win else None,
loser_next_match_id=id_map[real_loss.id] if real_loss else None,
loser_next_match_slot=loss_slot 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 or not t.matches:
return
matches = t.matches
match_map = {m.id: m for m in matches}
prereqs = defaultdict(list)
for m in matches:
if m.winner_next_match_id:
prereqs[m.winner_next_match_id].append(m.id)
if m.loser_next_match_id:
prereqs[m.loser_next_match_id].append(m.id)
depth_cache = {}
def get_depth(m_id):
if m_id not in match_map:
return 0
if m_id in depth_cache:
return depth_cache[m_id]
m = match_map[m_id]
child_depths = [0]
if m.winner_next_match_id:
child_depths.append(get_depth(m.winner_next_match_id))
if m.loser_next_match_id:
child_depths.append(get_depth(m.loser_next_match_id))
depth = 1 + max(child_depths)
depth_cache[m_id] = depth
return depth
for m in matches:
get_depth(m.id)
tournament_start = (
t.timestamp.replace(tzinfo=None) if t.timestamp.tzinfo else t.timestamp
)
court_timers = {c.id: tournament_start for c in t.courts}
planned_finish_times = {}
unscheduled = list(matches)
loop_limit = len(matches) * 2
while unscheduled and loop_limit > 0:
loop_limit -= 1
best_court_id = min(court_timers, key=lambda k: court_timers[k])
current_time = court_timers[best_court_id]
ready: list[models.Match] = []
for m in unscheduled:
is_ready = True
max_prereq_time = tournament_start
for p_id in prereqs[m.id]:
if p_id not in planned_finish_times:
is_ready = False
break
max_prereq_time = max(max_prereq_time, planned_finish_times[p_id])
if is_ready and max_prereq_time <= current_time:
ready.append(m)
if ready:
ready.sort(
key=lambda x: (-depth_cache[x.id], x.round_number, x.match_number)
)
cand = ready[0]
cand.court_id = best_court_id
cand.start_time = current_time
fin = current_time + timedelta(minutes=t.duration)
planned_finish_times[cand.id] = fin
court_timers[best_court_id] = fin
unscheduled.remove(cand)
else:
next_wake = None
for m in unscheduled:
is_ready = True
max_prereq_time = tournament_start
for p_id in prereqs[m.id]:
if p_id not in planned_finish_times:
is_ready = False
break
max_prereq_time = max(max_prereq_time, planned_finish_times[p_id])
if is_ready and max_prereq_time > current_time:
if next_wake is None or max_prereq_time < next_wake:
next_wake = max_prereq_time
if next_wake:
court_timers[best_court_id] = next_wake
else:
break
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: models.Match, team_id: int, target_slot: int | None
):
if not next_match or target_slot is None:
return
if target_slot == 0:
next_match.p1_team_id = team_id
elif target_slot == 1:
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, match.winner_next_match_slot)
if match.loser_next_match and loser_id:
update_next_match(match.loser_next_match, loser_id, match.loser_next_match_slot)
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: models.Match, team_id: int, target_slot: int | None
):
if not next_match or target_slot is None:
return
if next_match.winner_team_id:
undo_advancement(db, next_match)
next_match.winner_team_id = None
next_match.sets = []
if target_slot == 0:
next_match.p1_team_id = None
elif target_slot == 1:
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, match.winner_next_match_slot)
if match.loser_next_match and loser_id:
clear_from_next(match.loser_next_match, loser_id, match.loser_next_match_slot)