From df036ba384dbdc2d44f9691fe8859fb3c2716f2a Mon Sep 17 00:00:00 2001 From: Emmy D'Anello Date: Sun, 24 Mar 2024 21:37:37 +0100 Subject: [PATCH] Update draw with the new team repartition Signed-off-by: Emmy D'Anello --- draw/consumers.py | 72 ++++++++++++++++++++++++++++------------- participation/admin.py | 2 +- participation/models.py | 2 +- 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/draw/consumers.py b/draw/consumers.py index 1c804c4..d9292d5 100644 --- a/draw/consumers.py +++ b/draw/consumers.py @@ -152,7 +152,7 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): try: # Parse format from string - fmt: list[int] = sorted(map(int, fmt.split('+')), reverse=True) + fmt: list[int] = sorted(map(int, fmt.split('+'))) except ValueError: return await self.alert(_("Invalid format"), 'danger') @@ -416,10 +416,8 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): # For each pool of size N, put the N next teams into this pool async for p in Pool.objects.filter(round_id=self.tournament.draw.current_round_id).order_by('letter').all(): - # Fetch the N teams, then order them in a new order for the passages inside the pool - # We multiply the dice scores by 27 mod 100 (which order is 20 mod 100) for this new order - # This simulates a deterministic shuffle - pool_tds = sorted(tds_copy[:p.size], key=lambda td: (td.passage_dice * 27) % 100) + # Fetch the N teams + pool_tds = tds_copy[:p.size].copy() # Remove the head tds_copy = tds_copy[p.size:] for i, td in enumerate(pool_tds): @@ -428,34 +426,64 @@ class DrawConsumer(AsyncJsonWebsocketConsumer): td.passage_index = i await td.asave() - # The passages of the second round are determined from the scores of the dices - # The team that has the lowest dice score goes to the first pool, then the team - # that has the second-lowest score goes to the second pool, etc. - # This also determines the passage order, in the natural order this time. - # If there is a 5-teams pool, we force the last team to be in the first pool, - # which is this specific pool since they are ordered by decreasing size. - # This is not true for the final tournament, which considers the scores of the - # first round. + # The passages of the second round are determined from the order of the passages of the first round. + # We order teams by increasing passage index, and then by decreasing pool number. + # We keep teams that were at the last position in a 5-teams pool apart, as "jokers". + # Then, we fill pools one team by one team. + # As we fill one pool for the second round, we check if we can place a joker in it. + # We can add a joker team if there is not already a team in the pool that was in the same pool + # in the first round, and such that the number of such jokers is exactly the free space of the current pool. + # Exception: if there is one only pool with 5 teams, we exchange the first and the last teams of the pool. if not self.tournament.final: - tds_copy = tds.copy() + tds_copy = sorted(tds, key=lambda td: (td.passage_index, -td.pool.letter,)) + jokers = [td for td in tds if td.passage_index == 4] round2 = await self.tournament.draw.round_set.filter(number=2).aget() round2_pools = [p async for p in Pool.objects.filter(round__draw__tournament=self.tournament, round=round2) .order_by('letter').all()] current_pool_id, current_passage_index = 0, 0 for i, td in enumerate(tds_copy): - if i == len(tds) - 1 and round2_pools[0].size == 5: - current_pool_id = 0 - current_passage_index = 4 - td2 = await TeamDraw.objects.filter(participation=td.participation, round=round2).aget() td2.pool = round2_pools[current_pool_id] td2.passage_index = current_passage_index - current_pool_id += 1 - if current_pool_id == len(round2_pools): - current_pool_id = 0 - current_passage_index += 1 + if len(round2_pools) == 1 and len(tds) == 5: + # Exchange teams 1 and 5 if there is only one pool with 5 teams + if i == 0: + td2.passage_index = 4 + elif i == 4: + td2.passage_index = 0 + current_passage_index += 1 await td2.asave() + valid_jokers = [] + # A joker is valid if it was not in the same pool in the first round + # as a team that is already in the current pool in the second round + for joker in jokers: + async for td2 in round2_pools[current_pool_id].teamdraw_set.all(): + if await joker.pool.teamdraw_set.filter(participation_id=td2.participation_id).aexists(): + break + else: + valid_jokers.append(joker) + + # We can add a joker if there is exactly enough free space in the current pool + if valid_jokers and current_passage_index + len(valid_jokers) == td2.pool.size: + for joker in valid_jokers: + tds_copy.remove(joker) + jokers.remove(joker) + td2_joker = await TeamDraw.objects.filter(participation_id=joker.participation_id, + round=round2).aget() + td2_joker.pool = round2_pools[current_pool_id] + td2_joker.passage_index = current_passage_index + current_passage_index += 1 + await td2_joker.asave() + jokers = [] + + current_passage_index = 0 + current_pool_id += 1 + + if current_passage_index == round2_pools[current_pool_id].size: + current_passage_index = 0 + current_pool_id += 1 + # The current pool is the first pool of the current (first) round pool = await Pool.objects.filter(round=self.tournament.draw.current_round, letter=1).aget() self.tournament.draw.current_round.current_pool = pool diff --git a/participation/admin.py b/participation/admin.py index d3c22f6..746f8fe 100644 --- a/participation/admin.py +++ b/participation/admin.py @@ -93,7 +93,7 @@ class TeamAdmin(admin.ModelAdmin): class ParticipationAdmin(admin.ModelAdmin): list_display = ('team', 'tournament', 'valid', 'final',) search_fields = ('team__name', 'team__trigram',) - list_filter = ('valid',) + list_filter = ('valid', 'tournament',) autocomplete_fields = ('team', 'tournament',) inlines = (SolutionInline, SynthesisInline,) diff --git a/participation/models.py b/participation/models.py index 558d5a6..aebe02f 100644 --- a/participation/models.py +++ b/participation/models.py @@ -407,7 +407,7 @@ class Tournament(models.Model): def best_format(self): n = len(self.participations.filter(valid=True).all()) fmt = [n] if n <= 5 else [3] * (n // 3 - 1) + [3 + n % 3] - return '+'.join(map(str, sorted(fmt, reverse=True))) + return '+'.join(map(str, sorted(fmt))) def get_absolute_url(self): return reverse_lazy("participation:tournament_detail", args=(self.pk,))