Initial commit

This commit is contained in:
2022-03-31 19:44:24 +02:00
commit 5998d74a37
16 changed files with 1597 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
__pycache__/
*.py[cod]
settings.py
grammalecte.txt
wordlists

1
.python-version Normal file
View File

@@ -0,0 +1 @@
3.8.0

0
__init__.py Normal file
View File

0
cambot/__init__.py Normal file
View File

394
cambot/codenames.py Normal file
View File

@@ -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)"
" `<indice> <nombre>`, 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)

16
cambot/emojis.py Normal file
View File

@@ -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)

92
cambot/ephemeris.py Normal file
View File

@@ -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())

379
cambot/saints.py Normal file
View File

@@ -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"
}

View File

@@ -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)))

9
cambot/utils.py Normal file
View File

@@ -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

98
cambot/wordle.py Normal file
View File

@@ -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)

295
poetry.lock generated Normal file
View File

@@ -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"},
]

18
pyproject.toml Normal file
View File

@@ -0,0 +1,18 @@
[tool.poetry]
name = "cambot"
version = "0.1.0"
description = ""
authors = ["Simon Junod <sj@simonjunod.ch>"]
[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"

221
run.py Executable file
View File

@@ -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<number>\d+)?d[eé]s?(?P<maximum>\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)

View File

@@ -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)

23
scripts/wordlize.py Normal file
View File

@@ -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)