diff --git a/app.py b/app.py index 6818972..b1e62ba 100644 --- a/app.py +++ b/app.py @@ -4,7 +4,7 @@ import os import asyncio import logging from dotenv import load_dotenv -import asyncpg +import aiomysql import json from datetime import datetime from typing import Optional, List, Dict @@ -25,9 +25,51 @@ DB_PASSWORD = os.getenv('DB_PASSWORD') # Build DATABASE_URL from individual components if not provided if not DATABASE_URL and all([DB_HOST, DB_NAME, DB_USER, DB_PASSWORD]): - DATABASE_URL = f"postgresql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" + DATABASE_URL = f"mysql://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}" print(f"📝 Built DATABASE_URL from individual environment variables") +# Parse MySQL connection details from DATABASE_URL +def parse_database_url(url): + """Parse MySQL connection URL into components""" + if not url: + return None + + # Remove mysql:// prefix + if url.startswith('mysql://'): + url = url[8:] + + # Split user:pass@host:port/db + if '@' in url: + auth, host_db = url.split('@', 1) + if ':' in auth: + user, password = auth.split(':', 1) + else: + user, password = auth, '' + else: + return None + + if '/' in host_db: + host_port, database = host_db.split('/', 1) + else: + return None + + if ':' in host_port: + host, port = host_port.split(':', 1) + try: + port = int(port) + except ValueError: + port = 3306 + else: + host, port = host_port, 3306 + + return { + 'host': host, + 'port': port, + 'user': user, + 'password': password, + 'db': database + } + # Global database connection pool db_pool = None @@ -90,82 +132,106 @@ async def init_database(): """Initialize database connection and create tables""" global db_pool try: - db_pool = await asyncpg.create_pool(DATABASE_URL) + # Parse DATABASE_URL for MySQL connection + db_config = parse_database_url(DATABASE_URL) + if not db_config: + raise ValueError("Invalid DATABASE_URL format") + + print(f"🔌 Connecting to MySQL: {db_config['host']}:{db_config['port']}/{db_config['db']}") + + # Create MySQL connection pool + db_pool = await aiomysql.create_pool( + host=db_config['host'], + port=db_config['port'], + user=db_config['user'], + password=db_config['password'], + db=db_config['db'], + charset='utf8mb4', + autocommit=True, + maxsize=10 + ) async with db_pool.acquire() as conn: - # Create players table - await conn.execute(''' - CREATE TABLE IF NOT EXISTS players ( - id SERIAL PRIMARY KEY, - discord_id BIGINT UNIQUE NOT NULL, - username VARCHAR(255) NOT NULL, - standard_elo INTEGER DEFAULT 800, - competitive_elo INTEGER DEFAULT 800, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - - # Create games table - await conn.execute(''' - CREATE TABLE IF NOT EXISTS games ( - id SERIAL PRIMARY KEY, - game_name VARCHAR(255) NOT NULL, - game_type VARCHAR(50) NOT NULL, - status VARCHAR(50) DEFAULT 'setup', - players JSONB NOT NULL DEFAULT '[]', - winner_team VARCHAR(255), - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - finished_at TIMESTAMP - ) - ''') - - # Create game_results table for detailed match history - await conn.execute(''' - CREATE TABLE IF NOT EXISTS game_results ( - id SERIAL PRIMARY KEY, - game_id INTEGER REFERENCES games(id), - discord_id BIGINT NOT NULL, - team_name VARCHAR(255) NOT NULL, - t_level INTEGER NOT NULL, - old_elo INTEGER NOT NULL, - new_elo INTEGER NOT NULL, - elo_change INTEGER NOT NULL, - won BOOLEAN NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - ''') - + async with conn.cursor() as cursor: + # Create players table (MySQL syntax) + await cursor.execute(''' + CREATE TABLE IF NOT EXISTS players ( + id INT AUTO_INCREMENT PRIMARY KEY, + discord_id BIGINT UNIQUE NOT NULL, + username VARCHAR(255) NOT NULL, + standard_elo INT DEFAULT 800, + competitive_elo INT DEFAULT 800, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP + ) + ''') + + # Create games table (MySQL syntax) + await cursor.execute(''' + CREATE TABLE IF NOT EXISTS games ( + id INT AUTO_INCREMENT PRIMARY KEY, + game_name VARCHAR(255) NOT NULL, + game_type VARCHAR(50) NOT NULL, + status VARCHAR(50) DEFAULT 'setup', + players JSON NOT NULL, + winner_team VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + finished_at TIMESTAMP NULL + ) + ''') + + # Create game_results table (MySQL syntax) + await cursor.execute(''' + CREATE TABLE IF NOT EXISTS game_results ( + id INT AUTO_INCREMENT PRIMARY KEY, + game_id INT, + discord_id BIGINT NOT NULL, + team_name VARCHAR(255) NOT NULL, + t_level INT NOT NULL, + old_elo INT NOT NULL, + new_elo INT NOT NULL, + elo_change INT NOT NULL, + won BOOLEAN NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (game_id) REFERENCES games(id) + ) + ''') + print("✅ Database initialized successfully") except Exception as e: print(f"❌ Database initialization failed: {e}") + import traceback + traceback.print_exc() async def get_or_create_player(discord_id: int, username: str) -> Dict: """Get or create a player in the database""" async with db_pool.acquire() as conn: - # Try to get existing player - player = await conn.fetchrow( - "SELECT * FROM players WHERE discord_id = $1", discord_id - ) - - if not player: - # Create new player - await conn.execute( - "INSERT INTO players (discord_id, username) VALUES ($1, $2)", - discord_id, username - ) - player = await conn.fetchrow( - "SELECT * FROM players WHERE discord_id = $1", discord_id - ) - else: - # Update username if changed - await conn.execute( - "UPDATE players SET username = $1, updated_at = CURRENT_TIMESTAMP WHERE discord_id = $2", - username, discord_id + async with conn.cursor(aiomysql.DictCursor) as cursor: + # Try to get existing player + await cursor.execute( + "SELECT * FROM players WHERE discord_id = %s", (discord_id,) ) + player = await cursor.fetchone() - return dict(player) + if not player: + # Create new player + await cursor.execute( + "INSERT INTO players (discord_id, username) VALUES (%s, %s)", + (discord_id, username) + ) + await cursor.execute( + "SELECT * FROM players WHERE discord_id = %s", (discord_id,) + ) + player = await cursor.fetchone() + else: + # Update username if changed + await cursor.execute( + "UPDATE players SET username = %s, updated_at = CURRENT_TIMESTAMP WHERE discord_id = %s", + (username, discord_id) + ) + + 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""" @@ -240,21 +306,23 @@ async def hoi4create(ctx, game_type: str, game_name: str): try: async with db_pool.acquire() as conn: - # Check if game name already exists and is active - existing_game = await conn.fetchrow( - "SELECT * FROM games WHERE game_name = $1 AND status = 'setup'", - game_name - ) - - if existing_game: - await ctx.send(f"❌ A game with name '{game_name}' is already in setup phase!") - return - - # Create new game - await conn.execute( - "INSERT INTO games (game_name, game_type, status) VALUES ($1, $2, 'setup')", - game_name, game_type.lower() - ) + async with conn.cursor(aiomysql.DictCursor) as cursor: + # Check if game name already exists and is active + await cursor.execute( + "SELECT * FROM games WHERE game_name = %s AND status = 'setup'", + (game_name,) + ) + existing_game = await cursor.fetchone() + + if existing_game: + await ctx.send(f"❌ A game with name '{game_name}' is already in setup phase!") + return + + # 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(), '[]') + ) embed = discord.Embed( title="🎮 Game Created", @@ -280,43 +348,45 @@ async def hoi4setup(ctx, game_name: str, user: discord.Member, team_name: str, t try: async with db_pool.acquire() as conn: - # Get the game - game = await conn.fetchrow( - "SELECT * FROM games WHERE game_name = $1 AND status = 'setup'", - game_name - ) - - if not game: - await ctx.send(f"❌ No game found with name '{game_name}' in setup phase!") - return - - # Get or create player - player = await get_or_create_player(user.id, user.display_name) - - # Parse existing players - players = json.loads(game['players']) if game['players'] else [] - - # Check if player already in game - for p in players: - if p['discord_id'] == user.id: - await ctx.send(f"❌ {user.display_name} is already in this game!") + async with conn.cursor(aiomysql.DictCursor) as cursor: + # Get the game + await cursor.execute( + "SELECT * FROM games WHERE game_name = %s AND status = 'setup'", + (game_name,) + ) + game = await cursor.fetchone() + + if not game: + await ctx.send(f"❌ No game found with name '{game_name}' in setup phase!") return - - # Add player to game - 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"] - } - players.append(player_data) - - # Update game - await conn.execute( - "UPDATE games SET players = $1 WHERE id = $2", - json.dumps(players), game['id'] - ) + + # Get or create player + player = await get_or_create_player(user.id, user.display_name) + + # Parse existing players + players = json.loads(game['players']) if game['players'] else [] + + # Check if player already in game + for p in players: + if p['discord_id'] == user.id: + await ctx.send(f"❌ {user.display_name} is already in this game!") + return + + # Add player to game + 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"] + } + players.append(player_data) + + # Update game + await cursor.execute( + "UPDATE games SET players = %s WHERE id = %s", + (json.dumps(players), game['id']) + ) embed = discord.Embed( title="✅ Player Added", @@ -339,11 +409,13 @@ async def hoi4end(ctx, game_name: str, winner_team: str): """End a game and calculate ELO changes""" try: async with db_pool.acquire() as conn: - # Get the game - game = await conn.fetchrow( - "SELECT * FROM games WHERE game_name = $1 AND status = 'setup'", - game_name - ) + async with conn.cursor(aiomysql.DictCursor) as cursor: + # Get the game + await cursor.execute( + "SELECT * FROM games WHERE game_name = %s AND status = 'setup'", + (game_name,) + ) + game = await cursor.fetchone() if not game: await ctx.send(f"❌ No active game found with name '{game_name}'!") @@ -412,31 +484,31 @@ async def hoi4end(ctx, game_name: str, winner_team: str): 'won': won }) - # Update player ELOs and save game results - for change in elo_changes: - # Update player ELO - elo_field = f"{game['game_type']}_elo" - await conn.execute( - f"UPDATE players SET {elo_field} = $1, updated_at = CURRENT_TIMESTAMP WHERE discord_id = $2", - change['new_elo'], change['discord_id'] - ) + # 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 + 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'], change['discord_id'], change['team_name'], + change['t_level'], change['old_elo'], change['new_elo'], + change['elo_change'], change['won']) + ) - # Save game result - await conn.execute( - """INSERT INTO game_results - (game_id, discord_id, team_name, t_level, old_elo, new_elo, elo_change, won) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8)""", - game['id'], change['discord_id'], change['team_name'], - change['t_level'], change['old_elo'], change['new_elo'], - change['elo_change'], change['won'] + # Mark game as finished + await cursor.execute( + "UPDATE games SET status = 'finished', winner_team = %s, finished_at = CURRENT_TIMESTAMP WHERE id = %s", + (winner_team, game['id']) ) - # Mark game as finished - await conn.execute( - "UPDATE games SET status = 'finished', winner_team = $1, finished_at = CURRENT_TIMESTAMP WHERE id = $2", - winner_team, game['id'] - ) - # Create result embed embed = discord.Embed( title="🏆 Game Finished!", @@ -501,9 +573,11 @@ async def hoi4games(ctx): """Show all active games""" try: async with db_pool.acquire() as conn: - games = await conn.fetch( - "SELECT * FROM games WHERE status = 'setup' ORDER BY created_at DESC" - ) + async with conn.cursor(aiomysql.DictCursor) as cursor: + await cursor.execute( + "SELECT * FROM games WHERE status = 'setup' ORDER BY created_at DESC" + ) + games = await cursor.fetchall() if not games: await ctx.send("📝 No active games found. Use `/hoi4create` to create a new game!")