diff --git a/Contree/anounce.py b/Contree/anounce.py new file mode 100644 index 0000000000000000000000000000000000000000..3b24d25ab24785d2d53a21de75369d04bcd78c60 --- /dev/null +++ b/Contree/anounce.py @@ -0,0 +1,99 @@ +from carte import COLOR_DICT, Color + +TRUMP_DICT = { + "Ta": [Color.Pique, Color.Coeur, Color.Trefle, Color.Carreau], + "Toutat": [Color.Pique, Color.Coeur, Color.Trefle, Color.Carreau], + "Toutatout": [Color.Pique, Color.Coeur, Color.Trefle, Color.Carreau], + "Sa": [], + "Sansat": [], + "Sansatout": [] +} + + +class InvalidAnounceError(Exception): + pass + + +class Anounce(): + def __init__(self, goal, trump): + # Parse the goal + self.capot = (goal == "capot") + self.generale = (goal == "generale") + if self.capot or self.generale: + goal = 182 + if self.generale: + goal += 1 + else: + try: + goal = int(goal) + except ValueError: + raise InvalidAnounceError("Valeur de l'annonce non reconnue") + + self.goal = int(goal) + + self.coinchee = False + + # Parse the trump + trump = trump.capitalize() + try: + self.trumps = TRUMP_DICT[trump] + except KeyError: + try: + self.trumps = [COLOR_DICT[trump]] + except KeyError: + raise InvalidAnounceError("Atout non reconnu") + + def __le__(self, other): + if other is None: + goal = 0 + else: + goal = other.goal + return self.goal <= goal + + def __str__(self): + r = str(self.goal) + " " + if self.capot: + r = "Capot " + elif self.generale: + r = "Générale " + + c = "" + if self.coinchee: + c = " coinchée" + + t = "" + if len(self.trumps) == 0: + t = "Sans Atout" + elif len(self.trumps) == 4: + t = "Tout Atout" + else: + t = str(self.trumps[0]) + + return r + t + c + + def coinche(self): + self.coinchee = True + + def who_wins_game(self, results, pointsA, pointsB, taker): + # Check generale : taker takes all the tricks + if self.generale: + if results[taker.index][1] == 8: + return taker.team + else: + return (taker.team + 1) % 2 + + # Check capot : team taker takes all the tricks + if self.capot: + if (results[taker.index][1] + + results[(taker.index + 2) % 4][1] == 8): + return taker.team + else: + return (taker.team + 1) % 2 + + # Else it is a normal game + team_points = [pointsA, pointsB][taker.team] + + if team_points >= self.goal: + return taker.team + else: + return (taker.team + 1) % 2 diff --git a/Contree/bot.py b/Contree/bot.py new file mode 100644 index 0000000000000000000000000000000000000000..0379fcaa2fa0acc91438f6550fe1760d663928dc --- /dev/null +++ b/Contree/bot.py @@ -0,0 +1,515 @@ +import discord +from discord.ext import commands +import random +import os +import asyncio +from dateutil import parser + +from utils import delete_message +from coinche import BET_PHASE, Coinche, \ + InvalidActionError, InvalidActorError, InvalidMomentError +from anounce import InvalidAnounceError +from carte import InvalidCardError + + +# Load the bot token +path = os.path.dirname(os.path.abspath(__file__)) +TOKEN = "" +with open(path+"/.token", "r") as f: + TOKEN = f.readline() + +bot = commands.Bot(command_prefix="!") + +tables = {} +tables_msg = None +INDEX_CHAN = "tables-actives" +index_to_id = {} +index_to_id["next"] = 1 +avail_timers = {} + + +class InvalidCommandError(Exception): + pass + + +CONTROLED_ERRORS = [InvalidCardError, + InvalidActionError, + InvalidActorError, + InvalidMomentError, + InvalidAnounceError, + InvalidCommandError] + + +async def invalidChannelMessage(channel): + await channel.send("Tu peux pas faire ça hors d'un channel de coinche...", delete_after=5) + + +async def handleGenericError(e, channel): + if type(e) in CONTROLED_ERRORS: + await channel.send(e.args[0], delete_after=5) + else: + raise e + + +@bot.command() +async def start(ctx, p2: discord.Member, p3: discord.Member, p4: discord.Member): + global tables + await delete_message(ctx.message) + players = [ctx.author, p2, p3, p4] + + # Prepare the permission modifications + guild = ctx.guild + base = { + guild.default_role: discord.PermissionOverwrite(read_messages=False) + } + + overwrites_all = { + guild.default_role: discord.PermissionOverwrite(read_messages=False), + guild.me: discord.PermissionOverwrite(read_messages=True), + players[0]: discord.PermissionOverwrite(read_messages=True), + players[1]: discord.PermissionOverwrite(read_messages=True), + players[2]: discord.PermissionOverwrite(read_messages=True), + players[3]: discord.PermissionOverwrite(read_messages=True) + } + + # Create the category where we put the table + index = index_to_id["next"] + category = await ctx.guild.create_category(f"Table {index}", overwrites=base) + + # Create the table + channel = await ctx.guild.create_text_channel( + name="Zone de Jeu", + category=category, + overwrites=overwrites_all + ) + + # Create the vocal channel + vocal_channel = await ctx.guild.create_voice_channel( + name="Vocal", + category=category, + overwrites=overwrites_all + ) + + # Create the hands channels + hand_channels = {} + for p in players: + overwrites_perso = { + guild.default_role: discord.PermissionOverwrite( + read_messages=False), + p: discord.PermissionOverwrite(read_messages=True) + } + + hand_channels[p] = await ctx.guild.create_text_channel( + name=f"Main de {p.name}", + category=category, + overwrites=overwrites_perso + ) + + # Register the table in the index list + index = index_to_id["next"] + index_to_id[index] = channel.id + index_to_id["next"] += 1 + # Create the table + tables[channel.id] = Coinche( + channel, vocal_channel, hand_channels, players, index) + + # Set all players to not dispo anymore + for p in players: + await is_not_dispo(p, ctx.guild) + + await update_tables(ctx.guild) + await tables[channel.id].start() + + +@bot.command() +async def annonce(ctx, goal: int, trump: str): + global tables + await delete_message(ctx.message) + + # Find the table + try: + table = tables[ctx.channel.id] + except KeyError: + await invalidChannelMessage(ctx.channel) + return + + # Send the anounce + try: + async with table.lock: + await table.annonce(ctx, goal, trump) + except Exception as e: + await handleGenericError(e, ctx.channel) + return + + +@bot.command(aliases=["b"]) +async def bet(ctx, goal: str, trump: str): + global tables + await delete_message(ctx.message) + + # Find the table + try: + table = tables[ctx.channel.id] + except KeyError: + await invalidChannelMessage(ctx.channel) + return + + # Send the goal + try: + async with table.lock: + await table.bet(ctx, goal, trump) + except Exception as e: + await handleGenericError(e, ctx.channel) + + +@bot.command() +async def coinche(ctx): + global tables + await delete_message(ctx.message) + + # Find the table + try: + table = tables[ctx.channel.id] + except KeyError: + await invalidChannelMessage(ctx.channel) + return + + # Try to coinche the last bet + try: + async with table.lock: + await table.coinche(ctx) + except Exception as e: + await handleGenericError(e, ctx.channel) + + +@bot.command(name="pass", aliases=["nik"]) +async def pass_annonce(ctx): + global tables + await delete_message(ctx.message) + + # Find the table + try: + table = tables[ctx.channel.id] + except KeyError: + await invalidChannelMessage(ctx.channel) + return + + try: + async with table.lock: + await table.bet(ctx, 0, None) + except Exception as e: + await handleGenericError(e, ctx.channel) + + +@bot.command(name="p") +async def play(ctx, *args): + global tables + await delete_message(ctx.message) + # Find the table + try: + table = tables[ctx.channel.id] + except KeyError: + await invalidChannelMessage(ctx.channel) + return + + try: + # If we are in bet phase, consider !p as a bet + if table.phase == BET_PHASE: + try: + value, color = args + except ValueError: + raise InvalidCommandError("Utilisation en phase d'annonce : " + "`!p <valeur> <atout>`") + async with table.lock: + await table.bet(ctx, value, color) + else: + if len(args) == 0: + value, color = None, None + elif len(args) == 1: + [value] = args + color = None + elif len(args) == 2: + value, color = args + else: + raise InvalidCommandError("Utilisation :\n" + "- `!p` pour jouer une carte au hasard\n" + "- `!p <valeur>` pour jouer sans préciser la couleur\n" + "- `!p <valeur> <couleur>`") + + async with table.lock: + await table.play(ctx, value, color) + except Exception as e: + await handleGenericError(e, ctx.channel) + + +@bot.command(aliases=["akor"]) +async def again(ctx): + global tables + await delete_message(ctx.message) + + # Find the table + try: + table = tables[ctx.channel.id] + except KeyError: + await invalidChannelMessage(ctx.channel) + return + + try: + async with table.lock: + await table.reset() + except Exception as e: + await handleGenericError(e, ctx.channel) + + +@bot.command() +async def replay(ctx): + global tables + await delete_message(ctx.message) + + # Find the table + try: + table = tables[ctx.channel.id] + except KeyError: + await invalidChannelMessage(ctx.channel) + return + + try: + async with table.lock: + await table.reset(replay=True) + except Exception as e: + await handleGenericError(e, ctx.channel) + + +@bot.command() +async def end(ctx): + global tables + # Find the table + try: + table = tables[ctx.channel.id] + except KeyError: + await invalidChannelMessage(ctx.channel) + return + + async with table.lock: + await table.end_table() + + # Clean the table from the index and update it + del tables[ctx.channel.id] + await update_tables(ctx.guild) + + +@bot.command() +async def spectate(ctx, index: int): + global tables + await delete_message(ctx.message) + + # Parse the table ID + try: + id = index_to_id[index] + table = tables[id] + except KeyError: + await ctx.channel.send("Je reconnais pas l'id de la table", delete_after=5) + return + + try: + await table.add_spectator(ctx.author) + except Exception as e: + await handleGenericError(e, ctx.channel) + + await update_tables(ctx.guild) + + +@bot.command() +async def leave(ctx): + global tables + await delete_message(ctx.message) + + # Find the table + try: + table = tables[ctx.channel.id] + except KeyError: + await invalidChannelMessage(ctx.channel) + return + + try: + await table.remove_spectator(ctx.author) + except Exception as e: + await handleGenericError(e, ctx.channel) + + await update_tables(ctx.guild) + + +@bot.command() +async def swap(ctx, target: discord.Member): + global tables + await delete_message(ctx.message) + + # Find the table + try: + table = tables[ctx.channel.id] + except KeyError: + await invalidChannelMessage(ctx.channel) + return + + try: + async with table.lock: + await table.swap(ctx.author, target) + except Exception as e: + await handleGenericError(e, ctx.channel) + + await update_tables(ctx.guild) + + +async def update_tables(guild): + global tables + global tables_msg + global index_to_id + + txt = "__**Tables actives : **__" + tables_down = [] + for index in index_to_id: + id = index_to_id[index] + try: + table = tables[id] + txt += "\n - [{}] : ".format(str(index)) + txt += " | ".join([p.mention for p in table.all_players]) + if table.spectators: + txt += "\n Spectateurices : " + txt += " , ".join([s.mention for s in table.spectators]) + except KeyError: + tables_down.append(index) + + for index in tables_down: + if index != "next": + index_to_id.pop(index) + print("Table {} plus active. Suppression de l'index".format(index)) + + if tables_msg is None: + chan = discord.utils.find( + lambda c: c.name == INDEX_CHAN, guild.channels) + tables_msg = await chan.send(txt) + else: + await tables_msg.edit(content=txt) + + +@bot.command(aliases=["clear"]) +async def clean(ctx): + global tables + global bot + await delete_message(ctx.message) + + # Find the table + try: + table = tables[ctx.channel.id] + except KeyError: + # If table not found, it may be a DM + await invalidChannelMessage(ctx.channel) + return + + async with table.lock: + await table.clean(bot.user) + + +@bot.command(aliases=["nomore"]) +async def surrender(ctx): + global tables + await delete_message(ctx.message) + + # Find the table + try: + table = tables[ctx.channel.id] + except KeyError: + await invalidChannelMessage(ctx.channel) + return + + try: + async with table.lock: + await table.surrender(ctx.author) + except Exception as e: + await handleGenericError(e, ctx.channel) + + +@bot.command() +async def hand(ctx): + global tables + await delete_message(ctx.message) + + # Find the table + try: + table = tables[ctx.channel.id] + except KeyError: + await invalidChannelMessage(ctx.channel) + return + + try: + async with table.lock: + await table.print_initial_hand(ctx.author) + except Exception as e: + await handleGenericError(e, ctx.channel) + + +@bot.command() +async def dispo(ctx, time="1h"): + await delete_message(ctx.message) + + # Parse the time + try: + parsed = parser.parse(time) + h = parsed.hour + m = parsed.minute + s = parsed.second + s += 3600*h + 60*m + except Exception: + await handleGenericError(InvalidActionError("Je n'ai pas compris la durée. Utilise `<heures>h<minutes>`.")) + return + + await is_dispo(ctx.author, ctx.guild, s) + + +@bot.command(name="pudispo") +async def not_dispo(ctx): + await delete_message(ctx.message) + await is_not_dispo(ctx.author, ctx.guild) + + +async def is_dispo(user, guild, time): + global avail_timers + # Adding the role + role = discord.utils.find(lambda r: r.name == "Dispo", guild.roles) + await user.add_roles(role) + + # Delay the role deletion + loop = asyncio.get_event_loop() + timer = loop.call_later(time, lambda: asyncio.ensure_future( + user.remove_roles(role))) + + # Remember the timer, to delete it if needed + avail_timers[user] = timer + + +async def is_not_dispo(user, guild): + if user in avail_timers: + avail_timers[user].cancel() + + role = discord.utils.find(lambda r: r.name == "Dispo", guild.roles) + await user.remove_roles(role) + + +@bot.command() +async def roll(ctx, txt): + try: + n, _, f = txt.partition("d") + n, f = abs(int(n)), abs(int(f)) + if n == 1: + await ctx.send("Résultat du dé : **" + str(random.randint(1, f)) + "**") + else: + dices = [random.randint(1, f) for _ in range(n)] + s = sum(dices) + await ctx.send("Résultat des dés : (**" + "** + **".join( + [str(v) for v in dices]) + "**) = **" + str(s) + "**") + except ValueError: + await delete_message(ctx.message) + await ctx.send("Pour lancer des dés : `!roll <nb de dés>d<nombre de faces>`, par exemple `!roll 3d10`", delete_after=5) + return + +bot.run(TOKEN) diff --git a/Contree/carte.py b/Contree/carte.py new file mode 100644 index 0000000000000000000000000000000000000000..78079cd579f0b8af1338e194006f76f4d8b7a35d --- /dev/null +++ b/Contree/carte.py @@ -0,0 +1,215 @@ +from enum import Enum, IntEnum + + +class Color(Enum): + Pique = 1 + Coeur = 2 + Trefle = 3 + Carreau = 4 + + def __str__(self): + return COLOR_EMOJI[self] + + +COLOR_DICT = { + "Carreau": Color.Carreau, + "Ca": Color.Carreau, + "Carro": Color.Carreau, + "K": Color.Carreau, + "Trefle": Color.Trefle, + "T": Color.Trefle, + "Coeur": Color.Coeur, + "C": Color.Coeur, + "Co": Color.Coeur, + "Pique": Color.Pique, + "P": Color.Pique +} + + +class Value(IntEnum): + As = 8 + Dix = 7 + Roi = 6 + Dame = 5 + Valet = 4 + Neuf = 3 + Huit = 2 + Sept = 1 + + @staticmethod + def from_str(s): + try: + return VALUE_DICT[s.capitalize()] + except KeyError: + raise InvalidCardError( + "J'ai pas compris la valeur de ta carte") + + def __str__(self): + return VALUE_EMOJI[self] + + +VALUE_DICT = { + "As": Value.As, + "A": Value.As, + "Dix": Value.Dix, + "10": Value.Dix, + "Roi": Value.Roi, + "K": Value.Roi, + "R": Value.Roi, + "Dame": Value.Dame, + "D": Value.Dame, + "Q": Value.Dame, + "Valet": Value.Valet, + "V": Value.Valet, + "J": Value.Valet, + "Neuf": Value.Neuf, + "9": Value.Neuf, + "Ç": Value.Neuf, + "Huit": Value.Huit, + "8": Value.Huit, + "_": Value.Huit, + "Sept": Value.Sept, + "7": Value.Sept, + "È": Value.Sept +} + + +CARD_POINTS = { + Value.As: 11, + Value.Dix: 10, + Value.Roi: 4, + Value.Dame: 3, + Value.Valet: 2, + Value.Neuf: 0, + Value.Huit: 0, + Value.Sept: 0 +} + +SA_POINTS = { + Value.As: 19, + Value.Dix: 10, + Value.Roi: 4, + Value.Dame: 3, + Value.Valet: 2, + Value.Neuf: 0, + Value.Huit: 0, + Value.Sept: 0 +} + +TA_POINTS = { + Value.Valet: 14, + Value.Neuf: 9, + Value.As: 6, + Value.Dix: 5, + Value.Roi: 3, + Value.Dame: 1, + Value.Huit: 0, + Value.Sept: 0 +} + +VALUE_EMOJI = { + Value.As: ":regional_indicator_a:", + Value.Dix: ":keycap_ten:", + Value.Roi: ":regional_indicator_r:", + Value.Dame: ":regional_indicator_d:", + Value.Valet: ":regional_indicator_v:", + Value.Neuf: ":nine:", + Value.Huit: ":eight:", + Value.Sept: ":seven:" +} + +COLOR_EMOJI = { + Color.Carreau: ":diamonds:", + Color.Trefle: ":trefle:", + Color.Coeur: ":heart:", + Color.Pique: ":pique:" +} + + +class InvalidCardError(Exception): + pass + + +class Carte(): + def __init__(self, value, color): + # Parse the value + if type(value) == str: + try: + value = VALUE_DICT[value.capitalize()] + except KeyError: + raise InvalidCardError( + "J'ai pas compris la valeur de ta carte") + + # Parse the color + if type(color) == str: + try: + color = COLOR_DICT[color.capitalize()] + except KeyError: + raise InvalidCardError( + "J'ai pas compris la couleur de ta carte") + + self.color = color + self.value = value + + def __str__(self): + return VALUE_EMOJI[self.value] + COLOR_EMOJI[self.color] + + def __eq__(self, other): + return self.value == other.value and self.color == other.color + + def full_deck(): + return [Carte(v, c) for c in Color for v in Value] + + def classical_order(): + return sorted([v for v in Value], reverse=True) + + def trump_order(): + return [Value.Valet, + Value.Neuf, + Value.As, + Value.Dix, + Value.Roi, + Value.Dame, + Value.Huit, + Value.Sept] + + def points(self, trumps): + # If there's a single trump + if len(trumps) == 1: + if self.color == trumps[0]: + if self.value == Value.Valet: + return 20 + elif self.value == Value.Neuf: + return 14 + + return CARD_POINTS[self.value] + + # All trumps + elif len(trumps) == 4: + return TA_POINTS[self.value] + + # No trump + else: + return SA_POINTS[self.value] + + def strength(self, trumps, color): + # Start with the base strength of the card + force = self.value.value + + # Bonus if it is a trump + if self.color in trumps: + force += 20 + # More bonus so that V and 9 are the strongest trumps + if self.value == Value.Valet: + # Set the card strength to 10, As is at 8, normal Valet is at 4 + force += 6 + if self.value == Value.Neuf: + # Set the card strength to 9, As is at 8, normal 9 is at 3 + force += 6 + + # Bonus if you are the asked color + if self.color == color: + force += 10 + + # Results : Trump AND Color > Trump > Color > Nothing + return force diff --git a/Contree/coinche.py b/Contree/coinche.py new file mode 100644 index 0000000000000000000000000000000000000000..7ff55e8ba089b3293393033252fc5183859e3607 --- /dev/null +++ b/Contree/coinche.py @@ -0,0 +1,675 @@ +from random import shuffle, choice +from asyncio import Lock + +from carte import Carte, InvalidCardError, Value +from utils import append_line, remove_last_line, modify_line, check_belotte, \ + who_wins_trick, valid_card, OK, WRONG_COLOR, TRUMP, LOW_TRUMP +from utils import delete_message, shuffle_deck, deal_deck +from anounce import Anounce, InvalidAnounceError +from player import Player + + +class InvalidActorError(Exception): + pass + + +class InvalidMomentError(Exception): + pass + + +class InvalidActionError(Exception): + pass + + +TRICK_DEFAULT_MSG = """__**Pli actuel :**__ + - ... + - ... + - ... + - ...""" + + +# The different phases of the game: +BET_PHASE = 0 +PLAY_PHASE = 1 +AFTER_GAME = 2 + + +class Coinche(): + def __init__(self, channel, vocal_channel, hand_channels, players, index): + self.lock = Lock() + self.index = index + self.channel = channel + self.vocal = vocal_channel + self.hand_channels = hand_channels + + # Create the players + self.players = {} + for (id, user) in enumerate(players): + p = Player(user, id, index, hand_channels[user]) + self.players[user] = p + self.players[id] = p + + self.all_players = [self.players[i] for i in range(4)] + + for i, p in enumerate(self.all_players): + p.next = self.players[(i+1) % 4] + + # Register the spectators + self.spectators = set() + + # Generate the deck + self.deck = Carte.full_deck() + shuffle(self.deck) + + # Variables for announces + self.anounce = None + self.phase = BET_PHASE + self.pass_counter = 0 + self.annonce_msg = None + + # Score (number of games won) + self.global_score_msg = None + self.global_score = [0, 0] + + # Tricks (number of tricks won) + self.trick_msg = None + self.tricks = [0, 0] + + # Team points during a game + self.points = [0, 0] + + # Past trick and active trick + self.last_trick_msg = None + self.active_trick_msg = None + self.active_trick = [] + + # Indexes + p0 = self.players[0] + self.active_player = p0 + self.leader = p0 + self.dealer = p0 + self.taker = p0 + + async def start(self, replay=False): + await self.channel.send("Début de partie ! {} | {} VS {} | {}".format( + self.players[0].mention, + self.players[2].mention, + self.players[1].mention, + self.players[3].mention)) + + # Score message + self.global_score_msg = await self.channel.send("__**Score global :**__") + await self.update_global_score() + + txt = "Pour annoncer : `!bet <valeur> <atout>` ou `!pass`\nLes valeurs `generale` ou `capot` sont valides" + await self.channel.send(txt) + + # Anounce message + self.annonce_msg = await self.channel.send( + "__**Phase d'annonce :**__\n - " + + self.dealer.mention + " : ?") + + # Reset players + for p in self.all_players: + p.cards_won = [] + + self.active_player = self.dealer + await self.deal(replay=replay) + self.phase = BET_PHASE + + async def bet(self, ctx, goal: int, trump): + # Check if author is a player + if ctx.author not in self.players: + raise InvalidActorError( + "Les spectateurs ne sont pas autorisés à annoncer.") + + # Check if we are in Bet Phase + if self.phase != BET_PHASE: + raise InvalidMomentError( + "Tu ne peux pas faire ça hors de la phase d'annonce " + + ctx.author.mention) + + # Check if it is the author's turn + if ctx.author != self.active_player.user: + raise InvalidActorError( + "C'est pas à toi d'annoncer " + ctx.author.mention) + + # If goal is 0, the player passed + if goal == 0: + await remove_last_line(self.annonce_msg) + await append_line(self.annonce_msg, " - " + ctx.author.mention + " : Passe") + self.pass_counter += 1 + # Check if all players passed without anyone anouncing + if self.pass_counter == 4 and self.anounce is None: + await self.channel.send("Personne ne prend ?") + # We declare the game finished in order to reset it + await self.channel.send("Pour relancer une partie, entrez `!again`") + self.phase = AFTER_GAME + return + + # Check if all players passed with someone anouncing + if self.pass_counter == 3 and self.anounce is not None: + await append_line(self.annonce_msg, "Fin des annonces") + # Start the play phase + await self.setup_play() + return + + # Move to next player + self.active_player = self.active_player.next + await append_line(self.annonce_msg, " - " + self.active_player.mention + " : ?") + return + + # Then it is a normal bet. Try to cast it in an announce + anounce = Anounce(goal, trump) + + # Check if goal in reasonable bounds + if anounce.goal > 185: + raise InvalidActionError( + f"{anounce.goal} c'est beaucoup pour une annonce...") + + # If the player did not bet enough + if anounce <= self.anounce: + raise InvalidAnounceError( + "Il faut annoncer plus que l'annonce précédente") + + self.pass_counter = 0 + self.anounce = anounce + self.taker = self.active_player + + # Print the anounce + await remove_last_line(self.annonce_msg) + await append_line(self.annonce_msg, " - " + ctx.author.mention + " : " + str(self.anounce)) + + # Move to next player + self.active_player = self.active_player.next + await append_line(self.annonce_msg, " - " + self.active_player.mention + " : ?") + + async def coinche(self, ctx): + # Check if author is a player + if ctx.author not in self.players: + raise InvalidActorError( + "Les spectateurs ne sont pas autorisés à coincher") + + # Check if we are in Bet Phase + if self.phase != BET_PHASE: + raise InvalidMomentError("La phase d'annonces est terminée") + + # Check if there's something to coinche + if self.anounce is None: + raise InvalidMomentError( + "Il n'y a pas d'annonce à coincher pour le moment") + + # Check if the player is in opposite team from the taker (i.e he can coinche) + if self.taker.team == self.players[ctx.author].team: + raise InvalidActorError( + "Ton équipe a proposé le dernier contrat. Tu ne peux pas coincher") + + # Coinche the last anounce + self.anounce.coinche() + + # Update message + await remove_last_line(self.annonce_msg) + await append_line(self.annonce_msg, " - " + ctx.author.mention + " : Coinchée") + + await append_line(self.annonce_msg, "Fin des annonces") + + # Start the play phase + await self.setup_play() + + async def annonce(self, ctx, goal: int, trump, capot=False, generale=False): + if self.phase != BET_PHASE: + raise InvalidMomentError("Les annonces sont déjà faites") + + if ctx.author not in self.players: + raise InvalidActorError("Seul un joueur peut annoncer") + + self.anounce = Anounce(goal, trump) + self.taker = self.players[ctx.author] + + await self.setup_play() + + async def update_tricks(self): + cardsA = self.players[0].cards_won + \ + self.players[2].cards_won + cardsB = self.players[1].cards_won + \ + self.players[3].cards_won + + tricksA = len(cardsA) // 4 + tricksB = len(cardsB) // 4 + + await self.trick_msg.edit( + content=("__**Plis :**__\n" + "- {} | {} : {}\n" + "- {} | {} : {}").format( + self.players[0].mention, + self.players[2].mention, + tricksA, + self.players[1].mention, + self.players[3].mention, + tricksB)) + + async def update_global_score(self): + await self.global_score_msg.edit( + content=("__**Score Global: **__\n" + "- {} | {}: {} parties\n" + "- {} | {}: {} parties").format( + self.players[0].mention, + self.players[2].mention, + self.global_score[0], + self.players[1].mention, + self.players[3].mention, + self.global_score[1])) + + async def setup_play(self): + for p in self.all_players: + p.cards_won = [] + # Sort the hands with the new trumps + p.sort_hand(trumps=self.anounce.trumps) + await p.update_hand() + + self.leader = self.dealer + # If there is a generale, change the leader and active player + if self.anounce.generale: + self.leader = self.taker + + # The first active player is the leader + self.active_player = self.leader + + # Anounce message + await self.channel.send("__**Annonces :**__ " + + self.taker.mention + + " -> " + str(self.anounce)) + + # How to play message + await self.channel.send("Pour jouer : `!p <Valeur> <Couleur>`") + + # Number of tricks taken message + self.trick_msg = await self.channel.send("__**Plis :**__") + await self.update_tricks() + + # Last trick message + self.last_trick_msg = await self.channel.send("__**Dernier pli :**__") + + # Active trick message + self.active_trick_msg = await self.channel.send(TRICK_DEFAULT_MSG) + await modify_line(self.active_trick_msg, 1, + f" - {self.active_player.mention} : ?") + + # Check for belotte + hands = [p.hand for p in self.all_players if p.team == self.taker.team] + + if check_belotte(hands, self.anounce.trumps): + self.points[self.taker.team] += 20 + + self.phase = PLAY_PHASE + + async def deal(self, replay=False): + if len(self.deck) != 32: + raise InvalidCardError( + "Pourquoi mon deck a pas 32 cartes ? Ya un souci !") + + if not replay: + # Shuffle the deck + self.deck = shuffle_deck(self.deck) + + # Deal the cards + hands = deal_deck(self.deck) + + # Send the hands to the players + for (player, hand) in zip(self.all_players, hands): + await player.receive_hand(hand) + + self.active_player = self.dealer + self.active_trick = [] + self.pass_counter = 0 + + async def play(self, ctx, value, trump): + # Check if we are in play phase + if self.phase != PLAY_PHASE: + raise InvalidMomentError( + "Impossible de jouer hors de la phase de jeu.") + + if ctx.author not in self.players: + raise InvalidActorError("Un spectateur ne peut pas jouer de carte") + + # Find the player + player = self.players[ctx.author] + + # Check if it is player's turn + if player != self.active_player: + raise InvalidMomentError("Ce n'est pas ton tour de jouer") + + if trump is None: + # The command is `!p` or `!p <value>`. + # Get all the possible cards: + trick_cards = [c for (c, _) in self.active_trick] + possible = [c for c in player.hand + if valid_card(c, trick_cards, self.anounce.trumps, + player.hand) == OK] + + if value is not None: + # The command is `!p <value>`. + # We keep only the cards with the desired value. + value = Value.from_str(value) + + possible = [c for c in possible if c.value == value] + if possible == []: + raise InvalidCardError("Tu n'as pas cette carte en main") + + # If several cards are playable, choose one of them randomly + carte = choice(possible) + else: + # The command is `!p <value> <color>`. + # Parse the cards + carte = Carte(value, trump) + + # Check if player has this card in hand + if carte not in player.hand: + raise InvalidCardError("Tu n'as pas cette carte en main") + + # Check if player is allowed to play this card + trick_cards = [c for (c, _) in self.active_trick] + res = valid_card(carte, trick_cards, self.anounce.trumps, + player.hand) + if res == WRONG_COLOR: + raise InvalidCardError("Tu dois jouer à la couleur demandée.") + elif res == TRUMP: + raise InvalidCardError("Tu dois couper à l'atout.") + elif res == LOW_TRUMP: + raise InvalidCardError("Tu dois monter à l'atout.") + + # Remove it from the player's hand + await player.play_card(carte) + + # Add it to the stack + self.active_trick.append((carte, player)) + + # Move to next player + self.active_player = self.active_player.next + + # Update the message with the curent trick + cards_played = len(self.active_trick) + await modify_line(self.active_trick_msg, cards_played, + f" - {player.mention} : {carte}") + + # If we have 4 cards in the stack, trigger the gathering + if cards_played == 4: + await self.gather() + else: + # Update the message and notify the next player + await modify_line(self.active_trick_msg, cards_played + 1, + f" - {self.active_player.mention} : ?") + + async def gather(self): + # Find the winner + winner = who_wins_trick(self.active_trick, self.anounce.trumps) + + # Move actual trick to last trick message + text = self.active_trick_msg.content.split("\n") + text[0] = "__**Dernier pli :**__" + text.append("Pli remporté par " + winner.mention) + text = "\n".join(text) + await self.last_trick_msg.edit(content=text) + + # Put the cards in the winner's card stack + winner.cards_won += [c for (c, _) in self.active_trick] + # Empty the trick stack + self.active_trick = [] + + # Move to new leader + self.leader = winner + + # Check if players have no more cards + if len(self.players[0].hand) == 0: + # Count the 10 bonus points of the last trick + self.points[winner.team] += 10 + # Trigger end game + # Update number of points of each team + await self.update_tricks() + await self.end_game() + else: + # Reset actual trick + await self.active_trick_msg.edit(content=TRICK_DEFAULT_MSG) + await modify_line(self.active_trick_msg, 1, + f" - {self.leader.mention} : ?") + + # Update number of points of each team + await self.update_tricks() + self.active_player = self.leader + + async def end_game(self): + + points_tricks = [p.count_points(self.anounce.trumps) + for p in self.all_players] + + # Print the team points + self.points[0] += points_tricks[0][0] + points_tricks[2][0] + self.points[1] += points_tricks[1][0] + points_tricks[3][0] + + tricks = [0, 0] + tricks[0] = points_tricks[0][1] + points_tricks[2][1] + tricks[1] = points_tricks[1][1] + points_tricks[3][1] + txt = "__**Points d'équipe (avec Belote pour l'attaque) :**__\n" + + txt += " - Équipe {} | {} : {} points | {} plis\n".format( + self.players[0].mention, + self.players[2].mention, + self.points[0], + tricks[0]) + + txt += " - Équipe {} | {} : {} points | {} plis\n".format( + self.players[1].mention, + self.players[3].mention, + self.points[1], + tricks[1]) + + await self.channel.send(txt) + + # Find the winning team + winner_team = self.anounce.who_wins_game(points_tricks, self.points[0], + self.points[1], self.taker) + + # Increment points + self.global_score[winner_team] += 1 + + # Send results + await self.channel.send("Victoire de l'équipe {} | {} !".format( + self.players[0+winner_team].mention, + self.players[2+winner_team].mention)) + + await self.update_global_score() + + # Delete the hand messages + for p in self.all_players: + await p.clean_hand() + + await self.channel.send("Pour relancer une partie, entrez `!again`") + self.phase = AFTER_GAME + + async def reset(self, replay=False): + if self.phase != AFTER_GAME: + raise InvalidActionError( + "Cette action n'est possible qu'en fin de partie.") + + if not replay: + # Gather the cards to a new deck + # 1. the cards won + self.deck = sum([p.cards_won for p in self.all_players], []) + # 2. the cards in hand + for p in self.all_players: + self.deck += p.hand + # 3. the cards in trick + self.deck += [c for (c, _) in self.active_trick] + + # Delete all common messages + async for m in self.channel.history(): + await delete_message(m) + + # Delete all hands messages + for p in self.all_players: + await p.clean_hand() + + # Reset all the variables but not the global score + self.anounce = None + self.taker = None + + self.trick_msg = None + self.tricks = [0, 0] + + self.last_trick_msg = None + + self.active_trick_msg = None + self.active_trick = [] + + if not replay: + # Next dealer + self.dealer = self.dealer.next + + self.active_player = self.dealer + self.leader = self.dealer + + self.points = [0, 0] + + await self.start(replay=replay) + + async def swap(self, giver, receiver): + if giver not in self.players: + raise InvalidActorError( + "C'est au joueur de swap. Pas au spectateur") + + if receiver in self.players: + raise InvalidActionError( + "On échange avec un spectateur. Pas un joueur") + + if receiver not in self.spectators: + # Prevent from swapping with the bot or an admin who is not an + # active specator. + raise InvalidActionError(f"{receiver} n'est pas spectateurice") + + # Change the entry in self.players + player = self.players[giver] + await player.change_owner(receiver) + self.players.pop(giver) + self.players[receiver] = player + + # Set the permissions + self.remove_spectator(receiver) + self.set_player_permissions(receiver) + + self.add_spectator(giver) + + # Give the permission to read the hand chan + await player.hand_channel.set_permissions(receiver, read_messages=True) + await player.hand_channel.set_permissions(giver, read_messages=False) + + # Send notification + await self.channel.send("{} a laissé sa place à {} !".format( + giver.mention, receiver.mention), delete_after=5) + + async def surrender(self, player): + if self.phase != PLAY_PHASE: + raise InvalidMomentError( + "Impossible d'abandonner hors de la phase de jeu.") + + if player not in self.players: + raise InvalidActorError("Seul un joueur peut abandonner") + + await self.channel.send("{} abandonne.".format(player.mention)) + + # The player that surrenders is now the one on defence + self.taker = self.players[player].next + + # Give the active trick to the new attacker + self.taker.cards_won += [c for (c, _) in self.active_trick] + self.active_trick = [] + + # Give the remaining hands to the new attacker + for p in self.all_players: + self.taker.cards_won += p.hand + await p.clean_hand() + + # Set the goal to zero so that the attack wins + self.anounce.goal = 0 + self.anounce.capot = False + self.anounce.generale = False + + # Trigger end game + await self.end_game() + + async def end_table(self): + await self.channel.send("Cloture de la table. Merci d'avoir joué !", delete_after=5) + + # Clean the channels + for c in self.channel.category.channels: + await delete_message(c) + + # Delete the category + await delete_message(self.channel.category) + + async def set_spectator_permission(self, target): + # Give access to the text and vocal channel + await self.channel.set_permissions(target, read_messages=True) + await self.vocal.set_permissions(target, view_channel=True) + # Give access to the hands + for p in self.all_players: + await p.hand_channel.set_permissions(target, read_messages=True) + + async def set_player_permissions(self, target): + # Give access to the text and vocal channel + await self.channel.set_permissions(target, read_messages=True) + await self.vocal.set_permissions(target, view_channel=True) + # Give access to the player's hand + await self.players[target].hand_channel.set_permissions(target, read_messages=True) + + async def reset_permissions(self, target): + # Remove access to the text and vocal channel + await self.channel.set_permissions(target, read_messages=False) + await self.vocal.set_permissions(target, view_channel=False) + # Remove access to the hands + for p in self.all_players: + await p.hand_channel.set_permissions(target, read_messages=False) + + async def add_spectator(self, target): + if target in self.players: + raise InvalidActionError( + f"{target.mention} Tu joues déjà à cette table.") + if target in self.spectators: + raise InvalidActionError( + f"{target.mention} Tu es déjà spectateurice.") + + self.spectators.add(target) + + await self.set_spectator_permission(target) + + # Notify users + await self.channel.send("{} a rejoint en tant que spectateurice !".format(target.mention)) + + async def remove_spectator(self, target): + if target not in self.spectators: + raise InvalidActionError( + f"{target.mention} Tu n'es pas spectateurice. Tu ne peux " + "pas quitter la table.") + self.spectators.remove(target) + + # Set permissions + await self.reset_permissions(target) + + # Notify + await self.channel.send("{} n'est plus spectateurice !".format(target.mention)) + + async def clean(self, bot): + # Delete all messages not from CoinchoBot + async for m in self.channel.history(): + if m.author != bot: + await delete_message(m) + + async def print_initial_hand(self, user): + if user not in self.players: + raise InvalidActorError( + f"{user.mention} Tu es spectateurice. Tu n'as pas de main à montrer") + + if self.phase == BET_PHASE: + raise InvalidMomentError( + f"{user.mention} Impossible de montrer sa main pendant la phase d'annonce") + + await self.players[user].print_initial_hand(self.channel) diff --git a/Contree/player.py b/Contree/player.py new file mode 100644 index 0000000000000000000000000000000000000000..978a0a8345ab45fc703ce855f24d36cf82a256de --- /dev/null +++ b/Contree/player.py @@ -0,0 +1,77 @@ +from carte import Color +from utils import delete_message + + +class Player(): + def __init__(self, user, index, table, hand_channel): + self.user = user + self.initial_hand = [] + self.hand = [] + self.hand_channel = hand_channel + self.hand_msg = None + self.cards_won = [] + self.team = index % 2 + self.index = index + self.mention = user.mention + self.next = None + self.table = table + + async def update_hand(self): + if self.hand == []: + return + + txt = "[table {}] Ta main :".format(self.table) + for color in Color: + txt += "\n {} : ".format(color) + txt += "".join([str(card.value) for card in + self.hand if card.color == color]) + + if self.hand_msg is not None: + await self.hand_msg.edit(content=txt) + else: + self.hand_msg = await self.hand_channel.send(txt) + + def sort_hand(self, trumps=[]): + self.hand.sort( + key=lambda c: c.strength(trumps, None), + reverse=True) + self.initial_hand.sort( + key=lambda c: c.strength(trumps, None), + reverse=True) + + async def receive_hand(self, hand): + self.hand = hand + self.initial_hand = hand.copy() + self.sort_hand() + await self.update_hand() + + async def play_card(self, card): + self.hand.remove(card) + await self.update_hand() + + def count_points(self, trumps): + points = sum([c.points(trumps) for c in self.cards_won]) + tricks = len(self.cards_won) // 4 + return(points, tricks) + + async def clean_hand(self): + self.hand = [] + if self.hand_msg is not None: + await delete_message(self.hand_msg) + self.hand_msg = None + + async def change_owner(self, user): + self.user = user + self.mention = user.mention + if self.hand_msg is not None: + await delete_message(self.hand_msg) + self.hand_msg = None + await self.update_hand() + + async def print_initial_hand(self, channel): + txt = "La main de {} :".format(self.mention) + for color in Color: + txt += "\n {} : ".format(color) + txt += "".join([str(card.value) for card in + self.initial_hand if card.color == color]) + await channel.send(txt) diff --git a/Contree/utils.py b/Contree/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..03dec1b8c5898e851a21cb26cfe332b39b2bbd65 --- /dev/null +++ b/Contree/utils.py @@ -0,0 +1,125 @@ +from carte import Carte, Value +import discord +from random import randint + + +async def append_line(msg, line): + content = msg.content + content += "\n" + line + await msg.edit(content=content) + + +async def remove_last_line(msg): + content = msg.content.split("\n")[:-1] + content = "\n".join(content) + await msg.edit(content=content) + + +async def modify_line(msg, index, newline): + content = msg.content.split("\n") + content[index] = newline + content = "\n".join(content) + await msg.edit(content=content) + + +def check_belotte(hands, trumps): + if len(trumps) != 1: + return False + trump = trumps[0] + for hand in hands: + if (Carte(Value.Dame, trump) in hand + and Carte(Value.Roi, trump) in hand): + return True + return False + + +def who_wins_trick(stack, trumps): + # Get the stack color + color = stack[0][0].color + + # Sort the cards by strength + stack = sorted(stack, + key=lambda entry: entry[0].strength(trumps, color)) + + # Find the winner + return stack[-1][1] + + +async def delete_message(m): + try: + await m.delete() + except discord.errors.NotFound: + print("Message not found. Passing.") + + +def shuffle_deck(deck): + # Step 1 : cut the deck at a random point + pos = randint(1, len(deck) - 1) + deck = deck[pos:] + deck[:pos] + return deck + + +def deal_deck(deck): + hands = [[], [], [], []] + # First deal 3 cards to each + for h in hands: + h += deck[:3] + deck = deck[3:] + # Do it a second time + for h in hands: + h += deck[:3] + deck = deck[3:] + # Then deal 2 cards each + for h in hands: + h += deck[:2] + deck = deck[2:] + + return hands + + +# The different possibilities if someone tries to play a card: +OK = 0 +WRONG_COLOR = 1 +TRUMP = 2 +LOW_TRUMP = 3 + + +def valid_card(carte, trick, trumps, player_hand): + """Check if the player has the right to play `carte`.""" + # No card has been played, so the player can play anything. + if trick == []: + return OK + + color = trick[0].color + has_trumps = any([c.color in trumps for c in player_hand]) + has_color = any([c.color == color for c in player_hand]) + # The highest card played in the trick: + highest_trick = max([c.strength(trumps, color) for c in trick]) + # The highest card in the player's hand: + highest_player = max([c.strength(trumps, color) for c in player_hand]) + + if color in trumps: + if not has_color: + return OK + if carte.color != color: + return WRONG_COLOR + if carte.strength(trumps, color) < highest_trick < highest_player: + return LOW_TRUMP + else: + if has_color: + if carte.color != color: + return WRONG_COLOR + else: + if len(trick) >= 2 and \ + trick[-2].strength(trumps, color) == highest_trick: + # The partner has played the highest card in the trick, so the + # player can play anything. + return OK + if not has_trumps: + return OK + if carte.color not in trumps: + return TRUMP + if carte.strength(trumps, color) < highest_trick < highest_player: + return LOW_TRUMP + # If all tests passed, it's ok + return OK diff --git a/bot.py b/bot.py index df73091fac1515ab79889499944e69944d9b05c8..55b1e9f11176690a3a480a11b9474273fe4d0d36 100644 --- a/bot.py +++ b/bot.py @@ -243,12 +243,14 @@ async def on_ready(): @bot.command(name='roll', help='Roll dices. 2args : number of dice and number of sides') async def roll(ctx, number_of_dice: int, number_of_sides: int): mention=ctx.message.author.mention - await ctx.message.delete() + try: + await ctx.message.delete() + except Exception as e: + pass dice = [ str(random.choice(range(1, number_of_sides + 1))) for _ in range(number_of_dice) ] - await ctx.send('{} {} {} '.format(number_of_dice,number_of_sides,dice[0])) if (number_of_dice==1 and (dice[0]==1 or dice[0]==number_of_sides)): await ctx.send("{1}\n*Critique !*\nRolling d{2} : {0}".format(dice[0]),mention,number_of_sides) else: @@ -270,7 +272,10 @@ async def getmychars(ctx): async def getchar(ctx, name = None): mention=ctx.message.author.mention author = '{}'.format(ctx.message.author) - await ctx.message.delete() + try: + await ctx.message.delete() + except Exception as e: + pass try: if name is None: char= getMainCharactersByOwner(author) @@ -438,7 +443,10 @@ async def delsys(ctx, name): async def r(ctx, what, characterName = None): mention=ctx.message.author.mention author = '{}'.format(ctx.message.author) - await ctx.message.delete() + try: + await ctx.message.delete() + except Exception as e: + pass try: if characterName is None: char= getMainCharactersByOwner(author)