commit 5998d74a37fc0cf0a1f9254a531201a8d59e19a9 Author: Simon Junod Date: Thu Mar 31 19:44:24 2022 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1684025 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.py[cod] + +settings.py +grammalecte.txt +wordlists \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..1981190 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.8.0 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cambot/__init__.py b/cambot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cambot/codenames.py b/cambot/codenames.py new file mode 100644 index 0000000..62471aa --- /dev/null +++ b/cambot/codenames.py @@ -0,0 +1,394 @@ +import random +import re +import os +from collections import Counter +from itertools import cycle +from enum import auto +from .emojis import * +from .utils import serialize, EnumZero +from .settings import * + +class Teams(EnumZero): + RED = auto() + BLUE = auto() + NEUTRAL = auto() + BOMB = auto() + +class Roles(EnumZero): + CHOOSER = auto() + GUESSER = auto() + +class Recipients(EnumZero): + EVERYONE = auto() + CHOOSERS = auto() + +class Modes(EnumZero): + COOPERATION = auto() + COMPETITION = auto() + +class Statuses(EnumZero): + RED_CHOOSER = auto() + RED_GUESSER = auto() + BLUE_CHOOSER = auto() + BLUE_GUESSER = auto() + NEUTRAL_CHOOSER = auto() + NEUTRAL_GUESSER = auto() + +CYCLES = {Modes.COOPERATION: cycle([Statuses.NEUTRAL_CHOOSER, Statuses.NEUTRAL_GUESSER]), + Modes.COMPETITION: cycle([Statuses.RED_CHOOSER, Statuses.RED_GUESSER, Statuses.BLUE_CHOOSER, Statuses.BLUE_GUESSER])} + +CONFIGURATIONS = [Counter([Statuses.NEUTRAL_CHOOSER, Statuses.NEUTRAL_GUESSER]), # coop + Counter([Statuses.NEUTRAL_CHOOSER, Statuses.NEUTRAL_GUESSER, Statuses.NEUTRAL_GUESSER]), # coop + Counter([Statuses.RED_CHOOSER, Statuses.BLUE_CHOOSER, Statuses.NEUTRAL_GUESSER]), + Counter([Statuses.RED_CHOOSER, Statuses.RED_GUESSER, Statuses.BLUE_CHOOSER, Statuses.BLUE_GUESSER]), + Counter([Statuses.RED_CHOOSER, Statuses.RED_GUESSER, Statuses.BLUE_CHOOSER, Statuses.BLUE_GUESSER, Statuses.NEUTRAL_GUESSER]), + Counter([Statuses.RED_CHOOSER, Statuses.RED_GUESSER, Statuses.RED_GUESSER, Statuses.BLUE_CHOOSER, Statuses.BLUE_GUESSER, Statuses.BLUE_GUESSER])] + +SYMBOLS = {Statuses.RED_CHOOSER: f"{REDDOT}{SPEAKING}", + Statuses.RED_GUESSER: f"{REDDOT}{GLASS}", + Statuses.BLUE_CHOOSER: f"{BLUEDOT}{SPEAKING}", + Statuses.BLUE_GUESSER: f"{BLUEDOT}{GLASS}", + Statuses.NEUTRAL_CHOOSER: f"{WHITEDOT}{SPEAKING}", + Statuses.NEUTRAL_GUESSER: f"{WHITEDOT}{GLASS}"} + +class Game: + def __init__(self, channel): + self.channel = channel + self.reset(keep_players=False) + + async def say(self, message): + await self.channel.send(message) + + def get_player(self, client): + try: + player = next(p for p in self.players if p.client == client) + except StopIteration: + player = None + return player + + def current_team(self): + return Teams(self.current_step.value // 2) + + def current_role(self): + return Roles(self.current_step.value % 2) + + def reset(self, keep_players=True): + self.playing = False + self.mode = None + self.steps = None + self.current_step = None + if not keep_players: + self.players = [] + self.words = [] + self.clue = None + self.number = None + self.turn = 0 + +class Player: + def __init__(self, client, team, role): + self.client = client + self.team = team + self.role = role + + @property + def status(self): + return Statuses((2 * self.team.value) + self.role.value) + +class Word: + def __init__(self, label, team): + self.label = label + self.team = team + self.guessed = False + +async def maybe_join(game, message, team): + if game.playing: + await game.say(f"{message.author.mention} Une partie est en cours, impossible de rejoindre une équipe.") + return + + if len([p for p in game.players if p.team == team]) > 0: + role = Roles.GUESSER + else: + role = Roles.CHOOSER + + player = game.get_player(message.author) + if player: + if player.team == team: + await game.say(f"{message.author.mention} Tu fais déjà partie de cette équipe.") + return + else: + player.team = team + player.role = role + else: + player = Player(client=message.author, team=team, role=role) + game.players.append(player) + + await game.say(f"{message.author.mention} Équipe {('rouge', 'bleue', 'neutre')[team.value]} rejointe, rôle : {('choisisseur', 'devineur')[role.value]}.") + +async def maybe_change_role(game, message): + wannabe = game.get_player(message.author) + + if not wannabe: + await game.say(f"{message.author.mention} Tu n'es dans aucune équipe.") + return + + if game.playing: + await game.say(f"{message.author.mention} Une partie est en cours, impossible de changer de rôle.") + return + + wannabe.role = Roles(int(not(wannabe.role.value))) + + await game.say(f"{message.author.mention} est désormais {('choisisseur', 'devineur')[wannabe.role.value]} de l'équipe {('rouge', 'bleue', 'neutre')[wannabe.team.value]}.") + +async def print_teams(game): + if not game.players: + await game.say(f"Aucun joueur inscrit.") + return + + output = "" + + for team in (Teams.RED, Teams.BLUE, Teams.NEUTRAL): + output += f"{(REDDOT, BLUEDOT, WHITEDOT)[team.value]} Équipe {('rouge', 'bleue', 'neutre')[team.value]} :\n" + for player in [p for p in game.players if p.team == team]: + output += f"--- {(SPEAKING, GLASS)[player.role.value]} {player.client.name} ({('choisisseur', 'devineur')[player.role.value]})\n" + + await game.say(output) + +async def print_configurations(game, with_error=False): + + output = "" + + if with_error: + output += "La configuration actuelle des joueurs ne correspond à aucune configuration de jeu possible.\n\n" + + output += "__Configurations possibles__\n\n" + + possible_nb_of_players = sorted(set(sum(c.values()) for c in CONFIGURATIONS)) + + for nb_players in possible_nb_of_players: + output += f"**{nb_players} joueur{'s' if nb_players > 1 else ''}**\n" + configurations = [c for c in CONFIGURATIONS if sum(c.values()) == nb_players] + for configuration in configurations: + statuses = list(configuration.elements()) + competition = any(e in [0, 1, 2, 3] for e in statuses) + output += " + ".join([SYMBOLS[status] for status in statuses]) + if competition: + output += f" (compétition {SWORDS})" + else: + output += f" (coopération {HANDSHAKE})" + output += "\n" + output += "\n" + + await game.say(output) + +async def print_grid(game, revealed=False, to=Recipients.EVERYONE): + output = "" + + if to == Recipients.CHOOSERS: + output += "---------- Grille actuelle : ----------\n" + + widths = [] + for column in range(5): + words_in_column = [word for (idx, word) in enumerate(game.words) if idx % 5 == column] + widths.append(max(len(w.label) for w in words_in_column)) + + for y in range(5): + for x in range(5): + word = game.words[y*5 + x] + label = word.label + (" " * (widths[x] - len(word.label) + 5)) + label = f"`{label}`" + if revealed and word.guessed: + label = f"~~{label}~~" + output += f"{(REDDOT, BLUEDOT, WHITEDOT, SKULL)[word.team.value] if (revealed or word.guessed) else QUESTION}{label}" + output += "\n" + + if to == Recipients.EVERYONE: + await game.say(output) + elif to == Recipients.CHOOSERS: + choosers = [p for p in game.players if p.role == Roles.CHOOSER] + for chooser in choosers: + await chooser.client.send(output) + +async def maybe_leave(game, message): + player = game.get_player(message.author) + + if not player: + await game.say(f"{message.author.mention} Tu n'es dans aucune équipe.") + return + + if game.playing: + await game.say(f"{message.author.mention} Une partie est en cours, impossible quitter l'équipe.") + return + + team = player.team + game.players.remove(player) + await game.say(f"{message.author.mention} est parti de l'équipe {('rouge', 'bleue', 'neutre')[team.value]}.") + +async def maybe_start(game): + if not game.players: + await game.say(f"Aucun joueur inscrit.") + return + + configuration = Counter([p.status for p in game.players]) + + if not configuration in CONFIGURATIONS: + await print_configurations(game, with_error=True) + return + + game.playing = True + if any(p.team in (Teams.RED, Teams.BLUE) for p in game.players): + game.mode = Modes.COMPETITION + else: + game.mode = Modes.COOPERATION + game.steps = CYCLES[game.mode] + with open(CODENAMES_WORDS, "r") as f: + lexicon = f.read().splitlines() + game.words = [Word(label=word, team=Teams.NEUTRAL) for word in random.sample(lexicon, 25)] + if game.mode == Modes.COMPETITION: + for idx in range(0, 9): + game.words[idx].team = Teams.RED + for idx in range(9, 17): + game.words[idx].team = Teams.BLUE + game.words[17].team = Teams.BOMB + elif game.mode == Modes.COOPERATION: + for idx in range(0, 12): + game.words[idx].team = Teams.BOMB + random.shuffle(game.words) + + await proceed(game) + +async def proceed(game): + + if game.current_step: + + scores = [len([w for w in game.words if w.guessed and w.team == team]) for team in (Teams.RED, Teams.BLUE, Teams.NEUTRAL)] + if any(w for w in game.words if w.guessed and w.team == Teams.BOMB): + if game.mode == Modes.COOPERATION: + await game.say(f"{SAD} La bombe a gagné. Score final des neutres : **{scores[2]}/12** en {game.turn} tours.") + elif game.mode == Modes.COMPETITION: + winners = int(not(game.current_team())) + await game.say(f"{PARTY} L'équipe {('rouge', 'bleue')[winners]} a gagné pour cause d'explosion de l'équipe adverse !") + game.reset() + return + + for team in (Teams.RED, Teams.BLUE, Teams.NEUTRAL): + if [w for w in game.words if w.team == team] and not any(w for w in game.words if w.guessed == False and w.team == team): + await game.say(f"{PARTY} L'équipe {('rouge', 'bleue', 'neutre')[team.value]} a gagné ! Elle a trouvé tous ses mots.") + game.reset() + return + + game.current_step = next(game.steps) + current_players = [p.client.mention for p in game.players if p.status == game.current_step] + + if game.current_role() == Roles.CHOOSER: + game.turn += 1 + if game.turn == 1: + await print_grid(game, revealed=False, to=Recipients.EVERYONE) + await print_grid(game, revealed=True, to=Recipients.CHOOSERS) + await game.say(f"{SYMBOLS[game.current_step]}" + f" C'est au choisisseur de l'équipe" + f" {('rouge', 'bleue', 'neutre')[game.current_team().value]}" + f" ({', '.join(current_players)}) de m'indiquer" + f" **en privé** un indice et un nombre de mots souhaité.") + return + + if game.current_role() == Roles.GUESSER: + neutral_guessers = [p.client.mention for p in game.players if p.status == Statuses.NEUTRAL_GUESSER] + current_players += neutral_guessers + current_players = set(current_players) + plural = len(current_players) > 1 + await game.say(f"{SYMBOLS[game.current_step]}" + f" C'est au{('','x')[plural]} devineur{('','s')[plural]} de l'équipe" + f" {('rouge', 'bleue', 'neutre')[game.current_team().value]}" + f" ({', '.join(current_players)}) de retrouver la sélection" + f" effectuée par le choisisseur. L'indice est **{game.clue}**." + f" Nombre de mots à trouver : **{game.number}**") + +async def process_whisper(game, message): + player = game.get_player(message.author) + + if game.current_step != player.status: + await message.author.send("Ce n'est pas à toi de me parler pour l'instant.") + return + + content = message.content.strip() + + if game.current_role() == Roles.CHOOSER: + if regex := re.search(r"^([^ ]+) (\d+)$", content, re.I): + maybe_number = int(regex.group(2)) + number_max = len([w for w in game.words if w.guessed == False and w.team == game.current_team()]) + if not (1 <= maybe_number <= number_max): + await message.author.send(f"Nombre invalide (doit être entre 1 et {number_max}).") + return + game.number = maybe_number + + maybe_clue = regex.group(1) + if any(w for w in game.words if serialize(w.label) == serialize(maybe_clue)): + await message.author.send(f"L'indice ne peut pas être un des mots de la grille !") + return + game.clue = maybe_clue + + await message.author.send(f"OK, ça continue sur {game.channel.mention} !") + await proceed(game) + return + else: + await message.author.send("Choisis secrètement un ou plusieurs mots, puis envoie (ici)" + " ` `, par exemple `caisson 3`." + " L'indice doit être un seul mot, et il ne doit pas être de" + " la même famille qu'un mot de la grille !") + return + + if game.current_role() == Roles.GUESSER: + await message.author.send(f"Tu dois deviner le mot, ça se passe en public sur {game.channel.mention}.") + +async def maybe_guess(game, message, content): + + if not game.playing: + await game.say("Aucune partie en cours.") + return + + player = game.get_player(message.author) + if not player: + await game.say(f"{message.author.mention} Tu n'es dans aucune équipe.") + return + + if player.team != game.current_team() and player.team != Teams.NEUTRAL: + await game.say(f"{message.author.mention} Ce n'est pas à ton équipe de jouer.") + return + + if player.role == Roles.CHOOSER: + await game.say(f"{message.author.mention} Tu es choisisseur, pas devineur.") + return + + if game.current_role() == Roles.CHOOSER: + await game.say(f"{message.author.mention} Le choisisseur n'a pas encore donné son indice.") + return + + try: + word = next(w for w in game.words if serialize(w.label) == serialize(content)) + except StopIteration: + await game.say(f"{message.author.mention} Je ne reconnais pas ce mot dans la grille.") + return + if word.guessed: + await game.say(f"{message.author.mention} Ce mot a déjà été deviné et révélé.") + return + + word.guessed = True + await print_grid(game, revealed=False, to=Recipients.EVERYONE) + if word.team == game.current_team(): + await game.say(f"{SMILE} Bien ouej, c'est un mot de ton équipe !") + game.number -= 1 + if game.number > 0: + await game.say(f"Nombre de mots à trouver encore : {game.number}") + else: + await game.say(f"Le nombre de mots indiqué par le choisisseur a été atteint, bravo ! Fin du tour.") + await proceed(game) + else: + game.number = 0 + if word.team == Teams.NEUTRAL: + await game.say(f"{MEH} Oups, c'est un mot neutre... Fin du tour.") + elif word.team == Teams.BOMB: + await game.say(f"{BLOWN} **BOUM !** C'est {('une','la')[game.mode.value]} bombe ! Fin du jeu.") + else: + await game.say(f"{SAD} Mince... c'est un mot de l'équipe adverse... Fin du tour.") + + await proceed(game) diff --git a/cambot/emojis.py b/cambot/emojis.py new file mode 100644 index 0000000..10db4c8 --- /dev/null +++ b/cambot/emojis.py @@ -0,0 +1,16 @@ +PUSHPIN = chr(128204) +REDDOT = chr(128308) +BLUEDOT = chr(128309) +WHITEDOT = chr(9898) +SPEAKING = chr(128483) +GLASS = chr(128269) +HANDSHAKE = chr(129309) +SWORDS = chr(9876) +EXPLOSION = chr(128165) +QUESTION = chr(10068) +SKULL = chr(9760) +SMILE = chr(128515) +MEH = chr(128533) +SAD = chr(128543) +BLOWN = chr(129327) +PARTY = chr(129395) \ No newline at end of file diff --git a/cambot/ephemeris.py b/cambot/ephemeris.py new file mode 100644 index 0000000..b204d27 --- /dev/null +++ b/cambot/ephemeris.py @@ -0,0 +1,92 @@ +import os +import requests +from datetime import datetime +from discord import Embed, Color +import re +import json +import locale +from bs4 import BeautifulSoup as bs +from .settings import * +from .saints import SAINTS + +def citation(): + try: + ts = datetime.now().strftime("%s") + req = requests.get(f"https://fr.wikiquote.org/wiki/Wikiquote:Accueil?r={ts}") + soup = bs(req.text, features="html.parser") + citation = soup.find(lambda tag:tag.name=="i" and "Lumière sur" in tag.text).parent.parent.parent.findAll("div")[1].text.strip() + return f"*{citation}*" + except Exception: + return "Impossible de trouver la citation du jour. Bouuuh !" + +def saint(): + today = datetime.now().strftime("%m/%d") + saint = SAINTS[today] + return saint + +def weather_emoji(_id): + emojis = {re.compile(r"2.."):":thunder_cloud_rain:", + re.compile(r"3.."):":white_sun_rain_cloud:", + re.compile(r"5.."):":cloud_rain:", + re.compile(r"6.."):":snowflake:", + re.compile(r"7.."):":interrobang:", + re.compile(r"800"):":sunny:", + re.compile(r"801"):":white_sun_small_cloud:", + re.compile(r"80[23]"):":white_sun_cloud:", + re.compile(r"804"):":cloud:"} + try: + code = next(emoji[1] for emoji in emojis.items() if emoji[0].match(str(_id))) + except StopIteration: + code = ":negative_squared_cross_mark:" + return code + +def weather(lat, lon): + r = requests.get(f"https://api.openweathermap.org/data/2.5/" + f"onecall?lat={lat}&lon={lon}&appid={OWM_KEY}&lang=fr&units=metric") + j = json.loads(r.text) + + next_hours = [] + for i in (2, 8, 14): + hourly = j["hourly"][i] + time = datetime.fromtimestamp(hourly["dt"]).strftime("%H:%M") + temp = round(hourly["temp"]) + feels_like = hourly["feels_like"] + humidity = hourly["humidity"] + wind_speed = round(hourly["wind_speed"] * 3.6) + description = hourly["weather"][0]["description"] + emoji = weather_emoji(hourly["weather"][0]["id"]) + next_hours.append({"time":time, + "emoji":emoji, + "description":description, + "temp":temp, + "wind_speed":wind_speed + }) + + return next_hours + +def digest(): + try: + locale.setlocale(locale.LC_ALL, "fr_CH.utf-8") + except locale.Error: + pass + now = datetime.now() + d = now.strftime("%A %-d %B") + c = citation() + s = saint() + w_strings = [] + for w in weather(46.5196661, 6.6325467): + w_strings.append(f"{w['emoji']} {w['description']} ({w['temp']}°C, {w['wind_speed']} km/h)") + + embed = Embed() + embed.title = "Bonjour !" + embed.description = f"Nous sommes le {d} ({s})\n\n{c}\n\u200B" + colors = [Color.red(), Color.gold(), Color.orange(), Color.blue(), Color.green(), Color.magenta(), Color.purple()] + embed.color = colors[now.weekday()] + embed.add_field(name="Matin", value=w_strings[0]) + embed.add_field(name="Après-midi", value=w_strings[1]) + embed.add_field(name="Soir", value=w_strings[2]) + + return embed + +if __name__ == "__main__": + print(citation()) diff --git a/cambot/saints.py b/cambot/saints.py new file mode 100644 index 0000000..e7848aa --- /dev/null +++ b/cambot/saints.py @@ -0,0 +1,379 @@ +SAINTS = { + "01/01": "Jour de l'An", + "01/02": "Saint Basile", + "01/03": "Sainte Geneviève", + "01/04": "Saint Odilon", + "01/05": "Saint Edouard", + "01/06": "Saint André", + "01/07": "Saint Raymond", + "01/08": "Saint Lucien", + "01/09": "Sainte Alix", + "01/10": "Saint Guillaume", + "01/11": "Saint Paulin", + "01/12": "Sainte Tatiana", + "01/13": "Sainte Yvette", + "01/14": "Sainte Nina", + "01/15": "Saint Rémi", + "01/16": "Saint Marcel", + "01/17": "Sainte Roseline", + "01/18": "Sainte Prisca", + "01/19": "Saint Marius", + "01/20": "Saint Sébastien", + "01/21": "Sainte Agnès", + "01/22": "Saint Vincent", + "01/23": "Saint Barnard", + "01/24": "Saint François", + "01/25": "Saint Paul", + "01/26": "Sainte Paule", + "01/27": "Sainte Angèle", + "01/28": "Saint Thomas", + "01/29": "Saint Gildas", + "01/30": "Sainte Martine", + "01/31": "Sainte Marcelle", + + "02/01": "Sainte Ella", + "02/02": "Saint Théophane", + "02/03": "Saint Blaise", + "02/04": "Sainte Véronique", + "02/05": "Sainte Agathe", + "02/06": "Saint Gaston", + "02/07": "Sainte Eugénie", + "02/08": "Sainte Jacqueline", + "02/09": "Sainte Apolline", + "02/10": "Saint Arnaud", + "02/11": "Saint Séverin", + "02/12": "Saint Félix", + "02/13": "Sainte Béatrice", + "02/14": "Saint Valentin", + "02/15": "Saint Claude", + "02/16": "Sainte Julienne", + "02/17": "Saint Alexis", + "02/18": "Sainte Bernadette", + "02/19": "Saint Gabin", + "02/20": "Sainte Aimée", + "02/21": "Saint Damien", + "02/22": "Sainte Isabelle", + "02/23": "Saint Lazare", + "02/24": "Saint Modeste", + "02/25": "Saint Roméo", + "02/26": "Saint Nestor", + "02/27": "Sainte Honorine", + "02/28": "Saint Romain", + "02/29": "Saint Auguste", + + "03/01": "Saint Aubin", + "03/02": "Saint Charles", + "03/03": "Saint Gwenolé", + "03/04": "Saint Casimir", + "03/05": "Saint Olive", + "03/06": "Sainte Colette", + "03/07": "Sainte Félicité", + "03/08": "Saint Jean", + "03/09": "Sainte Françoise", + "03/10": "Saint Vivien", + "03/11": "Sainte Rosine", + "03/12": "Sainte Justine", + "03/13": "Saint Rodrigue", + "03/14": "Sainte Mathilde", + "03/15": "Sainte Louise", + "03/16": "Sainte Bénédicte", + "03/17": "Saint Patrick", + "03/18": "Saint Cyrille", + "03/19": "Saint Joseph", + "03/20": "Saint Herbert", + "03/21": "Sainte Clémence", + "03/22": "Sainte Léa", + "03/23": "Saint Victorien", + "03/24": "Sainte Catherine", + "03/25": "Saint Humbert", + "03/26": "Sainte Larissa", + "03/27": "Saint Habib", + "03/28": "Saint Gontran", + "03/29": "Sainte Gwladys", + "03/30": "Saint Amédée", + "03/31": "Saint Benjamin", + + "04/01": "Saint Hugues", + "04/02": "Sainte Sandrine", + "04/03": "Saint Richard", + "04/04": "Saint Isidore", + "04/05": "Sainte Irène", + "04/06": "Saint Marcellin", + "04/07": "Saint Jean-Baptiste", + "04/08": "Sainte Julie", + "04/09": "Saint Gautier", + "04/10": "Saint Fulbert", + "04/11": "Saint Stanislas", + "04/12": "Saint Jules 1er", + "04/13": "Sainte Ida", + "04/14": "Saint Maxime", + "04/15": "Saint Paterne", + "04/16": "Saint Benoît", + "04/17": "Saint Etienne", + "04/18": "Saint Parfait", + "04/19": "Sainte Emma", + "04/20": "Sainte Odette", + "04/21": "Saint Anselme", + "04/22": "Saint Alexandre", + "04/23": "Saint Georges", + "04/24": "Saint Fidèle", + "04/25": "Saint Marc", + "04/26": "Sainte Alida", + "04/27": "Sainte Zita", + "04/28": "Sainte Valérie", + "04/29": "Sainte Catherine", + "04/30": "Saint Robert", + + "05/01": "Saint Joseph", + "05/02": "Saint Boris", + "05/03": "Saint Jacques", + "05/04": "Saint Sylvain", + "05/05": "Sainte Judith", + "05/06": "Sainte Prudence", + "05/07": "Sainte Gisèle", + "05/08": "Saint Désiré", + "05/09": "Sainte Pacôme", + "05/10": "Sainte Solange", + "05/11": "Sainte Estelle", + "05/12": "Saint Achille", + "05/13": "Sainte Rolande", + "05/14": "Saint Matthias", + "05/15": "Sainte Denise", + "05/16": "Saint Honoré", + "05/17": "Saint Pascal", + "05/18": "Saint Eric", + "05/19": "Saint Yves", + "05/20": "Saint Bernardin", + "05/21": "Saint Constantin", + "05/22": "Saint Emile", + "05/23": "Saint Didier", + "05/24": "Saint Donatien", + "05/25": "Sainte Sophie", + "05/26": "Saint Bérenger", + "05/27": "Saint Augustin", + "05/28": "Saint Germain", + "05/29": "Saint Aymard", + "05/30": "Saint Ferdinand", + "05/31": "Sainte Perrine", + + "06/01": "Saint Justin", + "06/02": "Sainte Blandine", + "06/03": "Saint Charles", + "06/04": "Sainte Clotilde", + "06/05": "Saint Igor", + "06/06": "Saint Norbert", + "06/07": "Saint Gilbert", + "06/08": "Saint Médard", + "06/09": "Sainte Diane", + "06/10": "Saint Landry", + "06/11": "Saint Barnabé", + "06/12": "Saint Guy", + "06/13": "Saint Antoine", + "06/14": "Saint Elisée", + "06/15": "Sainte Germaine", + "06/16": "Saint Jean-François", + "06/17": "Saint Hervé", + "06/18": "Saint Léonce", + "06/19": "Saint Romuald", + "06/20": "Saint Silvère", + "06/21": "Saint Rodolphe", + "06/22": "Saint Alban", + "06/23": "Sainte Audrey", + "06/24": "Saint Jean-Baptiste", + "06/25": "Saint Prosper", + "06/26": "Saint Anthelme", + "06/27": "Saint Fernand", + "06/28": "Saint Irénée", + "06/29": "Saint Pierre", + "06/30": "Saint Martial", + + "07/01": "Saint Thierry", + "07/02": "Saint Martinien", + "07/03": "Saint Thomas", + "07/04": "Saint Florent", + "07/05": "Saint Antoine-Marie", + "07/06": "Sainte Marietta", + "07/07": "Saint Raoul", + "07/08": "Saint Thibaud", + "07/09": "Sainte Amandine", + "07/10": "Saint Ulric", + "07/11": "Saint Benoît", + "07/12": "Saint Olivier", + "07/13": "Saint Henri", + "07/14": "Saint Camille", + "07/15": "Saint Donald", + "07/16": "Sainte Elvire", + "07/17": "Sainte Charlotte", + "07/18": "Saint Frédéric", + "07/19": "Saint Arsène", + "07/20": "Sainte Marina", + "07/21": "Saint Victor", + "07/22": "Sainte Marie-Madeleine", + "07/23": "Sainte Brigitte", + "07/24": "Sainte Christine", + "07/25": "Saint Jacques", + "07/26": "Sainte Anne", + "07/27": "Sainte Nathalie", + "07/28": "Saint Samson", + "07/29": "Sainte Marthe", + "07/30": "Sainte Juliette", + "07/31": "Saint Ignace", + + "08/01": "Saint Alphonse-Marie", + "08/02": "Saint Pierre-Julien", + "08/03": "Sainte Lydie", + "08/04": "Saint Jean-Marie", + "08/05": "Saint Abel", + "08/06": "Saint Octavien", + "08/07": "Saint Gaétan", + "08/08": "Saint Dominique", + "08/09": "Saint Amour", + "08/10": "Saint Laurent", + "08/11": "Sainte Claire", + "08/12": "Sainte Clarisse", + "08/13": "Saint Hippolyte", + "08/14": "Saint Evrard", + "08/15": "Sainte Marie", + "08/16": "Saint Armel", + "08/17": "Saint Hyacinthe", + "08/18": "Sainte Hélène", + "08/19": "Saint Jean-Eudes", + "08/20": "Saint Bernard", + "08/21": "Saint Christophe", + "08/22": "Saint Fabrice", + "08/23": "Sainte Rose", + "08/24": "Saint Barthélémy", + "08/25": "Saint Louis", + "08/26": "Sainte Natacha", + "08/27": "Sainte Monique", + "08/28": "Saint Augustin", + "08/29": "Sainte Sabine", + "08/30": "Saint Fiacre", + "08/31": "Saint Aristide", + + "09/01": "Saint Gilles", + "09/02": "Sainte Ingrid", + "09/03": "Saint Grégoire", + "09/04": "Sainte Rosalie", + "09/05": "Sainte Raïssa", + "09/06": "Saint Bertrand", + "09/07": "Sainte Reine", + "09/08": "Saint Adrien", + "09/09": "Saint Alain", + "09/10": "Sainte Inès", + "09/11": "Saint Adelphe", + "09/12": "Saint Apollinaire", + "09/13": "Saint Aimé", + "09/14": "la Croix Glorieuse", + "09/15": "Saint Roland", + "09/16": "Sainte Edith", + "09/17": "Saint Renaud", + "09/18": "Sainte Nadège", + "09/19": "Sainte Emilie", + "09/20": "Saint Davy", + "09/21": "Saint Matthieu", + "09/22": "Saint Maurice", + "09/23": "Saint Constant", + "09/24": "Sainte Thècle", + "09/25": "Saint Hermann", + "09/26": "Sts Côme et Damien", + "09/27": "Saint Vincent", + "09/28": "Saint Venceslas", + "09/29": "Saint Michel", + "09/30": "Saint Jérôme", + + "10/01": "Sainte Thérèse", + "10/02": "Saint Léger", + "10/03": "Saint Gérard", + "10/04": "Saint François", + "10/05": "Sainte Fleur", + "10/06": "Saint Bruno", + "10/07": "Saint Serge", + "10/08": "Sainte Pélagie", + "10/09": "Saint Denis", + "10/10": "Saint Ghislain", + "10/11": "Saint Firmin", + "10/12": "Saint Wilfrid", + "10/13": "Saint Géraud", + "10/14": "Saint Juste", + "10/15": "Sainte Thérèse", + "10/16": "Sainte Edwige", + "10/17": "Saint Baudouin", + "10/18": "Saint Luc", + "10/19": "Saint René", + "10/20": "Sainte Adeline", + "10/21": "Sainte Céline", + "10/22": "Sainte Elodie", + "10/23": "Saint Jean", + "10/24": "Saint Florentin", + "10/25": "Sainte Doria", + "10/26": "Saint Dimitri", + "10/27": "Sainte Emeline", + "10/28": "Saint Simon", + "10/29": "Saint Narcisse", + "10/30": "Sainte Bienvenue", + "10/31": "Saint Quentin", + + "11/01": "La Toussaint", + "11/02": "Les Défunts", + "11/03": "Saint Hubert", + "11/04": "Saint Charles", + "11/05": "Sainte Sylvie", + "11/06": "Sainte Bertille", + "11/07": "Sainte Carine", + "11/08": "Saint Geoffroy", + "11/09": "Saint Théodore", + "11/10": "Saint Léon", + "11/11": "Saint Martin", + "11/12": "Saint Christian", + "11/13": "Saint Brice", + "11/14": "Saint Sidoine", + "11/15": "Saint Albert", + "11/16": "Sainte Marguerite", + "11/17": "Sainte Elisabeth", + "11/18": "Sainte Aude", + "11/19": "Saint Tanguy", + "11/20": "Saint Edmond", + "11/21": "Saint Albert", + "11/22": "Sainte Cécile", + "11/23": "Saint Clément", + "11/24": "Sainte Flora", + "11/25": "Sainte Catherine", + "11/26": "Sainte Delphine", + "11/27": "Saint Séverin", + "11/28": "Saint Jacques", + "11/29": "Saint Saturnin", + "11/30": "Saint André", + + "12/01": "Sainte Florence", + "12/02": "Sainte Viviane", + "12/03": "Saint François-Xavier", + "12/04": "Sainte Barbara", + "12/05": "Saint Gérald", + "12/06": "Saint Nicolas", + "12/07": "Saint Ambroise", + "12/08": "Sainte Elfie", + "12/09": "Saint Pierre", + "12/10": "Saint Romaric", + "12/11": "Saint Daniel", + "12/12": "Sainte Jeanne-Françoise", + "12/13": "Sainte Lucie", + "12/14": "Sainte Odile", + "12/15": "Sainte Ninon", + "12/16": "Sainte Alice", + "12/17": "Saint Gaël", + "12/18": "Saint Gatien", + "12/19": "Saint Urbain", + "12/20": "Saint Théophile", + "12/21": "Saint Pierre", + "12/22": "Sainte Françoise-Xavière", + "12/23": "Saint Armand", + "12/24": "Sainte Adèle", + "12/25": "Jour de Noël", + "12/26": "Saint Etienne", + "12/27": "Saint Jean", + "12/28": "Saints Innocents", + "12/29": "Saint David", + "12/30": "Saint Roger", + "12/31": "Saint Sylvestre" +} diff --git a/cambot/settings_example.py b/cambot/settings_example.py new file mode 100644 index 0000000..e322f28 --- /dev/null +++ b/cambot/settings_example.py @@ -0,0 +1,25 @@ +from datetime import time + +DISCORD_TOKEN = "xxx" +OWM_KEY = "xxx" + +CHANNEL1 = 111111111111111111 +CHANNEL2 = 222222222222222222 +CHANNEL3 = 333333333333333333 +EPHEMERIS_CHANNEL_IDS = (CHANNEL1,) +CODENAMES_CHANNEL_IDS = (CHANNEL1, CHANNEL2) +WORDLE_CHANNEL_IDS = (CHANNEL2, CHANNEL3) + +WORDLE_VALID_WORDS = "cambot/wordlists/valid_words.txt" +WORDLE_TARGET_WORDS = "cambot/wordlists/target_words.txt" +WORDLE_POINTS = (1, 3, 6) +WORDLE_SLOWMODE = 0 +WORDLE_MINLENGTH = 8 +WORDLE_FORCE_ALTERNATION = True + +HEARTBEAT = 60 +EPHEMERIS = 0 +WORDLE = 1 +EVENTS = ((EPHEMERIS, time(8)), + (WORDLE, time(8)), + (WORDLE, time(20))) \ No newline at end of file diff --git a/cambot/utils.py b/cambot/utils.py new file mode 100644 index 0000000..7bd3f5c --- /dev/null +++ b/cambot/utils.py @@ -0,0 +1,9 @@ +import unidecode +from enum import Enum + +def serialize(string): + return unidecode.unidecode(string.lower().strip().replace(" ", "").replace("-", "").replace("'", "").replace(".", "")) + +class EnumZero(Enum): + def _generate_next_value_(name, start, count, last_values): + return count \ No newline at end of file diff --git a/cambot/wordle.py b/cambot/wordle.py new file mode 100644 index 0000000..ea4580f --- /dev/null +++ b/cambot/wordle.py @@ -0,0 +1,98 @@ +import random +from unidecode import unidecode +from collections import defaultdict +from .settings import * + +with open(WORDLE_VALID_WORDS, "r") as f: + valid_words = set(f.read().splitlines()) + +with open(WORDLE_TARGET_WORDS, "r") as f: + target_words = set(f.read().splitlines()) + +valid_words = valid_words.union(target_words) + +def validate(target, guess): + assert len(target) == len(guess) + + copy = list(target) + output = [0] * len(target) + + # Look for the green squares : + + for idx in range(len(target)): + if target[idx] == guess[idx]: + output[idx] = 2 + copy[idx] = None + + # Look for the yellow squares : + + for idx in range(len(target)): + if target[idx] == guess[idx]: + continue # ignore the letters that are green + if guess[idx] in copy: + left_most_yellow_idx = copy.index(guess[idx]) + output[idx] = 1 + copy[left_most_yellow_idx] = None # mark ONE of the corresponding letters as done + + return output + +class Game: + def __init__(self, channel): + self.channel = channel + self.target = None + self.winner = None + self.tries = 0 + self.last_player = None + self.scores = None + self.tried = None + + async def reset(self): + self.target = random.choice(tuple(x for x in target_words if len(x) >= WORDLE_MINLENGTH)) + self.winner = None + self.tries = 0 + self.last_player = None + self.scores = defaultdict(int) + self.tried = set() + await self.channel.edit(slowmode_delay=WORDLE_SLOWMODE) + await self.channel.send(f"Il y a un nouveau mot à deviner ! Il fait {len(self.target)} lettres.") + + async def parse(self, message): + content = message.content.strip() + if (len(content) >= 1 and content[0] == "-") or self.winner or not self.target: # special char, or somebody won, or never initialized + return + guess = unidecode(content).upper() + if WORDLE_FORCE_ALTERNATION and message.author == self.last_player: + await self.channel.send(f"{message.author.mention} Laisse un peu jouer les autres !") + return + if len(guess) != len(self.target): + await self.channel.send(f"{message.author.mention} Le mot à deviner fait {len(self.target)} lettres (ça ne compte pas comme un tour).") + return + if guess not in valid_words: + await self.channel.send(f"{message.author.mention} `{guess}` n'est pas dans mon dictionnaire (ça ne compte pas comme un tour).") + return + + self.tries += 1 + self.last_player = message.author + result = validate(self.target, guess) + points = 0 + for idx, letter in enumerate(guess): + if (idx, letter) in self.tried: + continue + points += WORDLE_POINTS[result[idx]] + self.tried.add((idx, letter)) + + output_word = " ".join(f":regional_indicator_{x.lower()}:" for x in guess) + output_squares = " ".join((":white_large_square:", ":yellow_square:", ":green_square:")[x] for x in result) + + output = f"{message.author.mention} (essai {self.tries}, {points} point{'s' if points > 1 else ''})\n{output_word}\n{output_squares}" + self.scores[message.author] += points + + if guess == self.target: + self.winner = message.author + await self.channel.edit(slowmode_delay=0) + output += "\n\n:trophy: YOUPI :trophy:\n\nScores :\n\n" + for idx, score in enumerate(sorted(self.scores.items(), key=lambda x:x[1], reverse=True)): + player, points = score + output += f"{idx+1}) {player.display_name} ({points} point{'s' if points > 1 else ''})\n" + + await self.channel.send(output) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..96dab15 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,295 @@ +[[package]] +category = "main" +description = "Async http client/server framework (asyncio)" +name = "aiohttp" +optional = false +python-versions = ">=3.5.3" +version = "3.6.2" + +[package.dependencies] +async-timeout = ">=3.0,<4.0" +attrs = ">=17.3.0" +chardet = ">=2.0,<4.0" +multidict = ">=4.5,<5.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["aiodns", "brotlipy", "cchardet"] + +[[package]] +category = "main" +description = "Timeout context manager for asyncio programs" +name = "async-timeout" +optional = false +python-versions = ">=3.5.3" +version = "3.0.1" + +[[package]] +category = "main" +description = "Classes Without Boilerplate" +name = "attrs" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "20.2.0" + +[package.extras] +dev = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "sphinx", "sphinx-rtd-theme", "pre-commit"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] +tests_no_zope = ["coverage (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"] + +[[package]] +category = "main" +description = "Screen-scraping library" +name = "beautifulsoup4" +optional = false +python-versions = "*" +version = "4.9.3" + +[package.dependencies] +[package.dependencies.soupsieve] +python = ">=3.0" +version = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +category = "main" +description = "Dummy package for Beautiful Soup" +name = "bs4" +optional = false +python-versions = "*" +version = "0.0.1" + +[package.dependencies] +beautifulsoup4 = "*" + +[[package]] +category = "main" +description = "Python package for providing Mozilla's CA Bundle." +name = "certifi" +optional = false +python-versions = "*" +version = "2020.6.20" + +[[package]] +category = "main" +description = "Universal encoding detector for Python 2 and 3" +name = "chardet" +optional = false +python-versions = "*" +version = "3.0.4" + +[[package]] +category = "main" +description = "A mirror package for discord.py. Please install that instead." +name = "discord" +optional = false +python-versions = "*" +version = "1.0.1" + +[package.dependencies] +"discord.py" = ">=1.0.1" + +[[package]] +category = "main" +description = "A Python wrapper for the Discord API" +name = "discord.py" +optional = false +python-versions = ">=3.5.3" +version = "1.4.1" + +[package.dependencies] +aiohttp = ">=3.6.0,<3.7.0" + +[package.extras] +docs = ["sphinx (1.8.5)", "sphinxcontrib-trio (1.1.1)", "sphinxcontrib-websupport"] +voice = ["PyNaCl (1.3.0)"] + +[[package]] +category = "main" +description = "Internationalized Domain Names in Applications (IDNA)" +name = "idna" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.10" + +[[package]] +category = "main" +description = "multidict implementation" +name = "multidict" +optional = false +python-versions = ">=3.5" +version = "4.7.6" + +[[package]] +category = "main" +description = "Python HTTP for Humans." +name = "requests" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +version = "2.24.0" + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<4" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" + +[package.extras] +security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] + +[[package]] +category = "main" +description = "A modern CSS selector implementation for Beautiful Soup." +marker = "python_version >= \"3.0\"" +name = "soupsieve" +optional = false +python-versions = ">=3.5" +version = "2.0.1" + +[[package]] +category = "main" +description = "ASCII transliterations of Unicode text" +name = "unidecode" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.1.1" + +[[package]] +category = "main" +description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "urllib3" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +version = "1.25.11" + +[package.extras] +brotli = ["brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] + +[[package]] +category = "main" +description = "Yet another URL library" +name = "yarl" +optional = false +python-versions = ">=3.5" +version = "1.6.0" + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[metadata] +content-hash = "1c7fce4481730b6cbd329be4db35653cf7d827df9ac031227bbb6376d480bf8f" +python-versions = "^3.8" + +[metadata.files] +aiohttp = [ + {file = "aiohttp-3.6.2-cp35-cp35m-macosx_10_13_x86_64.whl", hash = "sha256:1e984191d1ec186881ffaed4581092ba04f7c61582a177b187d3a2f07ed9719e"}, + {file = "aiohttp-3.6.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:50aaad128e6ac62e7bf7bd1f0c0a24bc968a0c0590a726d5a955af193544bcec"}, + {file = "aiohttp-3.6.2-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:65f31b622af739a802ca6fd1a3076fd0ae523f8485c52924a89561ba10c49b48"}, + {file = "aiohttp-3.6.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:ae55bac364c405caa23a4f2d6cfecc6a0daada500274ffca4a9230e7129eac59"}, + {file = "aiohttp-3.6.2-cp36-cp36m-win32.whl", hash = "sha256:344c780466b73095a72c616fac5ea9c4665add7fc129f285fbdbca3cccf4612a"}, + {file = "aiohttp-3.6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:4c6efd824d44ae697814a2a85604d8e992b875462c6655da161ff18fd4f29f17"}, + {file = "aiohttp-3.6.2-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:2f4d1a4fdce595c947162333353d4a44952a724fba9ca3205a3df99a33d1307a"}, + {file = "aiohttp-3.6.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:6206a135d072f88da3e71cc501c59d5abffa9d0bb43269a6dcd28d66bfafdbdd"}, + {file = "aiohttp-3.6.2-cp37-cp37m-win32.whl", hash = "sha256:b778ce0c909a2653741cb4b1ac7015b5c130ab9c897611df43ae6a58523cb965"}, + {file = "aiohttp-3.6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:32e5f3b7e511aa850829fbe5aa32eb455e5534eaa4b1ce93231d00e2f76e5654"}, + {file = "aiohttp-3.6.2-py3-none-any.whl", hash = "sha256:460bd4237d2dbecc3b5ed57e122992f60188afe46e7319116da5eb8a9dfedba4"}, + {file = "aiohttp-3.6.2.tar.gz", hash = "sha256:259ab809ff0727d0e834ac5e8a283dc5e3e0ecc30c4d80b3cd17a4139ce1f326"}, +] +async-timeout = [ + {file = "async-timeout-3.0.1.tar.gz", hash = "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f"}, + {file = "async_timeout-3.0.1-py3-none-any.whl", hash = "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3"}, +] +attrs = [ + {file = "attrs-20.2.0-py2.py3-none-any.whl", hash = "sha256:fce7fc47dfc976152e82d53ff92fa0407700c21acd20886a13777a0d20e655dc"}, + {file = "attrs-20.2.0.tar.gz", hash = "sha256:26b54ddbbb9ee1d34d5d3668dd37d6cf74990ab23c828c2888dccdceee395594"}, +] +beautifulsoup4 = [ + {file = "beautifulsoup4-4.9.3-py2-none-any.whl", hash = "sha256:4c98143716ef1cb40bf7f39a8e3eec8f8b009509e74904ba3a7b315431577e35"}, + {file = "beautifulsoup4-4.9.3-py3-none-any.whl", hash = "sha256:fff47e031e34ec82bf17e00da8f592fe7de69aeea38be00523c04623c04fb666"}, + {file = "beautifulsoup4-4.9.3.tar.gz", hash = "sha256:84729e322ad1d5b4d25f805bfa05b902dd96450f43842c4e99067d5e1369eb25"}, +] +bs4 = [ + {file = "bs4-0.0.1.tar.gz", hash = "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"}, +] +certifi = [ + {file = "certifi-2020.6.20-py2.py3-none-any.whl", hash = "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"}, + {file = "certifi-2020.6.20.tar.gz", hash = "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3"}, +] +chardet = [ + {file = "chardet-3.0.4-py2.py3-none-any.whl", hash = "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"}, + {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, +] +discord = [ + {file = "discord-1.0.1-py3-none-any.whl", hash = "sha256:9d4debb4a37845543bd4b92cb195bc53a302797333e768e70344222857ff1559"}, + {file = "discord-1.0.1.tar.gz", hash = "sha256:ff6653655e342e7721dfb3f10421345fd852c2a33f2cca912b1c39b3778a9429"}, +] +"discord.py" = [ + {file = "discord.py-1.4.1-py3-none-any.whl", hash = "sha256:98ea3096a3585c9c379209926f530808f5fcf4930928d8cfb579d2562d119570"}, + {file = "discord.py-1.4.1.tar.gz", hash = "sha256:f9decb3bfa94613d922376288617e6a6f969260923643e2897f4540c34793442"}, +] +idna = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] +multidict = [ + {file = "multidict-4.7.6-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:275ca32383bc5d1894b6975bb4ca6a7ff16ab76fa622967625baeebcf8079000"}, + {file = "multidict-4.7.6-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:1ece5a3369835c20ed57adadc663400b5525904e53bae59ec854a5d36b39b21a"}, + {file = "multidict-4.7.6-cp35-cp35m-win32.whl", hash = "sha256:5141c13374e6b25fe6bf092052ab55c0c03d21bd66c94a0e3ae371d3e4d865a5"}, + {file = "multidict-4.7.6-cp35-cp35m-win_amd64.whl", hash = "sha256:9456e90649005ad40558f4cf51dbb842e32807df75146c6d940b6f5abb4a78f3"}, + {file = "multidict-4.7.6-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:e0d072ae0f2a179c375f67e3da300b47e1a83293c554450b29c900e50afaae87"}, + {file = "multidict-4.7.6-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3750f2205b800aac4bb03b5ae48025a64e474d2c6cc79547988ba1d4122a09e2"}, + {file = "multidict-4.7.6-cp36-cp36m-win32.whl", hash = "sha256:f07acae137b71af3bb548bd8da720956a3bc9f9a0b87733e0899226a2317aeb7"}, + {file = "multidict-4.7.6-cp36-cp36m-win_amd64.whl", hash = "sha256:6513728873f4326999429a8b00fc7ceddb2509b01d5fd3f3be7881a257b8d463"}, + {file = "multidict-4.7.6-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:feed85993dbdb1dbc29102f50bca65bdc68f2c0c8d352468c25b54874f23c39d"}, + {file = "multidict-4.7.6-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fcfbb44c59af3f8ea984de67ec7c306f618a3ec771c2843804069917a8f2e255"}, + {file = "multidict-4.7.6-cp37-cp37m-win32.whl", hash = "sha256:4538273208e7294b2659b1602490f4ed3ab1c8cf9dbdd817e0e9db8e64be2507"}, + {file = "multidict-4.7.6-cp37-cp37m-win_amd64.whl", hash = "sha256:d14842362ed4cf63751648e7672f7174c9818459d169231d03c56e84daf90b7c"}, + {file = "multidict-4.7.6-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:c026fe9a05130e44157b98fea3ab12969e5b60691a276150db9eda71710cd10b"}, + {file = "multidict-4.7.6-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:51a4d210404ac61d32dada00a50ea7ba412e6ea945bbe992e4d7a595276d2ec7"}, + {file = "multidict-4.7.6-cp38-cp38-win32.whl", hash = "sha256:5cf311a0f5ef80fe73e4f4c0f0998ec08f954a6ec72b746f3c179e37de1d210d"}, + {file = "multidict-4.7.6-cp38-cp38-win_amd64.whl", hash = "sha256:7388d2ef3c55a8ba80da62ecfafa06a1c097c18032a501ffd4cabbc52d7f2b19"}, + {file = "multidict-4.7.6.tar.gz", hash = "sha256:fbb77a75e529021e7c4a8d4e823d88ef4d23674a202be4f5addffc72cbb91430"}, +] +requests = [ + {file = "requests-2.24.0-py2.py3-none-any.whl", hash = "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"}, + {file = "requests-2.24.0.tar.gz", hash = "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b"}, +] +soupsieve = [ + {file = "soupsieve-2.0.1-py3-none-any.whl", hash = "sha256:1634eea42ab371d3d346309b93df7870a88610f0725d47528be902a0d95ecc55"}, + {file = "soupsieve-2.0.1.tar.gz", hash = "sha256:a59dc181727e95d25f781f0eb4fd1825ff45590ec8ff49eadfd7f1a537cc0232"}, +] +unidecode = [ + {file = "Unidecode-1.1.1-py2.py3-none-any.whl", hash = "sha256:1d7a042116536098d05d599ef2b8616759f02985c85b4fef50c78a5aaf10822a"}, + {file = "Unidecode-1.1.1.tar.gz", hash = "sha256:2b6aab710c2a1647e928e36d69c21e76b453cd455f4e2621000e54b2a9b8cce8"}, +] +urllib3 = [ + {file = "urllib3-1.25.11-py2.py3-none-any.whl", hash = "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"}, + {file = "urllib3-1.25.11.tar.gz", hash = "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2"}, +] +yarl = [ + {file = "yarl-1.6.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:db9eb8307219d7e09b33bcb43287222ef35cbcf1586ba9472b0a4b833666ada1"}, + {file = "yarl-1.6.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:e31fef4e7b68184545c3d68baec7074532e077bd1906b040ecfba659737df188"}, + {file = "yarl-1.6.0-cp35-cp35m-win32.whl", hash = "sha256:5d84cc36981eb5a8533be79d6c43454c8e6a39ee3118ceaadbd3c029ab2ee580"}, + {file = "yarl-1.6.0-cp35-cp35m-win_amd64.whl", hash = "sha256:5e447e7f3780f44f890360ea973418025e8c0cdcd7d6a1b221d952600fd945dc"}, + {file = "yarl-1.6.0-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:6f6898429ec3c4cfbef12907047136fd7b9e81a6ee9f105b45505e633427330a"}, + {file = "yarl-1.6.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d088ea9319e49273f25b1c96a3763bf19a882cff774d1792ae6fba34bd40550a"}, + {file = "yarl-1.6.0-cp36-cp36m-win32.whl", hash = "sha256:b7c199d2cbaf892ba0f91ed36d12ff41ecd0dde46cbf64ff4bfe997a3ebc925e"}, + {file = "yarl-1.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:67c5ea0970da882eaf9efcf65b66792557c526f8e55f752194eff8ec722c75c2"}, + {file = "yarl-1.6.0-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:04a54f126a0732af75e5edc9addeaa2113e2ca7c6fce8974a63549a70a25e50e"}, + {file = "yarl-1.6.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fcbe419805c9b20db9a51d33b942feddbf6e7fb468cb20686fd7089d4164c12a"}, + {file = "yarl-1.6.0-cp37-cp37m-win32.whl", hash = "sha256:c604998ab8115db802cc55cb1b91619b2831a6128a62ca7eea577fc8ea4d3131"}, + {file = "yarl-1.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c22607421f49c0cb6ff3ed593a49b6a99c6ffdeaaa6c944cdda83c2393c8864d"}, + {file = "yarl-1.6.0-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:7ce35944e8e61927a8f4eb78f5bc5d1e6da6d40eadd77e3f79d4e9399e263921"}, + {file = "yarl-1.6.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c15d71a640fb1f8e98a1423f9c64d7f1f6a3a168f803042eaf3a5b5022fde0c1"}, + {file = "yarl-1.6.0-cp38-cp38-win32.whl", hash = "sha256:3cc860d72ed989f3b1f3abbd6ecf38e412de722fb38b8f1b1a086315cf0d69c5"}, + {file = "yarl-1.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:e32f0fb443afcfe7f01f95172b66f279938fbc6bdaebe294b0ff6747fb6db020"}, + {file = "yarl-1.6.0.tar.gz", hash = "sha256:61d3ea3c175fe45f1498af868879c6ffeb989d4143ac542163c45538ba5ec21b"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..221b78c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[tool.poetry] +name = "cambot" +version = "0.1.0" +description = "" +authors = ["Simon Junod "] + +[tool.poetry.dependencies] +python = "^3.8" +discord = "^1.0.1" +unidecode = "^1.1.1" +requests = "^2.24.0" +bs4 = "^0.0.1" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry>=0.12"] +build-backend = "poetry.masonry.api" diff --git a/run.py b/run.py new file mode 100755 index 0000000..79f5726 --- /dev/null +++ b/run.py @@ -0,0 +1,221 @@ +import sys +import discord +import hashlib +import re +import asyncio +import random +from datetime import datetime +import cambot.codenames as codenames +import cambot.wordle as wordle +import cambot.ephemeris as ephemeris +from cambot.emojis import * +from cambot.settings import * + +bot = discord.Client() +codenames_games = {} +wordle_games = {} + +# Startup + +@bot.event +async def on_ready(): + print(f"Connecté, nom {bot.user.name}, id {bot.user.id}") + for channel_id in CODENAMES_CHANNEL_IDS: + channel = bot.get_channel(channel_id) + codenames_game = codenames.Game(channel) + codenames_games[channel_id] = codenames_game + print(f"Écoute pour Codenames sur {channel.guild} > {channel.name}") + for channel_id in WORDLE_CHANNEL_IDS: + channel = bot.get_channel(channel_id) + wordle_game = wordle.Game(channel) + wordle_games[channel_id] = wordle_game + print(f"Écoute pour Wordle sur {channel.guild} > {channel.name}") + + current_date = None + while True: + await asyncio.sleep(HEARTBEAT) + now = datetime.now() + if now.date() != current_date: # the bot was just started, OR it's a new day + events_done = [event[1] < now.time() for event in EVENTS] # mark events in the past as done + current_date = now.date() + for idx, event in enumerate(EVENTS): + if event[1] <= now.time() and not events_done[idx]: + events_done[idx] = True + if event[0] == EPHEMERIS: + embed = ephemeris.digest() + for channel_id in EPHEMERIS_CHANNEL_IDS: + await bot.get_channel(channel_id).send(embed=embed) + elif event[0] == WORDLE: + for wordle_game in wordle_games.values(): + await wordle_game.reset() + +# Receiving a message + +@bot.event +async def on_message(message): + + # Ignore own messages + + if message.author == bot.user: + return + + content = message.content.strip() + content_lowercase = content.lower() + + # Private messages + + if isinstance(message.channel, discord.channel.DMChannel): + games_entered = [codenames_game for codenames_game in codenames_games.values() if codenames_game.get_player(message.author)] + + if content_lowercase.startswith("say ") and message.author.name == "Biganon" and message.author.discriminator == "0001": + arguments = content[4:].split(" ") + channel_id = int(arguments[0]) + to_say = " ".join(arguments[1:]) + await bot.get_channel(channel_id).send(to_say) + + if content_lowercase == "target" and message.author.name == "Biganon" and message.author.discriminator == "0001": + output = "" + for wordle_game in wordle_games.values(): + output += f"{wordle_game.channel.guild} > {wordle_game.channel.name} : {wordle_game.target}\n" + await message.author.send(output) + + if len(games_entered) == 1: + await codenames.process_whisper(games_entered[0], message) + elif len(games_entered) > 1: + await message.author.send(f"Tu es inscrit dans plusieurs parties en même temps, je ne peux pas travailler dans ces conditions.") + else: + pass + + return + + # Help + + if re.search(r"^!(aide|help|commandes)$", content_lowercase): + await print_help(message.channel) + + # Judge something + + if regex := re.search(r"^!juger (.+)$", content_lowercase): + subject = regex.group(1).strip() + score = int(hashlib.md5(bytes(subject.lower(), "utf-8")).hexdigest(), 16) % 2 + opinion = ("c'est hyper bien", "c'est tellement pas OK")[score] + output = '{}, {}'.format(subject, opinion) + await message.channel.send(output) + + # Dice + + if regex := re.search(r"^!(?P\d+)?d[eé]s?(?P\d+)?$", content_lowercase): + thrower = message.author.display_name + maximum = 6 + try: + maximum = int(regex.group("maximum")) + except (TypeError, ValueError): + pass + + number = 1 + try: + number = int(regex.group("number")) + except (TypeError, ValueError): + pass + + results = [] + for n in range(number): + result = random.randint(1, maximum) + results.append(result) + + output = f"Résultat du lancer de {thrower} : **{sum(results)}**" + if len(results) > 1: + output += f" ({', '.join(map(str, results))})" + + await message.channel.send(output) + + # Codenames commands + + if message.channel.id in codenames_games.keys(): + + game = codenames_games[message.channel.id] + + if re.search(r"^!rouges?$", content_lowercase): + await codenames.maybe_join(game, message, codenames.Teams.RED) + + elif re.search(r"^!bleus?$", content_lowercase): + await codenames.maybe_join(game, message, codenames.Teams.BLUE) + + elif re.search(r"^!neutres?$", content_lowercase): + await codenames.maybe_join(game, message, codenames.Teams.NEUTRAL) + + elif re.search(r"^!r[oô]les?$", content_lowercase): + await codenames.maybe_change_role(game, message) + + elif re.search(r"^![eé]quipes?$", content_lowercase): + await codenames.print_teams(game) + + elif re.search(r"^!config(uration)?s?$", content_lowercase): + await codenames.print_configurations(game) + + elif re.search(r"^!(partir|quitter)$", content_lowercase): + await codenames.maybe_leave(game, message) + + elif re.search(r"^!(jouer|play|start)$", content_lowercase): + await codenames.maybe_start(game) + + elif regex := re.search(r"^!deviner (.+)$", content_lowercase): + await codenames.maybe_guess(game, message, regex.group(1)) + + # Wordle + + if message.channel.id in wordle_games.keys(): + await wordle_games[message.channel.id].parse(message) + +# Help + +async def print_help(channel): + output = """__Commandes de CamBot__ + + **Divers** + !juger `truc` : demander à Camille/CamBot de juger `truc` + !XdésY : lancer X dé(s) à Y faces. Le "s" de "dés" est optionnel. Si omis, X vaut 1 et Y vaut 6 + + **Codenames** + !rouge : rejoindre l'équipe rouge + !bleu : rejoindre l'équipe bleue + !neutre : rejoindre l'équipe neutre + !role : changer de rôle dans son équipe + !equipes : afficher la composition des équipes + !configurations : afficher les configurations de jeu possibles + !partir : quitter une équipe + !jouer : lancer la partie + !deviner `mot` : deviner le mot `mot` + + **Divers** + !aide : afficher ce message + """ + await channel.send(output) + +# Let unprivileged users pin messages + +async def reaction_changed(payload): + emoji = payload.emoji + if not emoji.is_unicode_emoji(): + return + if(len(emoji.name) != 1): # flags, Fitzpatrick skin tone, variation selectors... + return + if emoji.name != PUSHPIN: + return + event_type = payload.event_type + message = await bot.get_channel(payload.channel_id).fetch_message(payload.message_id) + if event_type == "REACTION_ADD": + await message.pin() + elif event_type == "REACTION_REMOVE": + await message.unpin() + +@bot.event +async def on_raw_reaction_add(payload): + await reaction_changed(payload) + +@bot.event +async def on_raw_reaction_remove(payload): + await reaction_changed(payload) + +if __name__ == "__main__": + bot.run(DISCORD_TOKEN) diff --git a/scripts/extract_from_grammalecte.py b/scripts/extract_from_grammalecte.py new file mode 100644 index 0000000..4ce9d91 --- /dev/null +++ b/scripts/extract_from_grammalecte.py @@ -0,0 +1,20 @@ +with open("grammalecte.txt", "r") as f: + lines = f.read().splitlines() + +lines = lines[16:] + +for line in lines: + (id_, fid, flexion, lemme, etiquettes, metagraphe, metaphone, + notes, semantique, etymologie, sous_dictionnaire, google_1_grams, + wikipedia, wikisource, litterature, total, doublons, multiples, + frequence, indice) = line.split("\t") + + etiquettes = etiquettes.split() + if "nom" in etiquettes: + print(flexion) + elif "adj" in etiquettes: + print(flexion) + elif "adv" in etiquettes: + print(flexion) + elif "infi" in etiquettes: + print(flexion) \ No newline at end of file diff --git a/scripts/wordlize.py b/scripts/wordlize.py new file mode 100644 index 0000000..df3e9a2 --- /dev/null +++ b/scripts/wordlize.py @@ -0,0 +1,23 @@ +import sys +import re +from unidecode import unidecode + +file = sys.argv[1] + +if file == "-": + lines = sys.stdin +else: + with open(file, "r") as f: + lines = f.read().splitlines() + +output = set() +for line in lines: + wordlized = unidecode(line).strip().upper() + if not re.match(r"^[A-Z]*$", wordlized): # ignore words with dashes, apostrophes... + continue + output.add(wordlized) + +output = sorted(list(output)) + +for line in output: + print(line) \ No newline at end of file