modified: app.py
This commit is contained in:
161
app.py
161
app.py
@@ -192,11 +192,22 @@ async def init_database():
|
|||||||
new_elo INT NOT NULL,
|
new_elo INT NOT NULL,
|
||||||
elo_change INT NOT NULL,
|
elo_change INT NOT NULL,
|
||||||
won BOOLEAN NOT NULL,
|
won BOOLEAN NOT NULL,
|
||||||
|
result_type VARCHAR(10) DEFAULT 'loss',
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (game_id) REFERENCES games(id)
|
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")
|
print("✅ Database initialized successfully")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -233,15 +244,41 @@ async def get_or_create_player(discord_id: int, username: str) -> Dict:
|
|||||||
|
|
||||||
return dict(player)
|
return dict(player)
|
||||||
|
|
||||||
def calculate_elo_change(player_elo: int, opponent_avg_elo: int, won: bool, t_level: int) -> int:
|
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"""
|
"""Calculate ELO change using standard ELO formula with T-level multiplier
|
||||||
expected_score = 1 / (1 + 10 ** ((opponent_avg_elo - player_elo) / 400))
|
result can be 'win', 'loss', or 'draw'
|
||||||
actual_score = 1 if won else 0
|
|
||||||
|
|
||||||
|
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)
|
base_change = K_FACTOR * (actual_score - expected_score)
|
||||||
|
|
||||||
|
# Apply T-level multiplier
|
||||||
t_multiplier = T_LEVEL_MULTIPLIERS.get(t_level, 1.0)
|
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
|
# Owner only decorator
|
||||||
def is_owner():
|
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')
|
@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: str):
|
||||||
"""End a game and calculate ELO changes"""
|
"""End a game and calculate ELO changes. Use 'draw' for ties."""
|
||||||
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:
|
||||||
@@ -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!")
|
await ctx.send("❌ Game needs at least 2 players to end!")
|
||||||
return
|
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}
|
teams = {p['team_name'] for p in players}
|
||||||
if winner_team not in teams:
|
is_draw = winner_team.lower() == 'draw'
|
||||||
await ctx.send(f"❌ Team '{winner_team}' not found in game! Available teams: {', '.join(teams)}")
|
|
||||||
|
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
|
return
|
||||||
|
|
||||||
# Calculate team averages
|
# Calculate team averages
|
||||||
@@ -454,7 +494,14 @@ async def hoi4end(ctx, game_name: str, winner_team: str):
|
|||||||
# Calculate ELO changes for each player
|
# Calculate ELO changes for each player
|
||||||
for player in players:
|
for player in players:
|
||||||
team = player['team_name']
|
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)
|
# Calculate opponent average (average of all other teams)
|
||||||
opponent_elos = []
|
opponent_elos = []
|
||||||
@@ -467,7 +514,7 @@ async def hoi4end(ctx, game_name: str, winner_team: str):
|
|||||||
elo_change = calculate_elo_change(
|
elo_change = calculate_elo_change(
|
||||||
player['current_elo'],
|
player['current_elo'],
|
||||||
opponent_avg,
|
opponent_avg,
|
||||||
won,
|
result,
|
||||||
player['t_level']
|
player['t_level']
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -481,7 +528,7 @@ async def hoi4end(ctx, game_name: str, winner_team: str):
|
|||||||
'old_elo': player['current_elo'],
|
'old_elo': player['current_elo'],
|
||||||
'new_elo': new_elo,
|
'new_elo': new_elo,
|
||||||
'elo_change': elo_change,
|
'elo_change': elo_change,
|
||||||
'won': won
|
'result': result
|
||||||
})
|
})
|
||||||
|
|
||||||
# Update player ELOs and save game results
|
# Update player ELOs and save game results
|
||||||
@@ -494,27 +541,36 @@ async def hoi4end(ctx, game_name: str, winner_team: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Save game result
|
# Save game result
|
||||||
|
won = change['result'] == 'win'
|
||||||
await cursor.execute(
|
await cursor.execute(
|
||||||
"""INSERT INTO game_results
|
"""INSERT INTO game_results
|
||||||
(game_id, discord_id, team_name, t_level, old_elo, new_elo, elo_change, won)
|
(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)""",
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)""",
|
||||||
(game['id'], change['discord_id'], change['team_name'],
|
(game['id'], change['discord_id'], change['team_name'],
|
||||||
change['t_level'], change['old_elo'], change['new_elo'],
|
change['t_level'], change['old_elo'], change['new_elo'],
|
||||||
change['elo_change'], change['won'])
|
change['elo_change'], won, change['result'])
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mark game as finished
|
# Mark game as finished
|
||||||
|
final_result = "Draw" if is_draw else winner_team
|
||||||
await cursor.execute(
|
await cursor.execute(
|
||||||
"UPDATE games SET status = 'finished', winner_team = %s, finished_at = CURRENT_TIMESTAMP WHERE id = %s",
|
"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
|
# Create result embed
|
||||||
embed = discord.Embed(
|
if is_draw:
|
||||||
title="🏆 Game Finished!",
|
embed = discord.Embed(
|
||||||
description=f"Game '{game_name}' has ended!\n**Winner: {winner_team}**",
|
title="🤝 Game Finished!",
|
||||||
color=discord.Color.gold()
|
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
|
# Group results by team
|
||||||
teams_results = {}
|
teams_results = {}
|
||||||
@@ -528,10 +584,26 @@ async def hoi4end(ctx, game_name: str, winner_team: str):
|
|||||||
team_text = ""
|
team_text = ""
|
||||||
for change in team_changes:
|
for change in team_changes:
|
||||||
emoji = "📈" if change['elo_change'] > 0 else "📉" if change['elo_change'] < 0 else "➡️"
|
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_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(
|
embed.add_field(
|
||||||
name=f"{'🏆 ' if team == winner_team else ''}Team {team}",
|
name=team_header,
|
||||||
value=team_text,
|
value=team_text,
|
||||||
inline=False
|
inline=False
|
||||||
)
|
)
|
||||||
@@ -575,16 +647,22 @@ async def hoi4stats(ctx, user: Optional[discord.Member] = None):
|
|||||||
total_players_result = await cursor.fetchone()
|
total_players_result = await cursor.fetchone()
|
||||||
total_players = total_players_result['total']
|
total_players = total_players_result['total']
|
||||||
|
|
||||||
# Get game statistics
|
# Get game statistics with proper draw detection
|
||||||
await cursor.execute(
|
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,)
|
(target_user.id,)
|
||||||
)
|
)
|
||||||
game_stats = await cursor.fetchone()
|
game_stats = await cursor.fetchone()
|
||||||
|
|
||||||
total_games = game_stats['total_games'] or 0
|
total_games = game_stats['total_games'] or 0
|
||||||
games_won = game_stats['games_won'] 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
|
win_rate = (games_won / total_games * 100) if total_games > 0 else 0
|
||||||
|
|
||||||
# Create rank indicators with medals
|
# Create rank indicators with medals
|
||||||
@@ -627,11 +705,20 @@ async def hoi4stats(ctx, user: Optional[discord.Member] = None):
|
|||||||
value=f"**{total_games}** total games",
|
value=f"**{total_games}** total games",
|
||||||
inline=True
|
inline=True
|
||||||
)
|
)
|
||||||
embed.add_field(
|
|
||||||
name="📈 Win/Loss",
|
if games_drawn > 0:
|
||||||
value=f"**{games_won}W** / **{games_lost}L**",
|
embed.add_field(
|
||||||
inline=True
|
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(
|
embed.add_field(
|
||||||
name="📊 Win Rate",
|
name="📊 Win Rate",
|
||||||
value=f"**{win_rate:.1f}%**",
|
value=f"**{win_rate:.1f}%**",
|
||||||
@@ -841,19 +928,27 @@ async def hoi4leaderboard(ctx, game_type: Optional[str] = "standard", limit: Opt
|
|||||||
# Get additional player stats
|
# Get additional player stats
|
||||||
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:
|
||||||
# Count games played
|
# Count games played with proper result detection
|
||||||
await cursor.execute(
|
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['discord_id'],)
|
||||||
)
|
)
|
||||||
player_stats = await cursor.fetchone()
|
player_stats = await cursor.fetchone()
|
||||||
|
|
||||||
games_played = player_stats['games_played'] or 0
|
games_played = player_stats['games_played'] or 0
|
||||||
games_won = player_stats['games_won'] 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
|
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"{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(
|
embed.add_field(
|
||||||
name="Rankings",
|
name="Rankings",
|
||||||
|
|||||||
Reference in New Issue
Block a user