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
Пример #2
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)
Пример #3
0
    def get_server_ratings(server_id: int, role: str = None, limit=100):
        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 == server_id).filter(
                    Game.winner != None)  # No currently running game
                .group_by(Player,
                          PlayerRating).order_by(PlayerRating.mmr.desc()))

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

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

        return ratings
Пример #4
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_queue_channels(
            bot=self.bot, server_id=ctx.guild.id)
        await remove_voice_channels(ctx, game)
Пример #5
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 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
            )

            # We put 15 people in the queue, but only the first ones should get picked
            for i in range(0, 15):
                game_queue.add_player(i, roles_list[i % 5], ctx.channel.id, ctx.guild.id, name=str(i))

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

            with session_scope() as session:
                session.add(game)
                winner = game.player_ids_list[int(random.random() * 10)]

            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=winner, server_id=ctx.guild.id)

            await ranking_channel_handler.update_ranking_channels(self.bot, ctx.guild.id)

        await ctx.send("100 games have been created in the database")
        await queue_channel_handler.update_queue_channels(bot=self.bot, server_id=ctx.guild.id)
Пример #6
0
    async def champion(self,
                       ctx: commands.Context,
                       champion_name: ChampionNameConverter(),
                       game_id: int = None):
        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_name

            game_id = game.id

        await ctx.send(
            f"Champion for game {game_id} was set to "
            f"{lol_id_tools.get_name(champion_name, object_type='champion')} for {ctx.author.display_name}"
        )
Пример #7
0
    async def history(self, ctx: commands.Context):
        # TODO LOW PRIO Add an @ user for admins

        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)
Пример #8
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)
Пример #9
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_player = session.query(QueuePlayer).filter(
            QueuePlayer.player_id == player_id)
        query_duos = session.query(QueuePlayer).filter(
            QueuePlayer.duo_id == player_id)

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

        query_player.delete(synchronize_session=False)
        query_duos.update({"duo_id": None}, synchronize_session=False)
Пример #10
0
    def __init__(self):
        with session_scope() as session:
            session.expire_on_commit = False

            self._ranking_channels = (session.query(
                ChannelInformation.id, ChannelInformation.server_id).filter(
                    ChannelInformation.channel_type == "RANKING").all())
Пример #11
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"))
Пример #12
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)
Пример #13
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))
Пример #14
0
    def unmark_queue_channel(self, channel_id):
        game_queue.reset_queue(channel_id)

        with session_scope() as session:
            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]

        queue_logger.info(f"Unmarked {channel_id} as a queue channel")
Пример #15
0
    def unmark_ranking_channel(self, channel_id):

        with session_scope() as session:
            channel_query = session.query(ChannelInformation).filter(
                ChannelInformation.id == channel_id)
            channel_query.delete(synchronize_session=False)

        self._ranking_channels = [
            c for c in self._ranking_channels if c.id != channel_id
        ]
Пример #16
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)
Пример #17
0
def remove_duo(player_id: int, channel_id: int):
    # Removes duos for all roles for this player in this channel
    # This could be called during a ready-check but it shouldn’t be too much of an issue

    with session_scope() as session:
        (session.query(QueuePlayer).filter(
            QueuePlayer.channel_id == channel_id).filter(
                sqlalchemy.or_(QueuePlayer.duo_id == player_id,
                               QueuePlayer.player_id == player_id)).update(
                                   {"duo_id": None},
                                   synchronize_session=False))
Пример #18
0
    async def mmr_history(self, ctx: commands.Context):
        """
        Displays a graph of your MMR history over the past month
        """
        date_start = datetime.now() - timedelta(hours=24 * 30)

        with session_scope() as session:

            participants = (
                session.query(
                    Game.start,
                    GameParticipant.role,
                    GameParticipant.mmr,
                    PlayerRating.mmr.label("latest_mmr"),
                )
                .select_from(Game)
                .join(GameParticipant)
                .join(PlayerRating)  # Join on rating first to select the right role
                .join(Player)
                .filter(GameParticipant.player_id == ctx.author.id)
                .filter(Game.start > date_start)
                .order_by(Game.start.asc())
            )

        mmr_history = defaultdict(lambda: {"dates": [], "mmr": []})

        latest_role_mmr = {}

        for row in participants:
            mmr_history[row.role]["dates"].append(row.start)
            mmr_history[row.role]["mmr"].append(row.mmr)

            latest_role_mmr[row.role] = row.latest_mmr

        legend = []
        for role in mmr_history:
            # We add a data point at the current timestamp with the player’s current MMR
            mmr_history[role]["dates"].append(datetime.now())
            mmr_history[role]["mmr"].append(latest_role_mmr[role])

            plt.plot(mmr_history[role]["dates"], mmr_history[role]["mmr"])
            legend.append(role)

        plt.legend(legend)
        plt.title(f"MMR variation in the last month for {ctx.author.display_name}")
        mplcyberpunk.add_glow_effects()

        # This looks to be unnecessary verbose with all the closing by hand, I should take a look
        with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp:
            plt.savefig(temp.name)
            file = discord.File(temp.name, filename=temp.name)
            await ctx.send(file=file)
            plt.close()
            temp.close()
Пример #19
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
Пример #20
0
    def mark_ranking_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="RANKING")
        with session_scope() as session:
            session.merge(channel)

        self._ranking_channels.append(channel)
