Пример #1
0
    async def remove_player_from_queue(self,
                                       player,
                                       channel_id=None,
                                       ctx=None):
        """
        Removes the given player from queue.

        If given a channel_id, only removes the player from the given channel.
        If given a context message, thumbs ups it.

        Returns a list of [channel_id, role] it was removed from
        """
        session = get_session()

        query = session.query(QueuePlayer).filter(
            QueuePlayer.player_id == player.discord_id)
        if channel_id:
            query = query.filter(QueuePlayer.channel_id == channel_id)

        query.delete(synchronize_session=False)

        session.commit()
        session.close()

        logging.info("Player <{}> has been removed from {}".format(
            player.discord_string, "all queues"
            if not channel_id else "the <{}> queue".format(channel_id)))

        if ctx:
            await ctx.message.add_reaction("👍")
            await self.send_queue(ctx)
Пример #2
0
    async def admin_score(self, ctx, game_id, winning_team):
        """
        Admin-only way to score an active match. Needs game_id and winner (blue/red)

        Example:
            !admin_score 1 blue
            !admin_score 3 red
        """
        session = get_session()
        game = session.query(Game).filter(Game.id == game_id).one()

        if game.winner:
            await ctx.send(
                'It is currently impossible the change the result of a game that was scored because it would '
                'create huge issues with rating recalculations.')
            return

        if winning_team not in ['blue', 'red']:
            await ctx.send('The winning team must be blue or red.\n'
                           'Example: `!admin_score 1 blue`')
            return

        game.winner = winning_team

        session.commit()
        game.update_trueskill(session)

        message = f'Game {game.id} has been scored as a win for {game.winner} and ratings have been updated.'

        logging.info(message)
        await ctx.send(message)
Пример #3
0
    async def send_queue(self, ctx: commands.context):
        """
        Deletes the previous queue message and sends a new one in the channel
        """
        channel_id = ctx.channel.id

        try:
            old_queue_message = self.latest_queue_message[channel_id]
        except KeyError:
            old_queue_message = None

        # Getting players in queue and not in a ready check
        session = get_session()
        players_in_queue = (session.query(QueuePlayer).options(
            joinedload(QueuePlayer.player)).filter(
                QueuePlayer.channel_id == ctx.channel.id).filter(
                    QueuePlayer.ready_check == None).all())
        session.close()

        rows = []
        for role in roles_list:
            # We add an empty string list or tabulate gets sad
            rows.append(f"{get_role_emoji(role)} " +
                        ", ".join(p.player.name
                                  for p in players_in_queue if p.role == role))

        embed = Embed(colour=discord.colour.Colour.dark_red())

        embed.add_field(name="Queue", value="\n".join(rows))

        self.latest_queue_message[channel_id] = await ctx.send(embed=embed)

        # Sequenced that way for smoother scrolling in discord
        if old_queue_message:
            await old_queue_message.delete()
Пример #4
0
    async def queue(self, ctx: commands.Context, *, roles):
        """
        Puts you in a queue in the current channel for the specified roles.
        Roles are top, jungle, mid, bot, and support.

        Example usage:
            !queue support
            !queue mid bot
        """
        player = await self.bot.get_player(ctx)

        # First, we check if the last game of the player is still ongoing.
        try:
            game, participant = player.get_last_game()
            if not game.winner:
                await ctx.send(
                    "Your last game looks to be ongoing. "
                    "Please use !won or !lost to inform the result if the game is over.",
                    delete_after=self.bot.short_notice_duration,
                )
                return
        # This happens if the player has not played a game yet as get_last returns None and can’t be unpacked
        except TypeError:
            pass

        session = get_session()
        queue_player = (session.query(QueuePlayer).filter(
            QueuePlayer.player_id == Player.discord_id).filter(
                QueuePlayer.ready_check != None)).first()
        session.close()

        if queue_player:
            await ctx.send(
                "It seems you are in a pre-game check. You will be able to queue again once it is over."
            )
            return

        clean_roles = set()
        for role in roles.split(" "):
            clean_role, score = process.extractOne(role, roles_list)
            if score < 80:
                continue
            else:
                clean_roles.add(clean_role)

        if not clean_roles:
            await ctx.send(self.bot.role_not_understood,
                           delete_after=self.bot.warning_duration)
            return

        for role in clean_roles:
            self.add_player_to_queue(player, role, ctx.channel.id)

        for role in clean_roles:
            # Dirty code to get the emoji related to the letters
            await ctx.message.add_reaction(get_role_emoji(role))

        await self.matchmaking_process(ctx)

        await self.send_queue(ctx)
