Esempio n. 1
0
    async def won(
        self, ctx: commands.Context,
    ):
        with session_scope() as session:
            # Get the latest game
            game, participant = get_last_game(
                player_id=ctx.author.id, server_id=ctx.guild.id, session=session
            )

            if not game:
                await ctx.send("You have not played a game on this server yet")
                return

            elif game and game.winner:
                await ctx.send(
                    "Your last game seem to have already been scored\n"
                    "If there was an issue, please contact an admin"
                )
                return

            elif game.id in self.games_getting_scored_ids:
                await ctx.send("There is already a scoring or cancellation message active for this game")
                return

            else:
                self.games_getting_scored_ids.add(game.id)

            win_validation_message = await ctx.send(
                f"{game.players_ping}"
                f"{ctx.author.display_name} wants to score game {game.id} as a win for {participant.side}\n"
                f"Result will be validated once 6 players from the game press ✅"
            )

            validated, players_who_refused = await checkmark_validation(
                bot=self.bot,
                message=win_validation_message,
                validating_players_ids=game.player_ids_list,
                validation_threshold=6,
            )

            # Whatever happens, we’re not scoring it anymore if we get here
            self.games_getting_scored_ids.remove(game.id)

            if not validated:
                await ctx.send("Score input was either cancelled or timed out")
                return

            # If we get there, the score was validated and we can simply update the game and the ratings
            queue_channel_handler.mark_queue_related_message(
                await ctx.send(
                    f"Game {game.id} has been scored as a win for {participant.side} and ratings have been updated"
                )
            )
            categoria = discord.utils.get(ctx.guild.categories, name="âš” Game-%s" % str(game.id))
            for channel in categoria.channels:
                await channel.delete()
            await categoria.delete()

        matchmaking_logic.score_game_from_winning_player(player_id=ctx.author.id, server_id=ctx.guild.id)
        await ranking_channel_handler.update_ranking_channels(self.bot, ctx.guild.id)
Esempio n. 2
0
    async def cancel(
        self,
        ctx: commands.Context,
    ):
        """
        Cancels your ongoing game

        Will require validation from at least 6 players in the game

        Example:
            !cancel
        """
        with session_scope() as session:
            # Get the latest game
            game, participant = get_last_game(player_id=ctx.author.id,
                                              server_id=ctx.guild.id,
                                              session=session)

            if game and game.winner:
                await ctx.send(
                    "It does not look like you are part of an ongoing game")
                return

            elif game.id in self.games_getting_scored_ids:
                await ctx.send(
                    "There is already a scoring or cancellation message active for this game"
                )
                return

            else:
                self.games_getting_scored_ids.add(game.id)

            cancel_validation_message = await ctx.send(
                f"{game.players_ping}"
                f"{ctx.author.display_name} wants to cancel game {game.id}\n"
                f"Game will be canceled once 6 players from the game press ✅")

            validated, players_who_refused = await checkmark_validation(
                bot=self.bot,
                message=cancel_validation_message,
                validating_players_ids=game.player_ids_list,
                validation_threshold=6,
            )

            self.games_getting_scored_ids.remove(game.id)

            if not validated:
                await ctx.send(f"Game {game.id} was not cancelled")

            else:

                for participant in game.participants.values():
                    self.players_whose_last_game_got_cancelled[
                        participant.player_id] = datetime.now()

                session.delete(game)

                queue_channel_handler.mark_queue_related_message(
                    await ctx.send(f"Game {game.id} was cancelled"))
Esempio n. 3
0
    async def won(
        self,
        ctx: commands.Context,
    ):
        """
        Scores your last game as a win

        Will require validation from at least 6 players in the game

        Example:
            !won
        """
        # TODO MED PRIO ONLY ONE ONGOING CANCEL/SCORING MESSAGE PER GAME
        with session_scope() as session:
            # Get the latest game
            game, participant = get_last_game(player_id=ctx.author.id,
                                              server_id=ctx.guild.id,
                                              session=session)

            if game and game.winner:
                await ctx.send(
                    "Your last game seem to have already been scored\n"
                    "If there was an issue, please contact an admin")
                return

            win_validation_message = await ctx.send(
                f"{ctx.author.display_name} wants to score game {game.id} as a win for {participant.side}\n"
                f"{', '.join([f'<@{discord_id}>' for discord_id in game.player_ids_list])} can validate the result\n"
                f"Result will be validated once 6 players from the game press ✅"
            )

            validated, players_who_refused = await checkmark_validation(
                bot=self.bot,
                message=win_validation_message,
                validating_players_ids=game.player_ids_list,
                validation_threshold=6,
                timeout=60,
            )

            if not validated:
                await ctx.send("Score input was either cancelled or timed out")
                return

            # If we get there, the score was validated and we can simply update the game and the ratings
            queue_channel_handler.mark_queue_related_message(await ctx.send(
                f"Game {game.id} has been scored as a win for {participant.side} and ratings have been updated"
            ))

        matchmaking_logic.score_game_from_winning_player(
            player_id=ctx.author.id, server_id=ctx.guild.id)
