diff --git a/djangofiles/frisbeer/admin.py b/djangofiles/frisbeer/admin.py index 567aead..7d02871 100644 --- a/djangofiles/frisbeer/admin.py +++ b/djangofiles/frisbeer/admin.py @@ -23,7 +23,9 @@ class TeamAdmin(admin.ModelAdmin): admin.site.register(Player) admin.site.register(Game, GameAdmin) +admin.site.register(GameRules) admin.site.register(Location) admin.site.register(Rank) admin.site.register(Season) +admin.site.register(SeasonRules) admin.site.register(Team, TeamAdmin) diff --git a/djangofiles/frisbeer/migrations/0032_auto_20200613_1432.py b/djangofiles/frisbeer/migrations/0032_auto_20200613_1432.py new file mode 100644 index 0000000..cbb3d73 --- /dev/null +++ b/djangofiles/frisbeer/migrations/0032_auto_20200613_1432.py @@ -0,0 +1,75 @@ +# Generated by Django 3.0.7 on 2020-06-13 14:32 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +def set_frisbeer_rules(apps, schema_editor): + GameRules = apps.get_model('frisbeer', 'GameRules') + + frisbeer_rules = GameRules(name='Frisbeer', + min_players=6, + max_players=6, + min_rounds=2, + max_rounds=3) + frisbeer_rules.save() + + Season = apps.get_model('frisbeer', 'Season') + Game = apps.get_model('frisbeer', 'Game') + + Season.objects.filter(game_rules__isnull=True).update(game_rules=frisbeer_rules) + Game.objects.filter(rules__isnull=True, season__isnull=True).update(rules=frisbeer_rules) + + +def unset_rules(apps, schem_editor): + Season = apps.get_model('frisbeer', 'Season') + Game = apps.get_model('frisbeer', 'Game') + + Season.objects.all().update(game_rules=None) + Game.objects.all().update(rules=None) + + +class Migration(migrations.Migration): + + dependencies = [ + ('frisbeer', '0031_auto_20190630_1123'), + ] + + operations = [ + migrations.CreateModel( + name='GameRules', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, unique=True)), + ('min_players', models.IntegerField(default=6, validators=[django.core.validators.MinValueValidator(0)])), + ('max_players', models.IntegerField(default=6, validators=[django.core.validators.MinValueValidator(0)])), + ('min_rounds', models.IntegerField(default=2, validators=[django.core.validators.MinValueValidator(0)])), + ('max_rounds', models.IntegerField(default=3, validators=[django.core.validators.MinValueValidator(0)])), + ], + ), + migrations.AlterField( + model_name='game', + name='location', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='frisbeer.Location'), + ), + migrations.AlterField( + model_name='game', + name='season', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='frisbeer.Season'), + ), + migrations.AddField( + model_name='game', + name='rules', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='frisbeer.GameRules'), + ), + migrations.AddField( + model_name='season', + name='game_rules', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='frisbeer.GameRules'), + ), + migrations.RunPython( + set_frisbeer_rules, + unset_rules + ) + ] diff --git a/djangofiles/frisbeer/migrations/0033_auto_20200614_0910.py b/djangofiles/frisbeer/migrations/0033_auto_20200614_0910.py new file mode 100644 index 0000000..5dd46a1 --- /dev/null +++ b/djangofiles/frisbeer/migrations/0033_auto_20200614_0910.py @@ -0,0 +1,22 @@ +# Generated by Django 3.0.7 on 2020-06-14 09:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('frisbeer', '0032_auto_20200613_1432'), + ] + + operations = [ + migrations.AlterModelOptions( + name='gamerules', + options={'verbose_name': 'Ruleset'}, + ), + migrations.RenameField( + model_name='game', + old_name='rules', + new_name='_rules', + ), + ] diff --git a/djangofiles/frisbeer/migrations/0034_season_rules.py b/djangofiles/frisbeer/migrations/0034_season_rules.py new file mode 100644 index 0000000..8a4bb23 --- /dev/null +++ b/djangofiles/frisbeer/migrations/0034_season_rules.py @@ -0,0 +1,56 @@ +# Generated by Django 3.0.7 on 2020-06-17 19:40 + +from django.db import migrations, models +import django.db.models.deletion + +def algorithm_to_season_rules(apps, schema_editor): + SeasonRules = apps.get_model('frisbeer', 'SeasonRules') + Season = apps.get_model('frisbeer', 'Season') + + for season in Season.objects.all(): + name = season.get_score_algorithm_display() + algorithm = season.score_algorithm + season.rules, _ = SeasonRules.objects.get_or_create(name=name, score_algorithm=algorithm) + season.save() + + +def season_rules_to_algorithm(apps, schema_editor): + Season = apps.get_model('frisbeer', 'Season') + for season in Season.objects.all(): + season.score_algorithm = season.rules.score_algorithm + season.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('frisbeer', '0033_auto_20200614_0910'), + ] + + operations = [ + migrations.CreateModel( + name='SeasonRules', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('elo_decay', models.BooleanField(default=True)), + ('score_algorithm', models.CharField(choices=[('2017', 'Season 2017'), ('2018', 'Season 2018'), ('elo', 'Elo'), ('top_elo', 'Best elo')], default='elo', max_length=16)), + ('rank_statistic', models.CharField(choices=[('RP', 'Rounds played'), ('RW', 'Rounds won'), ('GP', 'Games played'), ('GW', 'Games won')], default='RW', max_length=3)), + ('rank_min_value', models.IntegerField(default=5)), + ], + ), + migrations.AddField( + model_name='season', + name='rules', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='frisbeer.SeasonRules'), + ), + migrations.RunPython( + algorithm_to_season_rules, + season_rules_to_algorithm + ), + migrations.AlterField( + model_name='season', + name='game_rules', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='frisbeer.GameRules'), + ), + ] diff --git a/djangofiles/frisbeer/models.py b/djangofiles/frisbeer/models.py index 0ec1ec9..1bc75d5 100644 --- a/djangofiles/frisbeer/models.py +++ b/djangofiles/frisbeer/models.py @@ -1,11 +1,16 @@ import itertools from operator import itemgetter -from math import exp +from math import exp, ceil from datetime import date +from django.core.exceptions import ValidationError +from django.core.validators import MinValueValidator +from django.utils.translation import ugettext_lazy as _ from django.utils.timezone import now from django.db import models +from frisbeer import utils + class Rank(models.Model): name = models.CharField(max_length=100, blank=True, unique=True) @@ -30,29 +35,52 @@ def __str__(self): return self.name -class Season(models.Model): - ALGORITHM_2017 = '2017' - ALGORITHM_2018 = '2018' - ALGORITHM_TOP_ELO = 'elo' - ALGORITHM_CHOICES = ( - (ALGORITHM_2017, '2017'), - (ALGORITHM_2018, '2018'), - (ALGORITHM_TOP_ELO, 'Best elo') - ) +class GameRules(models.Model): + name = models.CharField(max_length=50, unique=True) + min_players = models.IntegerField(default=6, validators=[MinValueValidator(0)]) + max_players = models.IntegerField(default=6, validators=[MinValueValidator(0)]) + min_rounds = models.IntegerField(default=2, validators=[MinValueValidator(0)]) + max_rounds = models.IntegerField(default=3, validators=[MinValueValidator(0)]) - name = models.CharField(max_length=255, unique=True) - start_date = models.DateField(default=now) - end_date = models.DateField(null=True, blank=True) - score_algorithm = models.CharField(max_length=255, choices=ALGORITHM_CHOICES) + class Meta: + verbose_name = 'Ruleset' + + def clean(self): + if self.min_players > self.max_players: + self.max_players = self.min_players + if self.min_rounds > self.max_rounds: + self.max_rounds = self.min_rounds def __str__(self): return self.name - @staticmethod - def current(): - return Season.objects.filter(start_date__lte=date.today()).order_by('-start_date').first() - def score(self, *args, **kwargs): +class ScoreAlgorithm(models.TextChoices): + S_2017 = '2017', _('Season 2017') + S_2018 = '2018', _('Season 2018') + ELO = 'elo', _('Elo') + TOP_ELO = 'top_elo', _('Best elo') + + +class PlayerStatistic(models.TextChoices): + ROUNDS_PLAYED = 'RP', _('Rounds played') + ROUNDS_WON = 'RW', _('Rounds won') + GAMES_PLAYED = 'GP', _('Games played') + GAMES_WON = 'GW', _('Games won') + + +class SeasonRules(models.Model): + name = models.CharField(max_length=100) + elo_decay = models.BooleanField(default=True) + score_algorithm = models.CharField(max_length=16, choices=ScoreAlgorithm.choices, default=ScoreAlgorithm.ELO) + rank_statistic = models.CharField(max_length=3, choices=PlayerStatistic.choices, default=PlayerStatistic.ROUNDS_WON) + rank_min_value = models.IntegerField(default=5) + + def __str__(self): + return self.name or self.score_algorithm + + @property + def algorithm(self): def score_2017(games_played, rounds_played, rounds_won, *args, **kwargs): win_rate = rounds_won / rounds_played if rounds_played != 0 else 0 return int(win_rate * (1 - exp(-games_played / 4)) * 1000) @@ -61,15 +89,40 @@ def score_2018(games_played, rounds_played, rounds_won, *args, **kwargs): win_rate = rounds_won / rounds_played if rounds_played != 0 else 0 return int(rounds_won + win_rate * (1 / (1 + exp(3 - games_played / 2.5))) * 1000) + def score_best_elo(player, *args, **kwargs): + return player.season_best + def score_elo(player, *args, **kwargs): return player.elo - if self.score_algorithm == Season.ALGORITHM_2017: - return score_2017(*args, **kwargs) - elif self.score_algorithm == Season.ALGORITHM_2018: - return score_2018(*args, **kwargs) + if self.score_algorithm == ScoreAlgorithm.S_2017: + return score_2017 + elif self.score_algorithm == ScoreAlgorithm.S_2018: + return score_2018 + elif self.score_algorithm == ScoreAlgorithm.TOP_ELO: + return score_best_elo else: - return score_elo(*args, **kwargs) + return score_elo + + +class Season(models.Model): + + name = models.CharField(max_length=255, unique=True) + start_date = models.DateField(default=now) + end_date = models.DateField(null=True, blank=True) + rules = models.ForeignKey(SeasonRules, null=True, on_delete=models.PROTECT) + score_algorithm = models.CharField(max_length=255, choices=ScoreAlgorithm.choices) + game_rules = models.ForeignKey(GameRules, null=True, blank=True, on_delete=models.PROTECT) + + def __str__(self): + return self.name + + @staticmethod + def current(): + return Season.objects.filter(start_date__lte=date.today()).order_by('-start_date').first() + + def score(self, *args, **kwargs): + return self.rules.algorithm(*args, **kwargs) class Team(models.Model): @@ -129,7 +182,8 @@ class Game(models.Model): (PLAYED, "Played"), (APPROVED, "Approved")) - season = models.ForeignKey(Season, on_delete=models.SET_NULL, null=True) + season = models.ForeignKey(Season, null=True, blank=True, on_delete=models.SET_NULL) + _rules = models.ForeignKey(GameRules, null=True, blank=True, on_delete=models.SET_NULL) players = models.ManyToManyField(Player, related_name='games', through='GamePlayerRelation') date = models.DateTimeField(default=now) @@ -157,6 +211,14 @@ def _team(self, side): except GameTeamRelation.DoesNotExist: return None + def clean(self): + if self.rules is None and (self.season is None or self.season.game_rules is None): + raise ValidationError({'rules': _('Rules or season with rules must be set')}) + + @property + def rules(self): + return self._rules or self.season.game_rules + @property def team1(self): return self._team(1) @@ -181,35 +243,27 @@ def __str__(self): ) def can_create_teams(self): - return self.state == Game.READY \ - and self.players.count() == 6 \ - and self.players.filter(gameplayerrelation__team=0).count() == 6 + player_count = self.players.count() + return self.rules.min_players <= player_count <= self.rules.max_players \ + and player_count == self.players.filter(gameplayerrelation__team=0).count() def can_score(self): - return self.state >= Game.APPROVED and (self.team1_score == 2 or self.team2_score == 2) \ - and self.players.count() == 6 \ - and self.players.filter(gameplayerrelation__team=1).count() == 3 \ - and self.players.filter(gameplayerrelation__team=2).count() == 3 + players_count = self.players.count() + players_per_team = ceil(players_count / 2) + rounds_played = self.team1_score + self.team2_score + return self.state >= Game.APPROVED \ + and self.rules.min_rounds <= rounds_played <= self.rules.max_rounds \ + and self.rules.min_players <= players_count <= self.rules.max_players \ + and self.players.filter(gameplayerrelation__team=0).count() == 0 \ + and self.players.filter(gameplayerrelation__team=1).count() <= players_per_team \ + and self.players.filter(gameplayerrelation__team=2).count() <= players_per_team def create_teams(self): - def calculate_team_elo(team): - return int(sum([player.elo for player in team]) / len(team)) - - elo_list = [] - players = set(self.players.all()) - possibilities = itertools.combinations(players, 3) - for possibility in possibilities: - team1 = possibility - team2 = players - set(team1) - elo1 = calculate_team_elo(team1) - elo2 = calculate_team_elo(team2) - elo_list.append((abs(elo1 - elo2), team1, team2)) - ideal_teams = sorted(elo_list, key=itemgetter(0))[0] + ideal_teams = utils.create_equal_teams(set(self.players.all())) self.gameplayerrelation_set \ .filter(player__id__in=[player.id for player in ideal_teams[1]]).update(team=GamePlayerRelation.Team1) self.gameplayerrelation_set \ .filter(player__id__in=[player.id for player in ideal_teams[2]]).update(team=GamePlayerRelation.Team2) - print(ideal_teams[0]) self.save() diff --git a/djangofiles/frisbeer/serializers.py b/djangofiles/frisbeer/serializers.py index 2a072f2..bd33f89 100644 --- a/djangofiles/frisbeer/serializers.py +++ b/djangofiles/frisbeer/serializers.py @@ -2,7 +2,7 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError -from frisbeer.models import Rank, Player, GamePlayerRelation, Game, Location, Team, GameTeamRelation +from frisbeer.models import Rank, Player, GamePlayerRelation, Game, Location, Team, GameTeamRelation, Season, GameRules class LocationSerializer(serializers.ModelSerializer): @@ -84,14 +84,22 @@ class Meta: fields = ('name', 'side') +class GameRulesSerializer(serializers.ModelSerializer): + class Meta: + model = GameRules + fields = '__all__' + + class GameSerializer(serializers.ModelSerializer): + rules = GameRulesSerializer(read_only=True) location_repr = LocationSerializer(source='location', read_only=True) players = PlayerInGameSerializer(many=True, source='gameplayerrelation_set', partial=True, required=False) teams = TeamInGameSerializer(many=True, source='gameteamrelation_set', partial=True, required=False) class Meta: model = Game - fields = '__all__' + exclude = ['_rules'] + def validate(self, attrs): admin = self.context['request'].user.is_staff @@ -115,25 +123,20 @@ def validate(self, attrs): raise ValidationError("Only admins can roll back game state") if state >= Game.APPROVED and not admin: raise ValidationError("Only admins can approve games and edit approved games") - if state >= Game.READY and len(players) != 6: + if state >= Game.READY and not (game.rules.min_players <= len(players) <= game.rules.max_players): raise ValidationError("A game without exactly six players must be in pending state") - - if state >= Game.READY and len(team1) != 3 and len(team2) != 3: + if state >= Game.READY and (len(team1) + len(team2)) != len(players): raise ValidationError("Game that doesn't have teams must be in Pending state (0). " "Create teams manually or with create_teams endpoint") if state >= Game.PLAYED: - if game.team1_score + game.team2_score > 3: - raise ValidationError("Too many round wins. Frisbeer is played best of three") - if game.state >= Game.READY and game.players.count() != 6: - raise ValidationError("The game needs 6 players to be ready") - if state >= Game.PLAYED and team1_score != 2 and team2_score != 2: - raise ValidationError("One team needs two round wins to win the game") - if team1_score != 2 and team2_score != 2: + if game.team1_score + game.team2_score > game.rules.max_rounds: + raise ValidationError("Too many round wins.") + if team1_score != game.rules.min_rounds and team2_score != game.rules.min_rounds: raise ValidationError("One team needs two round wins to win the game") - if players and len(players) > 6: - raise ValidationError("Game can't have more than 6 players") + if players and len(players) > game.rules.max_players: + raise ValidationError(f"Game can't have more than {game.rules.max_players} players") return attrs diff --git a/djangofiles/frisbeer/signals.py b/djangofiles/frisbeer/signals.py index f29b238..99fc6c2 100644 --- a/djangofiles/frisbeer/signals.py +++ b/djangofiles/frisbeer/signals.py @@ -1,7 +1,6 @@ import logging from collections import OrderedDict, defaultdict -from typing import List from scipy.stats import zscore from django.contrib.auth.models import User @@ -10,11 +9,10 @@ from django.dispatch import receiver from rest_framework.authtoken.models import Token -from server import settings from frisbeer.models import * +from frisbeer.utils import * -@receiver(m2m_changed, sender=Game.players.through) @receiver(m2m_changed, sender=Game.players.through) @receiver(post_save, sender=Game) def update_statistics(sender, instance, **kwargs): @@ -28,17 +26,6 @@ def update_statistics(sender, instance, **kwargs): update_team_score() -def calculate_elo_change(my_elo, opponent_elo, win): - if win: - actual_score = 1 - else: - actual_score = 0 - Ra = my_elo - Rb = opponent_elo - Ea = 1 / (1 + 10 ** ((Rb - Ra) / 400)) - return settings.ELO_K * (actual_score - Ea) - - def update_elo(): """ Calculate new elos for all players. @@ -48,9 +35,6 @@ def update_elo(): logging.info("Updating elos (mabby)") - def calculate_team_elo(team): - return sum([player.elo for player in team]) / len(team) - def _elo_decay(): # Halves the distance from median elo for all players Player.objects.all().update(elo=(F('elo') - 1500) / 2 + 1500, season_best=0) @@ -190,16 +174,31 @@ def calculate_ranks(): players = Player.objects.all() players.update(rank=None) ranked_players_list = [] + stat = season.rules.rank_statistic for player in players: - s1 = player.gameplayerrelation_set.filter(team=1, game__season_id=season.id) \ - .aggregate(Sum('game__team1_score'))["game__team1_score__sum"] or 0 - s2 = player.gameplayerrelation_set.filter(team=2, game__season_id=season.id) \ - .aggregate(Sum('game__team2_score'))["game__team2_score__sum"] or 0 - if s1 + s2 > 4: + logging.info(f'Calculating ranks for player {player} with stat {stat}') + if stat == PlayerStatistic.GAMES_PLAYED: + value = player.gameplayerrelation_set.filter(game__season_id=season.id).count() + elif stat == PlayerStatistic.GAMES_WON: + gw1 = player.gameplayerrelation_set.filter(team=1, + game__season_id=season.id, + game__team1_score__gt=F('game__team2_score')).count() + gw2 = player.gameplayerrelation_set.filter(team=2, + game__season_id=season.id, + game__team2_score__gt=F('game__team1_score')) + value = gw1 + gw2 + elif stat == PlayerStatistic.ROUNDS_WON: + rw1 = player.gameplayerrelation_set.filter(team=1, game__season_id=season.id) \ + .aggregate(Sum('game__team1_score'))["game__team1_score__sum"] or 0 + rw2 = player.gameplayerrelation_set.filter(team=2, game__season_id=season.id) \ + .aggregate(Sum('game__team2_score'))["game__team2_score__sum"] or 0 + value = rw1 + rw2 + logging.info(f'Value is {value}, comparing to {season.rules.rank_min_value}') + if value >= season.rules.rank_min_value: ranked_players_list.append(player) if not ranked_players_list: - logging.debug("No players with four round victories") + logging.debug("No players with rank criteria") for player in players: player.rank = None player.save() diff --git a/djangofiles/frisbeer/utils.py b/djangofiles/frisbeer/utils.py new file mode 100644 index 0000000..dd06df9 --- /dev/null +++ b/djangofiles/frisbeer/utils.py @@ -0,0 +1,48 @@ +import itertools +from operator import itemgetter +from typing import Sequence + +from django.conf import settings + + +def calculate_team_elo(players): + return int(sum([player.elo for player in players]) / len(players)) + + +def calculate_elo_change(my_elo, opponent_elo, win): + """ + Calculates the elo change that should be applied + :param my_elo: Elo of the entity whose elo change is being calculated + :param opponent_elo: Elo of the opposing entity + :param win: True if the first player won, False if lost. + :return: Elo change to be applied to the first entity. + Opposing player's change is given through multiplication by -1 + """ + if win: + actual_score = 1 + else: + actual_score = 0 + Ra = my_elo + Rb = opponent_elo + Ea = 1 / (1 + 10 ** ((Rb - Ra) / 400)) + return settings.ELO_K * (actual_score - Ea) + + +def create_equal_teams(players): + """ + Creates teams with as close elo average as possible + :param players: Sequence of players to create teams from + :return: Tuple containing: Elo difference between the teams; players in team 1; players in team 2 + """ + + elo_list = [] + players_in_team = len(players) // 2 + possibilities = itertools.combinations(players, players_in_team) + + for team1 in possibilities: + team2 = players - set(team1) + elo1 = calculate_team_elo(team1) + elo2 = calculate_team_elo(team2) + elo_list.append((abs(elo1 - elo2), team1, team2)) + ideal_teams = sorted(elo_list, key=itemgetter(0))[0] + return ideal_teams diff --git a/djangofiles/frisbeer/views.py b/djangofiles/frisbeer/views.py index 1509d2f..9df7177 100644 --- a/djangofiles/frisbeer/views.py +++ b/djangofiles/frisbeer/views.py @@ -14,6 +14,7 @@ from frisbeer.models import * from frisbeer.serializers import RankSerializer, PlayerSerializer, PlayerInGameSerializer, GameSerializer, \ LocationSerializer, TeamSerializer +from frisbeer.utils import create_equal_teams, calculate_team_elo class RankViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewSet): @@ -59,7 +60,7 @@ def add_player(self, request, pk=None): with transaction.atomic(): relation = GamePlayerRelation(game=game, player=player) relation.save() - if GamePlayerRelation.objects.filter(game=game).count() > 6: + if GamePlayerRelation.objects.filter(game=game).count() > game.rules.max_players: raise APIException(detail="Game is already full", code=400) return redirect(reverse("frisbeer:games-detail", args=[pk])) @@ -74,8 +75,8 @@ def remove_player(self, request, pk=None): @action(detail=True, methods=['post']) def create_teams(self, request, pk=None): game = get_object_or_404(Game, pk=pk) - if GamePlayerRelation.objects.filter(game=game).count() != 6: - raise APIException("Game needs 6 players before teams can be created", code=400) + if not game.can_create_teams(): + raise APIException("Game has the wrong amount of players", code=400) force = request.data.get("re_create", False) game.create_teams() game.state = Game.READY @@ -95,9 +96,8 @@ class LocationViewSet(viewsets.ModelViewSet): def validate_players(value): - logging.debug("Validating players") - if len(value) != 6 or len(set(value)) != 6: - raise ValidationError("Select exactly six different players") + if len(value) <= 1 or len(set(value)) <= 1: + raise ValidationError("Select 2 players or more") class EqualTeamForm(forms.Form): @@ -114,26 +114,14 @@ class TeamCreateView(FormView): form_class = EqualTeamForm def form_valid(self, form): - def calculate_team_elo(team): - return int(sum([player.elo for player in team]) / len(team)) - - elo_list = [] players = set(Player.objects.filter(id__in=form.cleaned_data["players"])) - possibilities = itertools.combinations(players, 3) - for possibility in possibilities: - team1 = possibility - team2 = players - set(team1) - elo1 = calculate_team_elo(team1) - elo2 = calculate_team_elo(team2) - elo_list.append((abs(elo1 - elo2), team1, team2)) - ideal_teams = sorted(elo_list, key=itemgetter(0))[0] + ideal_teams = create_equal_teams(players) teams = { "team1": ideal_teams[1], "team1_elo": calculate_team_elo(ideal_teams[1]), "team2": ideal_teams[2], "team2_elo": calculate_team_elo(ideal_teams[2]), } - return render(self.request, 'frisbeer/team_select_form.html', {"form": form, "teams": teams})