Beispiel #1
0
def test_matchmaking_logic():

    game_queue.reset_queue()

    # We queue for everything except the red support
    for player_id in range(0, 9):
        game_queue.add_player(player_id, roles_list[player_id % 5], 0, 0)

        assert not find_best_game(GameQueue(0))

    # We add the last player
    game_queue.add_player(9, "SUP", 0, 0)

    game = find_best_game(GameQueue(0))

    assert game

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

    # We say player 0 won his last game on server 0
    score_game_from_winning_player(0, 0)

    # We check that everything got changed
    with session_scope() as session:
        # We recreate the game object so it’s associated with this new session
        game = session.query(Game).order_by(Game.start.desc()).first()

        for side, role in game.participants:
            participant = game.participants[side, role]

            assert participant.player.ratings[role].trueskill_mu != 25
Beispiel #2
0
def cancel_ready_check(
    ready_check_id: int, ids_to_drop: Optional[List[int]], channel_id=None, server_id=None,
):
    """
    Cancels an ongoing ready check by reverting players to ready_check_id=None

    Drops players in ids_to_drop[]

    If server_id is not None, drops the player from all queues in the server
    """
    with session_scope() as session:
        # First, we cancel the ready check for *all* players, even the ones we don’t drop
        (
            session.query(QueuePlayer)
            .filter(QueuePlayer.ready_check_id == ready_check_id)
            .update({"ready_check_id": None}, synchronize_session=False)
        )

        if ids_to_drop:
            query = session.query(QueuePlayer).filter(QueuePlayer.player_id.in_(ids_to_drop))

            if server_id and channel_id:
                raise Exception("channel_id and server_id should not be used together here")

            # This removes the player from *all* queues in the server (timeout)
            if server_id:
                query = query.filter(QueuePlayer.player_server_id == server_id)

            # This removes the player only from the given channel (cancellation)
            if channel_id:
                query = query.filter(QueuePlayer.channel_id == channel_id)

            query.delete(synchronize_session=False)
Beispiel #3
0
    async def games(self, ctx: commands.Context):
        """
        Creates 100 games in the database with random results
        """
        game_queue.reset_queue()

        for game_count in range(100):

            # We reset the queue
            # We put 20 people in the queue
            for i in range(0, 20):
                game_queue.add_player(i, roles_list[i % 5], ctx.channel.id, ctx.guild.id, name=str(i))

            # We add the context creator as well
            game_queue.add_player(
                ctx.author.id, roles_list[4], ctx.channel.id, ctx.guild.id, name=ctx.author.display_name
            )

            game = matchmaking_logic.find_best_game(GameQueue(ctx.channel.id))

            with session_scope() as session:
                session.add(game)

            game_queue.start_ready_check([i for i in range(0, 9)] + [ctx.author.id], ctx.channel.id, 0)
            game_queue.validate_ready_check(0)

            matchmaking_logic.score_game_from_winning_player(
                player_id=int(random.random() * 9), server_id=ctx.guild.id
            )

        await ctx.send("100 games have been created in the database")
        await queue_channel_handler.update_server_queues(bot=self.bot, server_id=ctx.guild.id)
Beispiel #4
0
def cancel_all_ready_checks():
    """
    Cancels all ready checks, used when restarting the bot
    """
    with session_scope() as session:
        # We put all ready_check_id to None
        session.query(QueuePlayer).update({"ready_check_id": None}, synchronize_session=False)
Beispiel #5
0
def add_player(player_id: int, role: str, channel_id: int, server_id: int = None, name: str = None):
    # Just in case
    assert role in roles_list

    with session_scope() as session:

        game, participant = get_last_game(player_id, server_id, session)

        if game and not game.winner:
            raise PlayerInGame

        # Then check if the player is in a ready-check
        if is_in_ready_check(player_id, session):
            raise PlayerInReadyCheck

        # This is where we add new Players to the server
        #   This is also useful to automatically update name changes
        session.merge(Player(id=player_id, server_id=server_id, name=name))

        # Finally, we actually add the player to the queue
        queue_player = QueuePlayer(
            channel_id=channel_id,
            player_id=player_id,
            player_server_id=server_id,
            role=role,
            queue_time=datetime.now(),
        )

        # We merge for simplicity (allows players to re-queue for the same role)
        session.merge(queue_player)
