def update_player(self, player_name: str, win: bool): if not isinstance(win, bool): raise OneHeadException("Win parameter must be a valid bool.") if not isinstance(player_name, str): raise OneHeadException("Player Name not a valid string.") if self.player_exists(player_name) is False: raise OneHeadException(f"{player_name} cannot be found.") if win: self.db.update_item( Key={"name": player_name}, UpdateExpression="set win = win + :val, win_streak = win_streak + :val, loss_streak = :zero", ExpressionAttributeValues={":val": decimal.Decimal(1)}, ReturnValues="UPDATED_NEW", ) else: self.db.update_item( Key={"name": player_name}, UpdateExpression="set loss = loss + :val, win_streak = :zero, loss_streak = loss_streak + :val", ExpressionAttributeValues={":val": decimal.Decimal(1), ":zero": decimal.Decimal(0)}, ReturnValues="UPDATED_NEW", )
def _calculate_balance(self, adjusted=False): """ :param adjusted: Specifies whether to use the 'adjusted_mmr' field to balance or just the 'mmr' field. :return: Returns a matchup of two, five-man teams that are evenly (or as close to evenly) matched based on a rating value associated with each player. """ profiles = self._get_profiles() profile_count = len(profiles) if profile_count != 10: raise OneHeadException( f"Error: Only {profile_count} profiles could be found in database." ) if adjusted is False: mmr_field_name = "mmr" else: mmr_field_name = "adjusted_mmr" OneHeadStats.calculate_rating(profiles) OneHeadStats.calculate_adjusted_mmr(profiles) all_5_man_lineups = list(itertools.combinations( profiles, 5)) # Calculate all possible 5 man lineups. all_5v5_matchups = list(itertools.combinations( all_5_man_lineups, 2)) # Calculate all possible 5v5 matchups. unique_combinations = self._calculate_unique_team_combinations( all_5v5_matchups ) # Calculate all valid 5v5 matchups where players are unique to either Team 1 or Team 2. if not unique_combinations: raise OneHeadException( "No valid matchups could be calculated. Possible Duplicate Player Name." ) rating_differences = self._calculate_rating_differences( unique_combinations, mmr_field_name ) # Calculate the net rating difference between each 5v5 matchup. rating_differences_mapping = dict( enumerate(rating_differences, start=0)) # Map the net rating differences to a key. rating_differences_mapping = { k: v for k, v in sorted(rating_differences_mapping.items(), key=lambda item: item[1]) } # Sort by ascending net rating difference. indices = list( rating_differences_mapping.keys() )[: 10] # Obtain the indices for the top 10 closest net rating matchups. balanced_teams = unique_combinations[random.choice( indices )] # Pick a random matchup from the top 10 closest net rating matchups. return balanced_teams
def remove_player(self, player_name: str): if not isinstance(player_name, str): raise OneHeadException("Player name not a valid string.") if self.player_exists(player_name): try: self.db.delete_item(Key={"name": player_name}) except ClientError as e: raise OneHeadException(e)
async def move_discord_channels(self, ctx: commands.Context): """ Move players to IHL Team Channels. :param ctx: Discord Context """ channel_count = len(self.ihl_discord_channels) if channel_count != 2: raise OneHeadException( f"Expected 2 Discord Channels, Identified {channel_count}.") t1_names, t2_names = OneHeadCommon.get_player_names(self.t1, self.t2) self._get_discord_members(ctx, t1_names, t2_names) await ctx.send("Moving Players to IHL Discord Channels...") t1_channel, t2_channel = self.ihl_discord_channels for member in self.t1_discord_members: try: await member.move_to(t1_channel) except discord.errors.HTTPException: pass for member in self.t2_discord_members: try: await member.move_to(t2_channel) except discord.errors.HTTPException: pass
def __init__(self, bot: commands.Bot, token: str): self.game_in_progress = False self.t1 = [] # type: list[dict] self.t2 = [] # type: list[dict] self.bot = bot self.token = token self.config = OneHeadCommon.load_config() self.database = bot.get_cog("OneHeadDB") self.scoreboard = bot.get_cog("OneHeadScoreBoard") self.pre_game = bot.get_cog("OneHeadPreGame") self.team_balance = bot.get_cog("OneHeadBalance") self.captains_mode = bot.get_cog("OneHeadCaptainsMode") self.channels = bot.get_cog("OneHeadChannels") self.registration = bot.get_cog("OneHeadRegistration") if None in ( self.database, self.scoreboard, self.pre_game, self.team_balance, self.captains_mode, self.channels, self.registration, ): raise OneHeadException("Unable to find cog(s)")
def add_player(self, player_name: str, mmr: int): if not isinstance(player_name, str): raise OneHeadException("Player Name not a valid string.") if self.player_exists(player_name) is False: self.db.put_item( Item={"name": player_name, "win": 0, "loss": 0, "mmr": mmr, "win_streak": 0, "loss_streak": 0} )
def lookup_player(self, player_name: str) -> dict: try: response = self.db.get_item(Key={"name": player_name}) except ClientError as e: raise OneHeadException(e) else: item = response.get("Item") player = json.dumps(item, indent=4, cls=DecimalEncoder) player = json.loads(player) return player
def player_exists(self, player_name: str) -> bool: try: response = self.db.get_item(Key={"name": player_name}) except ClientError as e: raise OneHeadException(e) else: if response.get("Item"): return True else: return False
def retrieve_table(self) -> list[dict]: try: response = self.db.scan() except ClientError as e: raise OneHeadException(e) else: table = response.get("Items") table = json.dumps(table, indent=4, cls=DecimalEncoder) table = json.loads(table) return table
async def register(self, ctx: commands.Context, mmr: str): """ Register yourself to the IHL by typing !register <your mmr>. """ name = ctx.author.display_name try: mmr = int(mmr) except ValueError: raise OneHeadException(f"{mmr} is not a valid integer.") if mmr < 1000: await ctx.send(f"{mmr} MMR is too low, must be greater or equal to 1000.") return if self.database.player_exists(name) is False: self.database.add_player(ctx.author.display_name, mmr) await ctx.send(f"{name} successfully registered.") else: await ctx.send(f"{name} is already registered.")
def _get_scoreboard(self) -> str: """ Returns current scoreboard for the IHL. :return: Scoreboard string to be displayed in Discord chat. """ scoreboard = self.db.retrieve_table() if not scoreboard: raise OneHeadException("No users found in database.") OneHeadStats.calculate_win_percentage(scoreboard) OneHeadStats.calculate_rating(scoreboard) scoreboard_sorted_rows = self._calculate_positions( scoreboard, "rating") scoreboard_sorted_rows_and_columns = self._sort_scoreboard_key_order( scoreboard_sorted_rows) sorted_scoreboard = tabulate(scoreboard_sorted_rows_and_columns, headers="keys", tablefmt="simple") return sorted_scoreboard
async def picking_phase(self, ctx: commands.Context): """ Initiates the picking phase where Captains can select which players they want to join their team using the '!pick' command. Each Captain has 30 seconds to select a player, if they fail to do this, a player will be randomly chosen for them. :param ctx: Discord context. :return: The teams selected by each Captain. :type: tuple of lists, each list contains 5 dicts corresponding to each player profile. """ self.remaining_players = self.pre_game.signups.copy() if not self.captain_1 or not self.captain_2: raise OneHeadException("Captains have not been selected.") for captain in (self.captain_1, self.captain_2): idx = self.remaining_players.index(captain) player = self.remaining_players.pop(idx) if captain == self.captain_1: self.team_1.append(player) else: self.team_2.append(player) self.pick_phase_in_progress = True await ctx.send("You have 30 seconds to pick a player each turn.") captain_round_order = ( self.captain_1, self.captain_2, self.captain_2, self.captain_1, self.captain_1, self.captain_2, self.captain_2, self.captain_1, ) while self.remaining_players: for round_number, captain in enumerate(captain_round_order): if captain == self.captain_1: self.captain_1_turn = True self.captain_2_turn = False else: self.captain_2_turn = True self.captain_1_turn = False await ctx.send(f"{captain}'s turn to pick.") await ctx.send( f"**Remaining Player Pool** : ```{self.remaining_players}```" ) self.future = self.event_loop.create_future() if round_number == 7: last_player = self.remaining_players.pop(0) self.team_1.append(last_player) await ctx.send( f"{last_player} has been automatically added to Team 2 as {last_player} was the last remaining player." ) else: try: await asyncio.wait_for(self.future, timeout=30) except TimeoutError: idx = self.remaining_players.index( random.choice(self.remaining_players)) pick = self.remaining_players.pop(idx) if captain == self.captain_1: self.team_1.append(pick) else: self.team_2.append(pick) await ctx.send( f"{captain} has randomed {pick} to join their team." ) self.pick_phase_in_progress = False self.captain_1_turn = False self.captain_2_turn = False t1 = [] t2 = [] for team in (self.team_1, self.team_2): for player in team: profile = self.database.lookup_player(player) if team == self.team_1: t1.append(profile) else: t2.append(profile) return t1, t2