diff --git a/config/docker/docker-compose.yml b/config/docker/docker-compose.yml index 4c14bfbb7..23c344e1f 100644 --- a/config/docker/docker-compose.yml +++ b/config/docker/docker-compose.yml @@ -12,7 +12,7 @@ services: - "othello_pgdata:/var/lib/postgresql/data" othello_redis: image: redis:latest - ports: - - "6379:6379" + expose: + - "6379" volumes: othello_pgdata: diff --git a/othello/apps/tournaments/forms.py b/othello/apps/tournaments/forms.py index f2b75c5c8..561d6f6e6 100644 --- a/othello/apps/tournaments/forms.py +++ b/othello/apps/tournaments/forms.py @@ -19,6 +19,8 @@ class TournamentCreateForm(forms.ModelForm): game_time_limit = forms.IntegerField(label="Game Time Limit: ", min_value=1, max_value=15) num_rounds = forms.IntegerField(label="Amount of Rounds: ", min_value=5, max_value=settings.MAX_ROUND_NUM) include_users = forms.ModelMultipleChoiceField(label="Include Users: ", queryset=Submission.objects.latest()) + pairing_algorithm = forms.ChoiceField(label="Pairing Algorithm: ", choices=Tournament.PAIRING_ALGORITHMS, initial="swiss") + round_robin_matches = forms.IntegerField(initial=2, label="Round Robin Matches Between Two Players: ") bye_player = forms.ModelChoiceField(label="Bye Player: ", queryset=Submission.objects.latest()) runoff = forms.BooleanField(label="Enable Time Hoarding?", initial=False, required=False) diff --git a/othello/apps/tournaments/migrations/0027_tournament_pairing_algorithm.py b/othello/apps/tournaments/migrations/0027_tournament_pairing_algorithm.py new file mode 100644 index 000000000..f07ca09de --- /dev/null +++ b/othello/apps/tournaments/migrations/0027_tournament_pairing_algorithm.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.24 on 2025-06-11 14:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0026_remove_tournamentplayer_black_games_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='tournament', + name='pairing_algorithm', + field=models.CharField(choices=[('random', 'Random'), ('round_robin', 'Round Robin'), ('swiss', 'Swiss'), ('danish', 'Danish')], default='swiss', max_length=20), + ), + ] diff --git a/othello/apps/tournaments/migrations/0028_auto_20250805_2306.py b/othello/apps/tournaments/migrations/0028_auto_20250805_2306.py new file mode 100644 index 000000000..63bf30131 --- /dev/null +++ b/othello/apps/tournaments/migrations/0028_auto_20250805_2306.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.24 on 2025-08-06 03:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tournaments', '0027_tournament_pairing_algorithm'), + ] + + operations = [ + migrations.AddField( + model_name='tournament', + name='round_robin_matches', + field=models.IntegerField(default=2, help_text='How many matches to play between two players in a round robin tournament'), + ), + migrations.AlterField( + model_name='tournament', + name='pairing_algorithm', + field=models.CharField(choices=[('random', 'Random'), ('swiss', 'Swiss'), ('danish', 'Danish'), ('round_robin', 'Round Robin')], default='swiss', max_length=20), + ), + ] diff --git a/othello/apps/tournaments/models.py b/othello/apps/tournaments/models.py index d5f61c220..bb3c713d9 100644 --- a/othello/apps/tournaments/models.py +++ b/othello/apps/tournaments/models.py @@ -20,6 +20,12 @@ def filter_future(self) -> "models.query.QuerySet[Tournament]": class Tournament(models.Model): + PAIRING_ALGORITHMS = ( + ("random", "Random"), + ("swiss", "Swiss"), + ("danish", "Danish"), + ("round_robin", "Round Robin"), + ) objects: Any = TournamentSet().as_manager() @@ -39,6 +45,12 @@ class Tournament(models.Model): related_name="bye", default=None, ) + pairing_algorithm = models.CharField( + choices=PAIRING_ALGORITHMS, + default="swiss", + max_length=20, + ) + round_robin_matches = models.IntegerField(default=2, help_text="How many matches to play between two players in a round robin tournament") finished = models.BooleanField(default=False) terminated = models.BooleanField(default=False) diff --git a/othello/apps/tournaments/pairings.py b/othello/apps/tournaments/pairings.py index e20db4862..056b41ad7 100644 --- a/othello/apps/tournaments/pairings.py +++ b/othello/apps/tournaments/pairings.py @@ -1,14 +1,22 @@ import random from typing import List, Tuple -from othello.apps.tournaments.models import TournamentPlayer +from othello.apps.tournaments.models import Tournament, TournamentPlayer from othello.apps.tournaments.utils import chunks, get_updated_ranking, logger Players = List[TournamentPlayer] Pairings = List[Tuple[TournamentPlayer, ...]] -def random_pairing(players: Players, bye_player: TournamentPlayer) -> Pairings: +def round_robin_pairing(players: Players, _bye_player: TournamentPlayer, **kwargs) -> Pairings: + matches: Pairings = [] + for i in range(len(players)): + for j in range(i + 1, len(players)): + matches.extend((players[i], players[j]) for _ in range(kwargs["round_robin_matches"])) + return matches + + +def random_pairing(players: Players, bye_player: TournamentPlayer, **kwargs) -> Pairings: randomized: Players = random.sample(players, len(players)) if len(randomized) % 2 == 1: randomized.append(bye_player) @@ -16,7 +24,7 @@ def random_pairing(players: Players, bye_player: TournamentPlayer) -> Pairings: return list(chunks(randomized, 2)) -def swiss_pairing(players: Players, bye_player: TournamentPlayer) -> Pairings: +def swiss_pairing(players: Players, bye_player: TournamentPlayer, **kwargs) -> Pairings: tournament = players[0].tournament # Default to danish pairing if there are enough rounds @@ -55,7 +63,7 @@ def swiss_pairing(players: Players, bye_player: TournamentPlayer) -> Pairings: return matches -def danish_pairing(players: Players, bye_player: TournamentPlayer) -> Pairings: +def danish_pairing(players: Players, bye_player: TournamentPlayer, **kwargs) -> Pairings: logger.warning("Using Danish Pairing") matches = [] players = sorted(players, key=get_updated_ranking, reverse=True) @@ -65,8 +73,8 @@ def danish_pairing(players: Players, bye_player: TournamentPlayer) -> Pairings: players.append(bye_player) # black = random.choice((players[i], players[i + 1])) # white = players[i] if black == players[i + 1] else players[i + 1] - matches.append((players[i], players[i+1])) - matches.append((players[i+1], players[i])) + matches.append((players[i], players[i + 1])) + matches.append((players[i + 1], players[i])) return matches @@ -75,10 +83,9 @@ def danish_pairing(players: Players, bye_player: TournamentPlayer) -> Pairings: "random": random_pairing, "swiss": swiss_pairing, "danish": danish_pairing, + "round_robin": round_robin_pairing, } -def pair(players: Players, bye_player: TournamentPlayer, algorithm: str = "swiss") -> Pairings: - if algorithm not in algorithms: - raise ValueError(f"Invalid pairing algorithm: {algorithm}") - return algorithms[algorithm](players, bye_player) +def pair(players: Players, bye_player: TournamentPlayer, tournament: Tournament, **kwargs) -> Pairings: + return algorithms[tournament.pairing_algorithm](players, bye_player, **kwargs) diff --git a/othello/apps/tournaments/tasks.py b/othello/apps/tournaments/tasks.py index 31325cbeb..4bc9b2763 100644 --- a/othello/apps/tournaments/tasks.py +++ b/othello/apps/tournaments/tasks.py @@ -100,7 +100,16 @@ def run_tournament(tournament_id: int) -> None: bye_player = TournamentPlayer.objects.create(tournament=t, submission=t.bye_player) for round_num in range(t.num_rounds): - matches: List[Tuple[TournamentPlayer, ...]] = pair(submissions, bye_player) + try: + matches: List[Tuple[TournamentPlayer, ...]] = pair( + submissions, + bye_player, + t, + round_robin_matches=t.round_robin_matches, + ) + except Exception as e: + logger.error(f"Error in pairing for tournament {tournament_id}, round {round_num + 1}: {e}") + raise t.refresh_from_db() if t.terminated: t.delete() diff --git a/othello/static/js/tournaments/create.js b/othello/static/js/tournaments/create.js index 6b7a80d3e..ba1bfd8f8 100644 --- a/othello/static/js/tournaments/create.js +++ b/othello/static/js/tournaments/create.js @@ -25,6 +25,11 @@ window.onload = function () { maxItems: 1, sortField: [{'field': 'text', 'direction': 'desc'}] }); + $("#id_pairing_algorithm").selectize({ + maxItems: 1, + sortField: [{'field': 'text', 'direction': 'desc'}] + }); + $("#includeUsersFile").on('change', function () { let reader = new FileReader(); diff --git a/othello/templates/tournaments/create.html b/othello/templates/tournaments/create.html index 324b5528f..440737614 100644 --- a/othello/templates/tournaments/create.html +++ b/othello/templates/tournaments/create.html @@ -44,6 +44,14 @@



+

+ {{ form.pairing_algorithm.label }} + {{ form.pairing_algorithm }} +

+

+ {{ form.round_robin_matches.label }} + {{ form.round_robin_matches }} +

{{ form.include_users.label }} {{ form.include_users }} @@ -82,4 +90,4 @@

Scheduled Tournaments

-{% endblock %} \ No newline at end of file +{% endblock %}