Beispiel #6
0
def get_active_queues() -> List[int]:
    """
    Returns a list of channel IDs where there is a queue ongoing
    """
    with session_scope() as session:
        output = [
            r.channel_id for r in session.query(QueuePlayer.channel_id).group_by(QueuePlayer.channel_id)
        ]

    return output
Beispiel #7
0
def score_game_from_winning_player(player_id: int, server_id: int):
    """
    Scores the last game of the player on the server as a *win*
    """
    with session_scope() as session:
        game, participant = get_last_game(player_id, server_id, session)

        game.winner = participant.side

        update_trueskill(game, session)
Beispiel #8
0
def remove_players(player_ids: Set[int], channel_id: int):
    """
    Removes all players from the queue in all roles in the channel, without any checks
    """
    with session_scope() as session:
        (
            session.query(QueuePlayer)
            .filter(QueuePlayer.channel_id == channel_id)
            .filter(QueuePlayer.player_id.in_(player_ids))
            .delete(synchronize_session=False)
        )
Beispiel #9
0
    def mark_queue_channel(self, channel_id, server_id):
        """
        Marks the given channel + server combo as a queue
        """
        channel = ChannelInformation(id=channel_id,
                                     server_id=server_id,
                                     channel_type="QUEUE")
        with session_scope() as session:
            session.merge(channel)

        self._queue_channels.append(channel)
Beispiel #10
0
def start_ready_check(player_ids: List[int], channel_id: int, ready_check_message_id: int):
    # Checking to make sure everything is fine
    assert len(player_ids) == int(os.environ["INHOUSE_BOT_QUEUE_SIZE"])

    with session_scope() as session:

        (
            session.query(QueuePlayer)
            .filter(QueuePlayer.channel_id == channel_id)
            .filter(QueuePlayer.player_id.in_(player_ids))
            .update({"ready_check_id": ready_check_message_id}, synchronize_session=False)
        )
Beispiel #11
0
    async def cancel(self, ctx: commands.Context):
        """
        Scores your last game as a win for (mostly made to be used after !test game)
        """
        with session_scope() as session:
            game, participant = get_last_game(
                player_id=ctx.author.id, server_id=ctx.guild.id, session=session
            )

            session.delete(game)

        await ctx.send(f"{ctx.author.display_name}’s last game was cancelled and deleted from the database")
        await queue_channel_handler.update_server_queues(bot=self.bot, server_id=ctx.guild.id)
Beispiel #12
0
    async def stats(self, ctx: commands.Context):
        """
        Returns your rank, MMR, and games played

        Example:
            !rank
        """
        # TODO LOW PRIO Make it not output the server only in DMs, otherwise filter on the server

        with session_scope() as session:
            rating_objects = (session.query(
                PlayerRating,
                func.count().label("count"),
                (sqlalchemy.func.sum(
                    (Game.winner == GameParticipant.side).cast(
                        sqlalchemy.Integer))).label("wins"),
            ).select_from(PlayerRating).join(GameParticipant).join(
                Game).filter(PlayerRating.player_id == ctx.author.id).group_by(
                    PlayerRating))

            if ctx.guild:
                rating_objects = rating_objects.filter(
                    PlayerRating.player_server_id == ctx.guild.id)

            table = []

            for row in rating_objects:
                rank = (session.query(
                    func.count()).select_from(PlayerRating).filter(
                        PlayerRating.player_server_id == row.player_server_id).
                        filter(PlayerRating.role == row.role).filter(
                            PlayerRating.mmr > row.mmr)).first()[0]

                table.append([
                    self.bot.get_guild(row.player_server_id).name,
                    row.role,
                    inflect_engine.ordinal(rank + 1),
                    round(row.mmr, 2),
                    row.count,
                    f"{int(row.wins / row.count * 100)}%",
                ])

            # Sorting the table by games played
            table = sorted(table, key=lambda x: -x[4])

            # Added afterwards to allow sorting first
            table.insert(0, ["Server", "Role", "Rank", "MMR", "Games", "Win%"])

        await ctx.send(f"Ranks for {ctx.author.display_name}"
                       f'```{tabulate(table, headers="firstrow")}```')
