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)