Пример #5
0
    async def ranking(self, ctx: commands.Context, role='all'):
        """
        Returns the top 20 players for the selected role.
        """
        if role == 'all':
            clean_role = role
        else:
            clean_role, score = process.extractOne(role, roles_list)
            if score < 80:
                await ctx.send(self.bot.role_not_understood, delete_after=30)
                return

        session = get_session()

        role_ranking = session.query(PlayerRating).order_by(
            -PlayerRating.mmr).filter(PlayerRating.games > 0)

        if clean_role != 'all':
            role_ranking = role_ranking.filter(PlayerRating.role == clean_role)

        table = [['Rank', 'Name', 'MMR', 'Games'] +
                 ['Role' if clean_role == 'all' else None]]

        for rank, rating in enumerate(role_ranking.limit(20)):
            table.append([
                inflect_engine.ordinal(rank + 1), rating.player.name,
                f'{rating.mmr:.1f}',
                rating.get_games()
            ] + [rating.role if clean_role == 'all' else None])

        await ctx.send(f'Ranking for {clean_role} is:\n'
                       f'```{tabulate(table, headers="firstrow")}```')
Пример #6
0
    def add_player_to_queue(self, player, role, channel_id):
        if role not in player.ratings:
            logging.info("Creating a new PlayerRating for <{}> <{}>".format(
                player.discord_string, role))
            new_rating = PlayerRating(player, role)
            self.bot.players_session.add(new_rating)
            self.bot.players_session.commit()
            # This step is required so our player object has access to the rating
            player = self.bot.players_session.merge(player)

        # Actually adding the player to the queue
        # TODO A player can reset his ready_check status that way since it’s not blocking, see if it makes sense
        session = get_session()

        # noinspection PyTypeChecker
        queue_player = QueuePlayer(channel_id=channel_id,
                                   player_id=player.discord_id,
                                   role=role,
                                   ready_check=None)
        session.merge(queue_player)
        session.commit()

        session.close()

        logging.info(
            f"Player <{player.discord_string}> has been added to the <{role}><{channel_id}> queue"
        )
Пример #7
0
    async def reset_queue(self, ctx):
        """
        Admin-only way to reset the queue in the current channel
        """
        session = get_session()

        query = session.query(QueuePlayer).filter(
            QueuePlayer.channel_id == ctx.channel.id)
        query.delete(synchronize_session=False)

        session.commit()
        session.close()

        await self.send_queue(ctx)