Beispiel #13
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)
Beispiel #14
0
    def remove_queue_channel(self, channel_id):
        game_queue.reset_queue(channel_id)

        with session_scope() as session:
            reset_queue(channel_id)

            channel_query = session.query(ChannelInformation).filter(
                ChannelInformation.id == channel_id)

            channel_query.delete(synchronize_session=False)

        self._queue_channels = [
            c for c in self._queue_channels if c.id != channel_id
        ]
Beispiel #15
0
    async def ranking(self,
                      ctx: commands.Context,
                      role: RoleConverter() = None):
        """
        Displays the top players on the server

        A role can be supplied to only display the ranking for this role

        Example:
            !ranking
            !ranking mid
        """
        with session_scope() as session:
            session.expire_on_commit = False

            ratings = (
                session.query(
                    Player,
                    PlayerRating.player_server_id,
                    PlayerRating.mmr,
                    PlayerRating.role,
                    func.count().label("count"),
                    (sqlalchemy.func.sum(
                        (Game.winner == GameParticipant.side).cast(
                            sqlalchemy.Integer))
                     ).label("wins"),  # A bit verbose for sure
                ).select_from(Player).join(PlayerRating).join(GameParticipant).
                join(Game).filter(Player.server_id == ctx.guild.id).group_by(
                    Player, PlayerRating).order_by(PlayerRating.mmr.desc()))

            if role:
                ratings = ratings.filter(PlayerRating.role == role)

            ratings = ratings.limit(100).all()

        if not ratings:
            await ctx.send("No games played yet")
            return

        pages = menus.MenuPages(
            source=RankingPagesSource(
                ratings,
                self.bot,
                embed_name_suffix=
                f"on {ctx.guild.name}{f' - {get_role_emoji(role)}' if role else ''}",
            ),
            clear_reactions_after=True,
        )
        await pages.start(ctx)
Beispiel #16
0
def reset_queue(channel_id: Optional[int] = None):
    """
    Resets queue in a specific channel.
    If channel_id is None, cancels *all* queues. Only for testing purposes.

    Args:
        channel_id: channel id of the queue to cancel
    """
    with session_scope() as session:
        query = session.query(QueuePlayer)

        if channel_id is not None:
            query = query.filter(QueuePlayer.channel_id == channel_id)

        query.delete(synchronize_session=False)
Beispiel #17
0
def validate_ready_check(ready_check_id: int):
    """
    When a ready check is validated, we drop all players from all queues
    """

    with session_scope() as session:
        player_ids = [
            r.player_id
            for r in session.query(QueuePlayer.player_id).filter(QueuePlayer.ready_check_id == ready_check_id)
        ]

        (
            session.query(QueuePlayer)
            .filter(QueuePlayer.player_id.in_(player_ids))
            .delete(synchronize_session=False)
        )
Beispiel #18
0
    async def cancel(self, ctx: commands.Context, member: discord.Member):
        """
        Cancels the user’s ongoing game

        Only works if the game has not been scored yet
        """
        with session_scope() as session:
            game, participant = get_last_game(player_id=member.id, server_id=ctx.guild.id, session=session)

            if game and game.winner:
                await ctx.send("The game has already been scored and cannot be canceled anymore")
                return

            session.delete(game)

        await ctx.send(f"{member.display_name}’s ongoing game was cancelled and deleted from the database")
        await queue_channel_handler.update_server_queues(bot=self.bot, server_id=ctx.guild.id)
Beispiel #19
0
    def __init__(self):
        # We reload the queue channels from the database on restart
        with session_scope() as session:
            session.expire_on_commit = False

            self._queue_channels = (session.query(
                ChannelInformation.id, ChannelInformation.server_id).filter(
                    ChannelInformation.channel_type == "QUEUE").all())

        # channel_id -> GameQueue, to know when there were updates?
        self._queue_cache = {}

        # Helps untag older message that needs to be deleted
        self.latest_queue_message_ids = {}

        # IDs of messages we do not want to delete
        self.queue_related_messages_ids = set()
