mirror of
				https://gitlab.com/ddorn/tfjm-discord-bot.git
				synced 2025-10-31 15:40:01 +01:00 
			
		
		
		
	♻️ factor tirage logic out
This commit is contained in:
		
							
								
								
									
										518
									
								
								src/cogs/tirage_logic.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										518
									
								
								src/cogs/tirage_logic.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,518 @@ | ||||
| #!/bin/python | ||||
| import asyncio | ||||
| import random | ||||
| from collections import defaultdict, namedtuple | ||||
| from typing import Type | ||||
|  | ||||
| import discord | ||||
| import yaml | ||||
| from discord.ext.commands import Context | ||||
| from discord.utils import get | ||||
|  | ||||
| from src.constants import * | ||||
| from src.errors import TfjmError, UnwantedCommand | ||||
|  | ||||
| __all__ = ["Tirage"] | ||||
|  | ||||
|  | ||||
| def in_passage_order(teams, round=0): | ||||
|     return sorted(teams, key=lambda team: team.passage_order[round] or 0, reverse=True) | ||||
|  | ||||
|  | ||||
| class Team(yaml.YAMLObject): | ||||
|     yaml_tag = "Team" | ||||
|  | ||||
|     def __init__(self, ctx, name): | ||||
|         self.name = name | ||||
|         self.mention = get(ctx.guild.roles, name=name).mention | ||||
|         self.tirage_order = [None, None] | ||||
|         self.passage_order = [None, None] | ||||
|  | ||||
|         self.accepted_problems = [None, None] | ||||
|         self.drawn_problem = None  # Waiting to be accepted or refused | ||||
|         self.rejected = [set(), set()] | ||||
|  | ||||
|     def coeff(self, round): | ||||
|         if len(self.rejected[round]) <= MAX_REFUSE: | ||||
|             return 2 | ||||
|         else: | ||||
|             return 2 - 0.5 * (len(self.rejected[round]) - MAX_REFUSE) | ||||
|  | ||||
|     def details(self, round): | ||||
|         return f"""{self.mention}: | ||||
|  - Accepté: {self.accepted_problems[round]} | ||||
|  - Refusés: {", ".join(p[0] for p in self.rejected[round]) if self.rejected[round] else "aucun"} | ||||
|  - Coefficient: {self.coeff(round)} | ||||
|  - Ordre au tirage: {self.tirage_order[round]} | ||||
|  - Ordre de passage: {self.passage_order[round]} | ||||
| """ | ||||
|  | ||||
|  | ||||
| class Tirage(yaml.YAMLObject): | ||||
|     yaml_tag = "Tirage" | ||||
|  | ||||
|     def __init__(self, ctx, channel, teams): | ||||
|         assert len(teams) in (3, 4) | ||||
|  | ||||
|         self.channel: int = channel | ||||
|         self.teams = [Team(ctx, team) for team in teams] | ||||
|         self.phase = TirageOrderPhase(self, round=0) | ||||
|  | ||||
|     def team_for(self, author): | ||||
|         for team in self.teams: | ||||
|             if get(author.roles, name=team.name): | ||||
|                 return team | ||||
|  | ||||
|         # Should theoretically not happen | ||||
|         raise TfjmError( | ||||
|             "Tu n'es pas dans une des équipes qui font le tirage, " | ||||
|             "merci de ne pas intervenir." | ||||
|         ) | ||||
|  | ||||
|     async def dice(self, ctx, n): | ||||
|         if n != 100: | ||||
|             raise UnwantedCommand( | ||||
|                 "C'est un dé à 100 faces qu'il faut tirer! (`!dice 100`)" | ||||
|             ) | ||||
|  | ||||
|         await self.phase.dice(ctx, ctx.author, random.randint(1, n)) | ||||
|         await self.update_phase(ctx) | ||||
|  | ||||
|     async def choose_problem(self, ctx): | ||||
|         await self.phase.choose_problem(ctx, ctx.author, random.choice(PROBLEMS)) | ||||
|         await self.update_phase(ctx) | ||||
|  | ||||
|     async def accept(self, ctx, yes): | ||||
|         await self.phase.accept(ctx, ctx.author, yes) | ||||
|         await self.update_phase(ctx) | ||||
|  | ||||
|     async def update_phase(self, ctx): | ||||
|         if self.phase.finished(): | ||||
|             next_class = await self.phase.next(ctx) | ||||
|  | ||||
|             if next_class is None: | ||||
|                 self.phase = None | ||||
|                 await ctx.send( | ||||
|                     "Le tirage est fini ! Bonne chance à tous pour la suite !" | ||||
|                 ) | ||||
|                 await self.show(ctx) | ||||
|                 await self.end(ctx) | ||||
|             else: | ||||
|                 # Continue on the same round. | ||||
|                 # If a Phase wants to change the round | ||||
|                 # it needs to change its own round. | ||||
|                 self.phase = next_class(self, self.phase.round) | ||||
|                 await self.phase.start(ctx) | ||||
|  | ||||
|     async def end(self, ctx): | ||||
|         if False: | ||||
|             # Allow everyone to send messages again | ||||
|             send = discord.PermissionOverwrite()  # reset | ||||
|             await ctx.channel.edit(overwrites={ctx.guild.default_role: send}) | ||||
|  | ||||
|         tl = [] | ||||
|         if TIRAGES_FILE.exists(): | ||||
|             with open(TIRAGES_FILE) as f: | ||||
|                 tl = list(yaml.load_all(f)) | ||||
|         else: | ||||
|             TIRAGES_FILE.touch() | ||||
|         tl.append(self) | ||||
|         with open(TIRAGES_FILE, "w") as f: | ||||
|             yaml.dump_all(tl, f) | ||||
|  | ||||
|         await ctx.send( | ||||
|             f"A tout moment, ce rapport peut être envoyé avec `!show {len(tl) - 1}`" | ||||
|         ) | ||||
|  | ||||
|     async def show(self, ctx): | ||||
|         teams = ", ".join(team.mention for team in self.teams) | ||||
|         msg = f"Voici un résumé du tirage entre les équipes {teams}." | ||||
|  | ||||
|         if len(self.teams) == 3: | ||||
|             table = """``` | ||||
|     +-----+---------+---------+---------+ | ||||
|     |     | Phase 1 | Phase 2 | Phase 3 | | ||||
|     |     |   Pb {0.pb}  |   Pb {1.pb}  |   Pb {2.pb}  | | ||||
|     +-----+---------+---------+---------+ | ||||
|     | {0.name} |   Déf   |   Rap   |   Opp   | | ||||
|     +-----+---------+---------+---------+ | ||||
|     | {1.name} |   Opp   |   Déf   |   Rap   | | ||||
|     +-----+---------+---------+---------+ | ||||
|     | {2.name} |   Rap   |   Opp   |   Déf   | | ||||
|     +-----+---------+---------+---------+ | ||||
| ```""" | ||||
|         else: | ||||
|             table = """``` | ||||
|     +-----+---------+---------+---------+---------+ | ||||
|     |     | Phase 1 | Phase 2 | Phase 3 | Phase 4 | | ||||
|     |     |   Pb {0.pb}  |   Pb {1.pb}  |   Pb {2.pb}  |   Pb {3.pb}  | | ||||
|     +-----+---------+---------+---------+---------+ | ||||
|     | {0.name} |   Déf   |         |   Rap   |   Opp   | | ||||
|     +-----+---------+---------+---------+---------+ | ||||
|     | {0.name} |   Opp   |   Déf   |         |   Rap   | | ||||
|     +-----+---------+---------+---------+---------+ | ||||
|     | {0.name} |   Rap   |   Opp   |   Déf   |         | | ||||
|     +-----+---------+---------+---------+---------+ | ||||
|     | {0.name} |         |   Rap   |   Opp   |   Déf   | | ||||
|     +-----+---------+---------+---------+---------+ | ||||
| ```""" | ||||
|         Record = namedtuple("Record", ["name", "pb", "penalite"]) | ||||
|  | ||||
|         for round in (0, 1): | ||||
|             records = [ | ||||
|                 Record( | ||||
|                     team.name, | ||||
|                     (team.accepted_problems[round] or "- None")[0], | ||||
|                     f"k = {team.coeff(round)} ", | ||||
|                 ) | ||||
|                 for team in in_passage_order(self.teams, round) | ||||
|             ] | ||||
|  | ||||
|             msg += f"\n\n**{ROUND_NAMES[round].capitalize()}**:\n" | ||||
|             msg += table.format(*records) + "\n" | ||||
|             for team in self.teams: | ||||
|                 msg += team.details(round) | ||||
|  | ||||
|         await ctx.send(msg) | ||||
|  | ||||
|  | ||||
| class Phase: | ||||
|     NEXT = None | ||||
|  | ||||
|     def __init__(self, tirage, round=0): | ||||
|         """ | ||||
|         A Phase of the tirage. | ||||
|  | ||||
|         :param tirage: Backreference to the tirage | ||||
|         :param round: round number, 0 for the first round and 1 for the second | ||||
|         """ | ||||
|  | ||||
|         assert round in (0, 1) | ||||
|         self.round = round | ||||
|         self.tirage: Tirage = tirage | ||||
|  | ||||
|     def team_for(self, author): | ||||
|         return self.tirage.team_for(author) | ||||
|  | ||||
|     @property | ||||
|     def teams(self): | ||||
|         return self.tirage.teams | ||||
|  | ||||
|     @teams.setter | ||||
|     def teams(self, teams): | ||||
|         self.tirage.teams = teams | ||||
|  | ||||
|     def captain_mention(self, ctx): | ||||
|         return get(ctx.guild.roles, name=Role.CAPTAIN).mention | ||||
|  | ||||
|     async def dice(self, ctx: Context, author, dice): | ||||
|         raise UnwantedCommand() | ||||
|  | ||||
|     async def choose_problem(self, ctx: Context, author, problem): | ||||
|         raise UnwantedCommand() | ||||
|  | ||||
|     async def accept(self, ctx: Context, author, yes): | ||||
|         raise UnwantedCommand() | ||||
|  | ||||
|     def finished(self) -> bool: | ||||
|         return NotImplemented | ||||
|  | ||||
|     async def start(self, ctx): | ||||
|         pass | ||||
|  | ||||
|     async def next(self, ctx: Context) -> "Type[Phase]": | ||||
|         return self.NEXT | ||||
|  | ||||
|  | ||||
| class OrderPhase(Phase): | ||||
|     def __init__(self, tirage, round, name, order_name, reverse=False): | ||||
|         super().__init__(tirage, round) | ||||
|         self.name = name | ||||
|         self.reverse = reverse | ||||
|         self.order_name = order_name | ||||
|  | ||||
|     def order_for(self, team): | ||||
|         return getattr(team, self.order_name)[self.round] | ||||
|  | ||||
|     def set_order_for(self, team, order): | ||||
|         getattr(team, self.order_name)[self.round] = order | ||||
|  | ||||
|     async def dice(self, ctx, author, dice): | ||||
|         team = self.team_for(author) | ||||
|  | ||||
|         if self.order_for(team) is None: | ||||
|             self.set_order_for(team, dice) | ||||
|             await ctx.send(f"L'équipe {team.mention} a obtenu... **{dice}**") | ||||
|         else: | ||||
|             raise UnwantedCommand("tu as déjà lancé un dé !") | ||||
|  | ||||
|     def finished(self) -> bool: | ||||
|         return all(self.order_for(team) is not None for team in self.teams) | ||||
|  | ||||
|     async def next(self, ctx) -> "Type[Phase]": | ||||
|         orders = [self.order_for(team) for team in self.teams] | ||||
|         if len(set(orders)) == len(orders): | ||||
|             # All dice are different: good | ||||
|             self.teams.sort(key=self.order_for, reverse=self.reverse) | ||||
|             await ctx.send( | ||||
|                 f"L'ordre {self.name} pour ce tour est donc :\n" | ||||
|                 " - " | ||||
|                 + "\n - ".join( | ||||
|                     f"{team.mention} ({self.order_for(team)})" for team in self.teams | ||||
|                 ) | ||||
|             ) | ||||
|             return self.NEXT | ||||
|         else: | ||||
|             # Find dice that are the same | ||||
|             count = defaultdict(list) | ||||
|             for team in self.teams: | ||||
|                 count[self.order_for(team)].append(team) | ||||
|  | ||||
|             re_do = [] | ||||
|             for teams in count.values(): | ||||
|                 if len(teams) > 1: | ||||
|                     re_do.extend(teams) | ||||
|  | ||||
|             teams_str = ", ".join(team.role.mention for team in re_do) | ||||
|             await ctx.send( | ||||
|                 f"Les equipes {teams_str} ont fait le même résultat " | ||||
|                 "et doivent relancer un dé. " | ||||
|                 "Le nouveau lancer effacera l'ancien." | ||||
|             ) | ||||
|             for team in re_do: | ||||
|                 self.set_order_for(team, None) | ||||
|             # We need to do this phase again. | ||||
|             return self.__class__ | ||||
|  | ||||
|  | ||||
| class TiragePhase(Phase): | ||||
|     """The phase where captains accept or refuse random problems.""" | ||||
|  | ||||
|     def __init__(self, tirage, round=0): | ||||
|         """ | ||||
|         The main phase of the Tirage. | ||||
|         :param tirage: Backreference to the tirage | ||||
|         :param round: round number, 0 for the first round and 1 for the second | ||||
|         """ | ||||
|  | ||||
|         super().__init__(tirage, round) | ||||
|         self.turn = 0 | ||||
|  | ||||
|     @property | ||||
|     def current_team(self): | ||||
|         return self.teams[self.turn] | ||||
|  | ||||
|     def available(self, problem): | ||||
|         return all(team.accepted_problems[self.round] != problem for team in self.teams) | ||||
|  | ||||
|     async def choose_problem(self, ctx: Context, author, problem): | ||||
|         team = self.current_team | ||||
|         if self.team_for(author) != team: | ||||
|             raise UnwantedCommand( | ||||
|                 f"C'est à {team.mention} de choisir " | ||||
|                 f"un problème, merci d'attendre :)" | ||||
|             ) | ||||
|  | ||||
|         assert ( | ||||
|             team.accepted_problems[self.round] is None | ||||
|         ), "Choosing pb for a team that has a pb..." | ||||
|  | ||||
|         if team.drawn_problem: | ||||
|             raise UnwantedCommand( | ||||
|                 "Vous avez déjà tiré un problème, merci de l'accepter (`!yes`) " | ||||
|                 "ou de le refuser (`!no)`." | ||||
|             ) | ||||
|  | ||||
|         await ctx.send(f"{team.mention} a tiré **{problem}** !") | ||||
|         if not self.available(problem): | ||||
|             await ctx.send( | ||||
|                 f"Malheureusement, **{problem}** à déjà été choisi, " | ||||
|                 f"vous pouvez tirer un nouveau problème." | ||||
|             ) | ||||
|         elif problem in team.accepted_problems: | ||||
|             await ctx.send( | ||||
|                 f"{team.mention} à tiré **{problem}** mais " | ||||
|                 f"l'a déjà présenté au premier tour. " | ||||
|                 f"Vous pouvez directement piocher un autre problème (`!rp`)." | ||||
|             ) | ||||
|         elif problem in team.rejected[self.round]: | ||||
|             team.drawn_problem = problem | ||||
|             await ctx.send( | ||||
|                 f"Vous avez déjà refusé **{problem}**, " | ||||
|                 f"vous pouvez le refuser à nouveau (`!refuse`) et " | ||||
|                 f"tirer immédiatement un nouveau problème " | ||||
|                 f"ou changer d'avis et l'accepter (`!accept`)." | ||||
|             ) | ||||
|         else: | ||||
|             team.drawn_problem = problem | ||||
|             if len(team.rejected[self.round]) >= MAX_REFUSE: | ||||
|                 await ctx.send( | ||||
|                     f"Vous pouvez accepter ou refuser **{problem}** " | ||||
|                     f"mais si vous choisissez de le refuser, il y " | ||||
|                     f"aura une pénalité de 0.5 sur le multiplicateur du " | ||||
|                     f"défenseur." | ||||
|                 ) | ||||
|             else: | ||||
|                 await ctx.send( | ||||
|                     f"Vous pouvez accepter (`!oui`) ou refuser (`!non`) **{problem}**. " | ||||
|                     f"Il reste {MAX_REFUSE - len(team.rejected[self.round])} refus sans pénalité " | ||||
|                     f"pour {team.mention}." | ||||
|                 ) | ||||
|  | ||||
|     async def accept(self, ctx: Context, author, yes): | ||||
|         team = self.current_team | ||||
|  | ||||
|         if self.team_for(author) != team: | ||||
|             raise UnwantedCommand( | ||||
|                 f"c'est à {team.mention} " | ||||
|                 f"de choisir un problème, merci d'attendre :)" | ||||
|             ) | ||||
|  | ||||
|         assert ( | ||||
|             team.accepted_problems[self.round] is None | ||||
|         ), "Choosing pb for a team that has a pb..." | ||||
|  | ||||
|         if not team.drawn_problem: | ||||
|             if yes: | ||||
|                 raise UnwantedCommand( | ||||
|                     "Tu es bien optimiste pour vouloir accepter un problème " | ||||
|                     "avant de l'avoir tiré !" | ||||
|                 ) | ||||
|             else: | ||||
|                 raise UnwantedCommand( | ||||
|                     "Halte là ! Ce serait bien de tirer un problème d'abord... " | ||||
|                     "et peut-être qu'il te plaira :) " | ||||
|                 ) | ||||
|         else: | ||||
|             if yes: | ||||
|                 team.accepted_problems[self.round] = team.drawn_problem | ||||
|                 await ctx.send( | ||||
|                     f"L'équipe {team.mention} a accepté " | ||||
|                     f"**{team.accepted_problems[self.round]}** ! Les autres équipes " | ||||
|                     f"ne peuvent plus l'accepter." | ||||
|                 ) | ||||
|             else: | ||||
|                 msg = f"{team.mention} a refusé **{team.drawn_problem}** " | ||||
|                 if team.drawn_problem in team.rejected[self.round]: | ||||
|                     msg += "sans pénalité." | ||||
|                 else: | ||||
|                     msg += "!" | ||||
|                     team.rejected[self.round].add(team.drawn_problem) | ||||
|                 await ctx.send(msg) | ||||
|  | ||||
|             team.drawn_problem = None | ||||
|  | ||||
|             # Next turn | ||||
|             if self.finished(): | ||||
|                 self.turn = None | ||||
|                 return | ||||
|  | ||||
|             # Find next team that needs to draw. | ||||
|             i = (self.turn + 1) % len(self.teams) | ||||
|             while self.teams[i].accepted_problems[self.round]: | ||||
|                 i = (i + 1) % len(self.teams) | ||||
|             self.turn = i | ||||
|  | ||||
|             await ctx.send( | ||||
|                 f"C'est au tour de {self.current_team.mention} de choisir un problème." | ||||
|             ) | ||||
|  | ||||
|     def finished(self) -> bool: | ||||
|         return all(team.accepted_problems[self.round] for team in self.teams) | ||||
|  | ||||
|     async def start(self, ctx: Context): | ||||
|         # First sort teams according to the tirage_order | ||||
|         self.teams.sort(key=lambda team: team.tirage_order[self.round]) | ||||
|  | ||||
|         if self.round == 0: | ||||
|             await asyncio.sleep(0.5) | ||||
|             await ctx.send("Passons au tirage des problèmes !") | ||||
|             await asyncio.sleep(0.5) | ||||
|             await ctx.send( | ||||
|                 f"Les {self.captain_mention(ctx)}s vont tirer des problèmes au " | ||||
|                 f"hasard, avec `!random-problem` ou `!rp` pour ceux qui aiment " | ||||
|                 f"les abbréviations." | ||||
|             ) | ||||
|             await asyncio.sleep(0.5) | ||||
|             await ctx.send( | ||||
|                 "Ils pouront ensuite accepter ou refuser les problèmes avec " | ||||
|                 "`!accept` ou `!refuse`." | ||||
|             ) | ||||
|             await asyncio.sleep(0.5) | ||||
|             await ctx.send( | ||||
|                 f"Chaque équipe peut refuser jusqu'a {MAX_REFUSE} " | ||||
|                 f"problèmes sans pénalité (voir §13 du règlement). " | ||||
|                 f"Un problème déjà rejeté ne compte pas deux fois." | ||||
|             ) | ||||
|             await ctx.send("Bonne chance à tous ! C'est parti...") | ||||
|  | ||||
|         else: | ||||
|             # Second round | ||||
|             await asyncio.sleep(0.5) | ||||
|             await ctx.send( | ||||
|                 "Il reste juste le tirage du deuxième tour. Les règles sont les mêmes qu'avant " | ||||
|                 "à la seule différence qu'une équipe ne peut pas tirer le problème " | ||||
|                 "sur lequel elle est passée au premier tour." | ||||
|             ) | ||||
|  | ||||
|         await asyncio.sleep(1.5) | ||||
|         await ctx.send( | ||||
|             f"{self.current_team.mention} à toi l'honneur! " | ||||
|             f"Lance `!random-problem` quand tu veux." | ||||
|         ) | ||||
|  | ||||
|     async def next(self, ctx: Context) -> "Type[Phase]": | ||||
|         if self.round == 0: | ||||
|             await ctx.send("Nous allons passer au deuxième tour") | ||||
|             self.round = 1 | ||||
|             return TirageOrderPhase | ||||
|         return None | ||||
|  | ||||
|  | ||||
| class PassageOrderPhase(OrderPhase): | ||||
|     """The phase to determine the chicken's order.""" | ||||
|  | ||||
|     NEXT = TiragePhase | ||||
|  | ||||
|     def __init__(self, tirage, round=0): | ||||
|         super().__init__(tirage, round, "de passage", "passage_order", True) | ||||
|  | ||||
|     async def start(self, ctx): | ||||
|         await ctx.send( | ||||
|             "Nous allons maintenant tirer l'ordre de passage durant le tour. " | ||||
|             "L'ordre du tour sera dans l'ordre décroissant des lancers, " | ||||
|             "c'est-à-dire que l'équipe qui tire le plus grand nombre " | ||||
|             "présentera en premier." | ||||
|         ) | ||||
|         await asyncio.sleep(0.5) | ||||
|  | ||||
|         await ctx.send( | ||||
|             f"Les {self.captain_mention(ctx)}s, vous pouvez lancer " | ||||
|             f"à nouveau un dé 100 (`!dice 100`)" | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class TirageOrderPhase(OrderPhase): | ||||
|     """Phase to determine the tirage's order.""" | ||||
|  | ||||
|     NEXT = PassageOrderPhase | ||||
|  | ||||
|     def __init__(self, tirage, round=0): | ||||
|         super().__init__(tirage, round, "des tirages", "tirage_order", False) | ||||
|  | ||||
|     async def start(self, ctx): | ||||
|  | ||||
|         await asyncio.sleep( | ||||
|             0.5 | ||||
|         )  # The bot is more human if it doesn't type at the speed of light | ||||
|         await ctx.send( | ||||
|             "Nous allons d'abord tirer au sort l'ordre de tirage des problèmes " | ||||
|             f"pour le {ROUND_NAMES[self.round]}, " | ||||
|             "puis l'ordre de passage lors de ce tour." | ||||
|         ) | ||||
|         await asyncio.sleep(0.5) | ||||
|         await ctx.send( | ||||
|             f"Les {self.captain_mention(ctx)}s, vous pouvez désormais lancer un dé 100 " | ||||
|             "comme ceci `!dice 100`. " | ||||
|             "L'ordre des tirages suivants sera l'ordre croissant des lancers. " | ||||
|         ) | ||||
| @@ -1,8 +1,5 @@ | ||||
| #!/bin/python | ||||
| import asyncio | ||||
| import random | ||||
| from collections import defaultdict, namedtuple | ||||
| from typing import Type | ||||
|  | ||||
| import discord | ||||
| import yaml | ||||
| @@ -10,511 +7,9 @@ from discord.ext import commands | ||||
| from discord.ext.commands import Context, group, Cog | ||||
| from discord.utils import get | ||||
|  | ||||
| from src.cogs.tirage_logic import TiragePhase, Tirage | ||||
| from src.constants import * | ||||
| from src.errors import TfjmError, UnwantedCommand | ||||
|  | ||||
|  | ||||
| def in_passage_order(teams, round=0): | ||||
|     return sorted(teams, key=lambda team: team.passage_order[round] or 0, reverse=True) | ||||
|  | ||||
|  | ||||
| class Team(yaml.YAMLObject): | ||||
|     yaml_tag = "Team" | ||||
|  | ||||
|     def __init__(self, ctx, name): | ||||
|         self.name = name | ||||
|         self.mention = get(ctx.guild.roles, name=name).mention | ||||
|         self.tirage_order = [None, None] | ||||
|         self.passage_order = [None, None] | ||||
|  | ||||
|         self.accepted_problems = [None, None] | ||||
|         self.drawn_problem = None  # Waiting to be accepted or refused | ||||
|         self.rejected = [set(), set()] | ||||
|  | ||||
|     def coeff(self, round): | ||||
|         if len(self.rejected[round]) <= MAX_REFUSE: | ||||
|             return 2 | ||||
|         else: | ||||
|             return 2 - 0.5 * (len(self.rejected[round]) - MAX_REFUSE) | ||||
|  | ||||
|     def details(self, round): | ||||
|         return f"""{self.mention}: | ||||
|  - Accepté: {self.accepted_problems[round]} | ||||
|  - Refusés: {", ".join(p[0] for p in self.rejected[round]) if self.rejected[round] else "aucun"} | ||||
|  - Coefficient: {self.coeff(round)} | ||||
|  - Ordre au tirage: {self.tirage_order[round]} | ||||
|  - Ordre de passage: {self.passage_order[round]} | ||||
| """ | ||||
|  | ||||
|  | ||||
| class Tirage(yaml.YAMLObject): | ||||
|     yaml_tag = "Tirage" | ||||
|  | ||||
|     def __init__(self, ctx, channel, teams): | ||||
|         assert len(teams) in (3, 4) | ||||
|  | ||||
|         self.channel: int = channel | ||||
|         self.teams = [Team(ctx, team) for team in teams] | ||||
|         self.phase = TirageOrderPhase(self, round=0) | ||||
|  | ||||
|     def team_for(self, author): | ||||
|         for team in self.teams: | ||||
|             if get(author.roles, name=team.name): | ||||
|                 return team | ||||
|  | ||||
|         # Should theoretically not happen | ||||
|         raise TfjmError( | ||||
|             "Tu n'es pas dans une des équipes qui font le tirage, " | ||||
|             "merci de ne pas intervenir." | ||||
|         ) | ||||
|  | ||||
|     async def dice(self, ctx, n) -> bool: | ||||
|         if n != 100: | ||||
|             raise UnwantedCommand( | ||||
|                 "C'est un dé à 100 faces qu'il faut tirer! (`!dice 100`)" | ||||
|             ) | ||||
|  | ||||
|         await self.phase.dice(ctx, ctx.author, random.randint(1, n)) | ||||
|         await self.update_phase(ctx) | ||||
|  | ||||
|     async def choose_problem(self, ctx): | ||||
|         await self.phase.choose_problem(ctx, ctx.author, random.choice(PROBLEMS)) | ||||
|         await self.update_phase(ctx) | ||||
|  | ||||
|     async def accept(self, ctx, yes): | ||||
|         await self.phase.accept(ctx, ctx.author, yes) | ||||
|         await self.update_phase(ctx) | ||||
|  | ||||
|     async def update_phase(self, ctx): | ||||
|         if self.phase.finished(): | ||||
|             next_class = await self.phase.next(ctx) | ||||
|  | ||||
|             if next_class is None: | ||||
|                 self.phase = None | ||||
|                 await ctx.send( | ||||
|                     "Le tirage est fini ! Bonne chance à tous pour la suite !" | ||||
|                 ) | ||||
|                 await self.show(ctx) | ||||
|                 await self.end(ctx) | ||||
|             else: | ||||
|                 # Continue on the same round. | ||||
|                 # If a Phase wants to change the round | ||||
|                 # it needs to change its own round. | ||||
|                 self.phase = next_class(self, self.phase.round) | ||||
|                 await self.phase.start(ctx) | ||||
|  | ||||
|     async def end(self, ctx): | ||||
|         if False: | ||||
|             # Allow everyone to send messages again | ||||
|             send = discord.PermissionOverwrite()  # reset | ||||
|             await ctx.channel.edit(overwrites={ctx.guild.default_role: send}) | ||||
|  | ||||
|         tl = [] | ||||
|         if TIRAGES_FILE.exists(): | ||||
|             with open(TIRAGES_FILE) as f: | ||||
|                 tl = list(yaml.load_all(f)) | ||||
|         else: | ||||
|             TIRAGES_FILE.touch() | ||||
|         tl.append(self) | ||||
|         with open(TIRAGES_FILE, "w") as f: | ||||
|             yaml.dump_all(tl, f) | ||||
|  | ||||
|         await ctx.send( | ||||
|             f"A tout moment, ce rapport peut être envoyé avec `!show {len(tl) - 1}`" | ||||
|         ) | ||||
|  | ||||
|     async def show(self, ctx): | ||||
|         teams = ", ".join(team.mention for team in self.teams) | ||||
|         msg = f"Voici un résumé du tirage entre les équipes {teams}." | ||||
|  | ||||
|         if len(self.teams) == 3: | ||||
|             table = """``` | ||||
|     +-----+---------+---------+---------+ | ||||
|     |     | Phase 1 | Phase 2 | Phase 3 | | ||||
|     |     |   Pb {0.pb}  |   Pb {1.pb}  |   Pb {2.pb}  | | ||||
|     +-----+---------+---------+---------+ | ||||
|     | {0.name} |   Déf   |   Rap   |   Opp   | | ||||
|     +-----+---------+---------+---------+ | ||||
|     | {1.name} |   Opp   |   Déf   |   Rap   | | ||||
|     +-----+---------+---------+---------+ | ||||
|     | {2.name} |   Rap   |   Opp   |   Déf   | | ||||
|     +-----+---------+---------+---------+ | ||||
| ```""" | ||||
|         else: | ||||
|             table = """``` | ||||
|     +-----+---------+---------+---------+---------+ | ||||
|     |     | Phase 1 | Phase 2 | Phase 3 | Phase 4 | | ||||
|     |     |   Pb {0.pb}  |   Pb {1.pb}  |   Pb {2.pb}  |   Pb {3.pb}  | | ||||
|     +-----+---------+---------+---------+---------+ | ||||
|     | {0.name} |   Déf   |         |   Rap   |   Opp   | | ||||
|     +-----+---------+---------+---------+---------+ | ||||
|     | {0.name} |   Opp   |   Déf   |         |   Rap   | | ||||
|     +-----+---------+---------+---------+---------+ | ||||
|     | {0.name} |   Rap   |   Opp   |   Déf   |         | | ||||
|     +-----+---------+---------+---------+---------+ | ||||
|     | {0.name} |         |   Rap   |   Opp   |   Déf   | | ||||
|     +-----+---------+---------+---------+---------+ | ||||
| ```""" | ||||
|         Record = namedtuple("Record", ["name", "pb", "penalite"]) | ||||
|  | ||||
|         for round in (0, 1): | ||||
|             records = [ | ||||
|                 Record( | ||||
|                     team.name, | ||||
|                     (team.accepted_problems[round] or "- None")[0], | ||||
|                     f"k = {team.coeff(round)} ", | ||||
|                 ) | ||||
|                 for team in in_passage_order(self.teams, round) | ||||
|             ] | ||||
|  | ||||
|             msg += f"\n\n**{ROUND_NAMES[round].capitalize()}**:\n" | ||||
|             msg += table.format(*records) + "\n" | ||||
|             for team in self.teams: | ||||
|                 msg += team.details(round) | ||||
|  | ||||
|         await ctx.send(msg) | ||||
|  | ||||
|  | ||||
| class Phase: | ||||
|     NEXT = None | ||||
|  | ||||
|     def __init__(self, tirage, round=0): | ||||
|         """ | ||||
|         A Phase of the tirage. | ||||
|  | ||||
|         :param tirage: Backreference to the tirage | ||||
|         :param round: round number, 0 for the first round and 1 for the second | ||||
|         """ | ||||
|  | ||||
|         assert round in (0, 1) | ||||
|         self.round = round | ||||
|         self.tirage: Tirage = tirage | ||||
|  | ||||
|     def team_for(self, author): | ||||
|         return self.tirage.team_for(author) | ||||
|  | ||||
|     @property | ||||
|     def teams(self): | ||||
|         return self.tirage.teams | ||||
|  | ||||
|     @teams.setter | ||||
|     def teams(self, teams): | ||||
|         self.tirage.teams = teams | ||||
|  | ||||
|     def captain_mention(self, ctx): | ||||
|         return get(ctx.guild.roles, name=Role.CAPTAIN).mention | ||||
|  | ||||
|     async def dice(self, ctx: Context, author, dice): | ||||
|         raise UnwantedCommand() | ||||
|  | ||||
|     async def choose_problem(self, ctx: Context, author, problem): | ||||
|         raise UnwantedCommand() | ||||
|  | ||||
|     async def accept(self, ctx: Context, author, yes): | ||||
|         raise UnwantedCommand() | ||||
|  | ||||
|     def finished(self) -> bool: | ||||
|         return NotImplemented | ||||
|  | ||||
|     async def start(self, ctx): | ||||
|         pass | ||||
|  | ||||
|     async def next(self, ctx: Context) -> "Type[Phase]": | ||||
|         return self.NEXT | ||||
|  | ||||
|  | ||||
| class OrderPhase(Phase): | ||||
|     def __init__(self, tirage, round, name, order_name, reverse=False): | ||||
|         super().__init__(tirage, round) | ||||
|         self.name = name | ||||
|         self.reverse = reverse | ||||
|         self.order_name = order_name | ||||
|  | ||||
|     def order_for(self, team): | ||||
|         return getattr(team, self.order_name)[self.round] | ||||
|  | ||||
|     def set_order_for(self, team, order): | ||||
|         getattr(team, self.order_name)[self.round] = order | ||||
|  | ||||
|     async def dice(self, ctx, author, dice): | ||||
|         team = self.team_for(author) | ||||
|  | ||||
|         if self.order_for(team) is None: | ||||
|             self.set_order_for(team, dice) | ||||
|             await ctx.send(f"L'équipe {team.mention} a obtenu... **{dice}**") | ||||
|         else: | ||||
|             raise UnwantedCommand("tu as déjà lancé un dé !") | ||||
|  | ||||
|     def finished(self) -> bool: | ||||
|         return all(self.order_for(team) is not None for team in self.teams) | ||||
|  | ||||
|     async def next(self, ctx) -> "Type[Phase]": | ||||
|         orders = [self.order_for(team) for team in self.teams] | ||||
|         if len(set(orders)) == len(orders): | ||||
|             # All dice are different: good | ||||
|             self.teams.sort(key=self.order_for, reverse=self.reverse) | ||||
|             await ctx.send( | ||||
|                 f"L'ordre {self.name} pour ce tour est donc :\n" | ||||
|                 " - " | ||||
|                 + "\n - ".join( | ||||
|                     f"{team.mention} ({self.order_for(team)})" for team in self.teams | ||||
|                 ) | ||||
|             ) | ||||
|             return self.NEXT | ||||
|         else: | ||||
|             # Find dice that are the same | ||||
|             count = defaultdict(list) | ||||
|             for team in self.teams: | ||||
|                 count[self.order_for(team)].append(team) | ||||
|  | ||||
|             re_do = [] | ||||
|             for teams in count.values(): | ||||
|                 if len(teams) > 1: | ||||
|                     re_do.extend(teams) | ||||
|  | ||||
|             teams_str = ", ".join(team.role.mention for team in re_do) | ||||
|             await ctx.send( | ||||
|                 f"Les equipes {teams_str} ont fait le même résultat " | ||||
|                 "et doivent relancer un dé. " | ||||
|                 "Le nouveau lancer effacera l'ancien." | ||||
|             ) | ||||
|             for team in re_do: | ||||
|                 self.set_order_for(team, None) | ||||
|             # We need to do this phase again. | ||||
|             return self.__class__ | ||||
|  | ||||
|  | ||||
| class TiragePhase(Phase): | ||||
|     """The phase where captains accept or refuse random problems.""" | ||||
|  | ||||
|     def __init__(self, tirage, round=0): | ||||
|         """ | ||||
|         The main phase of the Tirage. | ||||
|         :param tirage: Backreference to the tirage | ||||
|         :param round: round number, 0 for the first round and 1 for the second | ||||
|         """ | ||||
|  | ||||
|         super().__init__(tirage, round) | ||||
|         self.turn = 0 | ||||
|  | ||||
|     @property | ||||
|     def current_team(self): | ||||
|         return self.teams[self.turn] | ||||
|  | ||||
|     def available(self, problem): | ||||
|         return all(team.accepted_problems[self.round] != problem for team in self.teams) | ||||
|  | ||||
|     async def choose_problem(self, ctx: Context, author, problem): | ||||
|         team = self.current_team | ||||
|         if self.team_for(author) != team: | ||||
|             raise UnwantedCommand( | ||||
|                 f"C'est à {team.mention} de choisir " | ||||
|                 f"un problème, merci d'attendre :)" | ||||
|             ) | ||||
|  | ||||
|         assert ( | ||||
|             team.accepted_problems[self.round] is None | ||||
|         ), "Choosing pb for a team that has a pb..." | ||||
|  | ||||
|         if team.drawn_problem: | ||||
|             raise UnwantedCommand( | ||||
|                 "Vous avez déjà tiré un problème, merci de l'accepter (`!yes`) " | ||||
|                 "ou de le refuser (`!no)`." | ||||
|             ) | ||||
|  | ||||
|         await ctx.send(f"{team.mention} a tiré **{problem}** !") | ||||
|         if not self.available(problem): | ||||
|             await ctx.send( | ||||
|                 f"Malheureusement, **{problem}** à déjà été choisi, " | ||||
|                 f"vous pouvez tirer un nouveau problème." | ||||
|             ) | ||||
|         elif problem in team.accepted_problems: | ||||
|             await ctx.send( | ||||
|                 f"{team.mention} à tiré **{problem}** mais " | ||||
|                 f"l'a déjà présenté au premier tour. " | ||||
|                 f"Vous pouvez directement piocher un autre problème (`!rp`)." | ||||
|             ) | ||||
|         elif problem in team.rejected[self.round]: | ||||
|             team.drawn_problem = problem | ||||
|             await ctx.send( | ||||
|                 f"Vous avez déjà refusé **{problem}**, " | ||||
|                 f"vous pouvez le refuser à nouveau (`!refuse`) et " | ||||
|                 f"tirer immédiatement un nouveau problème " | ||||
|                 f"ou changer d'avis et l'accepter (`!accept`)." | ||||
|             ) | ||||
|         else: | ||||
|             team.drawn_problem = problem | ||||
|             if len(team.rejected[self.round]) >= MAX_REFUSE: | ||||
|                 await ctx.send( | ||||
|                     f"Vous pouvez accepter ou refuser **{problem}** " | ||||
|                     f"mais si vous choisissez de le refuser, il y " | ||||
|                     f"aura une pénalité de 0.5 sur le multiplicateur du " | ||||
|                     f"défenseur." | ||||
|                 ) | ||||
|             else: | ||||
|                 await ctx.send( | ||||
|                     f"Vous pouvez accepter (`!oui`) ou refuser (`!non`) **{problem}**. " | ||||
|                     f"Il reste {MAX_REFUSE - len(team.rejected[self.round])} refus sans pénalité " | ||||
|                     f"pour {team.mention}." | ||||
|                 ) | ||||
|  | ||||
|     async def accept(self, ctx: Context, author, yes): | ||||
|         team = self.current_team | ||||
|  | ||||
|         if self.team_for(author) != team: | ||||
|             raise UnwantedCommand( | ||||
|                 f"c'est à {team.mention} " | ||||
|                 f"de choisir un problème, merci d'attendre :)" | ||||
|             ) | ||||
|  | ||||
|         assert ( | ||||
|             team.accepted_problems[self.round] is None | ||||
|         ), "Choosing pb for a team that has a pb..." | ||||
|  | ||||
|         if not team.drawn_problem: | ||||
|             if yes: | ||||
|                 raise UnwantedCommand( | ||||
|                     "Tu es bien optimiste pour vouloir accepter un problème " | ||||
|                     "avant de l'avoir tiré !" | ||||
|                 ) | ||||
|             else: | ||||
|                 raise UnwantedCommand( | ||||
|                     "Halte là ! Ce serait bien de tirer un problème d'abord... " | ||||
|                     "et peut-être qu'il te plaira :) " | ||||
|                 ) | ||||
|         else: | ||||
|             if yes: | ||||
|                 team.accepted_problems[self.round] = team.drawn_problem | ||||
|                 await ctx.send( | ||||
|                     f"L'équipe {team.mention} a accepté " | ||||
|                     f"**{team.accepted_problems[self.round]}** ! Les autres équipes " | ||||
|                     f"ne peuvent plus l'accepter." | ||||
|                 ) | ||||
|             else: | ||||
|                 msg = f"{team.mention} a refusé **{team.drawn_problem}** " | ||||
|                 if team.drawn_problem in team.rejected[self.round]: | ||||
|                     msg += "sans pénalité." | ||||
|                 else: | ||||
|                     msg += "!" | ||||
|                     team.rejected[self.round].add(team.drawn_problem) | ||||
|                 await ctx.send(msg) | ||||
|  | ||||
|             team.drawn_problem = None | ||||
|  | ||||
|             # Next turn | ||||
|             if self.finished(): | ||||
|                 self.turn = None | ||||
|                 return | ||||
|  | ||||
|             # Find next team that needs to draw. | ||||
|             i = (self.turn + 1) % len(self.teams) | ||||
|             while self.teams[i].accepted_problems[self.round]: | ||||
|                 i = (i + 1) % len(self.teams) | ||||
|             self.turn = i | ||||
|  | ||||
|             await ctx.send( | ||||
|                 f"C'est au tour de {self.current_team.mention} de choisir un problème." | ||||
|             ) | ||||
|  | ||||
|     def finished(self) -> bool: | ||||
|         return all(team.accepted_problems[self.round] for team in self.teams) | ||||
|  | ||||
|     async def start(self, ctx: Context): | ||||
|         # First sort teams according to the tirage_order | ||||
|         self.teams.sort(key=lambda team: team.tirage_order[self.round]) | ||||
|  | ||||
|         if self.round == 0: | ||||
|             await asyncio.sleep(0.5) | ||||
|             await ctx.send("Passons au tirage des problèmes !") | ||||
|             await asyncio.sleep(0.5) | ||||
|             await ctx.send( | ||||
|                 f"Les {self.captain_mention(ctx)}s vont tirer des problèmes au " | ||||
|                 f"hasard, avec `!random-problem` ou `!rp` pour ceux qui aiment " | ||||
|                 f"les abbréviations." | ||||
|             ) | ||||
|             await asyncio.sleep(0.5) | ||||
|             await ctx.send( | ||||
|                 "Ils pouront ensuite accepter ou refuser les problèmes avec " | ||||
|                 "`!accept` ou `!refuse`." | ||||
|             ) | ||||
|             await asyncio.sleep(0.5) | ||||
|             await ctx.send( | ||||
|                 f"Chaque équipe peut refuser jusqu'a {MAX_REFUSE} " | ||||
|                 f"problèmes sans pénalité (voir §13 du règlement). " | ||||
|                 f"Un problème déjà rejeté ne compte pas deux fois." | ||||
|             ) | ||||
|             await ctx.send("Bonne chance à tous ! C'est parti...") | ||||
|  | ||||
|         else: | ||||
|             # Second round | ||||
|             await asyncio.sleep(0.5) | ||||
|             await ctx.send( | ||||
|                 "Il reste juste le tirage du deuxième tour. Les règles sont les mêmes qu'avant " | ||||
|                 "à la seule différence qu'une équipe ne peut pas tirer le problème " | ||||
|                 "sur lequel elle est passée au premier tour." | ||||
|             ) | ||||
|  | ||||
|         await asyncio.sleep(1.5) | ||||
|         await ctx.send( | ||||
|             f"{self.current_team.mention} à toi l'honneur! " | ||||
|             f"Lance `!random-problem` quand tu veux." | ||||
|         ) | ||||
|  | ||||
|     async def next(self, ctx: Context) -> "Type[Phase]": | ||||
|         if self.round == 0: | ||||
|             await ctx.send("Nous allons passer au deuxième tour") | ||||
|             self.round = 1 | ||||
|             return TirageOrderPhase | ||||
|         return None | ||||
|  | ||||
|  | ||||
| class PassageOrderPhase(OrderPhase): | ||||
|     """The phase to determine the chicken's order.""" | ||||
|  | ||||
|     NEXT = TiragePhase | ||||
|  | ||||
|     def __init__(self, tirage, round=0): | ||||
|         super().__init__(tirage, round, "de passage", "passage_order", True) | ||||
|  | ||||
|     async def start(self, ctx): | ||||
|         await ctx.send( | ||||
|             "Nous allons maintenant tirer l'ordre de passage durant le tour. " | ||||
|             "L'ordre du tour sera dans l'ordre décroissant des lancers, " | ||||
|             "c'est-à-dire que l'équipe qui tire le plus grand nombre " | ||||
|             "présentera en premier." | ||||
|         ) | ||||
|         await asyncio.sleep(0.5) | ||||
|  | ||||
|         await ctx.send( | ||||
|             f"Les {self.captain_mention(ctx)}s, vous pouvez lancer " | ||||
|             f"à nouveau un dé 100 (`!dice 100`)" | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class TirageOrderPhase(OrderPhase): | ||||
|     """Phase to determine the tirage's order.""" | ||||
|  | ||||
|     NEXT = PassageOrderPhase | ||||
|  | ||||
|     def __init__(self, tirage, round=0): | ||||
|         super().__init__(tirage, round, "des tirages", "tirage_order", False) | ||||
|  | ||||
|     async def start(self, ctx): | ||||
|  | ||||
|         await asyncio.sleep( | ||||
|             0.5 | ||||
|         )  # The bot is more human if it doesn't type at the speed of light | ||||
|         await ctx.send( | ||||
|             "Nous allons d'abord tirer au sort l'ordre de tirage des problèmes " | ||||
|             f"pour le {ROUND_NAMES[self.round]}, " | ||||
|             "puis l'ordre de passage lors de ce tour." | ||||
|         ) | ||||
|         await asyncio.sleep(0.5) | ||||
|         await ctx.send( | ||||
|             f"Les {self.captain_mention(ctx)}s, vous pouvez désormais lancer un dé 100 " | ||||
|             "comme ceci `!dice 100`. " | ||||
|             "L'ordre des tirages suivants sera l'ordre croissant des lancers. " | ||||
|         ) | ||||
| from src.errors import TfjmError | ||||
|  | ||||
|  | ||||
| class TirageCog(Cog, name="Tirages"): | ||||
| @@ -610,9 +105,10 @@ class TirageCog(Cog, name="Tirages"): | ||||
|         """ | ||||
|         Commence un tirage avec 3 ou 4 équipes. | ||||
|  | ||||
|         Cette commande attend trois trigrammes d'équipes, par ex: | ||||
|         Cette commande attend des trigrames d'équipes. | ||||
|  | ||||
|             !draw start AAA BBB CCC | ||||
|         Exemple: | ||||
|             `!draw start AAA BBB CCC` | ||||
|         """ | ||||
|  | ||||
|         channel: discord.TextChannel = ctx.channel | ||||
| @@ -696,11 +192,11 @@ class TirageCog(Cog, name="Tirages"): | ||||
|     @draw_group.command(name="show") | ||||
|     async def show_cmd(self, ctx: Context, tirage_id: str): | ||||
|         """ | ||||
|         Affiche le résumé d'un tirage | ||||
|         Affiche le résumé d'un tirage. | ||||
|  | ||||
|         Les ID de tirages valides sont visibles avec | ||||
|         `!draw show all` et les details avec `!draw show 42` | ||||
|          (si l'ID qui vous intéresse est 42). | ||||
|         Exemples: | ||||
|             `!draw show all` - Liste les ID possible | ||||
|             `!draw show 42` - Affiche le tirage n°42 | ||||
|         """ | ||||
|  | ||||
|         if not TIRAGES_FILE.exists(): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user