Пример #21
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)

        queue_logger.info(f"Marked {channel_id} as a queue channel")
Пример #22
0
    async def stats(self, ctx: commands.Context):
        """
        Returns your rank, MMR, and games played

        Example:
            !rank
        """
        with session_scope() as session:
            rating_objects = (
                session.query(
                    PlayerRating,
                    sqlalchemy.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)

            rows = []

            for row in sorted(rating_objects.all(), key=lambda r: -r.count):
                # TODO LOW PRIO Make that a subquery
                rank = (
                    session.query(sqlalchemy.func.count())
                    .select_from(PlayerRating)
                    .filter(PlayerRating.player_server_id == row.PlayerRating.player_server_id)
                    .filter(PlayerRating.role == row.PlayerRating.role)
                    .filter(PlayerRating.mmr > row.PlayerRating.mmr)
                ).first()[0]

                rank_str = get_rank_emoji(rank)

                row_string = (
                    f"{f'{self.bot.get_guild(row.PlayerRating.player_server_id).name} ' if not ctx.guild else ''}"
                    f"{get_role_emoji(row.PlayerRating.role)} "
                    f"{rank_str} "
                    f"`{int(row.PlayerRating.mmr)} MMR  "
                    f"{row.wins}W {row.count-row.wins}L`"
                )

                rows.append(row_string)

            embed = Embed(title=f"Ranks for {ctx.author.display_name}", description="\n".join(rows))

            await ctx.send(embed=embed)
Пример #23
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) == 10

    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))
Пример #24
0
def add_duo(
    first_player_id: int,
    first_player_role: str,
    second_player_id: int,
    second_player_role: str,
    channel_id: int,
    server_id: int = None,
    first_player_name: str = None,
    second_player_name: str = None,
    jump_ahead=False,
):
    # Marks this group of players and roles as a duo

    if first_player_role == second_player_role:
        raise SameRolesForDuo

    # Just in case, we drop the players from the queue first
    remove_player(first_player_id, channel_id)
    remove_player(second_player_id, channel_id)

    add_player(
        player_id=first_player_id,
        role=first_player_role,
        channel_id=channel_id,
        server_id=server_id,
        name=first_player_name,
        jump_ahead=jump_ahead,
    )

    add_player(
        player_id=second_player_id,
        role=second_player_role,
        channel_id=channel_id,
        server_id=server_id,
        name=second_player_name,
        jump_ahead=jump_ahead,
    )

    with session_scope() as session:
        # Finally, we add the duos by merging only the newer data (empty fields shouldn’t get merged)
        first_queue_player = QueuePlayer(player_id=first_player_id,
                                         role=first_player_role,
                                         channel_id=channel_id,
                                         duo_id=second_player_id)

        second_queue_player = QueuePlayer(player_id=second_player_id,
                                          role=second_player_role,
                                          channel_id=channel_id,
                                          duo_id=first_player_id)

        # We merge the new information
        session.merge(first_queue_player)
        session.merge(second_queue_player)
Пример #25
0
    def daily_jobs(self):
        """
        Runs a timer every 60 seconds, triggering jobs at the appropriate minute mark
        """
        threading.Timer(60, self.daily_jobs).start()
        now = datetime.now()

        if now.strftime("%H:%M") == QUEUE_RESET_TIME:
            with session_scope() as session:
                server_config = get_server_config(server_id=self.guilds[0].id, session=session)
                if server_config.config.get('queue_reset'):
                    game_queue.reset_queue()
                    self.loop.create_task(queue_channel_handler.update_queue_channels(bot=self, server_id=None))
Пример #26
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_queue_channels(bot=self.bot, server_id=ctx.guild.id)
Пример #27
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))
Пример #28
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)
Пример #29
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:
            # TODO This should be shared with remove_player and not duplicated
            players_query = session.query(QueuePlayer).filter(
                QueuePlayer.player_id.in_(ids_to_drop))
            duos_query = session.query(QueuePlayer).filter(
                QueuePlayer.duo_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:
                players_query = players_query.filter(
                    QueuePlayer.player_server_id == server_id)
                duos_query = duos_query.filter(
                    QueuePlayer.player_server_id == server_id)

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

            # Drop the player and remove his duo status with other players
            players_query.delete(synchronize_session=False)
            duos_query.update({"duo_id": None}, synchronize_session=False)
def test_duo_matchmaking():
    game_queue.reset_queue()

    # Playing 100 games with random outcomes and making sure 0 and 9 are always on the same team
    for game_count in range(100):

        # 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,
                                  name=str(player_id))

        # We add the last player as duo with player 0
        game_queue.add_duo(
            0,
            "TOP",
            9,
            "SUP",
            0,
            0,
            second_player_name="9",
            first_player_name="0",
        )

        game = find_best_game(GameQueue(0))

        player_0 = next(p for p in game.participants.values()
                        if p.player_id == 0)
        player_9 = next(p for p in game.participants.values()
                        if p.player_id == 9)

        assert player_0.side == player_9.side

        with session_scope() as session:
            session.add(game)
            winner = game.player_ids_list[int(random.random() * 10)]

        game_queue.start_ready_check([i for i in range(0, 10)], 0, 0)
        game_queue.validate_ready_check(0)

        score_game_from_winning_player(player_id=winner, server_id=0)