Files
cambot/codenames.py
2025-08-05 21:33:59 +02:00

394 lines
15 KiB
Python

import random
import re
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)