mirror of
				https://gitlab.com/ddorn/tfjm-discord-bot.git
				synced 2025-11-04 01:32:06 +01:00 
			
		
		
		
	🚧 WIP: better and more flexible tirages
This commit is contained in:
		
							
								
								
									
										264
									
								
								src/base_tirage.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								src/base_tirage.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,264 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import random
 | 
			
		||||
from pprint import pprint
 | 
			
		||||
 | 
			
		||||
from io import StringIO
 | 
			
		||||
from typing import Type, Union, Dict, List
 | 
			
		||||
 | 
			
		||||
import discord
 | 
			
		||||
 | 
			
		||||
from src.constants import *
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Event(asyncio.Event):
 | 
			
		||||
    def __init__(self, team: str, value: Union[bool, int, str]):
 | 
			
		||||
        super(Event, self).__init__()
 | 
			
		||||
        self.value = value
 | 
			
		||||
        self.team = team
 | 
			
		||||
        self.response = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Team:
 | 
			
		||||
    yaml_tag = "Team"
 | 
			
		||||
 | 
			
		||||
    def __init__(self, team_role):
 | 
			
		||||
        self.name = team_role.name
 | 
			
		||||
        self.mention = team_role.mention
 | 
			
		||||
 | 
			
		||||
        self.accepted_problems = [None, None]
 | 
			
		||||
        self.rejected = [set(), set()]
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        s = StringIO()
 | 
			
		||||
        pprint(self.__dict__, stream=s)
 | 
			
		||||
        s.seek(0)
 | 
			
		||||
        return s.read()
 | 
			
		||||
 | 
			
		||||
    __repr__ = __str__
 | 
			
		||||
 | 
			
		||||
    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):
 | 
			
		||||
 | 
			
		||||
        info = {
 | 
			
		||||
            # "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 passage": self.passage_order[round],
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        width = max(map(len, info))
 | 
			
		||||
 | 
			
		||||
        return "\n".join(f"`{n.rjust(width)}`: {v}" for n, v in info.items())
 | 
			
		||||
 | 
			
		||||
        return f""" - 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 Poule:
 | 
			
		||||
    def __init__(self, poule, rnd):
 | 
			
		||||
        self.poule = poule
 | 
			
		||||
        self.rnd = rnd
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        return f"{self.poule}{self.rnd + 1}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseTirage:
 | 
			
		||||
    def __init__(self, *teams: discord.Role, fmt=(3, 3)):
 | 
			
		||||
        assert sum(fmt) == len(teams)
 | 
			
		||||
 | 
			
		||||
        self.teams: Dict[str, Team] = {t.name: Team(t) for t in teams}
 | 
			
		||||
        self.format = fmt
 | 
			
		||||
        self.queue = asyncio.Queue()
 | 
			
		||||
        self.poules: Dict[Poule, List[str]] = {}
 | 
			
		||||
        """A mapping between the poule name and the list of teams in this poule."""
 | 
			
		||||
 | 
			
		||||
    async def event(self, event: Event):
 | 
			
		||||
        event.set()
 | 
			
		||||
        await self.queue.put(event)
 | 
			
		||||
        await event.wait()
 | 
			
		||||
        return event.response
 | 
			
		||||
 | 
			
		||||
    async def dice(self, trigram):
 | 
			
		||||
        return await self.event(Event(trigram, random.randint(1, 100)))
 | 
			
		||||
 | 
			
		||||
    async def rproblem(self, trigram):
 | 
			
		||||
        team = self.teams[trigram]
 | 
			
		||||
        rnd = 0 if team.accepted_problems[0] is None else 1
 | 
			
		||||
        for poule, teams in self.poules.items():
 | 
			
		||||
            if trigram in teams and poule.rnd == rnd:
 | 
			
		||||
                break
 | 
			
		||||
        else:
 | 
			
		||||
            return await self.warn_wrong_team(None, trigram)
 | 
			
		||||
 | 
			
		||||
        other_pbs = [self.teams[team].accepted_problems[rnd] for team in teams]
 | 
			
		||||
        available = [
 | 
			
		||||
            pb
 | 
			
		||||
            for pb in PROBLEMS
 | 
			
		||||
            if pb not in team.accepted_problems and pb not in other_pbs
 | 
			
		||||
        ]
 | 
			
		||||
        return await self.event(Event(trigram, random.choice(available)))
 | 
			
		||||
 | 
			
		||||
    async def next(self, typ, team=None):
 | 
			
		||||
        while True:
 | 
			
		||||
            event = await self.queue.get()
 | 
			
		||||
            if team is not None and event.team != team:
 | 
			
		||||
                await self.warn_wrong_team(team, event.team)
 | 
			
		||||
            elif not isinstance(event.value, typ):
 | 
			
		||||
                await self.warn_unwanted(typ, event.value)
 | 
			
		||||
            else:
 | 
			
		||||
                event.clear()
 | 
			
		||||
                return event
 | 
			
		||||
            event.clear()
 | 
			
		||||
 | 
			
		||||
    async def run(self):
 | 
			
		||||
 | 
			
		||||
        await self.info_start()
 | 
			
		||||
 | 
			
		||||
        self.poules = await self.make_poules()
 | 
			
		||||
 | 
			
		||||
        for poule in self.poules:
 | 
			
		||||
            await self.draw_poule(poule)
 | 
			
		||||
 | 
			
		||||
        await self.info_finish()
 | 
			
		||||
 | 
			
		||||
    async def get_dices(self, teams):
 | 
			
		||||
        dices = {t: None for t in teams}
 | 
			
		||||
        collisions = list(teams)
 | 
			
		||||
        while collisions:
 | 
			
		||||
 | 
			
		||||
            for t in collisions:
 | 
			
		||||
                dices[t] = None
 | 
			
		||||
            collisions = []
 | 
			
		||||
 | 
			
		||||
            while None in dices.values():
 | 
			
		||||
                event = await self.next(int)
 | 
			
		||||
 | 
			
		||||
                # TODO: avoid KeyError
 | 
			
		||||
                if dices[event.team] is None:
 | 
			
		||||
                    dices[event.team] = event.value
 | 
			
		||||
                else:
 | 
			
		||||
                    await self.warn_twice(int)
 | 
			
		||||
 | 
			
		||||
            if collisions:
 | 
			
		||||
                await self.warn_colisions(collisions)
 | 
			
		||||
        return dices
 | 
			
		||||
 | 
			
		||||
    async def make_poules(self):
 | 
			
		||||
        poules = {}
 | 
			
		||||
        for rnd in (0, 1):
 | 
			
		||||
            await self.start_make_poule(rnd)
 | 
			
		||||
 | 
			
		||||
            dices = await self.get_dices(self.teams)
 | 
			
		||||
            sorted_teams = sorted(self.teams, key=lambda t: dices[t])
 | 
			
		||||
 | 
			
		||||
            idx = 0
 | 
			
		||||
            for i, qte in enumerate(self.format):
 | 
			
		||||
                letter = chr(ord("A") + i)
 | 
			
		||||
                poules[Poule(letter, rnd)] = sorted_teams[idx : idx + qte]
 | 
			
		||||
                idx += qte
 | 
			
		||||
 | 
			
		||||
        await self.annonce_poules(poules)
 | 
			
		||||
        return poules
 | 
			
		||||
 | 
			
		||||
    async def draw_poule(self, poule):
 | 
			
		||||
        # Trigrams in draw order
 | 
			
		||||
        trigrams = await self.draw_order(poule)
 | 
			
		||||
 | 
			
		||||
        # Teams in draw order
 | 
			
		||||
        teams = [self.teams[tri] for tri in trigrams]
 | 
			
		||||
        current = 0
 | 
			
		||||
        while not all(team.accepted_problems[poule.rnd] for team in teams):
 | 
			
		||||
            team = teams[current]
 | 
			
		||||
            if team.accepted_problems[poule.rnd] is not None:
 | 
			
		||||
                # The team already accepted a problem
 | 
			
		||||
                current += 1
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # Choose problem
 | 
			
		||||
            await self.start_select_pb(team)
 | 
			
		||||
            event = await self.next(str, team.name)
 | 
			
		||||
            # TODO: Add check for already selected / taken by someone else
 | 
			
		||||
            # This is not a bug for now, since it cannot happen yet
 | 
			
		||||
            await self.info_draw_pb(team, event.value, rnd)
 | 
			
		||||
 | 
			
		||||
            # Accept it
 | 
			
		||||
            accept = await self.next(bool, team.name)
 | 
			
		||||
            if accept:
 | 
			
		||||
                team.accepted_problems[poule.rnd] = event.value
 | 
			
		||||
                await self.info_accepted(team, event.value)
 | 
			
		||||
            else:
 | 
			
		||||
                await self.info_rejected(team, event.value, rnd=poule.rnd)
 | 
			
		||||
                team.rejected[poule.rnd].add(event.value)
 | 
			
		||||
 | 
			
		||||
            current += 1
 | 
			
		||||
 | 
			
		||||
        await self.annonce_poule(poule)
 | 
			
		||||
 | 
			
		||||
    async def draw_order(self, poule):
 | 
			
		||||
        await self.start_draw_order(poule)
 | 
			
		||||
 | 
			
		||||
        teams = self.poules[poule]
 | 
			
		||||
        dices = await self.get_dices(teams)
 | 
			
		||||
 | 
			
		||||
        order = sorted(self.teams, key=lambda t: dices[t], reverse=True)
 | 
			
		||||
 | 
			
		||||
        await self.annonce_draw_order(order)
 | 
			
		||||
        return order
 | 
			
		||||
 | 
			
		||||
    async def warn_unwanted(self, wanted: Type, got: Type):
 | 
			
		||||
        """Called when a event of an unwanted type occurs."""
 | 
			
		||||
 | 
			
		||||
    async def warn_wrong_team(self, expected, got):
 | 
			
		||||
        """Called when a team that should not play now put an event"""
 | 
			
		||||
 | 
			
		||||
    async def warn_colisions(self, collisions: List[str]):
 | 
			
		||||
        """Called when there are collisions in a dice tirage."""
 | 
			
		||||
 | 
			
		||||
    async def warn_twice(self, typ: Type):
 | 
			
		||||
        """Called when an event appears once again and not wanted."""
 | 
			
		||||
 | 
			
		||||
    async def start_make_poule(self, rnd):
 | 
			
		||||
        """Called when it starts drawing the poules for round `rnd`"""
 | 
			
		||||
 | 
			
		||||
    async def start_draw_order(self, poule):
 | 
			
		||||
        """Called when we start to draw the order."""
 | 
			
		||||
 | 
			
		||||
    async def start_select_pb(self, team):
 | 
			
		||||
        """Called when a team needs to select a problem."""
 | 
			
		||||
 | 
			
		||||
    async def annonce_poules(self, poules):
 | 
			
		||||
        """Called when all poules are defined."""
 | 
			
		||||
 | 
			
		||||
    async def annonce_draw_order(self, order):
 | 
			
		||||
        """Called when the drawing order is defined."""
 | 
			
		||||
 | 
			
		||||
    async def annonce_poule(self, poule):
 | 
			
		||||
        """Called when the problems and order for a poule is known."""
 | 
			
		||||
 | 
			
		||||
    async def info_start(self):
 | 
			
		||||
        """Called at the start of the tirage."""
 | 
			
		||||
 | 
			
		||||
    async def info_finish(self):
 | 
			
		||||
        """Called when the tirage has ended."""
 | 
			
		||||
 | 
			
		||||
    async def info_draw_pb(self, team, pb, rnd):
 | 
			
		||||
        """Called when a team draws a problem."""
 | 
			
		||||
 | 
			
		||||
    async def info_accepted(self, team, pb):
 | 
			
		||||
        """Called when a team accepts a problem."""
 | 
			
		||||
 | 
			
		||||
    async def info_rejected(self, team, pb, rnd):
 | 
			
		||||
        """Called when a team rejects a problem,
 | 
			
		||||
        before it is added to the rejected set."""
 | 
			
		||||
@@ -3,9 +3,11 @@
 | 
			
		||||
import asyncio
 | 
			
		||||
import random
 | 
			
		||||
from collections import defaultdict, namedtuple
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from functools import wraps
 | 
			
		||||
from io import StringIO
 | 
			
		||||
from pprint import pprint
 | 
			
		||||
from typing import Type, Dict
 | 
			
		||||
from typing import Type, Dict, Union, Optional, List
 | 
			
		||||
 | 
			
		||||
import discord
 | 
			
		||||
import yaml
 | 
			
		||||
@@ -13,13 +15,14 @@ from discord.ext import commands
 | 
			
		||||
from discord.ext.commands import group, Cog, Context
 | 
			
		||||
from discord.utils import get
 | 
			
		||||
 | 
			
		||||
from src.base_tirage import BaseTirage
 | 
			
		||||
from src.constants import *
 | 
			
		||||
from src.core import CustomBot
 | 
			
		||||
from src.errors import TfjmError, UnwantedCommand
 | 
			
		||||
 | 
			
		||||
__all__ = ["Tirage", "TirageCog"]
 | 
			
		||||
 | 
			
		||||
from src.utils import send_and_bin
 | 
			
		||||
from src.utils import send_and_bin, french_join
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def in_passage_order(teams, round=0):
 | 
			
		||||
@@ -29,54 +32,246 @@ def in_passage_order(teams, round=0):
 | 
			
		||||
Record = namedtuple("Record", ["name", "pb", "penalite"])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Team(yaml.YAMLObject):
 | 
			
		||||
    yaml_tag = "Team"
 | 
			
		||||
def delete_and_pm(f):
 | 
			
		||||
    @wraps(f)
 | 
			
		||||
    def wrapper(self, *args, **kwargs):
 | 
			
		||||
        await self.ctx.message.delete()
 | 
			
		||||
        await self.ctx.author.send(
 | 
			
		||||
            "J'ai supprimé ton message:\n> "
 | 
			
		||||
            + self.ctx.message.clean_content
 | 
			
		||||
            + "\nC'est pas grave, c'est juste pour ne pas encombrer "
 | 
			
		||||
            "le chat lors du tirage."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def __init__(self, ctx, team_role):
 | 
			
		||||
        self.name = team_role.name
 | 
			
		||||
        self.mention = team_role.mention
 | 
			
		||||
        self.tirage_order = [None, None]
 | 
			
		||||
        self.passage_order = [None, None]
 | 
			
		||||
        msg = await f(self, *args, **kwargs)
 | 
			
		||||
        if msg:
 | 
			
		||||
            await self.ctx.author.send(f"Raison: {msg}")
 | 
			
		||||
 | 
			
		||||
        self.accepted_problems = [None, None]
 | 
			
		||||
        self.drawn_problem = None  # Waiting to be accepted or refused
 | 
			
		||||
        self.rejected = [set(), set()]
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        s = StringIO()
 | 
			
		||||
        pprint(self.__dict__, stream=s)
 | 
			
		||||
        s.seek(0)
 | 
			
		||||
        return s.read()
 | 
			
		||||
def send_all(f):
 | 
			
		||||
    @wraps(f)
 | 
			
		||||
    async def wrapper(self, *args, **kwargs):
 | 
			
		||||
        async for msg in f(self, *args, **kwargs):
 | 
			
		||||
            await self.ctx.send(msg)
 | 
			
		||||
 | 
			
		||||
    __repr__ = __str__
 | 
			
		||||
    return wrapper
 | 
			
		||||
 | 
			
		||||
    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):
 | 
			
		||||
class DiscordTirage(BaseTirage):
 | 
			
		||||
    def __init__(self, ctx, *teams, fmt):
 | 
			
		||||
        super(DiscordTirage, self).__init__(*teams, fmt=fmt)
 | 
			
		||||
        self.ctx = ctx
 | 
			
		||||
        self.captain_mention = get(ctx.guild.roles, name=Role.CAPTAIN).mention
 | 
			
		||||
 | 
			
		||||
        info = {
 | 
			
		||||
            # "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 passage": self.passage_order[round],
 | 
			
		||||
    def records(self, teams, rnd):
 | 
			
		||||
        """Get the strings needed for show the tirage in a list of Records"""
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            Record(
 | 
			
		||||
                team.name,
 | 
			
		||||
                (team.accepted_problems[rnd] or "- None")[0],
 | 
			
		||||
                f"k = {team.coeff(rnd)} ",
 | 
			
		||||
            )
 | 
			
		||||
            for team in teams
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    @delete_and_pm
 | 
			
		||||
    async def warn_unwanted(self, wanted: Type, got: Type):
 | 
			
		||||
 | 
			
		||||
        texts = {
 | 
			
		||||
            (int, str): "Il faut tirer un problème avec `!rp` et pas un dé.",
 | 
			
		||||
            (int, bool): "Il faut accepter `!oui` ou refuser `!non` "
 | 
			
		||||
            "le problème d'abord.",
 | 
			
		||||
            (str, int): "Tu dois lancer un dé (`!dice 100`), pas choisir un problème.",
 | 
			
		||||
            (str, bool): "Il faut accepter `!oui` ou refuser `!non` le "
 | 
			
		||||
            "problème avant d'en choisir un autre",
 | 
			
		||||
            (bool, str): "Tu es bien optimiste pour vouloir accepter un problème "
 | 
			
		||||
            "avant de l'avoir tiré !"
 | 
			
		||||
            if got
 | 
			
		||||
            else "Halte là ! Ce serait bien de tirer un problème d'abord... "
 | 
			
		||||
            "et peut-être qu'il te plaira :) ",
 | 
			
		||||
            (bool, int): "Il tirer un dé avec `!dice 100` d'abord.",
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        width = max(map(len, info))
 | 
			
		||||
        reason = texts.get((type(got), wanted))
 | 
			
		||||
 | 
			
		||||
        return "\n".join(f"`{n.rjust(width)}`: {v}" for n, v in info.items())
 | 
			
		||||
        if reason is None:
 | 
			
		||||
            print(f"Weird, arguments for warn_unwanted were {wanted} and {got}")
 | 
			
		||||
            reason = "Je sais pas, le code ne devrait pas venir ici..."
 | 
			
		||||
        return reason
 | 
			
		||||
 | 
			
		||||
        return f""" - 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]}
 | 
			
		||||
