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)
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"))
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)
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"))
async def run_matchmaking_logic( self, ctx: commands.Context, ): """ Runs the matchmaking logic in the channel defined by the context Should only be called inside guilds """ queue = game_queue.GameQueue(ctx.channel.id) game = matchmaking_logic.find_best_game(queue) if not game: return elif game and game.matchmaking_score < 0.2: embed = game.get_embed(embed_type="GAME_FOUND", validated_players=[], bot=self.bot) # We notify the players and send the message ready_check_message = await ctx.send(content=game.players_ping, embed=embed, delete_after=60 * 15) # We mark the ready check as ongoing (which will be used to the queue) game_queue.start_ready_check( player_ids=game.player_ids_list, channel_id=ctx.channel.id, ready_check_message_id=ready_check_message.id, ) # We update the queue in all channels await queue_channel_handler.update_queue_channels( bot=self.bot, server_id=ctx.guild.id) # And then we wait for the validation try: ready, players_to_drop = await checkmark_validation( bot=self.bot, message=ready_check_message, validating_players_ids=game.player_ids_list, validation_threshold=10, game=game, ) # We catch every error here to make sure it does not become blocking except Exception as e: self.bot.logger.error(e) game_queue.cancel_ready_check( ready_check_id=ready_check_message.id, ids_to_drop=game.player_ids_list, server_id=ctx.guild.id, ) await ctx.send( "There was a bug with the ready-check message, all players have been dropped from queue\n" "Please queue again to restart the process") await queue_channel_handler.update_queue_channels( bot=self.bot, server_id=ctx.guild.id) return if ready is True: # We drop all 10 players from the queue game_queue.validate_ready_check(ready_check_message.id) # We commit the game to the database (without a winner) with session_scope() as session: session.expire_on_commit = False game = session.merge(game) # This gets us the game ID queue_channel_handler.mark_queue_related_message( await ctx.send(embed=game.get_embed("GAME_ACCEPTED"), )) elif ready is False: # We remove the player who cancelled game_queue.cancel_ready_check( ready_check_id=ready_check_message.id, ids_to_drop=players_to_drop, channel_id=ctx.channel.id, ) await ctx.send( f"A player cancelled the game and was removed from the queue\n" f"All other players have been put back in the queue", ) # We restart the matchmaking logic await self.run_matchmaking_logic(ctx) elif ready is None: # We remove the timed out players from *all* channels (hence giving server id) game_queue.cancel_ready_check( ready_check_id=ready_check_message.id, ids_to_drop=players_to_drop, server_id=ctx.guild.id, ) await ctx.send( "The check timed out and players who did not answer have been dropped from all queues", ) # We restart the matchmaking logic await self.run_matchmaking_logic(ctx) elif game and game.matchmaking_score >= 0.2: # One side has over 70% predicted winrate, we do not start anything await ctx.send( f"The best match found had a side with a {(.5 + game.matchmaking_score)*100:.1f}%" f" predicted winrate and was not started")
async def checkmark_validation( bot: Bot, message: discord.Message, validating_players_ids: List[int], validation_threshold: int = 10, timeout=180, game=None, ) -> Tuple[Optional[bool], Optional[Set[int]]]: """ Implements a checkmark validation on the chosen message. If given a game object, will update the message’s embed with validation marks 3 possible outcomes: True and None It was validated by the necessary number of players False with the the player who cancelled (in a list) It was cancelled and the player should be dropped None with a list of players who did not validate It timed out and the players who didn't validate should be dropped """ checkmark_logger.info( f"Starting validation message {message.id} with threshold {validation_threshold}" f" for players {' '.join(str(i) for i in validating_players_ids)}") queue_channel_handler.mark_queue_related_message(message) await message.add_reaction("✅") await message.add_reaction("❌") def check(received_reaction: discord.Reaction, sending_user: discord.User): # This check is simply used to see if a player in the game responded to the message. return (received_reaction.message.id == message.id and sending_user.id in validating_players_ids and str(received_reaction.emoji) in ["✅", "❌"]) ids_of_players_who_validated = set() # Default values that will be output in case of success result = True ids_to_drop = None try: while len(ids_of_players_who_validated) < validation_threshold: reaction, user = await bot.wait_for("reaction_add", timeout=timeout, check=check) # A player accepted, we keep him in memory if str(reaction.emoji) == "✅": ids_of_players_who_validated.add(user.id) checkmark_logger.info(f"Player {user.id} validated") if game: await message.edit(embed=game.get_embed( embed_type="GAME_FOUND", validated_players=ids_of_players_who_validated, bot=bot)) # A player cancels, we return it and will drop him elif str(reaction.emoji) == "❌": checkmark_logger.info( f"Player {user.id} cancelled, exiting validation") result, ids_to_drop = False, {user.id} break # We get there if no player accepted in the last x minutes except asyncio.TimeoutError: checkmark_logger.info( f"The validation timed out, {' '.join(str(i) for i in ids_of_players_who_validated)} validated" ) result, ids_to_drop = ( None, set(i for i in validating_players_ids if i not in ids_of_players_who_validated), ) checkmark_logger.info(f"Unmarking message {message.id} as queue related") queue_channel_handler.unmark_queue_related_message(message) return result, ids_to_drop
async def checkmark_validation( bot: Bot, message: discord.Message, validating_players_ids: List[int], validation_threshold: int = 10, timeout=120.0, game=None, ) -> Tuple[Optional[bool], Optional[Set[int]]]: """ Implements a checkmark validation on the chosen message. If given a game object, will update the message’s embed with validation marks 3 possible outcomes: True and None It was validated by the necessary number of players False with the the player who cancelled (in a list) It was cancelled and the player should be dropped None with a list of players who did not validate It timed out and the players who didn't validate should be dropped """ queue_channel_handler.mark_queue_related_message(message) await message.add_reaction("✅") await message.add_reaction("❌") def check(received_reaction: discord.Reaction, sending_user: discord.User): # This check is simply used to see if a player in the game responded to the message. return (received_reaction.message.id == message.id and sending_user.id in validating_players_ids and str(received_reaction.emoji) in ["✅", "❌"]) ids_of_players_who_validated = set() # Default values that will be output in case of success result = True ids_to_drop = None try: while len(ids_of_players_who_validated) < validation_threshold: reaction, user = await bot.wait_for("reaction_add", timeout=timeout, check=check) # A player accepted, we keep him in memory if str(reaction.emoji) == "✅": ids_of_players_who_validated.add(user.id) if game: # TODO MEDIUM PRIO cleanup code duplication embed = Embed( title="📢 Game found 📢", description= f"Blue side expected winrate is {game.blue_expected_winrate * 100:.1f}%\n" "If you are ready to play, press ✅\n" "If you cannot play, press ❌", ) await message.edit(embed=game.add_game_field( embed, ids_of_players_who_validated)) # A player cancels, we return it and will drop him elif str(reaction.emoji) == "❌": result, ids_to_drop = False, {user.id} break # We get there if no player accepted in the last x minutes except asyncio.TimeoutError: result, ids_to_drop = ( None, set(i for i in validating_players_ids if i not in ids_of_players_who_validated), ) # Finally, we arrive here only if the validation went through queue_channel_handler.unmark_queue_related_message(message) return result, ids_to_drop
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")