Esempio n. 4
0
    async def cancel(
        self,
        ctx: commands.Context,
    ):
        """
        Cancels your ongoing game

        Will require validation from at least 6 players in the game

        Example:
            !cancel
        """
        # TODO MED PRIO ONLY ONE ONGOING CANCEL/SCORING MESSAGE PER GAME

        with session_scope() as session:
            # Get the latest game
            game, participant = get_last_game(player_id=ctx.author.id,
                                              server_id=ctx.guild.id,
                                              session=session)

            if game and game.winner:
                await ctx.send(
                    "It does not look like you are part of an ongoing game")
                return

            cancel_validation_message = await ctx.send(
                f"{ctx.author.display_name} wants to cancel game {game.id}\n"
                f"{', '.join([f'<@{discord_id}>' for discord_id in game.player_ids_list])} can cancel the game\n"
                f"Game will be canceled once 6 players from the game press ✅"
            )

            validated, players_who_refused = await checkmark_validation(
                bot=self.bot,
                message=cancel_validation_message,
                validating_players_ids=game.player_ids_list,
                validation_threshold=6,
                timeout=60,
            )

            if not validated:
                await ctx.send(f"Game {game.id} was not cancelled")
            else:
                session.delete(game)
                queue_channel_handler.mark_queue_related_message(
                    await ctx.send(f"Game {game.id} was cancelled"))
Esempio n. 5
0
    async def run_matchmaking_logic(
        self,
        ctx: commands.Context,
    ):
        """
        Runs the matchmaking logic in the channel defined by the context

        Should only be called inside guilds
        """
        queue = game_queue.GameQueue(ctx.channel.id)

        game = matchmaking_logic.find_best_game(queue)

        if not game:
            return

        elif game and game.matchmaking_score < 0.2:
            embed = game.get_embed(embed_type="GAME_FOUND",
                                   validated_players=[],
                                   bot=self.bot)

            # We notify the players and send the message
            ready_check_message = await ctx.send(content=game.players_ping,
                                                 embed=embed,
                                                 delete_after=60 * 15)

            # We mark the ready check as ongoing (which will be used to the queue)
            game_queue.start_ready_check(
                player_ids=game.player_ids_list,
                channel_id=ctx.channel.id,
                ready_check_message_id=ready_check_message.id,
            )

            # We update the queue in all channels
            await queue_channel_handler.update_queue_channels(
                bot=self.bot, server_id=ctx.guild.id)

            # And then we wait for the validation
            try:
                ready, players_to_drop = await checkmark_validation(
                    bot=self.bot,
                    message=ready_check_message,
                    validating_players_ids=game.player_ids_list,
                    validation_threshold=10,
                    game=game,
                )

            # We catch every error here to make sure it does not become blocking
            except Exception as e:
                self.bot.logger.error(e)
                game_queue.cancel_ready_check(
                    ready_check_id=ready_check_message.id,
                    ids_to_drop=game.player_ids_list,
                    server_id=ctx.guild.id,
                )
                await ctx.send(
                    "There was a bug with the ready-check message, all players have been dropped from queue\n"
                    "Please queue again to restart the process")

                await queue_channel_handler.update_queue_channels(
                    bot=self.bot, server_id=ctx.guild.id)

                return

            if ready is True:
                # We drop all 10 players from the queue
                game_queue.validate_ready_check(ready_check_message.id)

                # We commit the game to the database (without a winner)
                with session_scope() as session:
                    session.expire_on_commit = False
                    game = session.merge(game)  # This gets us the game ID

                queue_channel_handler.mark_queue_related_message(
                    await ctx.send(embed=game.get_embed("GAME_ACCEPTED"), ))

            elif ready is False:
                # We remove the player who cancelled
                game_queue.cancel_ready_check(
                    ready_check_id=ready_check_message.id,
                    ids_to_drop=players_to_drop,
                    channel_id=ctx.channel.id,
                )

                await ctx.send(
                    f"A player cancelled the game and was removed from the queue\n"
                    f"All other players have been put back in the queue", )

                # We restart the matchmaking logic
                await self.run_matchmaking_logic(ctx)

            elif ready is None:
                # We remove the timed out players from *all* channels (hence giving server id)
                game_queue.cancel_ready_check(
                    ready_check_id=ready_check_message.id,
                    ids_to_drop=players_to_drop,
                    server_id=ctx.guild.id,
                )

                await ctx.send(
                    "The check timed out and players who did not answer have been dropped from all queues",
                )

                # We restart the matchmaking logic
                await self.run_matchmaking_logic(ctx)

        elif game and game.matchmaking_score >= 0.2:
            # One side has over 70% predicted winrate, we do not start anything
            await ctx.send(
                f"The best match found had a side with a {(.5 + game.matchmaking_score)*100:.1f}%"
                f" predicted winrate and was not started")
