# Copyright (C) 2023 by Animath # SPDX-License-Identifier: GPL-3.0-or-later from asgiref.sync import sync_to_async from django.conf import settings from django.db import models from django.utils.text import format_lazy from django.utils.translation import gettext_lazy as _ from participation.models import Passage, Participation, Pool as PPool, Tournament class Draw(models.Model): tournament = models.OneToOneField( Tournament, on_delete=models.CASCADE, verbose_name=_('tournament'), ) current_round = models.ForeignKey( 'Round', on_delete=models.CASCADE, null=True, default=None, related_name='+', verbose_name=_('current round'), ) last_message = models.TextField( blank=True, default="", verbose_name=_("last message"), ) @property def exportable(self): return any(pool.exportable for r in self.round_set.all() for pool in r.pool_set.all()) def get_state(self): if self.current_round.current_pool is None: return 'DICE_SELECT_POULES' elif self.current_round.current_pool.current_team is None: return 'DICE_ORDER_POULE' elif self.current_round.current_pool.current_team.accepted is not None: if self.current_round.number == 1: return 'WAITING_FINAL' else: return 'DRAW_ENDED' elif self.current_round.current_pool.current_team.purposed is None: return 'WAITING_DRAW_PROBLEM' else: return 'WAITING_CHOOSE_PROBLEM' @property def information(self): s = "" if self.last_message: s += self.last_message + "

" match self.get_state(): case 'DICE_SELECT_POULES': if self.current_round.number == 1: s += """Nous allons commencer le tirage des problèmes.
Vous pouvez à tout moment poser toute question si quelque chose n'est pas clair ou ne va pas.

Nous allons d'abord tirer les poules et l'ordre de passage pour le premier tour avec toutes les équipes puis pour chaque poule, nous tirerons l'ordre de tirage pour le tour et les problèmes.

""" s += """ Les capitaines, vous pouvez désormais toustes lancer un dé 100, en cliquant sur le gros bouton. Les poules et l'ordre de passage lors du premier tour sera l'ordre croissant des dés, c'est-à-dire que le plus petit lancer sera le premier à passer dans la poule A.""" case 'DICE_ORDER_POULE': s += f"""Nous passons au tirage des problèmes pour la poule {self.current_round.current_pool}, entre les équipes {', '.join(td.participation.team.trigram for td in self.current_round.current_pool.teamdraw_set.all())}. Les capitaines peuvent lancer un dé 100 en cliquant sur le gros bouton pour déterminer l'ordre de tirage. L'équipe réalisant le plus gros score pourra tirer en premier.""" case 'WAITING_DRAW_PROBLEM': td = self.current_round.current_pool.current_team s += f"""C'est au tour de l'équipe {td.participation.team.trigram} de choisir son problème. Cliquez sur l'urne au milieu pour tirer un problème au sort.""" case 'WAITING_CHOOSE_PROBLEM': td = self.current_round.current_pool.current_team s += f"""L'équipe {td.participation.team.trigram} a tiré le problème {td.purposed}. """ if td.purposed in td.rejected: s += """Elle a déjà refusé ce problème auparavant, elle peut donc le refuser sans pénalité et tirer un nouveau problème immédiatement, ou bien revenir sur son choix.""" else: s += "Elle peut décider d'accepter ou de refuser ce problème. " if len(td.rejected) >= settings.PROBLEM_COUNT - 5: s += "Refuser ce problème ajoutera une nouvelle pénalité de 0.5 sur le coefficient de l'oral de læ défenseur⋅se." else: s += f"Il reste {settings.PROBLEM_COUNT - 5 - len(td.rejected)} refus sans pénalité." case 'WAITING_FINAL': s += "Le tirage au sort pour le tour 2 aura lieu à la fin du premier tour. Bon courage !" case 'DRAW_ENDED': s += "Le tirage au sort est terminé. Les solutions des autres équipes peuvent être trouvées dans l'onglet « Ma participation »." s += "

