Added seperate courts

This commit is contained in:
2026-05-24 15:53:54 +02:00 Verified
parent b2d341642e
commit 33e623b337
18 changed files with 1787 additions and 1282 deletions
+130 -55
View File
@@ -1,6 +1,6 @@
# backend/app/logic.py
from collections import defaultdict
from datetime import timedelta
from datetime import datetime, timedelta
from uuid import uuid4
from sqlalchemy.orm import Session
@@ -79,59 +79,108 @@ def generate_bracket(db: Session, t: models.Tournament):
def update_schedule_times(db: Session, t: models.Tournament):
if not t.courts or not t.matches:
if not t.timestamp:
return
matches = t.matches
match_map = {m.id: m for m in matches}
# 1. Grab the entire day of tournaments
target_date = t.timestamp.date()
start_of_day = datetime.combine(target_date, datetime.min.time())
end_of_day = start_of_day + timedelta(days=1)
tournaments = (
db.query(models.Tournament)
.filter(
models.Tournament.timestamp >= start_of_day,
models.Tournament.timestamp < end_of_day,
)
.all()
)
if not tournaments:
return
all_matches: list[models.Match] = []
match_map: dict[str, models.Match] = {}
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 = {}
# 2. Gather all matches and prereqs globally
for t_item in tournaments:
for m in t_item.matches:
all_matches.append(m)
match_map[m.id] = m
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)
# 3. Calculate depths
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:
for m in all_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}
# 4. Initialize timers & lock finished matches
all_courts: set[models.Court] = set()
court_sharing_count = defaultdict(
int
) # NEW: Track how highly contested each court is
for t_item in tournaments:
for c in t_item.courts:
all_courts.add(c)
court_sharing_count[c.id] += 1 # NEW
court_timers = {c.id: start_of_day for c in all_courts}
planned_finish_times = {}
unscheduled = list(matches)
unscheduled: list[models.Match] = []
loop_limit = len(matches) * 2
for m in all_matches:
if m.status == MatchStatus.FINISHED and m.start_time and m.court_id:
fin = m.start_time + timedelta(minutes=m.tournament.duration)
planned_finish_times[m.id] = fin
if court_timers.get(m.court_id, start_of_day) < fin:
court_timers[m.court_id] = fin
else:
unscheduled.append(m)
tournament_match_counts = {t_item.id: 0 for t_item in tournaments}
# 5. Global Interleaving Schedule Loop
loop_limit = len(unscheduled) * 3
while unscheduled and loop_limit > 0:
loop_limit -= 1
best_court_id = min(court_timers, key=lambda k: court_timers[k])
best_court_id = min(
court_timers.keys(), key=lambda k: (court_timers[k], court_sharing_count[k])
)
current_time = court_timers[best_court_id]
ready: list[models.Match] = []
for m in unscheduled:
if best_court_id not in [c.id for c in m.tournament.courts]:
continue
is_ready = True
max_prereq_time = tournament_start
max_prereq_time = (
m.tournament.timestamp.replace(tzinfo=None)
if m.tournament.timestamp.tzinfo
else m.tournament.timestamp
)
for p_id in prereqs[m.id]:
if p_id not in planned_finish_times:
is_ready = False
@@ -143,27 +192,41 @@ def update_schedule_times(db: Session, t: models.Tournament):
if ready:
ready.sort(
key=lambda x: (-depth_cache[x.id], x.round_number, x.match_number)
key=lambda x: (
len(x.tournament.courts),
tournament_match_counts[x.tournament_id],
-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)
fin = current_time + timedelta(minutes=cand.tournament.duration)
planned_finish_times[cand.id] = fin
court_timers[best_court_id] = fin
tournament_match_counts[cand.tournament_id] += 1
unscheduled.remove(cand)
else:
next_wake = None
for m in unscheduled:
if best_court_id not in [c.id for c in m.tournament.courts]:
continue
is_ready = True
max_prereq_time = tournament_start
max_prereq_time = (
m.tournament.timestamp.replace(tzinfo=None)
if m.tournament.timestamp.tzinfo
else m.tournament.timestamp
)
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
@@ -171,22 +234,26 @@ def update_schedule_times(db: Session, t: models.Tournament):
if next_wake:
court_timers[best_court_id] = next_wake
else:
break
court_timers[best_court_id] += timedelta(minutes=5)
# ==========================================
# --- DYNAMIC REFEREE ASSIGNMENT ---
# ==========================================
all_matches = sorted(list(matches), key=lambda x: (x.start_time, x.court_id))
# 6. Dynamic Referee Assignment (Runs globally across all interleaved matches!)
all_matches = sorted(
all_matches, key=lambda x: (x.start_time or start_of_day, x.court_id or 0)
)
duty_counts = defaultdict(int)
active_refs = []
def is_busy(outcome_tuple, source_m, start, end):
next_m_id = source_m.winner_next_match_id if outcome_tuple[0] == "W" else source_m.loser_next_match_id
def is_busy(outcome_tuple, source_m: models.Match, start, end):
next_m_id = (
source_m.winner_next_match_id
if outcome_tuple[0] == "W"
else source_m.loser_next_match_id
)
if next_m_id:
next_m = match_map[next_m_id]
next_start = next_m.start_time
if next_start:
next_end = next_start + timedelta(minutes=t.duration)
next_end = next_start + timedelta(minutes=next_m.tournament.duration)
if not (next_end <= start or next_start >= end):
return True
for r_outcome, r_start, r_end in active_refs:
@@ -198,29 +265,37 @@ def update_schedule_times(db: Session, t: models.Tournament):
for m in all_matches:
if not m.start_time:
continue
m_start = m.start_time
m_end = m_start + timedelta(minutes=t.duration)
m_end = m_start + timedelta(minutes=m.tournament.duration)
if m.bracket_type == BracketTypes.FINALS:
prev_final = next((p for p in all_matches if p.bracket_type == BracketTypes.FINALS and p.winner_next_match_id == m.id), None)
prev_final = next(
(
p
for p in all_matches
if p.bracket_type == BracketTypes.FINALS
and p.winner_next_match_id == m.id
),
None,
)
if prev_final:
m.ref_team_id = prev_final.ref_team_id
m.ref_label = prev_final.ref_label
identifier = f"TEAM_{m.ref_team_id}" if m.ref_team_id else prev_final.ref_label
identifier = (
f"TEAM_{m.ref_team_id}" if m.ref_team_id else prev_final.ref_label
)
active_refs.append((identifier, m_start, m_end))
continue
best_outcome = None
best_score = float('inf')
best_score = float("inf")
for prev_m in all_matches:
if not prev_m.start_time:
continue
prev_end = prev_m.start_time + timedelta(minutes=t.duration)
prev_end = prev_m.start_time + timedelta(minutes=prev_m.tournament.duration)
if prev_end <= m_start:
l_outcome = ("L", prev_m.id)
if not is_busy(l_outcome, prev_m, m_start, m_end):
@@ -231,17 +306,17 @@ def update_schedule_times(db: Session, t: models.Tournament):
if score < best_score:
best_score = score
best_outcome = (l_outcome, prev_m)
w_outcome = ("W", prev_m.id)
if not is_busy(w_outcome, prev_m, m_start, m_end):
wait_mins = (m_start - prev_end).total_seconds() / 60.0
score = (duty_counts[w_outcome] * 120) + wait_mins + 60
score = (duty_counts[w_outcome] * 120) + wait_mins + 60
if prev_m.court_id == m.court_id:
score -= 30
if score < best_score:
best_score = score
best_outcome = (w_outcome, prev_m)
if best_outcome:
outcome_tuple, prev_m = best_outcome
duty_counts[outcome_tuple] += 1
@@ -249,20 +324,18 @@ def update_schedule_times(db: Session, t: models.Tournament):
role = "Winner" if outcome_tuple[0] == "W" else "Loser"
m.ref_label = f"{role} of #{prev_m.match_number}"
else:
m.ref_label = "Staff / Volunteers"
m.ref_label = "Staff / Volunteers"
for future_m in all_matches:
if future_m.start_time and future_m.start_time >= m_end:
assigned = False
for t_id in [future_m.p1_team_id, future_m.p2_team_id]:
if t_id:
team_identifier = f"TEAM_{t_id}"
is_team_busy = any(
r_outcome == team_identifier and not (r_end <= m_start or r_start >= m_end)
r_outcome == team_identifier
and not (r_end <= m_start or r_start >= m_end)
for r_outcome, r_start, r_end in active_refs
)
if not is_team_busy:
m.ref_team_id = t_id
m.ref_label = None
@@ -271,7 +344,6 @@ def update_schedule_times(db: Session, t: models.Tournament):
break
if assigned:
break
# ----------------------------------
db.commit()
@@ -355,7 +427,10 @@ def undo_advancement(db: Session, match: models.Match):
return
for m in match.tournament.matches:
if m.ref_label == f"Loser of #{match.match_number}" or m.ref_label == f"Winner of #{match.match_number}":
if (
m.ref_label == f"Loser of #{match.match_number}"
or m.ref_label == f"Winner of #{match.match_number}"
):
m.ref_team_id = None
db.add(m)
@@ -385,5 +460,5 @@ def undo_advancement(db: Session, match: models.Match):
clear_from_next(match.winner_next_match, match.winner_next_match_slot)
if match.loser_next_match and loser_id:
clear_from_next(match.loser_next_match, match.loser_next_match_slot)
db.commit()
db.commit()