"""
 | 
			
		||||
    @delete_and_pm
 | 
			
		||||
    async def warn_wrong_team(self, expected, got):
 | 
			
		||||
        return "ce n'était pas à ton tour."
 | 
			
		||||
 | 
			
		||||
    async def warn_colisions(self, collisions: List[str]):
 | 
			
		||||
        await self.ctx.send(
 | 
			
		||||
            f"Les equipes {french_join(collisions)} ont fait le même résultat "
 | 
			
		||||
            "et doivent relancer un dé. "
 | 
			
		||||
            "Le nouveau lancer effacera l'ancien."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @delete_and_pm
 | 
			
		||||
    async def warn_twice(self, typ: Type):
 | 
			
		||||
 | 
			
		||||
        if typ == int:
 | 
			
		||||
            return "Tu as déjà lancé un dé, pas besoin de le refaire ;)"
 | 
			
		||||
 | 
			
		||||
        print("Weird, DiscordTirage.warn_twice was called with", typ)
 | 
			
		||||
        return "Je sais pas, le code ne devrait pas venir ici..."
 | 
			
		||||
 | 
			
		||||
    @send_all
 | 
			
		||||
    async def start_make_poule(self, rnd):
 | 
			
		||||
        if rnd == 0:
 | 
			
		||||
            yield (
 | 
			
		||||
                f"Les {self.captain_mention}s, vous pouvez désormais tous lancer un dé 100 "
 | 
			
		||||
                "comme ceci : `!dice 100`. "
 | 
			
		||||
                "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."
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            yield (
 | 
			
		||||
                f"Les {self.captain_mention}s, vous pouvez à nouveau tous lancer un dé 100, "
 | 
			
		||||
                f"afin de déterminer les poules du second tour."
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    async def start_draw_order(self, poule):
 | 
			
		||||
        print(poule)
 | 
			
		||||
 | 
			
		||||
    async def start_select_pb(self, team):
 | 
			
		||||
        await self.ctx.send(f"C'est au tour de {team.mention} de choisir un problème.")
 | 
			
		||||
 | 
			
		||||
    async def annonce_poules(self, poules):
 | 
			
		||||
        print(poules)
 | 
			
		||||
 | 
			
		||||
    @send_all
 | 
			
		||||
    async def annonce_draw_order(self, order):
 | 
			
		||||
        order_str = "\n ".join(f"{i}) {tri}" for i, tri in enumerate(order))
 | 
			
		||||
        yield "L'ordre de tirage des problèmes pour ce tour est donc: \n" + order_str
 | 
			
		||||
 | 
			
		||||
    async def annonce_poule(self, poule):
 | 
			
		||||
        teams = [self.teams[tri] for tri in self.poules[poule]]
 | 
			
		||||
 | 
			
		||||
        if len(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   |
 | 
			
		||||
            +-----+---------+---------+---------+---------+
 | 
			
		||||
            | {1.name} |   Opp   |   Déf   |         |   Rap   |
 | 
			
		||||
            +-----+---------+---------+---------+---------+
 | 
			
		||||
            | {2.name} |   Rap   |   Opp   |   Déf   |         |
 | 
			
		||||
            +-----+---------+---------+---------+---------+
 | 
			
		||||
            | {3.name} |         |   Rap   |   Opp   |   Déf   |
 | 
			
		||||
            +-----+---------+---------+---------+---------+
 | 
			
		||||
        ```"""
 | 
			
		||||
 | 
			
		||||
        embed = discord.Embed(
 | 
			
		||||
            title=f"Résumé du tirage entre {french_join([t.name for t in teams])}",
 | 
			
		||||
            color=EMBED_COLOR,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        embed.add_field(
 | 
			
		||||
            name=ROUND_NAMES[poule.rnd].capitalize(),
 | 
			
		||||
            value=table.format(*self.records(teams, poule.rnd)),
 | 
			
		||||
            inline=False,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for team in teams:
 | 
			
		||||
            embed.add_field(
 | 
			
		||||
                name=team.name + " - " + team.accepted_problems[poule.rnd],
 | 
			
		||||
                value=team.details(poule.rnd),
 | 
			
		||||
                inline=True,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        embed.set_footer(
 | 
			
		||||
            text="Ce tirage peut être affiché à tout moment avec `!draw show XXX`"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        await self.ctx.send(embed=embed)
 | 
			
		||||
 | 
			
		||||
    @send_all
 | 
			
		||||
    async def info_start(self):
 | 
			
		||||
        yield (
 | 
			
		||||
            "Nous allons commencer le tirage des problèmes. "
 | 
			
		||||
            "Seuls les capitaines de chaque équipe peuvent désormais écrire ici. "
 | 
			
		||||
            "Merci de d'envoyer seulement ce que est nécessaire et suffisant au "
 | 
			
		||||
            "bon déroulement du tirage. Vous pouvez à tout moment poser toute question "
 | 
			
		||||
            "si quelque chose n'est pas clair ou ne va pas."
 | 
			
		||||
            "\n"
 | 
			
		||||
            "\n"
 | 
			
		||||
            "Pour plus de détails sur le déroulement du tirgae au sort, le règlement "
 | 
			
		||||
            "est accessible sur https://tfjm.org/reglement."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        yield (
 | 
			
		||||
            "Nous allons d'abord tirer les poules et l'ordre de passage dans chaque tour "
 | 
			
		||||
            "avec toutes les équipes puis pour chaque poule de chaque tour, nous tirerons "
 | 
			
		||||
            "l'ordre de tirage pour le tour et les problèmes."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @send_all
 | 
			
		||||
    async def info_finish(self):
 | 
			
		||||
        yield "Le tirage est fini, merci à tout le monde !"
 | 
			
		||||
        yield (
 | 
			
		||||
            "Vous pouvez désormais trouver les problèmes que vous devrez opposer ou rapporter "
 | 
			
		||||
            "sur la page de votre équipe."
 | 
			
		||||
        )
 | 
			
		||||
        # TODO: Save it
 | 
			
		||||
        # TODO: make them available with the api
 | 
			
		||||
 | 
			
		||||
    async def info_draw_pb(self, team, pb, rnd):
 | 
			
		||||
        if pb in team.rejected[rnd]:
 | 
			
		||||
            await self.ctx.send(
 | 
			
		||||
                f"Vous avez déjà refusé **{pb}**, "
 | 
			
		||||
                f"vous pouvez le refuser à nouveau (`!non`) et "
 | 
			
		||||
                f"tirer immédiatement un nouveau problème "
 | 
			
		||||
                f"ou changer d'avis et l'accepter (`!oui`)."
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            if len(team.rejected[rnd]) >= MAX_REFUSE:
 | 
			
		||||
                await self.ctx.send(
 | 
			
		||||
                    f"Vous pouvez accepter ou refuser **{pb}** "
 | 
			
		||||
                    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 self.ctx.send(
 | 
			
		||||
                    f"Vous pouvez accepter (`!oui`) ou refuser (`!non`) **{pb}**. "
 | 
			
		||||
                    f"Il reste {MAX_REFUSE - len(team.rejected[rnd])} refus sans pénalité "
 | 
			
		||||
                    f"pour {team.mention}."
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
    async def info_accepted(self, team, pb):
 | 
			
		||||
        await self.ctx.send(
 | 
			
		||||
            f"L'équipe {team.mention} a accepté "
 | 
			
		||||
            f"**{pb}** ! Les autres équipes "
 | 
			
		||||
            f"ne peuvent plus l'accepter."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    async def info_rejected(self, team, pb, rnd):
 | 
			
		||||
        msg = f"{team.mention} a refusé **{pb}** "
 | 
			
		||||
        if pb in team.rejected[rnd]:
 | 
			
		||||
            msg += "sans pénalité."
 | 
			
		||||
        else:
 | 
			
		||||
            msg += "!"
 | 
			
		||||
        await self.ctx.send(msg)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Tirage(yaml.YAMLObject):
 | 
			
		||||
@@ -86,47 +281,14 @@ class Tirage(yaml.YAMLObject):
 | 
			
		||||
        assert len(teams) in (3, 4)
 | 
			
		||||
 | 
			
		||||
        self.channel: int = channel
 | 
			
		||||
        self.teams = [Team(ctx, team) for team in teams]
 | 
			
		||||
        self.teams = [Team(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)
 | 
			
		||||
        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.
 | 
			
		||||
@@ -163,70 +325,6 @@ class Tirage(yaml.YAMLObject):
 | 
			
		||||
        if self.channel in tirages:
 | 
			
		||||
            del tirages[self.channel]
 | 
			
		||||
 | 
			
		||||
    def records(self, round):
 | 
			
		||||
        """Get the strings needed for show the tirage in a list of Records"""
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            Record(
 | 
			
		||||
                team.name,
 | 
			
		||||
                (team.accepted_problems[round] or "- None")[0],
 | 
			
		||||
                f"k = {team.coeff(round)} ",
 | 
			
		||||
            )
 | 
			
		||||
            for team in in_passage_order(self.teams, round)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
    async def show(self, ctx):
 | 
			
		||||
        teams = ", ".join(team.name for team in self.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   |
 | 
			
		||||
    +-----+---------+---------+---------+---------+
 | 
			
		||||
    | {1.name} |   Opp   |   Déf   |         |   Rap   |
 | 
			
		||||
    +-----+---------+---------+---------+---------+
 | 
			
		||||
    | {2.name} |   Rap   |   Opp   |   Déf   |         |
 | 
			
		||||
    +-----+---------+---------+---------+---------+
 | 
			
		||||
    | {3.name} |         |   Rap   |   Opp   |   Déf   |
 | 
			
		||||
    +-----+---------+---------+---------+---------+
 | 
			
		||||
```"""
 | 
			
		||||
 | 
			
		||||
        embed = discord.Embed(
 | 
			
		||||
            title=f"Résumé du tirage entre {teams}", color=EMBED_COLOR
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for r in (0, 1):
 | 
			
		||||
            embed.add_field(
 | 
			
		||||
                name=ROUND_NAMES[r].capitalize(),
 | 
			
		||||
                value=table.format(*self.records(r)),
 | 
			
		||||
                inline=False,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            for team in in_passage_order(self.teams, r):
 | 
			
		||||
                embed.add_field(
 | 
			
		||||
                    name=team.name + " - " + team.accepted_problems[r],
 | 
			
		||||
                    value=team.details(r),
 | 
			
		||||
                    inline=True,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        await ctx.send(embed=embed)
 | 
			
		||||
 | 
			
		||||
    async def show_tex(self, ctx):
 | 
			
		||||
        if len(self.teams) == 3:
 | 
			
		||||
            table = r"""
 | 
			
		||||
@@ -261,51 +359,7 @@ class Tirage(yaml.YAMLObject):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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):
 | 
			
		||||
        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):
 | 
			
		||||
@@ -358,11 +412,7 @@ class OrderPhase(Phase):
 | 
			
		||||
                    re_do.extend(teams)
 | 
			
		||||
 | 
			
		||||
            teams_str = ", ".join(team.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.
 | 
			
		||||
@@ -462,32 +512,12 @@ class TiragePhase(Phase):
 | 
			
		||||
        ), "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 :) "
 | 
			
		||||
                )
 | 
			
		||||
            pass
 | 
			
		||||
        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.rejected[self.round].add(team.drawn_problem)
 | 
			
		||||
 | 
			
		||||
            team.drawn_problem = None
 | 
			
		||||
 | 
			
		||||
@@ -502,10 +532,6 @@ class TiragePhase(Phase):
 | 
			
		||||
                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)
 | 
			
		||||
 | 
			
		||||
@@ -558,61 +584,12 @@ class TiragePhase(Phase):
 | 
			
		||||
        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. "
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TirageCog(Cog, name="Tirages"):
 | 
			
		||||
    def __init__(self, bot):
 | 
			
		||||
        self.bot: CustomBot = bot
 | 
			
		||||
 | 
			
		||||
        # We retrieve the global variable.
 | 
			
		||||
        # We don't want tirages to be ust an attribute
 | 
			
		||||
        # We don't want tirages to be just an attribute
 | 
			
		||||
        # as we want them to outlive the Cog, for instance
 | 
			
		||||
        # if the cog is reloaded turing a tirage.
 | 
			
		||||
        from src.tfjm_discord_bot import tirages
 | 
			
		||||
@@ -773,16 +750,6 @@ class TirageCog(Cog, name="Tirages"):
 | 
			
		||||
            }
 | 
			
		||||
            await channel.edit(overwrites=overwrites)
 | 
			
		||||
 | 
			
		||||
        await ctx.send(
 | 
			
		||||
            "Nous allons commencer le tirage du premier tour. "
 | 
			
		||||
            "Seuls les capitaines de chaque équipe peuvent désormais écrire ici. "
 | 
			
		||||
            "Merci de d'envoyer seulement ce que est nécessaire et suffisant au "
 | 
			
		||||
            "bon déroulement du tournoi. Vous pouvez à tout moment poser toute question "
 | 
			
		||||
            "si quelque chose n'est pas clair ou ne va pas. \n\n"
 | 
			
		||||
            "Pour plus de détails sur le déroulement du tirgae au sort, le règlement "
 | 
			
		||||
            "est accessible sur https://tfjm.org/reglement."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.tirages[channel_id] = Tirage(ctx, channel_id, teams)
 | 
			
		||||
        await self.tirages[channel_id].phase.start(ctx)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,11 @@ import psutil
 | 
			
		||||
from discord.ext.commands import Bot
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def french_join(l):
 | 
			
		||||
    start = ", ".join(l[:-1])
 | 
			
		||||
    return f"{start} et {l[-1]}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_role(member, role: str):
 | 
			
		||||
    """Return whether the member has a role with this name."""
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user