Beispiel #20
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"))
Beispiel #21
0
def remove_player(player_id: int, channel_id: int = None):
    """
    Removes the player from the queue in all roles in the channel

    If no channel id is given, drop him from *all* queues, cross-server
    """
    with session_scope() as session:
        # First, check if he’s in a ready-check.
        if (
            is_in_ready_check(player_id, session) and channel_id
        ):  # If we have no channel ID, it’s an !admin reset and we bypass the issue here
            raise PlayerInReadyCheck

        # We select the player’s rows
        query = session.query(QueuePlayer).filter(QueuePlayer.player_id == player_id)

        # If given a channel ID (when the user calls !leave), we filter
        if channel_id:
            query = query.filter(QueuePlayer.channel_id == channel_id)

        query.delete(synchronize_session=False)
Beispiel #22
0
    async def history(self, ctx: commands.Context):
        # TODO LOW PRIO Add an @ user for admins
        """
        Displays your games history

        Example:
            !history
        """

        with session_scope() as session:
            session.expire_on_commit = False

            game_participant_query = (session.query(
                Game, GameParticipant).select_from(Game).join(
                    GameParticipant).filter(
                        GameParticipant.player_id == ctx.author.id).order_by(
                            Game.start.desc()))

            # If we’re on a server, we only show games played on that server
            if ctx.guild:
                game_participant_query = game_participant_query.filter(
                    Game.server_id == ctx.guild.id)

            game_participant_list = game_participant_query.limit(100).all()

        if not game_participant_list:
            await ctx.send("No games found")
            return

        pages = menus.MenuPages(
            source=HistoryPagesSource(
                game_participant_list,
                self.bot,
                player_name=ctx.author.display_name,
                is_dms=True if not ctx.guild else False,
            ),
            clear_reactions_after=True,
        )
        await pages.start(ctx)
Beispiel #23
0
    async def game(self, ctx: commands.Context):
        """
        Creating a fake game in the database with players 0 to 8 and the ctx author
        """
        # We reset the queue
        # We put 9 people in the queue
        for i in range(0, 9):
            game_queue.add_player(i, roles_list[i % 5], ctx.channel.id, ctx.guild.id, name=str(i))

        game_queue.add_player(
            ctx.author.id, roles_list[4], ctx.channel.id, ctx.guild.id, name=ctx.author.display_name
        )

        game = matchmaking_logic.find_best_game(GameQueue(ctx.channel.id))

        with session_scope() as session:
            session.add(game)

        msg = await ctx.send("The queue has been reset, filled again, and a game created (with no winner)")

        game_queue.start_ready_check([i for i in range(0, 9)] + [ctx.author.id], ctx.channel.id, msg.id)
        game_queue.validate_ready_check(msg.id)
        await queue_channel_handler.update_server_queues(bot=self.bot, server_id=ctx.guild.id)
Beispiel #24
0
    async def champion(self,
                       ctx: commands.Context,
                       champion_id: ChampionNameConverter(),
                       game_id: int = None):
        """
        Saves the champion you used in your last game

        Older games can be filled with !champion champion_name game_id
        You can find the ID of the games you played with !history

        Example:
            !champion riven
            !champion riven 1
        """

        with session_scope() as session:
            if not game_id:
                game, participant = get_last_game(player_id=ctx.author.id,
                                                  server_id=ctx.guild.id,
                                                  session=session)
            else:
                game, participant = (session.query(
                    Game, GameParticipant).select_from(Game).join(
                        GameParticipant).filter(Game.id == game_id).filter(
                            GameParticipant.player_id == ctx.author.id)
                                     ).one_or_none()

            # We write down the champion
            participant.champion_id = champion_id

            game_id = game.id

        await ctx.send(
            f"Champion for game {game_id} was set to "
            f"{lol_id_tools.get_name(champion_id, object_type='champion')} for {ctx.author.display_name}"
        )
Beispiel #25
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")