Пример #8
0
    async def mmr_history(self,
                          ctx: commands.Context,
                          user_id=None,
                          date_start=None):
        """Displays a graph of your MMR history over the past month.
        """
        try:
            player = await self.get_player_with_team_check(ctx, user_id)
        except PermissionError:
            return

        if not date_start:
            date_start = dateparser.parse("one month ago")
        else:
            date_start = dateparser.parse(date_start)

        session = get_session()

        # TODO Use the player_rating.game_participant_objects field?
        participants = (session.query(
            Game, GameParticipant).join(GameParticipant).filter(
                GameParticipant.player_id == player.discord_id).filter(
                    Game.date > date_start))

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

        for game, participant in participants:
            mmr_history[participant.role]["dates"].append(game.date)
            mmr_history[participant.role]["mmr"].append(participant.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(player.ratings[role].mmr)

            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 {player.name}")
        mplcyberpunk.add_glow_effects()

        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()
Пример #9
0
def config():
    from inhouse_bot.inhouse_bot import InhouseBot
    from inhouse_bot.sqlite.sqlite_utils import get_session

    session = get_session()

    # We create our bot object, a mock server, a mock channel, 10 mock members, and return our cog for testing
    bot = InhouseBot()
    dpytest.configure(bot, 1, 1, 10)
    config = dpytest.get_config()

    queue_cog = bot.get_cog('Queue')
    channel_id = config.channels[0].id
    ConfigTuple = collections.namedtuple(
        'config', ['config', 'bot', 'queue_cog', 'channel_id', 'session'])

    return ConfigTuple(config, bot, queue_cog, channel_id, session)
Пример #10
0
    async def cancel_game(self, ctx: commands.context):
        """
        Cancels and voids your ongoing game. Require validation from other players in the game.
        """
        player = await self.bot.get_player(ctx)

        session = get_session()
        game, participant = player.get_last_game()

        # If the game is already done and scored, we don’t offer cancellation anymore.
        if game.winner:
            no_cancel_notice = f"You don’t seem to currently be in a game."
            logging.info(no_cancel_notice)
            await ctx.send(no_cancel_notice)
            return

        discord_ids_list = [
            p.player.discord_id for p in game.participants.values()
        ]

        cancelling_game_message = await ctx.send(
            f"Trying to cancel the game including {self.get_tags(discord_ids_list)}.\n"
            "If you want to cancel the game, have at least 6 players press ✅.\n"
            "If you did not mean to cancel the game, press ❎.",
            delete_after=self.bot.validation_duration,
        )

        cancel, cancelling_players = await self.checkmark_validation(
            cancelling_game_message, discord_ids_list, 6)

        if not cancel:
            # If there’s no validation, we just inform players nothing happened and leave
            no_cancellation_message = f"Cancellation canceled."
            logging.info(no_cancellation_message)
            await ctx.send(no_cancellation_message, delete_after=30)
            return

        # If we get here, 6+ players accepted to cancel the game
        session.delete(game)
        session.commit()

        cancel_notice = f"Game {game.id} cancelled."

        logging.info(cancel_notice)
        await ctx.send(cancel_notice)
Пример #11
0
    async def view_games(self, ctx: commands.context):
        """
        Shows the ongoing inhouse games.
        """
        session = get_session()

        games_without_results = session.query(Game).filter(Game.winner == None).all()

        if not games_without_results:
            await ctx.send('No active games found', delete_after=self.bot.short_notice_duration)
            return

        embed = Embed(title='Ongoing games', colour=discord.colour.Colour.dark_blue())
        for game in games_without_results:
            embed.add_field(name=f'Game {game.id}',
                            value=f'```{game}```')

        await ctx.send(embed=embed)
Пример #12
0
    async def start_game(self, ctx, players, mismatch=False):
        """
        Attempts to start the given game by pinging players and waiting for their reactions.
        """
        # Start by removing all players from the channel queue before starting the game
        players_queues = {}
        for player in players.values():
            players_queues[
                player.discord_id] = await self.remove_player_from_queue(player
                                                                         )

        game_session = get_session()
        game = Game(players)
        game_session.add(game)
        game_session.commit()

        ready, ready_players = await self.ready_check(ctx, players, mismatch,
                                                      game)

        if not ready:
            # If the ready check fails, we delete the game and let !queue handle restarting matchmaking.
            for player_id in ready_players:
                for queue, role in players_queues[player_id]:
                    self.add_player_to_queue(
                        await self.bot.get_player(None, player_id), role,
                        queue)

            await ctx.send(
                f'The game has been cancelled.\n'
                f'Players who pressed ✅ have been put back in queue.\n'
                f'{self.get_tags([discord_id for discord_id in players_queues if discord_id not in ready_players])}'
                f' have been removed from queue.')

            game_session.delete(game)
            game_session.commit()

            # We return and restart matchmaking with the new queue
            await self.send_queue(ctx)
            return await self.matchmaking_process(ctx)

        validation_message = f'Game {game.id} has been accepted and can start!'

        logging.info(validation_message)
        await ctx.send(validation_message)
Пример #13
0
    async def mmr_history(self, ctx: commands.Context, date_start=None):
        """
        Displays a graph of your MMR history over the past month.
        """
        if not date_start:
            date_start = dateparser.parse('one month ago')
        else:
            date_start = dateparser.parse(date_start)

        player = await self.bot.get_player(ctx)
        session = get_session()

        # TODO Use the player_rating.game_participant_objects field?
        participants = session.query(Game, GameParticipant)\
            .join(GameParticipant)\
            .filter(GameParticipant.player_id == player.discord_id)\
            .filter(Game.date > date_start)

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

        for game, participant in participants:
            mmr_history[participant.role]['dates'].append(game.date)
            mmr_history[participant.role]['mmr'].append(participant.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(player.ratings[role].mmr)

            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 {player.name}')
        mplcyberpunk.add_glow_effects()

        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()
Пример #14
0
    async def ranking(self, ctx: commands.Context, role="all"):
        """
        Returns the top 20 players for the selected role in the server.
        """
        if not ctx.guild:
            await ctx.send(
                "!ranking can only be called inside a Discord server.",
                delete_after=30)
            return

        if role == "all":
            clean_role = role
        else:
            clean_role, score = process.extractOne(role, roles_list)
            if score < 80:
                await ctx.send(self.bot.role_not_understood, delete_after=30)
                return

        session = get_session()

        guild_player_ids = [m.id for m in ctx.guild.members]

        role_ranking = (session.query(PlayerRating).join(Player).order_by(
            -PlayerRating.mmr).filter(PlayerRating.games > 0).filter(
                Player.discord_id.in_(guild_player_ids)))

        if clean_role != "all":
            role_ranking = role_ranking.filter(PlayerRating.role == clean_role)

        table = [["Rank", "Name", "MMR", "Games"] +
                 ["Role" if clean_role == "all" else None]]

        for rank, rating in enumerate(role_ranking.limit(20)):
            table.append([
                inflect_engine.ordinal(rank + 1),
                rating.player.name,
                f"{rating.mmr:.1f}",
                rating.get_games_total(),
            ] + [rating.role if clean_role == "all" else None])

        await ctx.send(f"Ranking for {clean_role} is:\n"
                       f'```{tabulate(table, headers="firstrow")}```')
Пример #15
0
    async def view_team(self, ctx: commands.Context):
        """View your current team and team mates.
        """
        player = await self.bot.get_player(ctx)

        if not player.team:
            await ctx.send(
                f"Your team has not been set yet. Please contact a server admin to get tagged."
            )
            return

        teammates_session = get_session()

        teammates = teammates_session.query(Player).filter(
            Player.team == player.team)

        await ctx.send(
            f"You are currently part of {player.team}. Please contact a server admin for changes.\n"
            f'Currently in {player.team}: {", ".join([t.name for t in teammates])}'
        )
Пример #16
0
    async def champion(self, ctx, champion_name, game_id=None):
        """
        Informs the champion you used for the chosen game (or the last game by default)

        Example:
            !champion riven
            !champion riven 1
        """
        try:
            champion_id = lit.get_id(champion_name, object_type="champion")
        except lit.NoMatchingNameFound:
            await ctx.send(
                "Champion name was not understood properly.\n"
                "Use `!help champion` for more information.",
                delete_after=self.bot.warning_duration,
            )
            return

        player = await self.bot.get_player(ctx)
        session = get_session()

        if game_id:
            game, participant = (session.query(Game, GameParticipant).join(
                GameParticipant).filter(Game.id == game_id).filter(
                    GameParticipant.player_id == player.discord_id).order_by(
                        Game.date.desc()).first())
        else:
            game, participant = player.get_last_game()

        participant.champion_id = champion_id

        session.merge(participant)
        session.commit()

        log_message = (
            f"Champion for game {game.id} set to {lit.get_name(participant.champion_id)} for {player.name}"
        )

        logging.info(log_message)
        await ctx.send(log_message,
                       delete_after=self.bot.short_notice_duration)
Пример #17
0
async def test_player(caplog):
    """
    Tests the addition of the Bot user as a player itself.
    Also tests PlayerRating and makes sure the on_cascade flags are applied properly.
    """
    caplog.set_level(logging.INFO)

    from inhouse_bot.common_utils import discord_token
    from inhouse_bot.sqlite.player_rating import PlayerRating
    from inhouse_bot.sqlite.sqlite_utils import get_session
    from inhouse_bot.sqlite.player import Player

    client = discord.Client()
    await client.login(discord_token)
    test_user = await client.fetch_user(707853630681907253)
    await client.close()

    session = get_session()

    player = Player(test_user)

    player = session.merge(player)
    session.commit()

    assert player == session.query(Player).filter(
        Player.discord_id == test_user.id).one()

    player_rating = PlayerRating(player, 'mid')

    session.add(player_rating)
    session.commit()

    assert player.ratings

    session.delete(player)
    session.commit()

    assert session.query(Player).filter(
        Player.discord_id == test_user.id).one_or_none() is None
    assert session.query(PlayerRating).filter(
        PlayerRating.player_id == test_user.id).one_or_none() is None
Пример #18
0
async def test_game(caplog):
    """
    Tests the addition of a game.
    """
    # TODO Make this test cleaner
    from inhouse_bot.inhouse_bot import InhouseBot
    from inhouse_bot.sqlite.sqlite_utils import get_session
    from inhouse_bot.sqlite.sqlite_utils import roles_list
    from inhouse_bot.sqlite.game import Game
    from inhouse_bot.sqlite.player import Player
    from inhouse_bot.sqlite.player_rating import PlayerRating

    session = get_session()

    # We create our bot object, a mock server, a mock channel, 10 mock members, and return our cog for testing
    bot = InhouseBot()
    dpytest.configure(bot, 1, 1, 10)

    players = {}
    for member_id in range(0, 10):
        role = roles_list[member_id % 5]
        player = Player(dpytest.get_config().members[member_id])
        rating = PlayerRating(player, role)
        session.add(player)
        session.add(rating)
        session.commit()
        players["blue" if member_id % 2 else "red", role] = player

    game = Game(players)
    session.add(game)

    # Printing ahead of time
    try:
        print(game)
    except AttributeError:
        assert True

    session.commit()
    print(game)
Пример #19
0
    def __init__(self, **options):
        super().__init__('!',
                         help_command=IndexedHelpCommand(dm_help=True),
                         **options)

        self.discord_token = discord_token

        self.players_session = get_session()

        # Local imports to not have circular imports with type hinting
        from inhouse_bot.cogs.queue_cog import QueueCog
        from inhouse_bot.cogs.stats_cog import StatsCog

        self.add_cog(QueueCog(self))
        self.add_cog(StatsCog(self))

        self.role_not_understood = 'Role name was not properly understood. ' \
                                   'Working values are top, jungle, mid, bot, and support.'

        self.short_notice_duration = 10
        self.validation_duration = 60
        self.warning_duration = 30
Пример #20
0
    async def champion(self, ctx, champion_name, game_id=None):
        """
        Informs the champion you used for the chosen game (or the last game by default)

        Example:
            !champion riven
            !champion riven 1
        """
        champion_id, ratio = self.bot.lit.get_id(champion_name, input_type='champion', return_ratio=True)

        if ratio < 75:
            await ctx.send('Champion name was not understood properly.\n'
                           'Use `!help champion` for more information.',
                           delete_after=self.bot.warning_duration)
            return

        player = await self.bot.get_player(ctx)
        session = get_session()

        if game_id:
            game, participant = session.query(Game, GameParticipant).join(GameParticipant) \
                .filter(Game.id == game_id) \
                .filter(GameParticipant.player_id == player.discord_id) \
                .order_by(Game.date.desc()) \
                .first()
        else:
            game, participant = player.get_last_game()

        participant.champion_id = champion_id

        session.merge(participant)
        session.commit()

        log_message = f'Champion for game {game.id} set to {self.bot.lit.get_name(participant.champion_id)} for {player.name}'

        logging.info(log_message)
        await ctx.send(log_message, delete_after=self.bot.short_notice_duration)
Пример #21
0
def score_game(player_ids: Dict[Tuple[str, str], int], winner):
    """
    players: ("red", "top") -> discord_id
    """
    session = get_session()

    player_objects = session.query(Player).filter(
        Player.discord_id.in_(player_ids.values())).all()

    players = {}

    for k, v in player_ids.items():
        print(v)
        players[k] = next(p for p in player_objects if p.discord_id == v)

    # Changed for debugging
    # players = {k: next(p for p in player_objects if p.discord_id == v) for k, v in player_ids.items()}

    game = Game(players)
    session.add(game)

    # Deleting test
    # session.query(QueuePlayer).filter(QueuePlayer.player_id.in_(player_ids.values())).delete(
    #     synchronize_session=False
    # )
    # session.commit()
    # session.close()
    #
    # return

    # Necessary to get IDs
    session.flush()
    game.winner = winner
    game.update_trueskill()

    session.close()
Пример #22
0
    async def checkmark_validation(
        self,
        message: discord.Message,
        validating_members: list,
        validation_threshold: int,
        timeout=120.0,
        queue=False,
    ) -> Tuple[Optional[bool], set]:
        """
        Implements a checkmark validation on the chosen message.

        Returns True if validation_threshold members in validating_members pressed '✅' before the timeout.
        """
        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.
            # Queue logic is handled below
            return (received_reaction.message.id == message.id
                    and sending_user.id in validating_members
                    and str(received_reaction.emoji) in ["✅", "❎"])

        members_who_validated = set()
        try:
            # TODO Remove that while True for something smarter
            while True:
                reaction, user = await self.bot.wait_for("reaction_add",
                                                         timeout=timeout,
                                                         check=check)

                if str(reaction.emoji) == "✅":
                    if queue:
                        session = get_session()
                        session.query(QueuePlayer).filter(
                            QueuePlayer.player_id == user.id).filter(
                                QueuePlayer.channel_id ==
                                message.channel.id).update(
                                    {"ready_check": True},
                                    synchronize_session=False)
                        session.commit()
                        session.close()

                    members_who_validated.add(user.id)
                    if members_who_validated.__len__() >= validation_threshold:
                        return True, members_who_validated

                elif str(reaction.emoji) == "❎":
                    if queue:
                        session = get_session()
                        session.query(QueuePlayer).filter(
                            QueuePlayer.player_id == user.id).filter(
                                QueuePlayer.channel_id == message.channel.id
                            ).delete(synchronize_session=False)
                        session.commit()
                        session.close()

                    return False, members_who_validated

        # We get there if no player accepted in the last x minutes
        except asyncio.TimeoutError:
            return None, members_who_validated
Пример #23
0
    def find_best_game(
            channel_id) -> Tuple[Dict[Tuple[str, str], Player], int]:
        """
        Looks at the queue in the channel and returns the best match-made game (as a {team, role} -> Player dict).
        """
        # Getting players in queue
        session = get_session()

        players_in_queue = (session.query(QueuePlayer).options(
            joinedload(QueuePlayer.player)).filter(
                QueuePlayer.channel_id == channel_id).filter(
                    QueuePlayer.ready_check == None).all())

        queue = {}
        for role in roles_list:
            queue[role] = [
                p.player for p in players_in_queue if p.role == role
            ]

        # Do not do anything if there’s not at least 2 players in queue per role
        for role in queue:
            if len(queue[role]) < 2:
                logging.debug("Not enough players to start matchmaking")
                return {}, -1

        logging.info("Starting matchmaking process")

        # Simply testing all permutations because it should be pretty lightweight
        # TODO Spot mirrored team compositions (full blue/red -> red/blue) to not calculate them twice
        role_permutations = []
        for role in roles_list:
            role_permutations.append(
                [p for p in itertools.permutations(queue[role], 2)])

        # Very simple maximum search
        best_score = -1
        best_players = {}
        for team_composition in itertools.product(*role_permutations):
            # players: [team, role] -> Player
            players = {
                ("red" if tuple_idx else "blue", roles_list[role_idx]):
                players_tuple[tuple_idx]
                for role_idx, players_tuple in enumerate(team_composition)
                for tuple_idx in (0, 1)
            }
            # We check to make sure all 10 players are different
            if set(players.values()).__len__() != 10:
                continue

            # Defining the score as -|0.5-expected_blue_winrate| to be side-agnostic.
            score = -abs(0.5 - trueskill_blue_side_winrate(players))

            if score > best_score:
                best_players = players
                best_score = score
                # If the game is seen as being below 51% winrate for one side, we simply stop there
                if best_score > -0.01:
                    break

        session.close()

        logging.info(
            "The best match found had a score of {}".format(best_score))

        return best_players, best_score
Пример #24
0
    async def start_game(self,
                         ctx,
                         players: Dict[Tuple[str, str], Player],
                         mismatch=False):
        """
        Attempts to start the given game by pinging players and waiting for their reactions.
        """
        player_ids = [p.discord_id for p in players.values()]

        session = get_session()

        session.query(QueuePlayer).filter(
            QueuePlayer.player_id.in_(player_ids)).update(
                {"ready_check": False}, synchronize_session=False)

        session.commit()
        session.close()

        await self.send_queue(ctx)

        ready, ready_players = await self.ready_check(ctx, players, mismatch)

        if ready is True:
            game = Game(players)

            session = get_session()
            # We create the game
            session.add(game)

            # We drop the players from queue
            session.query(QueuePlayer).filter(
                QueuePlayer.player_id.in_(player_ids)).delete(
                    synchronize_session=False)
            session.commit()
            session.close()

            validation_message = (
                f"Game has been accepted and can start!\n"
                f"Score it with `!won` or `!lost` after it has been played.")

            logging.info(validation_message)
            await ctx.send(validation_message)

            await self.send_queue(ctx)

        # The queue failed
        else:
            if ready is False:
                # Someone refused
                session = get_session()

                message = (
                    f"The game has been cancelled.\n"
                    f"The player has been removed from queue and others have been put back in queue."
                )

            else:
                # It timed out, so we quick the players who did not accept
                session = get_session()

                session.query(QueuePlayer).filter(
                    QueuePlayer.ready_check == False).delete()

                message = f"The game has timed out.\n" f"Players who pressed ✅ have been put back in queue."

            # We get there only if ready is False or None
            session.query(QueuePlayer).filter(
                QueuePlayer.player_id.in_(player_ids)).update(
                    {"ready_check": None}, synchronize_session=False)

            session.commit()
            session.close()

            await ctx.send(message)

            # We return and restart matchmaking with the new queue
            await self.send_queue(ctx)
            return await self.matchmaking_process(ctx)