Esempio n. 6
0
async def checkmark_validation(
    bot: Bot,
    message: discord.Message,
    validating_players_ids: List[int],
    validation_threshold: int = 10,
    timeout=180,
    game=None,
) -> Tuple[Optional[bool], Optional[Set[int]]]:
    """
    Implements a checkmark validation on the chosen message.

    If given a game object, will update the message’s embed with validation marks

    3 possible outcomes:
        True and None
            It was validated by the necessary number of players
        False with the the player who cancelled (in a list)
            It was cancelled and the player should be dropped
        None with a list of players who did not validate
            It timed out and the players who didn't validate should be dropped
    """
    checkmark_logger.info(
        f"Starting validation message {message.id} with threshold {validation_threshold}"
        f" for players {' '.join(str(i) for i in validating_players_ids)}")

    queue_channel_handler.mark_queue_related_message(message)

    await message.add_reaction("✅")
    await message.add_reaction("❌")

    def check(received_reaction: discord.Reaction, sending_user: discord.User):
        # This check is simply used to see if a player in the game responded to the message.
        return (received_reaction.message.id == message.id
                and sending_user.id in validating_players_ids
                and str(received_reaction.emoji) in ["✅", "❌"])

    ids_of_players_who_validated = set()

    # Default values that will be output in case of success
    result = True
    ids_to_drop = None
    try:
        while len(ids_of_players_who_validated) < validation_threshold:
            reaction, user = await bot.wait_for("reaction_add",
                                                timeout=timeout,
                                                check=check)

            # A player accepted, we keep him in memory
            if str(reaction.emoji) == "✅":
                ids_of_players_who_validated.add(user.id)

                checkmark_logger.info(f"Player {user.id} validated")

                if game:
                    await message.edit(embed=game.get_embed(
                        embed_type="GAME_FOUND",
                        validated_players=ids_of_players_who_validated,
                        bot=bot))

            # A player cancels, we return it and will drop him
            elif str(reaction.emoji) == "❌":
                checkmark_logger.info(
                    f"Player {user.id} cancelled, exiting validation")

                result, ids_to_drop = False, {user.id}
                break

    # We get there if no player accepted in the last x minutes
    except asyncio.TimeoutError:
        checkmark_logger.info(
            f"The validation timed out, {' '.join(str(i) for i in ids_of_players_who_validated)} validated"
        )

        result, ids_to_drop = (
            None,
            set(i for i in validating_players_ids
                if i not in ids_of_players_who_validated),
        )

    checkmark_logger.info(f"Unmarking message {message.id} as queue related")
    queue_channel_handler.unmark_queue_related_message(message)

    return result, ids_to_drop
Esempio n. 7
0
async def checkmark_validation(
    bot: Bot,
    message: discord.Message,
    validating_players_ids: List[int],
    validation_threshold: int = 10,
    timeout=120.0,
    game=None,
) -> Tuple[Optional[bool], Optional[Set[int]]]:
    """
    Implements a checkmark validation on the chosen message.

    If given a game object, will update the message’s embed with validation marks

    3 possible outcomes:
        True and None
            It was validated by the necessary number of players
        False with the the player who cancelled (in a list)
            It was cancelled and the player should be dropped
        None with a list of players who did not validate
            It timed out and the players who didn't validate should be dropped
    """
    queue_channel_handler.mark_queue_related_message(message)

    await message.add_reaction("✅")
    await message.add_reaction("❌")

    def check(received_reaction: discord.Reaction, sending_user: discord.User):
        # This check is simply used to see if a player in the game responded to the message.
        return (received_reaction.message.id == message.id
                and sending_user.id in validating_players_ids
                and str(received_reaction.emoji) in ["✅", "❌"])

    ids_of_players_who_validated = set()

    # Default values that will be output in case of success
    result = True
    ids_to_drop = None
    try:
        while len(ids_of_players_who_validated) < validation_threshold:
            reaction, user = await bot.wait_for("reaction_add",
                                                timeout=timeout,
                                                check=check)

            # A player accepted, we keep him in memory
            if str(reaction.emoji) == "✅":
                ids_of_players_who_validated.add(user.id)

                if game:
                    # TODO MEDIUM PRIO cleanup code duplication
                    embed = Embed(
                        title="📢 Game found 📢",
                        description=
                        f"Blue side expected winrate is {game.blue_expected_winrate * 100:.1f}%\n"
                        "If you are ready to play, press ✅\n"
                        "If you cannot play, press ❌",
                    )
                    await message.edit(embed=game.add_game_field(
                        embed, ids_of_players_who_validated))

            # A player cancels, we return it and will drop him
            elif str(reaction.emoji) == "❌":
                result, ids_to_drop = False, {user.id}
                break

    # We get there if no player accepted in the last x minutes
    except asyncio.TimeoutError:
        result, ids_to_drop = (
            None,
            set(i for i in validating_players_ids
                if i not in ids_of_players_who_validated),
        )

    # Finally, we arrive here only if the validation went through
    queue_channel_handler.unmark_queue_related_message(message)
    return result, ids_to_drop
