Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions backend/tournaments/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,28 @@ class GameSerializer(serializers.ModelSerializer):
"""Serializer for games."""
team1_name = serializers.CharField(source='team1.name', read_only=True)
team2_name = serializers.CharField(source='team2.name', read_only=True)
team1_pool = serializers.CharField(source='team1.pool', read_only=True)
team2_pool = serializers.CharField(source='team2.pool', read_only=True)
round_number = serializers.IntegerField(source='round.round_number', read_only=True)
room_name = serializers.CharField(source='room.name', read_only=True)
winner_name = serializers.SerializerMethodField()

pool = serializers.SerializerMethodField()

class Meta:
model = Game
fields = [
'id', 'round_number', 'room_name',
'team1_name', 'team2_name', 'team1_score', 'team2_score',
'id', 'round_number', 'room', 'room_name', 'pool',
'team1_name', 'team2_name', 'team1_pool', 'team2_pool',
'team1_score', 'team2_score',
'current_tossup', 'is_complete', 'winner_name',
'started_at', 'completed_at'
]

def get_winner_name(self, obj):
winner = obj.winner
return winner.name if winner else None

def get_pool(self, obj):
"""Get the pool this game belongs to (from either team)."""
# Both teams should be in the same pool for a valid game
return obj.team1.pool if obj.team1.pool else obj.team2.pool
167 changes: 121 additions & 46 deletions backend/tournaments/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,23 @@ def games(self, request, pk=None):
serializer = GameSerializer(games, many=True)
return Response(serializer.data)

@action(detail=True, methods=['delete'])
def clear_schedule(self, request, pk=None):
"""
Delete all games and rounds for this tournament.
"""
tournament = self.get_object()

games_count = tournament.games.count()
rounds_count = tournament.rounds.count()

tournament.games.all().delete()
tournament.rounds.all().delete()

return Response({
'message': f'Deleted {games_count} games and {rounds_count} rounds'
}, status=status.HTTP_200_OK)

@action(detail=True, methods=['post'])
def generate_schedule(self, request, pk=None):
"""
Expand Down Expand Up @@ -108,57 +125,108 @@ def generate_schedule(self, request, pk=None):
generated_games = []

with transaction.atomic():
round_number = 1
# Generate round-robin schedules for each pool using round-robin algorithm
from collections import deque

# For each pool, generate round-robin matchups
pool_round_schedules = {}
for pool_name, pool_teams in pools.items():
if pool_name == 'Unassigned' or len(pool_teams) < 2:
continue

# Generate all possible matchups (combinations of 2)
matchups = list(combinations(pool_teams, 2))

# Create games for each matchup
for idx, (team1, team2) in enumerate(matchups):
# Get or create round
round_obj, _ = Round.objects.get_or_create(
tournament=tournament,
round_number=round_number,
defaults={'name': f'Pool {pool_name} - Round {round_number}'}
)

# Assign to room (rotate through available rooms)
room = rooms[idx % len(rooms)]

# Create game
game = Game.objects.create(
tournament=tournament,
round=round_obj,
room=room,
team1=team1,
team2=team2
)

generated_games.append({
'id': game.id,
'pool': pool_name,
'round_number': round_number,
'room_name': room.name,
'team1_name': team1.name,
'team2_name': team2.name,
})

# Move to next round after filling all rooms
if (idx + 1) % len(rooms) == 0:
round_number += 1

# Ensure next pool starts on a new round
round_number += 1
# Round-robin scheduling algorithm
# For even number of teams, use standard round-robin
# For odd number, add a "bye" team
teams_list = list(pool_teams)
if len(teams_list) % 2 == 1:
teams_list.append(None) # Add bye

num_teams = len(teams_list)
num_rounds = num_teams - 1
games_per_round = num_teams // 2

pool_round_schedules[pool_name] = []

# Use circle method for round-robin scheduling
# Fix first team, rotate others
for round_num in range(num_rounds):
round_games = []
for game_num in range(games_per_round):
if game_num == 0:
# First game: fixed team vs team at end of rotation
team1 = teams_list[0]
team2 = teams_list[num_teams - 1]
else:
# Other games: pairs from the rotating part
team1 = teams_list[game_num]
team2 = teams_list[num_teams - 1 - game_num]

# Skip games with bye
if team1 is not None and team2 is not None:
round_games.append((team1, team2))

pool_round_schedules[pool_name].append(round_games)

# Rotate all teams except the first one
teams_list = [teams_list[0]] + [teams_list[-1]] + teams_list[1:-1]

# Find maximum number of rounds needed (same for all pools in round-robin)
max_rounds = max(len(rounds) for rounds in pool_round_schedules.values()) if pool_round_schedules else 0

# Create games round by round
# Each tournament round contains one round of games from EACH pool
room_idx = 0
for round_num in range(max_rounds):
round_number = round_num + 1

# Get or create the round
round_obj, _ = Round.objects.get_or_create(
tournament=tournament,
round_number=round_number,
defaults={'name': f'Round {round_number}'}
)

# Create all games for this round across all pools
# Each pool contributes its round_num-th set of games to this tournament round
for pool_name in sorted(pool_round_schedules.keys()):
if round_num < len(pool_round_schedules[pool_name]):
round_games = pool_round_schedules[pool_name][round_num]

for team1, team2 in round_games:
# Assign to room (rotate through available rooms)
room = rooms[room_idx % len(rooms)]
room_idx += 1

# Create game
game = Game.objects.create(
tournament=tournament,
round=round_obj,
room=room,
team1=team1,
team2=team2
)

generated_games.append({
'id': game.id,
'pool': pool_name,
'round_number': round_number,
'room_name': room.name,
'team1_name': team1.name,
'team2_name': team2.name,
})

# Build pool info for response
pool_info = {}
for pool_name, pool_teams in pools.items():
if pool_name != 'Unassigned':
pool_info[pool_name] = {
'team_count': len(pool_teams),
'teams': [t.name for t in pool_teams]
}

return Response({
'message': f'Successfully generated {len(generated_games)} games across {len(pools) - (1 if "Unassigned" in pools else 0)} pools',
'games': generated_games,
'pools': {pool: len(teams) for pool, teams in pools.items() if pool != 'Unassigned'},
'pool_info': pool_info,
}, status=status.HTTP_201_CREATED)


Expand Down Expand Up @@ -208,15 +276,22 @@ class PlayerViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.AllowAny]


class RoomViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet for viewing rooms."""
class RoomViewSet(viewsets.ModelViewSet):
"""
ViewSet for managing rooms.
Supports full CRUD operations.
"""
queryset = Room.objects.all()
serializer_class = RoomSerializer
permission_classes = [permissions.AllowAny]


class GameViewSet(viewsets.ReadOnlyModelViewSet):
"""ViewSet for viewing games."""
class GameViewSet(viewsets.ModelViewSet):
"""
ViewSet for managing games.
Allows updating room assignments.
"""
queryset = Game.objects.all()
serializer_class = GameSerializer
permission_classes = [permissions.AllowAny]
http_method_names = ['get', 'patch', 'head', 'options'] # Only allow GET and PATCH
Loading