Better ref logic and better redundancy on the scheduling
This commit is contained in:
+5
-3
@@ -1,7 +1,7 @@
|
|||||||
# backend/app/crud.py
|
# backend/app/crud.py
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session, joinedload
|
||||||
|
|
||||||
from . import logic, models, schemas
|
from . import logic, models, schemas
|
||||||
|
|
||||||
@@ -201,10 +201,12 @@ def get_tournament_matches(db: Session, tournament_id: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_match(db: Session, tournament_id: str, match_id: str):
|
def get_match(db: Session, match_id: str):
|
||||||
return (
|
return (
|
||||||
db.query(models.Match)
|
db.query(models.Match)
|
||||||
.filter(models.Match.tournament_id == tournament_id)
|
.options(
|
||||||
|
joinedload(models.Match.tournament).joinedload(models.Tournament.matches)
|
||||||
|
)
|
||||||
.filter(models.Match.id == match_id)
|
.filter(models.Match.id == match_id)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
|||||||
+75
-67
@@ -1,4 +1,5 @@
|
|||||||
# backend/app/logic.py
|
# backend/app/logic.py
|
||||||
|
import heapq
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
@@ -9,6 +10,7 @@ from sqlalchemy.orm.attributes import flag_modified
|
|||||||
from . import models
|
from . import models
|
||||||
from .constants import BracketTypes, MatchStatus, TournamentTypes
|
from .constants import BracketTypes, MatchStatus, TournamentTypes
|
||||||
from .core.brackets import BracketGenerator
|
from .core.brackets import BracketGenerator
|
||||||
|
from .core.structures import Match
|
||||||
|
|
||||||
|
|
||||||
def generate_bracket(db: Session, t: models.Tournament):
|
def generate_bracket(db: Session, t: models.Tournament):
|
||||||
@@ -21,7 +23,7 @@ def generate_bracket(db: Session, t: models.Tournament):
|
|||||||
abstract_matches = gen.generate(len(teams), double_elimination=is_double)
|
abstract_matches = gen.generate(len(teams), double_elimination=is_double)
|
||||||
id_map = {m.id: str(uuid4()) for m in abstract_matches}
|
id_map = {m.id: str(uuid4()) for m in abstract_matches}
|
||||||
|
|
||||||
def resolve_target(match_node, is_winner_path):
|
def resolve_target(match_node: Match, is_winner_path):
|
||||||
if is_winner_path:
|
if is_winner_path:
|
||||||
curr = match_node.next_win
|
curr = match_node.next_win
|
||||||
slot = getattr(match_node, "next_win_slot", None)
|
slot = getattr(match_node, "next_win_slot", None)
|
||||||
@@ -133,16 +135,14 @@ def update_schedule_times(db: Session, t: models.Tournament):
|
|||||||
for m in all_matches:
|
for m in all_matches:
|
||||||
get_depth(m.id)
|
get_depth(m.id)
|
||||||
|
|
||||||
# 4. Initialize timers & lock finished matches
|
# 4. Initialize timers
|
||||||
all_courts: set[models.Court] = set()
|
all_courts: set[models.Court] = set()
|
||||||
court_sharing_count = defaultdict(
|
court_sharing_count = defaultdict(int)
|
||||||
int
|
|
||||||
) # NEW: Track how highly contested each court is
|
|
||||||
|
|
||||||
for t_item in tournaments:
|
for t_item in tournaments:
|
||||||
for c in t_item.courts:
|
for c in t_item.courts:
|
||||||
all_courts.add(c)
|
all_courts.add(c)
|
||||||
court_sharing_count[c.id] += 1 # NEW
|
court_sharing_count[c.id] += 1
|
||||||
|
|
||||||
court_timers = {c.id: start_of_day for c in all_courts}
|
court_timers = {c.id: start_of_day for c in all_courts}
|
||||||
planned_finish_times = {}
|
planned_finish_times = {}
|
||||||
@@ -159,8 +159,9 @@ def update_schedule_times(db: Session, t: models.Tournament):
|
|||||||
|
|
||||||
tournament_match_counts = {t_item.id: 0 for t_item in tournaments}
|
tournament_match_counts = {t_item.id: 0 for t_item in tournaments}
|
||||||
|
|
||||||
# 5. Global Interleaving Schedule Loop
|
# 5. Critical Path Time-Stepping Scheduler
|
||||||
loop_limit = len(unscheduled) * 3
|
loop_limit = max(5000, len(unscheduled) * 50)
|
||||||
|
|
||||||
while unscheduled and loop_limit > 0:
|
while unscheduled and loop_limit > 0:
|
||||||
loop_limit -= 1
|
loop_limit -= 1
|
||||||
|
|
||||||
@@ -173,7 +174,6 @@ def update_schedule_times(db: Session, t: models.Tournament):
|
|||||||
for m in unscheduled:
|
for m in unscheduled:
|
||||||
if best_court_id not in [c.id for c in m.tournament.courts]:
|
if best_court_id not in [c.id for c in m.tournament.courts]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
is_ready = True
|
is_ready = True
|
||||||
max_prereq_time = (
|
max_prereq_time = (
|
||||||
m.tournament.timestamp.replace(tzinfo=None)
|
m.tournament.timestamp.replace(tzinfo=None)
|
||||||
@@ -236,36 +236,40 @@ def update_schedule_times(db: Session, t: models.Tournament):
|
|||||||
else:
|
else:
|
||||||
court_timers[best_court_id] += timedelta(minutes=5)
|
court_timers[best_court_id] += timedelta(minutes=5)
|
||||||
|
|
||||||
# 6. Dynamic Referee Assignment (Runs globally across all interleaved matches!)
|
# 6. Ultra-Fast Smart Referee Assignment
|
||||||
all_matches = sorted(
|
all_matches.sort(key=lambda x: (x.start_time or start_of_day, x.court_id or 0))
|
||||||
all_matches, key=lambda x: (x.start_time or start_of_day, x.court_id or 0)
|
|
||||||
)
|
|
||||||
duty_counts = defaultdict(int)
|
duty_counts = defaultdict(int)
|
||||||
active_refs = []
|
|
||||||
|
|
||||||
def is_busy(outcome_tuple, source_m: models.Match, start, end):
|
team_first_match = {}
|
||||||
next_m_id = (
|
for m in all_matches:
|
||||||
source_m.winner_next_match_id
|
if m.start_time:
|
||||||
if outcome_tuple[0] == "W"
|
if m.p1_team_id and m.p1_team_id not in team_first_match:
|
||||||
else source_m.loser_next_match_id
|
team_first_match[m.p1_team_id] = m.start_time
|
||||||
)
|
if m.p2_team_id and m.p2_team_id not in team_first_match:
|
||||||
if next_m_id:
|
team_first_match[m.p2_team_id] = m.start_time
|
||||||
next_m = match_map[next_m_id]
|
|
||||||
next_start = next_m.start_time
|
active_refs = []
|
||||||
if next_start:
|
match_outcome_next_start = {}
|
||||||
next_end = next_start + timedelta(minutes=next_m.tournament.duration)
|
|
||||||
if not (next_end <= start or next_start >= end):
|
for m in all_matches:
|
||||||
return True
|
if m.winner_next_match_id:
|
||||||
for r_outcome, r_start, r_end in active_refs:
|
nm = match_map.get(m.winner_next_match_id)
|
||||||
if r_outcome == outcome_tuple:
|
if nm and nm.start_time:
|
||||||
if not (r_end <= start or r_start >= end):
|
match_outcome_next_start[("W", m.id)] = nm.start_time
|
||||||
|
if m.loser_next_match_id:
|
||||||
|
nm = match_map.get(m.loser_next_match_id)
|
||||||
|
if nm and nm.start_time:
|
||||||
|
match_outcome_next_start[("L", m.id)] = nm.start_time
|
||||||
|
|
||||||
|
def is_ref_busy(identifier, start, end):
|
||||||
|
for ref_id, r_start, r_end in active_refs:
|
||||||
|
if ref_id == identifier and start < r_end and end > r_start:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
for m in all_matches:
|
for m in all_matches:
|
||||||
if not m.start_time:
|
if not m.start_time:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
m_start = m.start_time
|
m_start = m.start_time
|
||||||
m_end = m_start + timedelta(minutes=m.tournament.duration)
|
m_end = m_start + timedelta(minutes=m.tournament.duration)
|
||||||
|
|
||||||
@@ -292,59 +296,63 @@ def update_schedule_times(db: Session, t: models.Tournament):
|
|||||||
best_score = float("inf")
|
best_score = float("inf")
|
||||||
|
|
||||||
for prev_m in all_matches:
|
for prev_m in all_matches:
|
||||||
if not prev_m.start_time:
|
if not prev_m.start_time or prev_m.start_time >= m_start:
|
||||||
continue
|
continue
|
||||||
prev_end = prev_m.start_time + timedelta(minutes=prev_m.tournament.duration)
|
prev_end = prev_m.start_time + timedelta(minutes=prev_m.tournament.duration)
|
||||||
|
if prev_end > m_start:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for outcome in ["W", "L"]:
|
||||||
|
out_key = (outcome, prev_m.id)
|
||||||
|
next_start = match_outcome_next_start.get(out_key)
|
||||||
|
if next_start and next_start < m_end:
|
||||||
|
continue
|
||||||
|
|
||||||
|
role_str = "Winner" if outcome == "W" else "Loser"
|
||||||
|
identifier = f"{role_str} of #{prev_m.match_number}"
|
||||||
|
|
||||||
|
if is_ref_busy(identifier, m_start, m_end):
|
||||||
|
continue
|
||||||
|
|
||||||
if prev_end <= m_start:
|
|
||||||
l_outcome = ("L", prev_m.id)
|
|
||||||
if not is_busy(l_outcome, prev_m, m_start, m_end):
|
|
||||||
wait_mins = (m_start - prev_end).total_seconds() / 60.0
|
wait_mins = (m_start - prev_end).total_seconds() / 60.0
|
||||||
score = (duty_counts[l_outcome] * 120) + wait_mins
|
score = (duty_counts[identifier] * 120) + wait_mins
|
||||||
|
if outcome == "W":
|
||||||
|
score += 60
|
||||||
if prev_m.court_id == m.court_id:
|
if prev_m.court_id == m.court_id:
|
||||||
score -= 30
|
score -= 30
|
||||||
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
|
|
||||||
if prev_m.court_id == m.court_id:
|
|
||||||
score -= 30
|
|
||||||
if score < best_score:
|
if score < best_score:
|
||||||
best_score = score
|
best_score = score
|
||||||
best_outcome = (w_outcome, prev_m)
|
best_outcome = identifier
|
||||||
|
|
||||||
if best_outcome:
|
if best_outcome:
|
||||||
outcome_tuple, prev_m = best_outcome
|
m.ref_label = best_outcome
|
||||||
duty_counts[outcome_tuple] += 1
|
m.ref_team_id = None
|
||||||
active_refs.append((outcome_tuple, m_start, m_end))
|
duty_counts[best_outcome] += 1
|
||||||
role = "Winner" if outcome_tuple[0] == "W" else "Loser"
|
active_refs.append((best_outcome, m_start, m_end))
|
||||||
m.ref_label = f"{role} of #{prev_m.match_number}"
|
|
||||||
else:
|
else:
|
||||||
m.ref_label = "Staff / Volunteers"
|
assigned_bye_team = False
|
||||||
for future_m in all_matches:
|
sorted_teams = sorted(
|
||||||
if future_m.start_time and future_m.start_time >= m_end:
|
m.tournament.teams, key=lambda t: duty_counts[f"TEAM_{t.id}"]
|
||||||
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)
|
|
||||||
for r_outcome, r_start, r_end in active_refs
|
|
||||||
)
|
)
|
||||||
if not is_team_busy:
|
|
||||||
|
for team in sorted_teams:
|
||||||
|
t_id = team.id
|
||||||
|
t_first_start = team_first_match.get(t_id)
|
||||||
|
identifier = f"TEAM_{t_id}"
|
||||||
|
if t_first_start and t_first_start >= m_end:
|
||||||
|
if not is_ref_busy(identifier, m_start, m_end):
|
||||||
m.ref_team_id = t_id
|
m.ref_team_id = t_id
|
||||||
m.ref_label = None
|
m.ref_label = None
|
||||||
active_refs.append((team_identifier, m_start, m_end))
|
duty_counts[identifier] += 1
|
||||||
assigned = True
|
active_refs.append((identifier, m_start, m_end))
|
||||||
break
|
assigned_bye_team = True
|
||||||
if assigned:
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
if not assigned_bye_team:
|
||||||
|
m.ref_label = "Staff / Volunteers"
|
||||||
|
m.ref_team_id = None
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ async def report_score(
|
|||||||
|
|
||||||
_check_auth(t, user, report.code)
|
_check_auth(t, user, report.code)
|
||||||
|
|
||||||
match = crud.get_match(db, id, match_id)
|
match = crud.get_match(db, match_id)
|
||||||
if not match:
|
if not match:
|
||||||
raise HTTPException(404, "Match not found")
|
raise HTTPException(404, "Match not found")
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ async def edit_score(
|
|||||||
|
|
||||||
_check_auth(t, user, report.code)
|
_check_auth(t, user, report.code)
|
||||||
|
|
||||||
match = crud.get_match(db, id, match_id)
|
match = crud.get_match(db, match_id)
|
||||||
if not match:
|
if not match:
|
||||||
raise HTTPException(404, "Match not found")
|
raise HTTPException(404, "Match not found")
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ async def clear_score(
|
|||||||
|
|
||||||
_check_auth(t, user, code)
|
_check_auth(t, user, code)
|
||||||
|
|
||||||
match = crud.get_match(db, id, match_id)
|
match = crud.get_match(db, match_id)
|
||||||
if not match:
|
if not match:
|
||||||
raise HTTPException(404, "Match not found")
|
raise HTTPException(404, "Match not found")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user