Esempio n. 8
0
    async def run_matchmaking_logic(
        self,
        ctx: commands.Context,
    ):
        """
        Runs the matchmaking logic in the channel defined by the context

        Should only be called inside guilds
        """
        queue = game_queue.GameQueue(ctx.channel.id)

        game = matchmaking_logic.find_best_game(queue)

        if not game:
            return

        elif game and game.matchmaking_score < 0.2:
            embed = Embed(
                title="📢 Game found 📢",
                description=
                f"Blue side expected winrate is {game.blue_expected_winrate * 100:.1f}%\n"
                "If you are ready to play, press ✅\n"
                "If you cannot play, press �",
            )

            embed = game.add_game_field(embed, [])

            # We notify the players and send the message
            ready_check_message = await ctx.send(
                content=
                f"||{' '.join([f'<@{discord_id}>' for discord_id in game.player_ids_list])}||",
                embed=embed,
            )

            # Because it still takes some time, we *directly* add it to the *no delete* list
            # That’s dirty and should likely be handled in a better way (maybe by *not* using purge
            # and choosing what to delete instead, but it also has its issues)
            # TODO HIGH PRIO Think about saving a "messages to not delete list" in the queue handler memory and
            #  use it in the cog listener, and automatically delete any other one after 5s? should work better (less bugs)

            queue_channel_handler.mark_queue_related_message(
                ready_check_message)

            # We mark the ready check as ongoing (which will be used to the queue)
            game_queue.start_ready_check(
                player_ids=game.player_ids_list,
                channel_id=ctx.channel.id,
                ready_check_message_id=ready_check_message.id,
            )

            # We update the queue in all channels
            await queue_channel_handler.update_server_queues(
                bot=self.bot, server_id=ctx.guild.id)

            # And then we wait for the validation
            ready, players_to_drop = await checkmark_validation(
                bot=self.bot,
                message=ready_check_message,
                validating_players_ids=game.player_ids_list,
                validation_threshold=10,
                timeout=3 * 60,
                game=game,
            )

            if ready is True:
                # We drop all 10 players from the queue
                game_queue.validate_ready_check(ready_check_message.id)

                # We commit the game to the database (without a winner)
                with session_scope() as session:
                    session.add(game)

                    embed = Embed(
                        title="📢 Game accepted 📢",
                        description=
                        f"Game {game.id} has been validated and added to the database\n"
                        f"Once the game has been played, one of the winners can score it with `!won`\n"
                        f"If you wish to cancel the game, use `!cancel`",
                    )

                    embed = game.add_game_field(embed)

                    queue_channel_handler.mark_queue_related_message(
                        await ctx.send(embed=embed, ))

            elif ready is False:
                # We remove the player who cancelled
                game_queue.cancel_ready_check(
                    ready_check_id=ready_check_message.id,
                    ids_to_drop=players_to_drop,
                    channel_id=ctx.channel.id,
                )

                queue_channel_handler.mark_queue_related_message(await ctx.send(
                    f"A player cancelled the game and was removed from the queue\n"
                    f"All other players have been put back in the queue"))

                # We restart the matchmaking logic
                await self.run_matchmaking_logic(ctx)

            elif ready is None:
                # We remove the timed out players from *all* channels (hence giving server id)
                game_queue.cancel_ready_check(
                    ready_check_id=ready_check_message.id,
                    ids_to_drop=players_to_drop,
                    server_id=ctx.guild.id,
                )

                queue_channel_handler.mark_queue_related_message(await ctx.send(
                    "The check timed out and players who did not answer have been dropped from all queues"
                ))

                # We restart the matchmaking logic
                await self.run_matchmaking_logic(ctx)

        elif game and game.matchmaking_score >= 0.2:
            # One side has over 70% predicted winrate, we do not start anything
            await ctx.send(
                f"The best match found had a side with a {(.5 + game.matchmaking_score)*100:.1f}%"
                f" predicted winrate and was not started")