From eefe4d37c25680303ad9f1ffa42842d6180454f4 Mon Sep 17 00:00:00 2001 From: SimolZimol <70102430+SimolZimol@users.noreply.github.com> Date: Tue, 10 Feb 2026 18:20:57 +0100 Subject: [PATCH] modified: app.py --- app.py | 849 +++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 591 insertions(+), 258 deletions(-) diff --git a/app.py b/app.py index 0fa4c21..9e40bc7 100644 --- a/app.py +++ b/app.py @@ -461,6 +461,7 @@ async def init_database(): id INT AUTO_INCREMENT PRIMARY KEY, game_name VARCHAR(255) NOT NULL, game_type VARCHAR(50) NOT NULL, + game_mode VARCHAR(10) DEFAULT 'team', status VARCHAR(50) DEFAULT 'setup', players JSON NOT NULL, winner_team VARCHAR(255), @@ -480,6 +481,16 @@ async def init_database(): # Column already exists, ignore error pass + # Add game_mode column if it doesn't exist (for existing databases) + try: + await cursor.execute(''' + ALTER TABLE games + ADD COLUMN game_mode VARCHAR(10) DEFAULT 'team' + ''') + except: + # Column already exists, ignore error + pass + # Create game_results table (MySQL syntax) await cursor.execute(''' CREATE TABLE IF NOT EXISTS game_results ( @@ -638,9 +649,12 @@ GAME_NOTIFICATION_CHANNEL_ID = 1432368177685332030 async def create_game_notification_embed(game_data: Dict, players_data: List[Dict]) -> discord.Embed: """Create an embed for game notifications""" + game_mode = game_data.get('game_mode', 'team') + mode_label = "Team-Based" if game_mode == 'team' else "Free-For-All" + embed = discord.Embed( title=f"🎮 {game_data['game_name']}", - description=f"**Type:** {game_data['game_type'].title()}\n**Status:** Setup Phase", + description=f"**Type:** {game_data['game_type'].title()}\n**Mode:** {mode_label}\n**Status:** Setup Phase", color=discord.Color.blue() ) @@ -648,34 +662,58 @@ async def create_game_notification_embed(game_data: Dict, players_data: List[Dic embed.add_field(name="Players", value="No players yet", inline=False) return embed - # Group players by team - teams = {} - for p in players_data: - team = p['team_name'] - if team not in teams: - teams[team] = [] - teams[team].append(p) - - # Add team fields - for team_name, members in teams.items(): - team_emoji = "🎖️" # Simple fallback since we don't have ctx here - avg_elo = sum(m['current_elo'] for m in members) / len(members) if members else 0 + if game_mode == 'team': + # Group players by team + teams = {} + for p in players_data: + team = p['team_name'] + if team not in teams: + teams[team] = [] + teams[team].append(p) - field_name = f"{team_emoji} {team_name} (avg {avg_elo:.0f})" + # Add team fields + for team_name, members in teams.items(): + team_emoji = "🎖️" # Simple fallback since we don't have ctx here + avg_elo = sum(m['current_elo'] for m in members) / len(members) if members else 0 + + field_name = f"{team_emoji} {team_name} (avg {avg_elo:.0f})" + + lines = [] + for m in sorted(members, key=lambda mm: (-mm.get('t_level', 2), mm['username'])): + t_level = m.get('t_level', 2) + t_emoji = {1: "🔹", 2: "🔸", 3: "🔺"}.get(t_level, "🔹") + + country = m.get('country') + country_text = f" [{country}]" if country else "" + + lines.append(f"{t_emoji} {m['username']}{country_text} ({m['current_elo']})") + + embed.add_field(name=field_name, value="\n".join(lines), inline=True) + + embed.set_footer(text=f"Players: {len(players_data)} | Teams: {len(teams)}") + else: # FFA mode + # List all players individually + avg_elo = sum(p['current_elo'] for p in players_data) / len(players_data) if players_data else 0 lines = [] - for m in sorted(members, key=lambda mm: (-mm.get('t_level', 2), mm['username'])): - t_level = m.get('t_level', 2) + for p in sorted(players_data, key=lambda pp: (-pp.get('current_elo', 0))): + t_level = p.get('t_level', 2) t_emoji = {1: "🔹", 2: "🔸", 3: "🔺"}.get(t_level, "🔹") - country = m.get('country') + country = p.get('country') country_text = f" [{country}]" if country else "" - lines.append(f"{t_emoji} {m['username']}{country_text} ({m['current_elo']})") + lines.append(f"{t_emoji} {p['username']}{country_text} ({p['current_elo']})") - embed.add_field(name=field_name, value="\n".join(lines), inline=True) + # Split into multiple fields if too many players (max 1024 chars per field) + field_size = 15 # players per field + for i in range(0, len(lines), field_size): + chunk = lines[i:i+field_size] + field_name = f"⚔️ Players {i+1}-{min(i+field_size, len(lines))}" if len(lines) > field_size else f"⚔️ Players (avg {avg_elo:.0f})" + embed.add_field(name=field_name, value="\n".join(chunk), inline=True) + + embed.set_footer(text=f"Players: {len(players_data)} | Mode: FFA") - embed.set_footer(text=f"Players: {len(players_data)} | Teams: {len(teams)}") return embed async def update_game_notification(game_data: Dict, players_data: List[Dict]): @@ -717,12 +755,16 @@ async def update_game_notification(game_data: Dict, players_data: List[Dict]): # HOI4 ELO Commands @bot.hybrid_command(name='hoi4create', description='Create a new HOI4 game') -async def hoi4create(ctx, game_type: str, game_name: str): - """Create a new HOI4 game""" +async def hoi4create(ctx, game_type: str, game_name: str, mode: str = 'team'): + """Create a new HOI4 game (mode: 'team' for team-based or 'ffa' for free-for-all)""" if game_type.lower() not in ['standard', 'competitive']: await ctx.send("❌ Game type must be either 'standard' or 'competitive'") return + if mode.lower() not in ['team', 'ffa']: + await ctx.send("❌ Game mode must be either 'team' (team-based) or 'ffa' (free-for-all)") + return + try: async with db_pool.acquire() as conn: async with conn.cursor(aiomysql.DictCursor) as cursor: @@ -739,8 +781,8 @@ async def hoi4create(ctx, game_type: str, game_name: str): # Create new game await cursor.execute( - "INSERT INTO games (game_name, game_type, status, players) VALUES (%s, %s, 'setup', %s)", - (game_name, game_type.lower(), '[]') + "INSERT INTO games (game_name, game_type, game_mode, status, players) VALUES (%s, %s, %s, 'setup', %s)", + (game_name, game_type.lower(), mode.lower(), '[]') ) # Get the created game data for notification @@ -750,6 +792,7 @@ async def hoi4create(ctx, game_type: str, game_name: str): ) game_data = await cursor.fetchone() + mode_label = "Team-Based" if mode.lower() == 'team' else "Free-For-All" embed = discord.Embed( title="🎮 Game Created", description=f"HOI4 {game_type.title()} game '{game_name}' has been created!", @@ -757,7 +800,8 @@ async def hoi4create(ctx, game_type: str, game_name: str): ) embed.add_field(name="Game Name", value=game_name, inline=True) embed.add_field(name="Type", value=game_type.title(), inline=True) - embed.add_field(name="Status", value="Setup Phase", inline=True) + embed.add_field(name="Mode", value=mode_label, inline=True) + embed.add_field(name="Status", value="Setup Phase", inline=False) embed.set_footer(text="Use /hoi4setup to add players to this game") await ctx.send(embed=embed) @@ -865,8 +909,11 @@ async def hoi4remove(ctx, game_name: str, user: discord.Member): await ctx.send(f"❌ Error removing player: {str(e)}") @bot.hybrid_command(name='hoi4setup', description='Add a player to an existing game') -async def hoi4setup(ctx, game_name: str, user: discord.Member, team_name: str, t_level: int, country: Optional[str] = None, modifier: Optional[str] = None): - """Add a player to an existing game. Use modifier='--force' to bypass MP ban.""" +async def hoi4setup(ctx, game_name: str, user: discord.Member, t_level: int, team_name: Optional[str] = None, country: Optional[str] = None, modifier: Optional[str] = None): + """Add a player to an existing game. + In team mode: team_name is required. + In FFA mode: team_name is optional (each player fights individually). + Use modifier='--force' to bypass MP ban.""" if t_level not in [1, 2, 3]: await ctx.send("❌ T-Level must be 1, 2, or 3") return @@ -901,6 +948,12 @@ async def hoi4setup(ctx, game_name: str, user: discord.Member, team_name: str, t await ctx.send(f"❌ No game found with name '{game_name}' in setup phase!") return + # Check mode and team_name requirements + game_mode = game.get('game_mode', 'team') + if game_mode == 'team' and not team_name: + await ctx.send("❌ This is a team-based game. Please provide a team_name!") + return + # Get or create player player = await get_or_create_player(user.id, user.display_name) @@ -917,11 +970,18 @@ async def hoi4setup(ctx, game_name: str, user: discord.Member, team_name: str, t player_data = { 'discord_id': user.id, 'username': user.display_name, - 'team_name': team_name, 't_level': t_level, 'current_elo': player[f"{game['game_type']}_elo"], 'country': country.strip() if country else None } + + # Add team_name if provided (for team mode or if specified in FFA) + if team_name: + player_data['team_name'] = team_name + else: + # In FFA mode, use player's username as team (for individual tracking) + player_data['team_name'] = user.display_name + players.append(player_data) # Update game @@ -936,7 +996,10 @@ async def hoi4setup(ctx, game_name: str, user: discord.Member, team_name: str, t color=discord.Color.green() ) embed.add_field(name="Player", value=user.display_name, inline=True) - embed.add_field(name="Team", value=team_name, inline=True) + if game_mode == 'team' or team_name: + embed.add_field(name="Team", value=player_data['team_name'], inline=True) + else: + embed.add_field(name="Mode", value="FFA (Individual)", inline=True) embed.add_field(name="T-Level", value=f"T{t_level}", inline=True) embed.add_field(name="Current ELO", value=player[f"{game['game_type']}_elo"], inline=True) if country: @@ -955,8 +1018,18 @@ async def hoi4setup(ctx, game_name: str, user: discord.Member, team_name: str, t await ctx.send(f"❌ Error adding player: {str(e)}") @bot.hybrid_command(name='hoi4end', description='End a game and calculate ELO changes') -async def hoi4end(ctx, game_name: str, winner_team: str): - """End a game and calculate ELO changes. Use 'draw' for ties.""" +async def hoi4end(ctx, game_name: str, winner_team: Optional[str] = None, winners: Optional[str] = None, losers: Optional[str] = None, draw_players: Optional[str] = None): + """End a game and calculate ELO changes. + + For TEAM mode: Use winner_team parameter (team name or 'draw'). + For FFA mode: Use winners/losers/draw_players (mention players separated by spaces). + + Examples: + - Team mode: /hoi4end game_name:MyGame winner_team:Axis + - Team mode draw: /hoi4end game_name:MyGame winner_team:draw + - FFA mode: /hoi4end game_name:MyGame winners:@user1 @user2 losers:@user3 @user4 + - FFA mode with draw: /hoi4end game_name:MyGame winners:@user1 draw_players:@user2 @user3 losers:@user4 + """ try: async with db_pool.acquire() as conn: async with conn.cursor(aiomysql.DictCursor) as cursor: @@ -977,205 +1050,426 @@ async def hoi4end(ctx, game_name: str, winner_team: str): await ctx.send("❌ Game needs at least 2 players to end!") return - # Check if winner team exists or if it's a draw - teams = {p['team_name'] for p in players} - is_draw = winner_team.lower() == 'draw' + game_mode = game.get('game_mode', 'team') - if not is_draw and winner_team not in teams: - available_teams = ', '.join(teams) - await ctx.send(f"❌ Team '{winner_team}' not found in game! Available teams: {available_teams}, draw") - return - - # Calculate team averages - team_elos = {} - team_players = {} - - for player in players: - team = player['team_name'] - if team not in team_elos: - team_elos[team] = [] - team_players[team] = [] + # ===== TEAM MODE ===== + if game_mode == 'team': + if not winner_team: + await ctx.send("❌ For team-based games, you must specify winner_team (team name or 'draw')!") + return - team_elos[team].append(player['current_elo']) - team_players[team].append(player) - - # Calculate average ELOs for each team - team_averages = {team: sum(elos) / len(elos) for team, elos in team_elos.items()} - - elo_changes = [] - - # Calculate ELO changes for each player - for player in players: - team = player['team_name'] + # Check if winner team exists or if it's a draw + teams = {p['team_name'] for p in players} + is_draw = winner_team.lower() == 'draw' + + if not is_draw and winner_team not in teams: + available_teams = ', '.join(teams) + await ctx.send(f"❌ Team '{winner_team}' not found in game! Available teams: {available_teams}, draw") + return + + # Calculate team averages + team_elos = {} + team_players = {} + + for player in players: + team = player['team_name'] + if team not in team_elos: + team_elos[team] = [] + team_players[team] = [] + + team_elos[team].append(player['current_elo']) + team_players[team].append(player) + + # Calculate average ELOs for each team + team_averages = {team: sum(elos) / len(elos) for team, elos in team_elos.items()} + + elo_changes = [] + + # Calculate ELO changes for each player + for player in players: + team = player['team_name'] + + # Determine result for this player + if is_draw: + result = 'draw' + elif team == winner_team: + result = 'win' + else: + result = 'loss' + + # Calculate opponent average (average of all other teams) + opponent_elos = [] + for other_team, elos in team_elos.items(): + if other_team != team: + opponent_elos.extend(elos) + + opponent_avg = sum(opponent_elos) / len(opponent_elos) if opponent_elos else player['current_elo'] + + elo_change = calculate_elo_change( + player['current_elo'], + opponent_avg, + result, + player['t_level'] + ) + + new_elo = max(0, player['current_elo'] + elo_change) # Prevent negative ELO + + elo_changes.append({ + 'discord_id': player['discord_id'], + 'username': player['username'], + 'team_name': team, + 't_level': player['t_level'], + 'old_elo': player['current_elo'], + 'new_elo': new_elo, + 'elo_change': elo_change, + 'result': result + }) + + # Update player ELOs and save game results + for change in elo_changes: + # Update player ELO + elo_field = f"{game['game_type']}_elo" + await cursor.execute( + f"UPDATE players SET {elo_field} = %s, updated_at = CURRENT_TIMESTAMP WHERE discord_id = %s", + (change['new_elo'], change['discord_id']) + ) + + # Save game result + won = change['result'] == 'win' + await cursor.execute( + """INSERT INTO game_results + (game_id, discord_id, team_name, t_level, old_elo, new_elo, elo_change, won, result_type) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)""", + (game['id'], change['discord_id'], change['team_name'], + change['t_level'], change['old_elo'], change['new_elo'], + change['elo_change'], won, change['result']) + ) + + # Mark game as finished + final_result = "Draw" if is_draw else winner_team + await cursor.execute( + "UPDATE games SET status = 'finished', winner_team = %s, finished_at = CURRENT_TIMESTAMP WHERE id = %s", + (final_result, game['id']) + ) + + # After DB updates, try to sync Discord roles for affected players (only for this game's category) + try: + guild = ctx.guild + if guild: + for change in elo_changes: + member = guild.get_member(change['discord_id']) + if member is None: + try: + member = await guild.fetch_member(change['discord_id']) + except Exception: + member = None + if member: + await update_member_elo_role( + member, + change['new_elo'], + game['game_type'], + reason=f"HOI4 {game['game_type']} ELO updated in '{game_name}'" + ) + except Exception as e: + logging.warning(f"Role sync after game end failed: {e}") + + # ===== FFA MODE ===== + else: # game_mode == 'ffa' + if not winners and not losers and not draw_players: + await ctx.send("❌ For FFA games, you must specify at least one of: winners, losers, or draw_players (mention players)!") + return + + # Parse user mentions from the parameters + def parse_user_ids(text): + """Extract user IDs from mentions or direct IDs""" + if not text: + return [] + ids = [] + import re + # Match <@123...> or <@!123...> mentions + mentions = re.findall(r'<@!?(\d+)>', text) + ids.extend([int(uid) for uid in mentions]) + # Also try to parse direct numbers + numbers = re.findall(r'(? 0 else "📉" if change['elo_change'] < 0 else "➡️" + team_text += f"{change['username']}: {change['old_elo']} → {change['new_elo']} ({change['elo_change']:+d}) {emoji}\n" + + # Team header with appropriate icon + if is_draw: + team_header = f"🤝 Team {team}" + elif team == winner_team: + team_header = f"🏆 Team {team}" + else: + team_header = f"💔 Team {team}" + + embed.add_field( + name=team_header, + value=team_text, + inline=False + ) - elo_change = calculate_elo_change( - player['current_elo'], - opponent_avg, - result, - player['t_level'] + embed.set_footer(text=f"Game Type: {game['game_type'].title()} | Mode: Team") + + else: # FFA mode + winner_names = [c['username'] for c in elo_changes if c['result'] == 'win'] + + embed = discord.Embed( + title="🏁 FFA Game Finished!", + description=f"Game '{game_name}' has ended!\n**Winners: {', '.join(winner_names) if winner_names else 'None'}**", + color=discord.Color.gold() ) - new_elo = max(0, player['current_elo'] + elo_change) # Prevent negative ELO + # Group by result type + result_groups = {'win': [], 'draw': [], 'loss': []} + for change in elo_changes: + result_groups[change['result']].append(change) - elo_changes.append({ - 'discord_id': player['discord_id'], - 'username': player['username'], - 'team_name': team, - 't_level': player['t_level'], - 'old_elo': player['current_elo'], - 'new_elo': new_elo, - 'elo_change': elo_change, - 'result': result - }) - - # Update player ELOs and save game results - for change in elo_changes: - # Update player ELO - elo_field = f"{game['game_type']}_elo" - await cursor.execute( - f"UPDATE players SET {elo_field} = %s, updated_at = CURRENT_TIMESTAMP WHERE discord_id = %s", - (change['new_elo'], change['discord_id']) - ) + if result_groups['win']: + win_text = "" + for change in sorted(result_groups['win'], key=lambda x: -x['elo_change']): + emoji = "📈" if change['elo_change'] > 0 else "📉" if change['elo_change'] < 0 else "➡️" + win_text += f"{change['username']}: {change['old_elo']} → {change['new_elo']} ({change['elo_change']:+d}) {emoji}\n" + embed.add_field(name="🏆 Winners", value=win_text, inline=False) - # Save game result - won = change['result'] == 'win' - await cursor.execute( - """INSERT INTO game_results - (game_id, discord_id, team_name, t_level, old_elo, new_elo, elo_change, won, result_type) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)""", - (game['id'], change['discord_id'], change['team_name'], - change['t_level'], change['old_elo'], change['new_elo'], - change['elo_change'], won, change['result']) - ) + if result_groups['draw']: + draw_text = "" + for change in sorted(result_groups['draw'], key=lambda x: -x['elo_change']): + emoji = "📈" if change['elo_change'] > 0 else "📉" if change['elo_change'] < 0 else "➡️" + draw_text += f"{change['username']}: {change['old_elo']} → {change['new_elo']} ({change['elo_change']:+d}) {emoji}\n" + embed.add_field(name="🤝 Draw", value=draw_text, inline=False) + + if result_groups['loss']: + loss_text = "" + for change in sorted(result_groups['loss'], key=lambda x: -x['elo_change']): + emoji = "📈" if change['elo_change'] > 0 else "📉" if change['elo_change'] < 0 else "➡️" + loss_text += f"{change['username']}: {change['old_elo']} → {change['new_elo']} ({change['elo_change']:+d}) {emoji}\n" + embed.add_field(name="💔 Losers", value=loss_text, inline=False) + + embed.set_footer(text=f"Game Type: {game['game_type'].title()} | Mode: FFA") - # Mark game as finished - final_result = "Draw" if is_draw else winner_team - await cursor.execute( - "UPDATE games SET status = 'finished', winner_team = %s, finished_at = CURRENT_TIMESTAMP WHERE id = %s", - (final_result, game['id']) - ) + await ctx.send(embed=embed) - # After DB updates, try to sync Discord roles for affected players (only for this game's category) + # Post final game result to notification channel try: - guild = ctx.guild - if guild: - for change in elo_changes: - member = guild.get_member(change['discord_id']) - if member is None: - try: - member = await guild.fetch_member(change['discord_id']) - except Exception: - member = None - if member: - await update_member_elo_role( - member, - change['new_elo'], - game['game_type'], - reason=f"HOI4 {game['game_type']} ELO updated in '{game_name}'" + channel = bot.get_channel(GAME_NOTIFICATION_CHANNEL_ID) + if channel: + if game_mode == 'team': + is_draw = winner_team.lower() == 'draw' + teams = {p['team_name'] for p in players} + teams_results = {} + for change in elo_changes: + team = change['team_name'] + if team not in teams_results: + teams_results[team] = [] + teams_results[team].append(change) + + final_embed = discord.Embed( + title=f"🏁 Game Finished: {game_name}", + description=f"**Result:** {'Draw' if is_draw else f'{winner_team} Victory'}", + color=discord.Color.gold() if not is_draw else discord.Color.orange() + ) + + final_embed.add_field(name="Game Type", value=game['game_type'].title(), inline=True) + final_embed.add_field(name="Mode", value="Team", inline=True) + final_embed.add_field(name="Players", value=len(players), inline=True) + + # Add team results + for team, team_changes in teams_results.items(): + avg_change = sum(c['elo_change'] for c in team_changes) / len(team_changes) + emoji = "🏆" if team == winner_team and not is_draw else "🤝" if is_draw else "💔" + final_embed.add_field( + name=f"{emoji} {team}", + value=f"{len(team_changes)} players\nAvg ELO change: {avg_change:+.1f}", + inline=True ) + else: # FFA mode + winner_names = [c['username'] for c in elo_changes if c['result'] == 'win'] + final_embed = discord.Embed( + title=f"🏁 FFA Game Finished: {game_name}", + description=f"**Winners:** {', '.join(winner_names) if winner_names else 'None'}", + color=discord.Color.gold() + ) + + final_embed.add_field(name="Game Type", value=game['game_type'].title(), inline=True) + final_embed.add_field(name="Mode", value="FFA", inline=True) + final_embed.add_field(name="Players", value=len(players), inline=True) + + # Count winners, draw, losers + win_count = len([c for c in elo_changes if c['result'] == 'win']) + draw_count = len([c for c in elo_changes if c['result'] == 'draw']) + loss_count = len([c for c in elo_changes if c['result'] == 'loss']) + + if win_count > 0: + final_embed.add_field(name="🏆 Winners", value=f"{win_count} players", inline=True) + if draw_count > 0: + final_embed.add_field(name="🤝 Draw", value=f"{draw_count} players", inline=True) + if loss_count > 0: + final_embed.add_field(name="💔 Losers", value=f"{loss_count} players", inline=True) + + await channel.send(embed=final_embed) except Exception as e: - logging.warning(f"Role sync after game end failed: {e}") - - # Create result embed - if is_draw: - embed = discord.Embed( - title="🤝 Game Finished!", - description=f"Game '{game_name}' has ended!\n**Result: Draw**", - color=discord.Color.orange() - ) - else: - embed = discord.Embed( - title="🏆 Game Finished!", - description=f"Game '{game_name}' has ended!\n**Winner: {winner_team}**", - color=discord.Color.gold() - ) - - # Group results by team - teams_results = {} - for change in elo_changes: - team = change['team_name'] - if team not in teams_results: - teams_results[team] = [] - teams_results[team].append(change) - - for team, team_changes in teams_results.items(): - team_text = "" - for change in team_changes: - emoji = "📈" if change['elo_change'] > 0 else "📉" if change['elo_change'] < 0 else "➡️" - result_emoji = "" - if change['result'] == 'win': - result_emoji = "🏆" - elif change['result'] == 'draw': - result_emoji = "🤝" - else: - result_emoji = "💔" - - team_text += f"{change['username']}: {change['old_elo']} → {change['new_elo']} ({change['elo_change']:+d}) {emoji}\n" - - # Team header with appropriate icon - if is_draw: - team_header = f"🤝 Team {team}" - elif team == winner_team: - team_header = f"🏆 Team {team}" - else: - team_header = f"💔 Team {team}" - - embed.add_field( - name=team_header, - value=team_text, - inline=False - ) - - embed.set_footer(text=f"Game Type: {game['game_type'].title()}") - - await ctx.send(embed=embed) - - # Post final game result to notification channel - try: - channel = bot.get_channel(GAME_NOTIFICATION_CHANNEL_ID) - if channel: - final_embed = discord.Embed( - title=f"🏁 Game Finished: {game_name}", - description=f"**Result:** {'Draw' if is_draw else f'{winner_team} Victory'}", - color=discord.Color.gold() if not is_draw else discord.Color.orange() - ) - - final_embed.add_field(name="Game Type", value=game['game_type'].title(), inline=True) - final_embed.add_field(name="Players", value=len(players), inline=True) - final_embed.add_field(name="Teams", value=len(teams), inline=True) - - # Add team results - for team, team_changes in teams_results.items(): - avg_change = sum(c['elo_change'] for c in team_changes) / len(team_changes) - emoji = "🏆" if team == winner_team and not is_draw else "🤝" if is_draw else "💔" - final_embed.add_field( - name=f"{emoji} {team}", - value=f"{len(team_changes)} players\nAvg ELO change: {avg_change:+.1f}", - inline=True - ) - - await channel.send(embed=final_embed) - except Exception as e: - logging.warning(f"Failed to post final game notification: {e}") + logging.warning(f"Failed to post final game notification: {e}") except Exception as e: await ctx.send(f"❌ Error ending game: {str(e)}") + import traceback + traceback.print_exc() @bot.hybrid_command(name='hoi4stats', description='Show your HOI4 ELO statistics') async def hoi4stats(ctx, user: Optional[discord.Member] = None): @@ -1346,28 +1640,92 @@ async def hoi4games(ctx): for game in games: players = json.loads(game['players']) if game['players'] else [] + game_mode = game.get('game_mode', 'team') + mode_label = "Team" if game_mode == 'team' else "FFA" - # Build team structures - teams: Dict[str, List[Dict]] = {} - for p in players: - teams.setdefault(p['team_name'], []).append(p) + # Add a header field per game + embed.add_field( + name=f"🎯 {game['game_name']} ({game['game_type'].title()} - {mode_label})", + value=f"Players: {len(players)}", + inline=False + ) - # Compute average ELO per team - team_avgs = {t: (sum(m['current_elo'] for m in mlist) / len(mlist)) if mlist else 0 for t, mlist in teams.items()} + if game_mode == 'team': + # Build team structures + teams: Dict[str, List[Dict]] = {} + for p in players: + teams.setdefault(p['team_name'], []).append(p) - # Sort teams by name to keep stable order - ordered_teams = sorted(teams.items(), key=lambda x: x[0].lower()) + # Compute average ELO per team + team_avgs = {t: (sum(m['current_elo'] for m in mlist) / len(mlist)) if mlist else 0 for t, mlist in teams.items()} - # Helper to format a team's field - def build_team_field(team_name: str, members: List[Dict]) -> Dict[str, str]: - t_emoji = get_team_emoji(ctx, team_name) - avg = team_avgs.get(team_name, 0) - name = f"{t_emoji} {team_name} (avg {avg:.0f})" - # Each player line: T-level emoji, name, elo + # Sort teams by name to keep stable order + ordered_teams = sorted(teams.items(), key=lambda x: x[0].lower()) + + # Helper to format a team's field + def build_team_field(team_name: str, members: List[Dict]) -> Dict[str, str]: + t_emoji = get_team_emoji(ctx, team_name) + avg = team_avgs.get(team_name, 0) + name = f"{t_emoji} {team_name} (avg {avg:.0f})" + # Each player line: T-level emoji, name, elo + lines = [] + for m in sorted(members, key=lambda mm: (-mm.get('t_level', 2), mm['username'].lower())): + te = get_t_emoji(ctx, int(m.get('t_level', 2))) + ctry = m.get('country') + flag = get_country_emoji(ctx, ctry) if ctry else "" + label = get_country_label(ctry) if ctry else None + parts = [te] + if flag: + parts.append(flag) + if label: + parts.append(label) + parts.append(m['username']) + lines.append(f"{' '.join(parts)} ({m['current_elo']})") + value = "\n".join(lines) if lines else "No players yet" + # Discord field value max ~1024 chars; trim if necessary + if len(value) > 1000: + value = value[:997] + "..." + return {"name": name, "value": value} + + # If no teams yet, show empty placeholder for this game + if not ordered_teams: + embed.add_field( + name="Status", + value="No players yet", + inline=False + ) + continue + + # For two teams, show A vs B with a center VS field; otherwise, list each team inline + team_fields = [build_team_field(tn, members) for tn, members in ordered_teams] + + if len(team_fields) == 1: + f1 = team_fields[0] + embed.add_field(name=f1["name"], value=f1["value"], inline=False) + elif len(team_fields) >= 2: + f1, f2 = team_fields[0], team_fields[1] + embed.add_field(name=f1["name"], value=f1["value"], inline=True) + embed.add_field(name="⚔️ VS ⚔️", value="\u200b", inline=True) + embed.add_field(name=f2["name"], value=f2["value"], inline=True) + # If more teams exist, add them below + for extra in team_fields[2:]: + embed.add_field(name=extra["name"], value=extra["value"], inline=False) + + else: # FFA mode + if not players: + embed.add_field( + name="⚔️ Free-For-All", + value="No players yet", + inline=False + ) + continue + + # List all players sorted by ELO + avg_elo = sum(p['current_elo'] for p in players) / len(players) if players else 0 lines = [] - for m in sorted(members, key=lambda mm: (-mm.get('t_level', 2), mm['username'].lower())): - te = get_t_emoji(ctx, int(m.get('t_level', 2))) - ctry = m.get('country') + for p in sorted(players, key=lambda pp: -pp.get('current_elo', 0)): + te = get_t_emoji(ctx, int(p.get('t_level', 2))) + ctry = p.get('country') flag = get_country_emoji(ctx, ctry) if ctry else "" label = get_country_label(ctry) if ctry else None parts = [te] @@ -1375,44 +1733,19 @@ async def hoi4games(ctx): parts.append(flag) if label: parts.append(label) - parts.append(m['username']) - lines.append(f"{' '.join(parts)} ({m['current_elo']})") + parts.append(p['username']) + lines.append(f"{' '.join(parts)} ({p['current_elo']})") + value = "\n".join(lines) if lines else "No players yet" # Discord field value max ~1024 chars; trim if necessary if len(value) > 1000: value = value[:997] + "..." - return {"name": name, "value": value} - - # If no teams yet, show empty placeholder for this game - if not ordered_teams: + embed.add_field( - name=f"{game['game_name']} ({game['game_type'].title()})", - value="No players yet", + name=f"⚔️ Free-For-All (avg {avg_elo:.0f})", + value=value, inline=False ) - continue - - # For two teams, show A vs B with a center VS field; otherwise, list each team inline - team_fields = [build_team_field(tn, members) for tn, members in ordered_teams] - - # Add a header field per game - embed.add_field( - name=f"🎯 {game['game_name']} ({game['game_type'].title()})", - value=f"Players: {len(players)} | Teams: {len(ordered_teams)}", - inline=False - ) - - if len(team_fields) == 1: - f1 = team_fields[0] - embed.add_field(name=f1["name"], value=f1["value"], inline=False) - elif len(team_fields) >= 2: - f1, f2 = team_fields[0], team_fields[1] - embed.add_field(name=f1["name"], value=f1["value"], inline=True) - embed.add_field(name="⚔️ VS ⚔️", value="\u200b", inline=True) - embed.add_field(name=f2["name"], value=f2["value"], inline=True) - # If more teams exist, add them below - for extra in team_fields[2:]: - embed.add_field(name=extra["name"], value=extra["value"], inline=False) await ctx.send(embed=embed)