" if s else "" s += """Pour plus de détails sur le déroulement du tirage au sort, le règlement est accessible sur https://tfjm.org/reglement.""" return s async def ainformation(self): return await sync_to_async(lambda: self.information)() class Meta: verbose_name = _('draw') verbose_name_plural = _('draws') class Round(models.Model): draw = models.ForeignKey( Draw, on_delete=models.CASCADE, verbose_name=_('draw'), ) number = models.PositiveSmallIntegerField( choices=[ (1, _('Round 1')), (2, _('Round 2')), ], verbose_name=_('number'), ) current_pool = models.ForeignKey( 'Pool', on_delete=models.CASCADE, null=True, default=None, related_name='+', verbose_name=_('current pool'), ) @property def team_draws(self): return self.teamdraw_set.order_by('pool__letter', 'passage_index').all() async def next_pool(self): pool = await sync_to_async(lambda: self.current_pool)() return await self.pool_set.aget(letter=pool.letter + 1) def __str__(self): return self.get_number_display() class Meta: verbose_name = _('round') verbose_name_plural = _('rounds') class Pool(models.Model): round = models.ForeignKey( Round, on_delete=models.CASCADE, ) letter = models.PositiveSmallIntegerField( choices=[ (1, 'A'), (2, 'B'), (3, 'C'), ], verbose_name=_('letter'), ) size = models.PositiveSmallIntegerField( verbose_name=_('size'), ) current_team = models.ForeignKey( 'TeamDraw', on_delete=models.CASCADE, null=True, default=None, related_name='+', verbose_name=_('current team'), ) associated_pool = models.OneToOneField( 'participation.Pool', on_delete=models.SET_NULL, null=True, default=None, related_name='draw_pool', verbose_name=_("associated pool"), ) @property def team_draws(self): return self.teamdraw_set.order_by('passage_index').all() @property def trigrams(self): return [td.participation.team.trigram for td in self.teamdraw_set.order_by('passage_index').all()] async def atrigrams(self): return await sync_to_async(lambda: self.trigrams)() async def next_td(self): td = await sync_to_async(lambda: self.current_team)() current_index = (td.choose_index + 1) % self.size td = await self.teamdraw_set.aget(choose_index=current_index) while td.accepted: current_index += 1 current_index %= self.size td = await self.teamdraw_set.aget(choose_index=current_index) return td @property def exportable(self): return self.associated_pool is None and self.teamdraw_set.exists() \ and all(td.accepted is not None for td in self.teamdraw_set.all()) def export(self): from django.db import transaction with transaction.atomic(): self.associated_pool = PPool.objects.create( tournament=self.round.draw.tournament, round=self.round.number, ) self.associated_pool.juries.set(self.round.draw.tournament.organizers.all()) tds = list(self.team_draws) self.associated_pool.participations.set([td.participation for td in tds]) self.save() if len(tds) == 3: table = [ [0, 1, 2], [1, 2, 0], [2, 0, 1], ] elif len(tds) == 4: table = [ [0, 1, 2], [1, 2, 3], [2, 3, 0], [3, 0, 1], ] elif len(tds) == 5: table = [ [0, 2, 3], [1, 3, 4], [2, 0, 1], [3, 4, 0], [4, 1, 2], ] for line in table: Passage.objects.create( pool=self.associated_pool, solution_number=tds[line[0]].accepted, defender=tds[line[0]].participation, opponent=tds[line[1]].participation, reporter=tds[line[2]].participation, defender_penalties=tds[line[0]].penalty_int, ) def __str__(self): return f"{self.get_letter_display()}{self.round.number}" class Meta: verbose_name = _('pool') verbose_name_plural = _('pools') class TeamDraw(models.Model): participation = models.ForeignKey( Participation, on_delete=models.CASCADE, verbose_name=_('participation'), ) round = models.ForeignKey( Round, on_delete=models.CASCADE, verbose_name=_('round'), ) pool = models.ForeignKey( Pool, on_delete=models.CASCADE, null=True, default=None, verbose_name=_('pool'), ) passage_index = models.PositiveSmallIntegerField( choices=zip(range(1, 5), range(1, 5)), null=True, default=None, verbose_name=_('passage index'), ) choose_index = models.PositiveSmallIntegerField( choices=zip(range(1, 5), range(1, 5)), null=True, default=None, verbose_name=_('choose index'), ) accepted = models.PositiveSmallIntegerField( choices=[ (i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1) ], null=True, default=None, verbose_name=_("accepted problem"), ) last_dice = models.PositiveSmallIntegerField( choices=zip(range(1, 101), range(1, 101)), null=True, default=None, verbose_name=_("last dice"), ) purposed = models.PositiveSmallIntegerField( choices=[ (i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1) ], null=True, default=None, verbose_name=_("accepted problem"), ) rejected = models.JSONField( default=list, verbose_name=_('rejected problems'), ) @property def penalty_int(self): return max(0, len(self.rejected) - (settings.PROBLEM_COUNT - 5)) @property def penalty(self): return 0.5 * self.penalty_int class Meta: verbose_name = _('team draw') verbose_name_plural = _('team draws')