diff --git a/app.py b/app.py index 9f13372..35aff92 100644 --- a/app.py +++ b/app.py @@ -192,11 +192,22 @@ async def init_database(): new_elo INT NOT NULL, elo_change INT NOT NULL, won BOOLEAN NOT NULL, + result_type VARCHAR(10) DEFAULT 'loss', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (game_id) REFERENCES games(id) ) ''') + # Add result_type column if it doesn't exist (for existing databases) + try: + await cursor.execute(''' + ALTER TABLE game_results + ADD COLUMN result_type VARCHAR(10) DEFAULT 'loss' + ''') + except: + # Column already exists, ignore error + pass + print("✅ Database initialized successfully") except Exception as e: @@ -233,15 +244,41 @@ async def get_or_create_player(discord_id: int, username: str) -> Dict: return dict(player) -def calculate_elo_change(player_elo: int, opponent_avg_elo: int, won: bool, t_level: int) -> int: - """Calculate ELO change using standard ELO formula with T-level multiplier""" - expected_score = 1 / (1 + 10 ** ((opponent_avg_elo - player_elo) / 400)) - actual_score = 1 if won else 0 +def calculate_elo_change(player_elo: int, opponent_avg_elo: int, result: str, t_level: int) -> int: + """Calculate ELO change using standard ELO formula with T-level multiplier + result can be 'win', 'loss', or 'draw' + In draws: + - If you're expected to win (higher ELO), you lose points for drawing + - If you're expected to lose (lower ELO), you gain points for drawing + - The bigger the ELO difference, the bigger the swing + """ + expected_score = 1 / (1 + 10 ** ((opponent_avg_elo - player_elo) / 400)) + + if result == 'win': + actual_score = 1.0 + elif result == 'draw': + actual_score = 0.5 # Draw = 50% score + else: # loss + actual_score = 0.0 + + # Calculate the base ELO change base_change = K_FACTOR * (actual_score - expected_score) + + # Apply T-level multiplier t_multiplier = T_LEVEL_MULTIPLIERS.get(t_level, 1.0) - return round(base_change * t_multiplier) + final_change = base_change * t_multiplier + + # For debugging/logging purposes, let's see what happens in draws + if result == 'draw': + elo_diff = player_elo - opponent_avg_elo + expected_percentage = expected_score * 100 + print(f"📊 Draw calculation: Player ELO {player_elo} vs Opponent Avg {opponent_avg_elo}") + print(f" ELO Difference: {elo_diff:+d} | Expected win chance: {expected_percentage:.1f}%") + print(f" ELO change: {final_change:+.1f} (T{t_level} multiplier: {t_multiplier})") + + return round(final_change) # Owner only decorator def is_owner(): @@ -406,7 +443,7 @@ async def hoi4setup(ctx, game_name: str, user: discord.Member, team_name: str, t @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""" + """End a game and calculate ELO changes. Use 'draw' for ties.""" try: async with db_pool.acquire() as conn: async with conn.cursor(aiomysql.DictCursor) as cursor: @@ -427,10 +464,13 @@ 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 + # Check if winner team exists or if it's a draw teams = {p['team_name'] for p in players} - if winner_team not in teams: - await ctx.send(f"❌ Team '{winner_team}' not found in game! Available teams: {', '.join(teams)}") + 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 @@ -454,7 +494,14 @@ async def hoi4end(ctx, game_name: str, winner_team: str): # Calculate ELO changes for each player for player in players: team = player['team_name'] - won = team == winner_team + + # 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 = [] @@ -467,7 +514,7 @@ async def hoi4end(ctx, game_name: str, winner_team: str): elo_change = calculate_elo_change( player['current_elo'], opponent_avg, - won, + result, player['t_level'] ) @@ -481,7 +528,7 @@ async def hoi4end(ctx, game_name: str, winner_team: str): 'old_elo': player['current_elo'], 'new_elo': new_elo, 'elo_change': elo_change, - 'won': won + 'result': result }) # Update player ELOs and save game results @@ -494,27 +541,36 @@ async def hoi4end(ctx, game_name: str, winner_team: str): ) # 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) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s)""", + (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'], change['won']) + 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", - (winner_team, game['id']) + (final_result, game['id']) ) # Create result embed - embed = discord.Embed( - title="🏆 Game Finished!", - description=f"Game '{game_name}' has ended!\n**Winner: {winner_team}**", - color=discord.Color.gold() - ) + 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 = {} @@ -528,10 +584,26 @@ async def hoi4end(ctx, game_name: str, winner_team: str): 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=f"{'🏆 ' if team == winner_team else ''}Team {team}", + name=team_header, value=team_text, inline=False ) @@ -575,16 +647,22 @@ async def hoi4stats(ctx, user: Optional[discord.Member] = None): total_players_result = await cursor.fetchone() total_players = total_players_result['total'] - # Get game statistics + # Get game statistics with proper draw detection await cursor.execute( - "SELECT COUNT(*) as total_games, SUM(won) as games_won FROM game_results WHERE discord_id = %s", + """SELECT + COUNT(*) as total_games, + SUM(CASE WHEN result_type = 'win' THEN 1 ELSE 0 END) as games_won, + SUM(CASE WHEN result_type = 'draw' THEN 1 ELSE 0 END) as games_drawn, + SUM(CASE WHEN result_type = 'loss' THEN 1 ELSE 0 END) as games_lost + FROM game_results WHERE discord_id = %s""", (target_user.id,) ) game_stats = await cursor.fetchone() total_games = game_stats['total_games'] or 0 games_won = game_stats['games_won'] or 0 - games_lost = total_games - games_won + games_drawn = game_stats['games_drawn'] or 0 + games_lost = game_stats['games_lost'] or 0 win_rate = (games_won / total_games * 100) if total_games > 0 else 0 # Create rank indicators with medals @@ -627,11 +705,20 @@ async def hoi4stats(ctx, user: Optional[discord.Member] = None): value=f"**{total_games}** total games", inline=True ) - embed.add_field( - name="📈 Win/Loss", - value=f"**{games_won}W** / **{games_lost}L**", - inline=True - ) + + if games_drawn > 0: + embed.add_field( + name="📊 W/D/L Record", + value=f"**{games_won}W** / **{games_drawn}D** / **{games_lost}L**", + inline=True + ) + else: + embed.add_field( + name="📈 Win/Loss", + value=f"**{games_won}W** / **{games_lost}L**", + inline=True + ) + embed.add_field( name="📊 Win Rate", value=f"**{win_rate:.1f}%**", @@ -841,19 +928,27 @@ async def hoi4leaderboard(ctx, game_type: Optional[str] = "standard", limit: Opt # Get additional player stats async with db_pool.acquire() as conn: async with conn.cursor(aiomysql.DictCursor) as cursor: - # Count games played + # Count games played with proper result detection await cursor.execute( - "SELECT COUNT(*) as games_played, SUM(won) as games_won FROM game_results WHERE discord_id = %s", + """SELECT + COUNT(*) as games_played, + SUM(CASE WHEN result_type = 'win' THEN 1 ELSE 0 END) as games_won, + SUM(CASE WHEN result_type = 'draw' THEN 1 ELSE 0 END) as games_drawn + FROM game_results WHERE discord_id = %s""", (player['discord_id'],) ) player_stats = await cursor.fetchone() games_played = player_stats['games_played'] or 0 games_won = player_stats['games_won'] or 0 + games_drawn = player_stats['games_drawn'] or 0 win_rate = (games_won / games_played * 100) if games_played > 0 else 0 leaderboard_text += f"{rank_indicator} **{username}** - {elo_value} ELO\n" - leaderboard_text += f" 📊 {games_played} games | {win_rate:.1f}% win rate\n\n" + if games_drawn > 0: + leaderboard_text += f" 📊 {games_played} games | {games_won}W-{games_drawn}D-{games_played - games_won - games_drawn}L | {win_rate:.1f}% win rate\n\n" + else: + leaderboard_text += f" 📊 {games_played} games | {win_rate:.1f}% win rate\n\n" embed.add_field( name="Rankings",