Fixed Scheduling logic
This commit is contained in:
+88
-70
@@ -1,5 +1,5 @@
|
|||||||
# backend/app/logic.py
|
# backend/app/logic.py
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
@@ -8,6 +8,7 @@ from sqlalchemy.orm import Session
|
|||||||
from . import models
|
from . import models
|
||||||
from .constants import MatchStatus, TournamentTypes
|
from .constants import MatchStatus, TournamentTypes
|
||||||
from .core.brackets import BracketGenerator
|
from .core.brackets import BracketGenerator
|
||||||
|
from .core import structures
|
||||||
|
|
||||||
|
|
||||||
def generate_bracket(db: Session, t: models.Tournament):
|
def generate_bracket(db: Session, t: models.Tournament):
|
||||||
@@ -20,7 +21,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):
|
def resolve_target(match_node: structures.Match | None):
|
||||||
curr = match_node
|
curr = match_node
|
||||||
while curr and curr.is_bye:
|
while curr and curr.is_bye:
|
||||||
curr = curr.next_win
|
curr = curr.next_win
|
||||||
@@ -68,86 +69,99 @@ def generate_bracket(db: Session, t: models.Tournament):
|
|||||||
|
|
||||||
|
|
||||||
def update_schedule_times(db: Session, t: models.Tournament):
|
def update_schedule_times(db: Session, t: models.Tournament):
|
||||||
if not t.courts:
|
if not t.courts or not t.matches:
|
||||||
return
|
return
|
||||||
|
|
||||||
matches = t.matches
|
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}
|
match_map = {m.id: m for m in matches}
|
||||||
|
|
||||||
|
prereqs = defaultdict(list)
|
||||||
for m in matches:
|
for m in matches:
|
||||||
if m.id not in adj:
|
if m.winner_next_match_id:
|
||||||
adj[m.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)
|
||||||
|
|
||||||
if m.winner_next_match_id and m.winner_next_match_id in match_map:
|
depth_cache = {}
|
||||||
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:
|
def get_depth(m_id):
|
||||||
adj[m.id].append(m.loser_next_match_id)
|
if m_id not in match_map:
|
||||||
in_degree[m.loser_next_match_id] += 1
|
return 0
|
||||||
|
if m_id in depth_cache:
|
||||||
|
return depth_cache[m_id]
|
||||||
|
|
||||||
# 2. Initialize constraints
|
m = match_map[m_id]
|
||||||
# FIX: Ensure tournament_start is naive (no timezone) to match SQLite DB datetimes
|
child_depths = [0]
|
||||||
tournament_start = t.timestamp
|
if m.winner_next_match_id:
|
||||||
if tournament_start.tzinfo is not None:
|
child_depths.append(get_depth(m.winner_next_match_id))
|
||||||
tournament_start = tournament_start.replace(tzinfo=None)
|
if m.loser_next_match_id:
|
||||||
|
child_depths.append(get_depth(m.loser_next_match_id))
|
||||||
|
|
||||||
match_earliest_start = {m.id: tournament_start for m in matches}
|
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}
|
court_timers = {c.id: tournament_start for c in t.courts}
|
||||||
|
|
||||||
# 3. Topological Sort
|
planned_finish_times = {}
|
||||||
queue = deque([m.id for m in matches if in_degree[m.id] == 0])
|
unscheduled = list(matches)
|
||||||
|
|
||||||
# Sort initial batch to prioritize logical order
|
loop_limit = len(matches) * 2
|
||||||
initial_order = sorted(
|
while unscheduled and loop_limit > 0:
|
||||||
list(queue),
|
loop_limit -= 1
|
||||||
key=lambda mid: (match_map[mid].bracket_type, match_map[mid].match_number),
|
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)
|
||||||
)
|
)
|
||||||
queue = deque(initial_order)
|
cand = ready[0]
|
||||||
|
|
||||||
while queue:
|
cand.court_id = best_court_id
|
||||||
current_id = queue.popleft()
|
cand.start_time = current_time
|
||||||
match = match_map[current_id]
|
|
||||||
|
|
||||||
# FIX: Ensure DB start_time is treated as naive
|
fin = current_time + timedelta(minutes=t.duration)
|
||||||
m_start = match.start_time
|
planned_finish_times[cand.id] = fin
|
||||||
if m_start and m_start.tzinfo is not None:
|
court_timers[best_court_id] = fin
|
||||||
m_start = m_start.replace(tzinfo=None)
|
unscheduled.remove(cand)
|
||||||
|
|
||||||
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:
|
else:
|
||||||
# Schedule: Must wait for dependencies (min_start) AND court availability
|
next_wake = None
|
||||||
min_start = match_earliest_start[current_id]
|
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
|
||||||
|
|
||||||
best_court_id = min(
|
if next_wake:
|
||||||
court_timers, key=lambda k: max(court_timers[k], min_start)
|
court_timers[best_court_id] = next_wake
|
||||||
)
|
else:
|
||||||
|
break
|
||||||
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()
|
db.commit()
|
||||||
|
|
||||||
@@ -158,7 +172,7 @@ def advance_winner(db: Session, match: models.Match, winner_id: int):
|
|||||||
|
|
||||||
loser_id = match.p1_team_id if match.p1_team_id != winner_id else match.p2_team_id
|
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):
|
def update_next_match(next_match: models.Match, team_id: int):
|
||||||
if not next_match:
|
if not next_match:
|
||||||
return
|
return
|
||||||
if not next_match.p1_team_id:
|
if not next_match.p1_team_id:
|
||||||
@@ -189,16 +203,22 @@ def undo_advancement(db: Session, match: models.Match):
|
|||||||
winner_id = match.winner_team_id
|
winner_id = match.winner_team_id
|
||||||
loser_id = match.p1_team_id if match.p1_team_id != winner_id else match.p2_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):
|
def clear_from_next(next_match: models.Match, team_id: int):
|
||||||
if not next_match:
|
if not next_match:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if next_match.winner_team_id:
|
||||||
|
undo_advancement(db, next_match)
|
||||||
|
next_match.winner_team_id = None
|
||||||
|
next_match.sets = []
|
||||||
|
|
||||||
if next_match.p1_team_id == team_id:
|
if next_match.p1_team_id == team_id:
|
||||||
next_match.p1_team_id = None
|
next_match.p1_team_id = None
|
||||||
elif next_match.p2_team_id == team_id:
|
elif next_match.p2_team_id == team_id:
|
||||||
next_match.p2_team_id = None
|
next_match.p2_team_id = None
|
||||||
|
if next_match.p1_team_id and next_match.p2_team_id:
|
||||||
if next_match.status == MatchStatus.PENDING:
|
next_match.status = MatchStatus.PENDING
|
||||||
|
else:
|
||||||
next_match.status = MatchStatus.SCHEDULED
|
next_match.status = MatchStatus.SCHEDULED
|
||||||
|
|
||||||
db.add(next_match)
|
db.add(next_match)
|
||||||
@@ -206,5 +226,3 @@ def undo_advancement(db: Session, match: models.Match):
|
|||||||
clear_from_next(match.winner_next_match, winner_id)
|
clear_from_next(match.winner_next_match, winner_id)
|
||||||
if match.loser_next_match and loser_id:
|
if match.loser_next_match and loser_id:
|
||||||
clear_from_next(match.loser_next_match, loser_id)
|
clear_from_next(match.loser_next_match, loser_id)
|
||||||
|
|
||||||
db.commit()
|
|
||||||
|
|||||||
@@ -67,9 +67,7 @@ export default function Tournament() {
|
|||||||
time: m.start_time ? new Date(m.start_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '',
|
time: m.start_time ? new Date(m.start_time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '',
|
||||||
p1_sets: m.sets?.filter(s => s.p1 > s.p2).length || 0,
|
p1_sets: m.sets?.filter(s => s.p1 > s.p2).length || 0,
|
||||||
p2_sets: m.sets?.filter(s => s.p2 > s.p1).length || 0,
|
p2_sets: m.sets?.filter(s => s.p2 > s.p1).length || 0,
|
||||||
|
|
||||||
hasTeams,
|
hasTeams,
|
||||||
isReady,
|
|
||||||
isFinished
|
isFinished
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user