- Complete rewrite for single Court Manager user - Feature 1: Player Management (CRUD, ELO, skill level) - Feature 2: Court Reservation (timeline, override) - Feature 3: Match Event Builder (3-step wizard) - Step 1: Event setup with player selection + stage calculator - Step 2: Stage configurator (Open/Skill/RR/Tournament) - Step 3: Auto bracket generation - Feature 4: Live Court View (main dashboard, 2x2 grid) - Event Control: score entry, bracket advancement, court assignment - Screen view: TV display per court (/screen/:id) - Seed: 8 players, 4 courts, 1 completed + 1 active event - Routes: / players courts events events/new events/:id screen/:id
149 lines
4.8 KiB
Python
149 lines
4.8 KiB
Python
"""
|
|
Bracket and schedule generation logic for ServeSync
|
|
"""
|
|
import math
|
|
from typing import List, Optional, Tuple
|
|
|
|
|
|
def calculate_stages(num_players: int, format: str, courts: int) -> dict:
|
|
"""Calculate suggested stages and rounds for an event"""
|
|
if format == "Doubles":
|
|
teams = num_players // 2
|
|
else:
|
|
teams = num_players
|
|
|
|
matches_per_round = courts
|
|
|
|
if teams <= 4:
|
|
suggested_stages = 2
|
|
stage_names = ["Open Stage", "Tournament"]
|
|
elif teams <= 8:
|
|
suggested_stages = 2
|
|
stage_names = ["Open Stage", "Tournament"]
|
|
else:
|
|
suggested_stages = 3
|
|
stage_names = ["Open Stage", "Skill Stage", "Tournament"]
|
|
|
|
rounds_single_elim = math.ceil(math.log2(max(teams, 2)))
|
|
rounds_double_elim = rounds_single_elim * 2
|
|
|
|
return {
|
|
"teams": teams,
|
|
"matches_per_round": matches_per_round,
|
|
"rounds_single_elim": rounds_single_elim,
|
|
"rounds_double_elim": rounds_double_elim,
|
|
"suggested_stages": suggested_stages,
|
|
"stage_names": stage_names,
|
|
}
|
|
|
|
|
|
def generate_round_robin(player_ids: List[int]) -> List[Tuple[int, int]]:
|
|
"""Generate all round-robin pairings"""
|
|
pairings = []
|
|
n = len(player_ids)
|
|
players = list(player_ids)
|
|
if n % 2 == 1:
|
|
players.append(None) # bye
|
|
|
|
for round_num in range(len(players) - 1):
|
|
for i in range(len(players) // 2):
|
|
p1 = players[i]
|
|
p2 = players[len(players) - 1 - i]
|
|
if p1 is not None and p2 is not None:
|
|
pairings.append((p1, p2))
|
|
# Rotate: keep first fixed, rotate the rest
|
|
players = [players[0]] + [players[-1]] + players[1:-1]
|
|
|
|
return pairings
|
|
|
|
|
|
def generate_single_elim_bracket(player_ids: List[int]) -> List[dict]:
|
|
"""
|
|
Generate single elimination bracket matches.
|
|
Returns list of match dicts with round, match_number, player1, player2, next_winner_match_id
|
|
"""
|
|
n = len(player_ids)
|
|
# Pad to power of 2
|
|
bracket_size = 2 ** math.ceil(math.log2(max(n, 2)))
|
|
players = list(player_ids) + [None] * (bracket_size - n)
|
|
|
|
matches = []
|
|
match_id_counter = 1
|
|
|
|
# First round
|
|
round1_matches = []
|
|
for i in range(0, len(players), 2):
|
|
match = {
|
|
"temp_id": match_id_counter,
|
|
"round_number": 1,
|
|
"match_number": i // 2 + 1,
|
|
"player1_id": players[i],
|
|
"player2_id": players[i + 1],
|
|
"next_winner_temp_id": None,
|
|
"bracket_position": "winners",
|
|
"status": "scheduled",
|
|
}
|
|
# Handle byes
|
|
if players[i] is None or players[i + 1] is None:
|
|
match["status"] = "bye"
|
|
match["winner_temp"] = players[i] if players[i] is not None else players[i + 1]
|
|
round1_matches.append(match)
|
|
matches.append(match)
|
|
match_id_counter += 1
|
|
|
|
# Subsequent rounds
|
|
current_round_matches = round1_matches
|
|
round_num = 2
|
|
while len(current_round_matches) > 1:
|
|
next_round_matches = []
|
|
for i in range(0, len(current_round_matches), 2):
|
|
match = {
|
|
"temp_id": match_id_counter,
|
|
"round_number": round_num,
|
|
"match_number": i // 2 + 1,
|
|
"player1_id": None,
|
|
"player2_id": None,
|
|
"next_winner_temp_id": None,
|
|
"bracket_position": "winners",
|
|
"status": "scheduled",
|
|
}
|
|
# Set next_winner_temp_id for previous round matches
|
|
current_round_matches[i]["next_winner_temp_id"] = match_id_counter
|
|
if i + 1 < len(current_round_matches):
|
|
current_round_matches[i + 1]["next_winner_temp_id"] = match_id_counter
|
|
next_round_matches.append(match)
|
|
matches.append(match)
|
|
match_id_counter += 1
|
|
current_round_matches = next_round_matches
|
|
round_num += 1
|
|
|
|
return matches
|
|
|
|
|
|
def generate_open_schedule(player_ids: List[int], rounds: int, courts: int) -> List[dict]:
|
|
"""
|
|
Generate open match schedule - round robin up to 'rounds' rounds.
|
|
"""
|
|
all_pairings = generate_round_robin(player_ids)
|
|
matches = []
|
|
match_num = 1
|
|
round_num = 1
|
|
court_idx = 0
|
|
|
|
for round_round in range(min(rounds, math.ceil(len(all_pairings) / max(courts, 1)))):
|
|
start = round_round * courts
|
|
end = start + courts
|
|
round_pairings = all_pairings[start:end]
|
|
for i, (p1, p2) in enumerate(round_pairings):
|
|
matches.append({
|
|
"round_number": round_num,
|
|
"match_number": i + 1,
|
|
"player1_id": p1,
|
|
"player2_id": p2,
|
|
"status": "scheduled",
|
|
"bracket_position": "main",
|
|
})
|
|
round_num += 1
|
|
|
|
return matches
|