modified: app.py
This commit is contained in:
833
app.py
833
app.py
@@ -461,6 +461,7 @@ async def init_database():
|
|||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
game_name VARCHAR(255) NOT NULL,
|
game_name VARCHAR(255) NOT NULL,
|
||||||
game_type VARCHAR(50) NOT NULL,
|
game_type VARCHAR(50) NOT NULL,
|
||||||
|
game_mode VARCHAR(10) DEFAULT 'team',
|
||||||
status VARCHAR(50) DEFAULT 'setup',
|
status VARCHAR(50) DEFAULT 'setup',
|
||||||
players JSON NOT NULL,
|
players JSON NOT NULL,
|
||||||
winner_team VARCHAR(255),
|
winner_team VARCHAR(255),
|
||||||
@@ -480,6 +481,16 @@ async def init_database():
|
|||||||
# Column already exists, ignore error
|
# Column already exists, ignore error
|
||||||
pass
|
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)
|
# Create game_results table (MySQL syntax)
|
||||||
await cursor.execute('''
|
await cursor.execute('''
|
||||||
CREATE TABLE IF NOT EXISTS game_results (
|
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:
|
async def create_game_notification_embed(game_data: Dict, players_data: List[Dict]) -> discord.Embed:
|
||||||
"""Create an embed for game notifications"""
|
"""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(
|
embed = discord.Embed(
|
||||||
title=f"🎮 {game_data['game_name']}",
|
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()
|
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)
|
embed.add_field(name="Players", value="No players yet", inline=False)
|
||||||
return embed
|
return embed
|
||||||
|
|
||||||
# Group players by team
|
if game_mode == 'team':
|
||||||
teams = {}
|
# Group players by team
|
||||||
for p in players_data:
|
teams = {}
|
||||||
team = p['team_name']
|
for p in players_data:
|
||||||
if team not in teams:
|
team = p['team_name']
|
||||||
teams[team] = []
|
if team not in teams:
|
||||||
teams[team].append(p)
|
teams[team] = []
|
||||||
|
teams[team].append(p)
|
||||||
|
|
||||||
# Add team fields
|
# Add team fields
|
||||||
for team_name, members in teams.items():
|
for team_name, members in teams.items():
|
||||||
team_emoji = "🎖️" # Simple fallback since we don't have ctx here
|
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
|
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})"
|
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 = []
|
lines = []
|
||||||
for m in sorted(members, key=lambda mm: (-mm.get('t_level', 2), mm['username'])):
|
for p in sorted(players_data, key=lambda pp: (-pp.get('current_elo', 0))):
|
||||||
t_level = m.get('t_level', 2)
|
t_level = p.get('t_level', 2)
|
||||||
t_emoji = {1: "🔹", 2: "🔸", 3: "🔺"}.get(t_level, "🔹")
|
t_emoji = {1: "🔹", 2: "🔸", 3: "🔺"}.get(t_level, "🔹")
|
||||||
|
|
||||||
country = m.get('country')
|
country = p.get('country')
|
||||||
country_text = f" [{country}]" if country else ""
|
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
|
return embed
|
||||||
|
|
||||||
async def update_game_notification(game_data: Dict, players_data: List[Dict]):
|
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
|
# HOI4 ELO Commands
|
||||||
@bot.hybrid_command(name='hoi4create', description='Create a new HOI4 game')
|
@bot.hybrid_command(name='hoi4create', description='Create a new HOI4 game')
|
||||||
async def hoi4create(ctx, game_type: str, game_name: str):
|
async def hoi4create(ctx, game_type: str, game_name: str, mode: str = 'team'):
|
||||||
"""Create a new HOI4 game"""
|
"""Create a new HOI4 game (mode: 'team' for team-based or 'ffa' for free-for-all)"""
|
||||||
if game_type.lower() not in ['standard', 'competitive']:
|
if game_type.lower() not in ['standard', 'competitive']:
|
||||||
await ctx.send("❌ Game type must be either 'standard' or 'competitive'")
|
await ctx.send("❌ Game type must be either 'standard' or 'competitive'")
|
||||||
return
|
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:
|
try:
|
||||||
async with db_pool.acquire() as conn:
|
async with db_pool.acquire() as conn:
|
||||||
async with conn.cursor(aiomysql.DictCursor) as cursor:
|
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
|
# Create new game
|
||||||
await cursor.execute(
|
await cursor.execute(
|
||||||
"INSERT INTO games (game_name, game_type, status, players) VALUES (%s, %s, 'setup', %s)",
|
"INSERT INTO games (game_name, game_type, game_mode, status, players) VALUES (%s, %s, %s, 'setup', %s)",
|
||||||
(game_name, game_type.lower(), '[]')
|
(game_name, game_type.lower(), mode.lower(), '[]')
|
||||||
)
|
)
|
||||||
|
|
||||||
# Get the created game data for notification
|
# 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()
|
game_data = await cursor.fetchone()
|
||||||
|
|
||||||
|
mode_label = "Team-Based" if mode.lower() == 'team' else "Free-For-All"
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title="🎮 Game Created",
|
title="🎮 Game Created",
|
||||||
description=f"HOI4 {game_type.title()} game '{game_name}' has been 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="Game Name", value=game_name, inline=True)
|
||||||
embed.add_field(name="Type", value=game_type.title(), 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")
|
embed.set_footer(text="Use /hoi4setup to add players to this game")
|
||||||
|
|
||||||
await ctx.send(embed=embed)
|
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)}")
|
await ctx.send(f"❌ Error removing player: {str(e)}")
|
||||||
|
|
||||||
@bot.hybrid_command(name='hoi4setup', description='Add a player to an existing game')
|
@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):
|
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. Use modifier='--force' to bypass MP ban."""
|
"""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]:
|
if t_level not in [1, 2, 3]:
|
||||||
await ctx.send("❌ T-Level must be 1, 2, or 3")
|
await ctx.send("❌ T-Level must be 1, 2, or 3")
|
||||||
return
|
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!")
|
await ctx.send(f"❌ No game found with name '{game_name}' in setup phase!")
|
||||||
return
|
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
|
# Get or create player
|
||||||
player = await get_or_create_player(user.id, user.display_name)
|
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 = {
|
player_data = {
|
||||||
'discord_id': user.id,
|
'discord_id': user.id,
|
||||||
'username': user.display_name,
|
'username': user.display_name,
|
||||||
'team_name': team_name,
|
|
||||||
't_level': t_level,
|
't_level': t_level,
|
||||||
'current_elo': player[f"{game['game_type']}_elo"],
|
'current_elo': player[f"{game['game_type']}_elo"],
|
||||||
'country': country.strip() if country else None
|
'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)
|
players.append(player_data)
|
||||||
|
|
||||||
# Update game
|
# Update game
|
||||||
@@ -936,7 +996,10 @@ async def hoi4setup(ctx, game_name: str, user: discord.Member, team_name: str, t
|
|||||||
color=discord.Color.green()
|
color=discord.Color.green()
|
||||||
)
|
)
|
||||||
embed.add_field(name="Player", value=user.display_name, inline=True)
|
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="T-Level", value=f"T{t_level}", inline=True)
|
||||||
embed.add_field(name="Current ELO", value=player[f"{game['game_type']}_elo"], inline=True)
|
embed.add_field(name="Current ELO", value=player[f"{game['game_type']}_elo"], inline=True)
|
||||||
if country:
|
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)}")
|
await ctx.send(f"❌ Error adding player: {str(e)}")
|
||||||
|
|
||||||
@bot.hybrid_command(name='hoi4end', description='End a game and calculate ELO changes')
|
@bot.hybrid_command(name='hoi4end', description='End a game and calculate ELO changes')
|
||||||
async def hoi4end(ctx, game_name: str, winner_team: str):
|
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. Use 'draw' for ties."""
|
"""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:
|
try:
|
||||||
async with db_pool.acquire() as conn:
|
async with db_pool.acquire() as conn:
|
||||||
async with conn.cursor(aiomysql.DictCursor) as cursor:
|
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!")
|
await ctx.send("❌ Game needs at least 2 players to end!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if winner team exists or if it's a draw
|
game_mode = game.get('game_mode', 'team')
|
||||||
teams = {p['team_name'] for p in players}
|
|
||||||
is_draw = winner_team.lower() == 'draw'
|
|
||||||
|
|
||||||
if not is_draw and winner_team not in teams:
|
# ===== TEAM MODE =====
|
||||||
available_teams = ', '.join(teams)
|
if game_mode == 'team':
|
||||||
await ctx.send(f"❌ Team '{winner_team}' not found in game! Available teams: {available_teams}, draw")
|
if not winner_team:
|
||||||
return
|
await ctx.send("❌ For team-based games, you must specify winner_team (team name or 'draw')!")
|
||||||
|
return
|
||||||
|
|
||||||
# Calculate team averages
|
# Check if winner team exists or if it's a draw
|
||||||
team_elos = {}
|
teams = {p['team_name'] for p in players}
|
||||||
team_players = {}
|
is_draw = winner_team.lower() == 'draw'
|
||||||
|
|
||||||
for player in players:
|
if not is_draw and winner_team not in teams:
|
||||||
team = player['team_name']
|
available_teams = ', '.join(teams)
|
||||||
if team not in team_elos:
|
await ctx.send(f"❌ Team '{winner_team}' not found in game! Available teams: {available_teams}, draw")
|
||||||
team_elos[team] = []
|
return
|
||||||
team_players[team] = []
|
|
||||||
|
|
||||||
team_elos[team].append(player['current_elo'])
|
# Calculate team averages
|
||||||
team_players[team].append(player)
|
team_elos = {}
|
||||||
|
team_players = {}
|
||||||
|
|
||||||
# Calculate average ELOs for each team
|
for player in players:
|
||||||
team_averages = {team: sum(elos) / len(elos) for team, elos in team_elos.items()}
|
team = player['team_name']
|
||||||
|
if team not in team_elos:
|
||||||
|
team_elos[team] = []
|
||||||
|
team_players[team] = []
|
||||||
|
|
||||||
elo_changes = []
|
team_elos[team].append(player['current_elo'])
|
||||||
|
team_players[team].append(player)
|
||||||
|
|
||||||
# Calculate ELO changes for each player
|
# Calculate average ELOs for each team
|
||||||
for player in players:
|
team_averages = {team: sum(elos) / len(elos) for team, elos in team_elos.items()}
|
||||||
team = player['team_name']
|
|
||||||
|
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'(?<![<@!])\b(\d{17,20})\b', text)
|
||||||
|
ids.extend([int(uid) for uid in numbers])
|
||||||
|
return list(set(ids)) # Remove duplicates
|
||||||
|
|
||||||
|
winner_ids = parse_user_ids(winners) if winners else []
|
||||||
|
loser_ids = parse_user_ids(losers) if losers else []
|
||||||
|
draw_ids = parse_user_ids(draw_players) if draw_players else []
|
||||||
|
|
||||||
|
# Validate all mentioned players are in the game
|
||||||
|
player_ids = {p['discord_id'] for p in players}
|
||||||
|
all_mentioned = set(winner_ids + loser_ids + draw_ids)
|
||||||
|
|
||||||
|
invalid_ids = all_mentioned - player_ids
|
||||||
|
if invalid_ids:
|
||||||
|
await ctx.send(f"❌ Some mentioned players are not in this game: {invalid_ids}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check for duplicate assignments
|
||||||
|
overlaps = []
|
||||||
|
if set(winner_ids) & set(loser_ids):
|
||||||
|
overlaps.append("winners and losers")
|
||||||
|
if set(winner_ids) & set(draw_ids):
|
||||||
|
overlaps.append("winners and draw")
|
||||||
|
if set(loser_ids) & set(draw_ids):
|
||||||
|
overlaps.append("losers and draw")
|
||||||
|
if overlaps:
|
||||||
|
await ctx.send(f"❌ Players cannot be in multiple categories: {', '.join(overlaps)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Assign results to all players (remaining are losers by default)
|
||||||
|
assigned_ids = set(winner_ids + loser_ids + draw_ids)
|
||||||
|
unassigned_ids = player_ids - assigned_ids
|
||||||
|
|
||||||
|
# Create result mapping
|
||||||
|
player_results = {}
|
||||||
|
for pid in winner_ids:
|
||||||
|
player_results[pid] = 'win'
|
||||||
|
for pid in loser_ids:
|
||||||
|
player_results[pid] = 'loss'
|
||||||
|
for pid in draw_ids:
|
||||||
|
player_results[pid] = 'draw'
|
||||||
|
# Unassigned players are losers
|
||||||
|
for pid in unassigned_ids:
|
||||||
|
player_results[pid] = 'loss'
|
||||||
|
|
||||||
|
# Calculate ELO changes for FFA mode
|
||||||
|
# In FFA: winners gain points based on average of all other players
|
||||||
|
# losers lose points, draw players have minimal change
|
||||||
|
all_elos = [p['current_elo'] for p in players]
|
||||||
|
avg_elo = sum(all_elos) / len(all_elos)
|
||||||
|
|
||||||
|
elo_changes = []
|
||||||
|
for player in players:
|
||||||
|
result = player_results[player['discord_id']]
|
||||||
|
|
||||||
|
# Calculate opponent average (all other players)
|
||||||
|
opponent_elos = [p['current_elo'] for p in players if p['discord_id'] != player['discord_id']]
|
||||||
|
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': player['team_name'], # Will be player's username in FFA
|
||||||
|
'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 (for FFA, winner_team is comma-separated winner names)
|
||||||
|
winner_names = [p['username'] for p in players if p['discord_id'] in winner_ids]
|
||||||
|
final_result = ', '.join(winner_names) if winner_names else "No winners"
|
||||||
|
await cursor.execute(
|
||||||
|
"UPDATE games SET status = 'finished', winner_team = %s, finished_at = CURRENT_TIMESTAMP WHERE id = %s",
|
||||||
|
(final_result, game['id'])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Sync Discord roles
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# ===== CREATE RESULT EMBED =====
|
||||||
|
if game_mode == 'team':
|
||||||
|
is_draw = winner_team.lower() == 'draw'
|
||||||
|
teams = {p['team_name'] for p in players}
|
||||||
|
|
||||||
# Determine result for this player
|
|
||||||
if is_draw:
|
if is_draw:
|
||||||
result = 'draw'
|
embed = discord.Embed(
|
||||||
elif team == winner_team:
|
title="🤝 Game Finished!",
|
||||||
result = 'win'
|
description=f"Game '{game_name}' has ended!\n**Result: Draw**",
|
||||||
|
color=discord.Color.orange()
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
result = 'loss'
|
embed = discord.Embed(
|
||||||
|
title="🏆 Game Finished!",
|
||||||
|
description=f"Game '{game_name}' has ended!\n**Winner: {winner_team}**",
|
||||||
|
color=discord.Color.gold()
|
||||||
|
)
|
||||||
|
|
||||||
# Calculate opponent average (average of all other teams)
|
# Group results by team
|
||||||
opponent_elos = []
|
teams_results = {}
|
||||||
for other_team, elos in team_elos.items():
|
for change in elo_changes:
|
||||||
if other_team != team:
|
team = change['team_name']
|
||||||
opponent_elos.extend(elos)
|
if team not in teams_results:
|
||||||
|
teams_results[team] = []
|
||||||
|
teams_results[team].append(change)
|
||||||
|
|
||||||
opponent_avg = sum(opponent_elos) / len(opponent_elos) if opponent_elos else player['current_elo']
|
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 "➡️"
|
||||||
|
team_text += f"{change['username']}: {change['old_elo']} → {change['new_elo']} ({change['elo_change']:+d}) {emoji}\n"
|
||||||
|
|
||||||
elo_change = calculate_elo_change(
|
# Team header with appropriate icon
|
||||||
player['current_elo'],
|
if is_draw:
|
||||||
opponent_avg,
|
team_header = f"🤝 Team {team}"
|
||||||
result,
|
elif team == winner_team:
|
||||||
player['t_level']
|
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()} | 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({
|
if result_groups['win']:
|
||||||
'discord_id': player['discord_id'],
|
win_text = ""
|
||||||
'username': player['username'],
|
for change in sorted(result_groups['win'], key=lambda x: -x['elo_change']):
|
||||||
'team_name': team,
|
emoji = "📈" if change['elo_change'] > 0 else "📉" if change['elo_change'] < 0 else "➡️"
|
||||||
't_level': player['t_level'],
|
win_text += f"{change['username']}: {change['old_elo']} → {change['new_elo']} ({change['elo_change']:+d}) {emoji}\n"
|
||||||
'old_elo': player['current_elo'],
|
embed.add_field(name="🏆 Winners", value=win_text, inline=False)
|
||||||
'new_elo': new_elo,
|
|
||||||
'elo_change': elo_change,
|
|
||||||
'result': result
|
|
||||||
})
|
|
||||||
|
|
||||||
# Update player ELOs and save game results
|
if result_groups['draw']:
|
||||||
for change in elo_changes:
|
draw_text = ""
|
||||||
# Update player ELO
|
for change in sorted(result_groups['draw'], key=lambda x: -x['elo_change']):
|
||||||
elo_field = f"{game['game_type']}_elo"
|
emoji = "📈" if change['elo_change'] > 0 else "📉" if change['elo_change'] < 0 else "➡️"
|
||||||
await cursor.execute(
|
draw_text += f"{change['username']}: {change['old_elo']} → {change['new_elo']} ({change['elo_change']:+d}) {emoji}\n"
|
||||||
f"UPDATE players SET {elo_field} = %s, updated_at = CURRENT_TIMESTAMP WHERE discord_id = %s",
|
embed.add_field(name="🤝 Draw", value=draw_text, inline=False)
|
||||||
(change['new_elo'], change['discord_id'])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Save game result
|
if result_groups['loss']:
|
||||||
won = change['result'] == 'win'
|
loss_text = ""
|
||||||
await cursor.execute(
|
for change in sorted(result_groups['loss'], key=lambda x: -x['elo_change']):
|
||||||
"""INSERT INTO game_results
|
emoji = "📈" if change['elo_change'] > 0 else "📉" if change['elo_change'] < 0 else "➡️"
|
||||||
(game_id, discord_id, team_name, t_level, old_elo, new_elo, elo_change, won, result_type)
|
loss_text += f"{change['username']}: {change['old_elo']} → {change['new_elo']} ({change['elo_change']:+d}) {emoji}\n"
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)""",
|
embed.add_field(name="💔 Losers", value=loss_text, inline=False)
|
||||||
(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
|
embed.set_footer(text=f"Game Type: {game['game_type'].title()} | Mode: FFA")
|
||||||
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)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
# Post final game result to notification channel
|
||||||
try:
|
try:
|
||||||
guild = ctx.guild
|
channel = bot.get_channel(GAME_NOTIFICATION_CHANNEL_ID)
|
||||||
if guild:
|
if channel:
|
||||||
for change in elo_changes:
|
if game_mode == 'team':
|
||||||
member = guild.get_member(change['discord_id'])
|
is_draw = winner_team.lower() == 'draw'
|
||||||
if member is None:
|
teams = {p['team_name'] for p in players}
|
||||||
try:
|
teams_results = {}
|
||||||
member = await guild.fetch_member(change['discord_id'])
|
for change in elo_changes:
|
||||||
except Exception:
|
team = change['team_name']
|
||||||
member = None
|
if team not in teams_results:
|
||||||
if member:
|
teams_results[team] = []
|
||||||
await update_member_elo_role(
|
teams_results[team].append(change)
|
||||||
member,
|
|
||||||
change['new_elo'],
|
final_embed = discord.Embed(
|
||||||
game['game_type'],
|
title=f"🏁 Game Finished: {game_name}",
|
||||||
reason=f"HOI4 {game['game_type']} ELO updated in '{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:
|
except Exception as e:
|
||||||
logging.warning(f"Role sync after game end failed: {e}")
|
logging.warning(f"Failed to post final game notification: {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}")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await ctx.send(f"❌ Error ending game: {str(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')
|
@bot.hybrid_command(name='hoi4stats', description='Show your HOI4 ELO statistics')
|
||||||
async def hoi4stats(ctx, user: Optional[discord.Member] = None):
|
async def hoi4stats(ctx, user: Optional[discord.Member] = None):
|
||||||
@@ -1346,28 +1640,92 @@ async def hoi4games(ctx):
|
|||||||
|
|
||||||
for game in games:
|
for game in games:
|
||||||
players = json.loads(game['players']) if game['players'] else []
|
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
|
# Add a header field per game
|
||||||
teams: Dict[str, List[Dict]] = {}
|
embed.add_field(
|
||||||
for p in players:
|
name=f"🎯 {game['game_name']} ({game['game_type'].title()} - {mode_label})",
|
||||||
teams.setdefault(p['team_name'], []).append(p)
|
value=f"Players: {len(players)}",
|
||||||
|
inline=False
|
||||||
|
)
|
||||||
|
|
||||||
# Compute average ELO per team
|
if game_mode == 'team':
|
||||||
team_avgs = {t: (sum(m['current_elo'] for m in mlist) / len(mlist)) if mlist else 0 for t, mlist in teams.items()}
|
# 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
|
# Compute average ELO per team
|
||||||
ordered_teams = sorted(teams.items(), key=lambda x: x[0].lower())
|
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
|
# Sort teams by name to keep stable order
|
||||||
def build_team_field(team_name: str, members: List[Dict]) -> Dict[str, str]:
|
ordered_teams = sorted(teams.items(), key=lambda x: x[0].lower())
|
||||||
t_emoji = get_team_emoji(ctx, team_name)
|
|
||||||
avg = team_avgs.get(team_name, 0)
|
# Helper to format a team's field
|
||||||
name = f"{t_emoji} {team_name} (avg {avg:.0f})"
|
def build_team_field(team_name: str, members: List[Dict]) -> Dict[str, str]:
|
||||||
# Each player line: T-level emoji, name, elo
|
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 = []
|
lines = []
|
||||||
for m in sorted(members, key=lambda mm: (-mm.get('t_level', 2), mm['username'].lower())):
|
for p in sorted(players, key=lambda pp: -pp.get('current_elo', 0)):
|
||||||
te = get_t_emoji(ctx, int(m.get('t_level', 2)))
|
te = get_t_emoji(ctx, int(p.get('t_level', 2)))
|
||||||
ctry = m.get('country')
|
ctry = p.get('country')
|
||||||
flag = get_country_emoji(ctx, ctry) if ctry else ""
|
flag = get_country_emoji(ctx, ctry) if ctry else ""
|
||||||
label = get_country_label(ctry) if ctry else None
|
label = get_country_label(ctry) if ctry else None
|
||||||
parts = [te]
|
parts = [te]
|
||||||
@@ -1375,44 +1733,19 @@ async def hoi4games(ctx):
|
|||||||
parts.append(flag)
|
parts.append(flag)
|
||||||
if label:
|
if label:
|
||||||
parts.append(label)
|
parts.append(label)
|
||||||
parts.append(m['username'])
|
parts.append(p['username'])
|
||||||
lines.append(f"{' '.join(parts)} ({m['current_elo']})")
|
lines.append(f"{' '.join(parts)} ({p['current_elo']})")
|
||||||
|
|
||||||
value = "\n".join(lines) if lines else "No players yet"
|
value = "\n".join(lines) if lines else "No players yet"
|
||||||
# Discord field value max ~1024 chars; trim if necessary
|
# Discord field value max ~1024 chars; trim if necessary
|
||||||
if len(value) > 1000:
|
if len(value) > 1000:
|
||||||
value = value[:997] + "..."
|
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(
|
embed.add_field(
|
||||||
name=f"{game['game_name']} ({game['game_type'].title()})",
|
name=f"⚔️ Free-For-All (avg {avg_elo:.0f})",
|
||||||
value="No players yet",
|
value=value,
|
||||||
inline=False
|
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)
|
await ctx.send(embed=embed)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user