From 039b927bb78be80e4f6055c38598016a6b61cc70 Mon Sep 17 00:00:00 2001 From: David Gwilliam Date: Tue, 31 Mar 2026 21:49:20 -0700 Subject: [PATCH] refactor: move Python scripts to scripts/python/ directory --- scripts/python/generate_games.py | 138 ++++++++++++ scripts/python/update_partnership_stats.py | 234 +++++++++++++++++++++ scripts/python/update_player_stats.py | 195 +++++++++++++++++ 3 files changed, 567 insertions(+) create mode 100644 scripts/python/generate_games.py create mode 100644 scripts/python/update_partnership_stats.py create mode 100644 scripts/python/update_player_stats.py diff --git a/scripts/python/generate_games.py b/scripts/python/generate_games.py new file mode 100644 index 0000000..df38ad6 --- /dev/null +++ b/scripts/python/generate_games.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +""" +Generate 100+ games to test ELO rating calculations +""" + +import sqlite3 +import random +from datetime import datetime, timedelta + +DB_PATH = "prisma/prisma/dev.db" + + +def get_players(): + """Get all players from the database""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute("SELECT id, name, currentElo FROM players") + players = cursor.fetchall() + conn.close() + return players + + +def generate_game(players, game_num, base_date): + """Generate a single game with random players and scores""" + # Randomly select 4 different players + selected_players = random.sample(players, 4) + p1, p2, p3, p4 = selected_players + + # Randomly assign teams (Team 1 vs Team 2) + team1_p1 = p1 + team1_p2 = p2 + team2_p1 = p3 + team2_p2 = p4 + + # Generate realistic scores (Euchre games typically 10 points max) + # Team 1 wins 60% of the time for variety + team1_wins = random.random() < 0.6 + if team1_wins: + # Team 1 wins - generate scores + team1_score = random.randint(10, 15) + team2_score = random.randint(0, 9) + else: + # Team 2 wins - generate scores + team2_score = random.randint(10, 15) + team1_score = random.randint(0, 9) + + # Generate random date in the past 30 days + days_ago = random.randint(0, 30) + game_date = base_date - timedelta( + days=days_ago, hours=random.randint(0, 23), minutes=random.randint(0, 59) + ) + + return { + "team1P1Id": team1_p1[0], + "team1P2Id": team1_p2[0], + "team2P1Id": team2_p1[0], + "team2P2Id": team2_p2[0], + "team1Score": team1_score, + "team2Score": team2_score, + "playedAt": game_date.isoformat() + "Z", + "status": "completed", + "eventId": None, + } + + +def insert_games(games): + """Insert games into the database""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + for game in games: + cursor.execute( + """ + INSERT INTO matches + (team1P1Id, team1P2Id, team2P1Id, team2P2Id, team1Score, team2Score, playedAt, status, eventId, createdAt, updatedAt) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + game["team1P1Id"], + game["team1P2Id"], + game["team2P1Id"], + game["team2P2Id"], + game["team1Score"], + game["team2Score"], + game["playedAt"], + game["status"], + game["eventId"], + datetime.now().isoformat() + "Z", + datetime.now().isoformat() + "Z", + ), + ) + + conn.commit() + conn.close() + + +def main(): + print("Generating 150 games to test ELO ratings...") + print("=" * 60) + + players = get_players() + print(f"Found {len(players)} players in database") + + # Generate 150 games + num_games = 150 + base_date = datetime.now() + + games = [] + for i in range(num_games): + game = generate_game(players, i, base_date) + games.append(game) + + print(f"Generated {len(games)} games") + + # Insert games into database + print("Inserting games into database...") + insert_games(games) + print("Games inserted successfully!") + + # Show sample games + print("\nSample games:") + print("-" * 80) + for i, game in enumerate(games[:3]): + print( + f"Game {i + 1}: Team 1 ({game['team1Score']}) vs Team 2 ({game['team2Score']})" + ) + print(f" Team 1: Player {game['team1P1Id']} + Player {game['team1P2Id']}") + print(f" Team 2: Player {game['team2P1Id']} + Player {game['team2P2Id']}") + print(f" Date: {game['playedAt']}") + print() + + print("=" * 60) + print(f"Total games generated: {num_games}") + print("Now check the ELO ratings by running the application!") + + +if __name__ == "__main__": + main() diff --git a/scripts/python/update_partnership_stats.py b/scripts/python/update_partnership_stats.py new file mode 100644 index 0000000..7d9f82a --- /dev/null +++ b/scripts/python/update_partnership_stats.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +""" +Update partnership statistics based on matches in the database +""" + +import sqlite3 +import math +from datetime import datetime + +DB_PATH = "prisma/prisma/dev.db" +K_FACTOR = 32 + + +def get_all_matches(): + """Get all matches from the database""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(""" + SELECT id, team1P1Id, team1P2Id, team2P1Id, team2P2Id, + team1Score, team2Score, playedAt + FROM matches + ORDER BY playedAt + """) + matches = cursor.fetchall() + conn.close() + return matches + + +def get_or_create_partnership(player1_id, player2_id): + """Get or create a partnership record""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Sort IDs to ensure consistent partnership lookup + p1 = min(player1_id, player2_id) + p2 = max(player1_id, player2_id) + + cursor.execute( + """ + SELECT id, gamesPlayed, wins, losses, totalEloChange + FROM partnership_stats + WHERE player1Id = ? AND player2Id = ? + """, + (p1, p2), + ) + + result = cursor.fetchone() + if result: + conn.close() + return result + + # Create new partnership record + cursor.execute( + """ + INSERT INTO partnership_stats (player1Id, player2Id, gamesPlayed, wins, losses, totalEloChange, lastPlayed, createdAt, updatedAt) + VALUES (?, ?, 0, 0, 0, 0, NULL, ?, ?) + """, + (p1, p2, datetime.now().isoformat() + "Z", datetime.now().isoformat() + "Z"), + ) + + partnership_id = cursor.lastrowid + conn.commit() + conn.close() + + return (partnership_id, 0, 0, 0, 0) + + +def update_partnership_stats(player1_id, player2_id, won, elo_change): + """Update partnership statistics""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Sort IDs to ensure consistent partnership lookup + p1 = min(player1_id, player2_id) + p2 = max(player1_id, player2_id) + + # Get current partnership stats + cursor.execute( + """ + SELECT id, gamesPlayed, wins, losses, totalEloChange + FROM partnership_stats + WHERE player1Id = ? AND player2Id = ? + """, + (p1, p2), + ) + + result = cursor.fetchone() + if not result: + # Create new partnership record + cursor.execute( + """ + INSERT INTO partnership_stats (player1Id, player2Id, gamesPlayed, wins, losses, totalEloChange, lastPlayed, createdAt, updatedAt) + VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?) + """, + ( + p1, + p2, + 1 if won else 0, + 0 if won else 1, + elo_change, + datetime.now().isoformat() + "Z", + datetime.now().isoformat() + "Z", + datetime.now().isoformat() + "Z", + ), + ) + else: + partnership_id, games_played, wins, losses, total_elo_change = result + + # Update partnership stats + new_games = games_played + 1 + new_wins = wins + 1 if won else wins + new_losses = losses if won else losses + 1 + new_total_elo_change = total_elo_change + elo_change + + cursor.execute( + """ + UPDATE partnership_stats + SET gamesPlayed = ?, wins = ?, losses = ?, totalEloChange = ?, lastPlayed = ?, updatedAt = ? + WHERE id = ? + """, + ( + new_games, + new_wins, + new_losses, + new_total_elo_change, + datetime.now().isoformat() + "Z", + datetime.now().isoformat() + "Z", + partnership_id, + ), + ) + + conn.commit() + conn.close() + + +def main(): + print("Updating partnership statistics based on matches...") + print("=" * 60) + + # Get all matches + matches = get_all_matches() + print(f"Found {len(matches)} matches in database") + + # Reset partnership stats + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute("DELETE FROM partnership_stats") + conn.commit() + conn.close() + print("Reset all partnership statistics") + + # Process each match + match_count = 0 + for ( + match_id, + team1_p1, + team1_p2, + team2_p1, + team2_p2, + team1_score, + team2_score, + played_at, + ) in matches: + # Determine winners + team1_won = team1_score > team2_score + team2_won = team2_score > team1_score + + # Update partnership stats for Team 1 + if team1_won: + update_partnership_stats( + team1_p1, team1_p2, True, 0 + ) # Elo change will be calculated separately + else: + update_partnership_stats(team1_p1, team1_p2, False, 0) + + # Update partnership stats for Team 2 + if team2_won: + update_partnership_stats(team2_p1, team2_p2, True, 0) + else: + update_partnership_stats(team2_p1, team2_p2, False, 0) + + match_count += 1 + if match_count % 20 == 0: + print(f"Processed {match_count}/{len(matches)} matches...") + + print(f"Processed {match_count} matches") + + # Display partnership stats for top players + print("\n" + "=" * 60) + print("Partnership Stats for Top Players:") + print("-" * 60) + + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + + # Get top players by ELO + cursor.execute("SELECT id, name FROM players ORDER BY currentElo DESC LIMIT 5") + top_players = cursor.fetchall() + + for player_id, player_name in top_players: + print(f"\n{player_name}:") + + # Get partnership stats for this player + cursor.execute( + """ + SELECT + CASE + WHEN player1Id = ? THEN (SELECT name FROM players WHERE id = player2Id) + ELSE (SELECT name FROM players WHERE id = player1Id) + END as partner_name, + gamesPlayed, wins, losses, totalEloChange + FROM partnership_stats + WHERE player1Id = ? OR player2Id = ? + ORDER BY gamesPlayed DESC + LIMIT 3 + """, + (player_id, player_id, player_id), + ) + + partnerships = cursor.fetchall() + for partner_name, games, wins, losses, elo_change in partnerships: + win_rate = (wins / games * 100) if games > 0 else 0 + print( + f" - with {partner_name}: {games} games, {wins}/{losses} ({win_rate:.1f}%) ELO: {elo_change:+d}" + ) + + conn.close() + + print("\n" + "=" * 60) + print("Partnership statistics updated successfully!") + + +if __name__ == "__main__": + main() diff --git a/scripts/python/update_player_stats.py b/scripts/python/update_player_stats.py new file mode 100644 index 0000000..b2525da --- /dev/null +++ b/scripts/python/update_player_stats.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +""" +Update player statistics (ELO, gamesPlayed, wins, losses) based on matches in the database +""" + +import sqlite3 +import math + +DB_PATH = "prisma/prisma/dev.db" +K_FACTOR = 32 # Standard K-factor for Elo calculations + + +def get_all_matches(): + """Get all matches from the database""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(""" + SELECT id, team1P1Id, team1P2Id, team2P1Id, team2P2Id, + team1Score, team2Score, playedAt + FROM matches + ORDER BY playedAt + """) + matches = cursor.fetchall() + conn.close() + return matches + + +def get_player(player_id): + """Get a player by ID""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute( + "SELECT id, name, currentElo, gamesPlayed, wins, losses FROM players WHERE id = ?", + (player_id,), + ) + player = cursor.fetchone() + conn.close() + return player + + +def update_player(player_id, current_elo, games_played, wins, losses): + """Update a player's statistics""" + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute( + """ + UPDATE players + SET currentElo = ?, gamesPlayed = ?, wins = ?, losses = ? + WHERE id = ? + """, + (current_elo, games_played, wins, losses, player_id), + ) + conn.commit() + conn.close() + + +def calculate_elo_change(rating_a, rating_b, score_a, score_b): + """Calculate Elo change for a match""" + # Calculate expected scores + expected_a = 1 / (1 + math.pow(10, (rating_b - rating_a) / 400)) + expected_b = 1 - expected_a + + # Actual scores (1 for win, 0.5 for tie, 0 for loss) + actual_a = 0.5 if score_a == score_b else (1 if score_a > score_b else 0) + actual_b = 0.5 if score_a == score_b else (1 if score_b > score_a else 0) + + # Calculate Elo change + elo_change_a = K_FACTOR * (actual_a - expected_a) + elo_change_b = K_FACTOR * (actual_b - expected_b) + + return elo_change_a, elo_change_b + + +def main(): + print("Updating player statistics based on matches in database...") + print("=" * 60) + + # Get all matches + matches = get_all_matches() + print(f"Found {len(matches)} matches in database") + + # Reset all player stats to 0 before recalculating + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute( + "UPDATE players SET currentElo = 1000, gamesPlayed = 0, wins = 0, losses = 0" + ) + conn.commit() + conn.close() + print("Reset all player statistics to initial values (ELO: 1000, games: 0)") + + # Process each match + match_count = 0 + for ( + match_id, + team1_p1, + team1_p2, + team2_p1, + team2_p2, + team1_score, + team2_score, + played_at, + ) in matches: + # Get player data + p1 = get_player(team1_p1) + p2 = get_player(team1_p2) + p3 = get_player(team2_p1) + p4 = get_player(team2_p2) + + if not all([p1, p2, p3, p4]): + print(f"Warning: Could not find all players for match {match_id}") + continue + + # Calculate team ratings + team1_rating = (p1[2] + p2[2]) / 2 # currentElo + team2_rating = (p3[2] + p4[2]) / 2 # currentElo + + # Calculate Elo changes + team1_elo_change, team2_elo_change = calculate_elo_change( + team1_rating, team2_rating, team1_score, team2_score + ) + + # Individual Elo changes (split evenly between team members) + p1_elo_change = team1_elo_change / 2 + p2_elo_change = team1_elo_change / 2 + p3_elo_change = team2_elo_change / 2 + p4_elo_change = team2_elo_change / 2 + + # Determine winners + team1_won = team1_score > team2_score + team2_won = team2_score > team1_score + + # Update player 1 stats + p1_new_elo = int(p1[2] + p1_elo_change) + p1_new_games = p1[3] + 1 + p1_new_wins = p1[4] + 1 if team1_won else p1[4] + p1_new_losses = p1[5] if team1_won else p1[5] + 1 + update_player(p1[0], p1_new_elo, p1_new_games, p1_new_wins, p1_new_losses) + + # Update player 2 stats + p2_new_elo = int(p2[2] + p2_elo_change) + p2_new_games = p2[3] + 1 + p2_new_wins = p2[4] + 1 if team1_won else p2[4] + p2_new_losses = p2[5] if team1_won else p2[5] + 1 + update_player(p2[0], p2_new_elo, p2_new_games, p2_new_wins, p2_new_losses) + + # Update player 3 stats + p3_new_elo = int(p3[2] + p3_elo_change) + p3_new_games = p3[3] + 1 + p3_new_wins = p3[4] + 1 if team2_won else p3[4] + p3_new_losses = p3[5] if team2_won else p3[5] + 1 + update_player(p3[0], p3_new_elo, p3_new_games, p3_new_wins, p3_new_losses) + + # Update player 4 stats + p4_new_elo = int(p4[2] + p4_elo_change) + p4_new_games = p4[3] + 1 + p4_new_wins = p4[4] + 1 if team2_won else p4[4] + p4_new_losses = p4[5] if team2_won else p4[5] + 1 + update_player(p4[0], p4_new_elo, p4_new_games, p4_new_wins, p4_new_losses) + + match_count += 1 + if match_count % 20 == 0: + print(f"Processed {match_count}/{len(matches)} matches...") + + print(f"Processed {match_count} matches") + + # Display updated player rankings + print("\n" + "=" * 60) + print("Top 10 Players by ELO Rating:") + print("-" * 60) + + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute(""" + SELECT id, name, currentElo, gamesPlayed, wins, losses + FROM players + ORDER BY currentElo DESC + LIMIT 10 + """) + top_players = cursor.fetchall() + conn.close() + + for rank, (player_id, name, elo, games, wins, losses) in enumerate(top_players, 1): + win_rate = (wins / games * 100) if games > 0 else 0 + print( + f"{rank:2}. {name:15} | ELO: {elo:4} | Games: {games:3} | W/L: {wins}/{losses} ({win_rate:.1f}%)" + ) + + print("\n" + "=" * 60) + print("Player statistics updated successfully!") + print("Now run the application to see updated rankings.") + + +if __name__ == "__main__": + main()