feat: Rebuild as Court Manager Dashboard v2
- 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
This commit is contained in:
148
backend/app/bracket.py
Normal file
148
backend/app/bracket.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user