diff --git a/CASAOS_DEPLOYMENT.md b/CASAOS_DEPLOYMENT.md deleted file mode 100644 index 2bcccfa..0000000 --- a/CASAOS_DEPLOYMENT.md +++ /dev/null @@ -1,273 +0,0 @@ -# EuchreCamp CasaOS Deployment Guide - -This guide explains how to deploy EuchreCamp on your CasaOS server. - -## Prerequisites - -1. **CasaOS** installed on your server -2. **Docker** and **Docker Compose** available in CasaOS -3. **Port 3000** available for the web interface -4. **Port 5432** available for PostgreSQL (or use external database) - -## Prerequisites - -**External PostgreSQL Database Required** -- PostgreSQL 15 or higher -- Database name: `euchre_camp` -- User: `euchre_camp` -- Privileges: ALL on database `euchre_camp` - -## Quick Start - -### Option 1: Using CasaOS App Store (Recommended) - -1. **Set up PostgreSQL database first** (see below) -2. Open CasaOS Dashboard -3. Go to **App Store** → **Custom App** -4. Click **Install** and configure the following: - - **Basic Settings:** - - App Name: `EuchreCamp` - - Image: `euchre-camp:latest` (build locally first) - - Port: `3000` - - **Environment Variables:** - ``` - DATABASE_URL=postgresql://euchre_camp:password@your-postgres-host:5432/euchre_camp - DATABASE_SHADOW_URL=postgresql://euchre_camp:password@your-postgres-host:5432/euchre_camp_shadow - BETTER_AUTH_SECRET=your-secret-key-here - BETTER_AUTH_URL=http://your-casaos-ip:3000 - TRUSTED_ORIGINS=http://your-casaos-ip:3000 - ``` - -5. Click **Install** - -### Option 2: Using Docker Compose - -1. **Set up PostgreSQL database first** (see below) -2. **Build the Docker image:** - ```bash - docker-compose -f docker-compose.casaos.yml build - ``` - -3. **Start the application:** - ```bash - docker-compose -f docker-compose.casaos.yml up -d - ``` - -4. **Check logs:** - ```bash - docker-compose -f docker-compose.casaos.yml logs -f - ``` - -## PostgreSQL Database Setup - -### Option A: External PostgreSQL Server - -Connect to your PostgreSQL server and run: - -```sql --- Create database -CREATE DATABASE euchre_camp; - --- Create user -CREATE USER euchre_camp WITH PASSWORD 'your-strong-password'; - --- Grant privileges -GRANT ALL PRIVILEGES ON DATABASE euchre_camp TO euchre_camp; - --- Connect to database -\c euchre_camp - --- Grant schema privileges -GRANT ALL ON SCHEMA public TO euchre_camp; -``` - -### Option B: CasaOS PostgreSQL Container - -If you want to run PostgreSQL in CasaOS: - -1. Install PostgreSQL from CasaOS App Store -2. Configure it with: - - Database: `euchre_camp` - - User: `euchre_camp` - - Password: `your-strong-password` -3. Use the connection string in your EuchreCamp configuration: - ``` - DATABASE_URL=postgresql://euchre_camp:your-strong-password@192.168.1.100:5432/euchre_camp - ``` - (Replace `192.168.1.100` with your CasaOS IP) - -## Configuration - -### Required Environment Variables - -| Variable | Description | Example | -|----------|-------------|---------| -| `DATABASE_URL` | PostgreSQL connection string (REQUIRED) | `postgresql://euchre_camp:pass@host:5432/euchre_camp` | -| `BETTER_AUTH_SECRET` | Secret key for session encryption (generate with `openssl rand -base64 32`) | `your-generated-secret` | -| `BETTER_AUTH_URL` | Public URL of your application | `http://192.168.1.100:3000` | -| `TRUSTED_ORIGINS` | Comma-separated list of trusted origins | `http://192.168.1.100:3000` | - -### Optional Environment Variables - -| Variable | Description | Default | -|----------|-------------|---------| -| `DATABASE_SHADOW_URL` | Shadow database for migrations | Same as DATABASE_URL with `_shadow` suffix | - -## Database Management - -### First-Time Setup - -1. **Create the database and user** (see PostgreSQL Setup above) -2. **Run database migrations:** - ```bash - docker exec -it euchre-camp npx prisma migrate deploy - ``` -3. **Create admin user:** - ```bash - docker exec -it euchre-camp node scripts/create-admin-via-api.js - ``` - -### Accessing the Database - -```bash -# Connect to external PostgreSQL server -psql -h your-postgres-host -U euchre_camp -d euchre_camp - -# List tables -\dt - -# Exit -\q -``` - -### Backing Up Data - -```bash -# Backup PostgreSQL data (run on your PostgreSQL server) -pg_dump -h your-postgres-host -U euchre_camp euchre_camp > backup.sql - -# Backup uploaded files -tar -czf euchre_camp_files.tar.gz /var/lib/docker/volumes/euchre_camp_app_data -``` - -## Security Considerations - -### Change Default Passwords - -1. **PostgreSQL Password:** - - Edit `docker-compose.casaos.yml` or set via CasaOS environment variables - - Update `POSTGRES_PASSWORD` to a strong password - -2. **Better Auth Secret:** - ```bash - openssl rand -base64 32 - ``` - - Use this value for `BETTER_AUTH_SECRET` - -### HTTPS Setup - -For production use with HTTPS: - -1. **Option A: CasaOS Reverse Proxy** - - Configure CasaOS to use HTTPS - - Update `BETTER_AUTH_URL` to `https://your-domain.com` - -2. **Option B: External Reverse Proxy (Nginx, Traefik)** - - Configure your reverse proxy - - Update `TRUSTED_ORIGINS` to include your domain - -### Firewall Rules - -Ensure only necessary ports are exposed: -- **Port 3000**: Web interface (HTTP/HTTPS) -- **Port 5432**: PostgreSQL (only if external access needed) - -## Troubleshooting - -### Database Connection Issues - -```bash -# Check PostgreSQL logs -docker logs euchre-camp-postgres - -# Check if PostgreSQL is healthy -docker exec euchre-camp-postgres pg_isready -U euchre -d euchre_camp -``` - -### Application Not Starting - -```bash -# Check application logs -docker logs euchre-camp - -# Check if Prisma migrations are applied -docker exec euchre-camp npx prisma migrate status -``` - -### Permission Issues - -If you see permission errors: -```bash -# Fix volume permissions -docker exec euchre-camp chown -R euchre:euchre /app/public/uploads -``` - -## Performance Tuning - -### PostgreSQL Optimization - -Add to `docker-compose.casaos.yml` under `postgres` service: -```yaml -environment: - - POSTGRES_SHARED_BUFFERS=256MB - - POSTGRES_WORK_MEM=16MB -``` - -### Application Optimization - -The Dockerfile is already optimized with: -- Multi-stage build (smaller image size) -- Non-root user for security -- Health checks -- Production-ready settings - -## Updating - -To update EuchreCamp: - -```bash -# Pull latest changes -git pull origin main - -# Rebuild and restart -docker-compose -f docker-compose.casaos.yml down -docker-compose -f docker-compose.casaos.yml build --no-cache -docker-compose -f docker-compose.casaos.yml up -d -``` - -## Monitoring - -### View Logs -```bash -docker-compose -f docker-compose.casaos.yml logs -f -``` - -### Check Container Status -```bash -docker ps | grep euchre -``` - -### Resource Usage -```bash -docker stats euchre-camp euchre-camp-postgres -``` - -## Support - -For issues or questions: -- Check the application logs first -- Verify environment variables are set correctly -- Ensure PostgreSQL is running and accessible -- Check CasaOS app logs for deployment issues diff --git a/DOCKER.md b/DOCKER.md deleted file mode 100644 index 9f4ea0f..0000000 --- a/DOCKER.md +++ /dev/null @@ -1,370 +0,0 @@ -# Docker Deployment Guide - -This guide covers how to run EuchreCamp using Docker and Docker Compose. - -## Prerequisites - -- Docker Engine 20.10+ -- Docker Compose v2.0+ -- Make (optional, for using Makefile commands) - -## Quick Start - -### 1. Environment Setup - -Create a `.env` file in the project root: - -```bash -cp .env.example .env -``` - -Update the `.env` file with your configuration: - -```env -# Database Configuration (PostgreSQL in Docker) -DATABASE_PROVIDER=postgresql -DATABASE_URL=postgresql://euchre:euchrepassword@postgres:5432/euchre_camp -DATABASE_SHADOW_URL=postgresql://euchre:euchrepassword@postgres:5432/euchre_camp_shadow - -# Better Auth (generate a secure secret for production) -BETTER_AUTH_SECRET=your-secret-key-change-in-production -BETTER_AUTH_URL=http://localhost:3000 - -# Application -NODE_ENV=production -TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 -``` - -### 2. Build and Run - -#### Production Mode - -```bash -# Build and start all services -docker-compose up -d - -# View logs -docker-compose logs -f - -# Stop all services -docker-compose down -``` - -#### Development Mode - -```bash -# Use development override configuration -docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d - -# Or use the shorthand -docker-compose --profile dev up -d -``` - -### 3. Access the Application - -- **Web Application**: http://localhost:3000 -- **PostgreSQL**: localhost:5432 (default) or 5433 in development - -## Service Architecture - -``` -┌─────────────────────────────────────────────┐ -│ EuchreCamp │ -├─────────────────────────────────────────────┤ -│ │ -│ ┌──────────────┐ ┌──────────────────┐ │ -│ │ Next.js │◄───│ PostgreSQL │ │ -│ │ App │ │ Database │ │ -│ │ │ │ │ │ -│ │ Port: 3000 │ │ Port: 5432 │ │ -│ └──────────────┘ └──────────────────┘ │ -│ │ -└─────────────────────────────────────────────┘ -``` - -## Service Details - -### App Service - -- **Image**: Multi-stage Next.js build -- **Port**: 3000 -- **Environment**: Production or Development -- **Volumes**: - - `app_data`: Persistent storage for uploads - -### PostgreSQL Service - -- **Image**: postgres:15-alpine -- **Port**: 5432 (production) / 5433 (development) -- **Data Volume**: `postgres_data` (persistent) -- **Health Check**: Uses `pg_isready` - -## Database Management - -### View Database Data - -```bash -# Access PostgreSQL shell -docker exec -it euchre-camp-postgres psql -U euchre -d euchre_camp - -# List tables -\dt - -# Exit -\q -``` - -### Backup Database - -```bash -# Create backup -docker exec euchre-camp-postgres pg_dump -U euchre euchre_camp > backup.sql - -# Restore backup -docker exec -i euchre-camp-postgres psql -U euchre euchre_camp < backup.sql -``` - -### Reset Database - -```bash -# Remove all data and restart -docker-compose down -v -docker-compose up -d -``` - -## Running Commands - -### Run Database Migrations - -```bash -# Apply migrations -docker-compose exec app npx prisma migrate deploy - -# Generate Prisma client -docker-compose exec app npx prisma generate - -# Open Prisma Studio -docker-compose exec app npx prisma studio -``` - -### Create Admin User - -```bash -# Create admin user via script -docker-compose exec app node scripts/create-admin-via-api.js -``` - -### Seed Database - -```bash -# Run seed script -docker-compose exec app node scripts/seed.js -``` - -## Development Workflow - -### Hot Reloading - -Development mode supports hot reloading: - -```bash -# Start in development mode -docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d - -# View logs -docker-compose logs -f app -``` - -### Modifying Code - -Changes to source code are automatically reflected due to volume mounting: - -```bash -# Edit a file locally -nano src/app/page.tsx - -# Next.js will automatically reload -``` - -## Troubleshooting - -### Common Issues - -**1. Database Connection Failed** - -```bash -# Check if PostgreSQL is running -docker ps | grep postgres - -# Check logs -docker logs euchre-camp-postgres - -# Restart services -docker-compose restart postgres -``` - -**2. Port Already in Use** - -```bash -# Check what's using port 3000 -lsof -i :3000 - -# Change port in docker-compose.yml -# ports: -# - "3001:3000" -``` - -**3. Permission Denied** - -```bash -# Ensure scripts are executable -chmod +x scripts/*.sh - -# Check volume permissions -docker exec euchre-camp-app ls -la /app -``` - -**4. Migration Errors** - -```bash -# Reset database -docker-compose down -v -docker-compose up -d - -# Apply migrations manually -docker-compose exec app npx prisma migrate deploy -``` - -### View Logs - -```bash -# All services -docker-compose logs -f - -# Specific service -docker-compose logs -f app -docker-compose logs -f postgres -``` - -### Container Shell Access - -```bash -# App container -docker exec -it euchre-camp-app sh - -# PostgreSQL container -docker exec -it euchre-camp-postgres sh -``` - -## Production Deployment - -### Environment Variables - -For production, ensure you set secure values: - -```env -# Generate a secure secret -BETTER_AUTH_SECRET=$(openssl rand -base64 32) - -# Use real domain -BETTER_AUTH_URL=https://euchrecamp.example.com - -# Trusted origins -TRUSTED_ORIGINS=https://euchrecamp.example.com,https://www.euchrecamp.example.com -``` - -### Security Considerations - -1. **Change default PostgreSQL password**: - ```env - POSTGRES_PASSWORD=your-secure-password - ``` - -2. **Use SSL/TLS for database connections**: - ```env - DATABASE_URL=postgresql://euchre:password@postgres:5432/euchre_camp?sslmode=require - ``` - -3. **Restrict network access**: - - Don't expose PostgreSQL port to public internet - - Use firewall rules to restrict access - -4. **Regular backups**: - - Schedule automated database backups - - Test restoration process - -### Docker Swarm / Kubernetes - -For production orchestration, consider: - -1. **Separate database service** - Use managed PostgreSQL (RDS, Cloud SQL) -2. **Secrets management** - Use Docker secrets or Kubernetes Secrets -3. **Health checks** - Configure proper health checks -4. **Resource limits** - Set CPU and memory limits -5. **Replicas** - Run multiple app instances for high availability - -## Makefile (Optional) - -Create a `Makefile` for convenient commands: - -```makefile -.PHONY: up down logs build shell-migrate - -up: - docker-compose up -d - -down: - docker-compose down - -logs: - docker-compose logs -f - -build: - docker-compose build --no-cache - -shell-app: - docker exec -it euchre-camp-app sh - -shell-db: - docker exec -it euchre-camp-postgres psql -U euchre -d euchre_camp - -migrate: - docker-compose exec app npx prisma migrate deploy - -seed: - docker-compose exec app node scripts/seed.js -``` - -## Performance Tuning - -### PostgreSQL Configuration - -Edit `docker-compose.yml` to tune PostgreSQL: - -```yaml -postgres: - # ... other config ... - command: > - postgres - -c max_connections=200 - -c shared_buffers=256MB - -c effective_cache_size=1GB - -c maintenance_work_mem=64MB - -c checkpoint_completion_target=0.9 - -c wal_buffers=16MB - -c default_statistics_target=100 -``` - -### Next.js Optimization - -The Dockerfile uses multi-stage build for optimal size and performance: - -- **Builder stage**: Installs all dependencies and builds the app -- **Runner stage**: Copies only the built artifacts and production dependencies - -## References - -- [Docker Compose Documentation](https://docs.docker.com/compose/) -- [PostgreSQL Docker Image](https://hub.docker.com/_/postgres) -- [Next.js Deployment](https://nextjs.org/docs/deployment) -- [Prisma Deployment](https://www.prisma.io/docs/concepts/components/prisma-migrate/deployment) diff --git a/TODO.md b/TODO.md deleted file mode 100644 index b5529c0..0000000 --- a/TODO.md +++ /dev/null @@ -1,102 +0,0 @@ -# EuchreCamp - Todo List - -## Current Tasks - -### Completed ✅ -- [x] Add `site_admin` role to database schema and permissions system -- [x] Add `isCasual` boolean field to Match model (already existed) -- [x] Update match upload API to support casual matches -- [x] Update match upload UI to include casual checkbox -- [x] Add tournament deletion API endpoint with delete/orphan options -- [x] Add delete tournament button and modal to tournament detail page -- [x] Run tests and verify implementation (84 tests passing) -- [x] Fix session issues with tournament admin access -- [x] Fix Elo recalculation error for player merge (delete elo snapshots before deleting players) -- [x] Add admin player management page -- [x] Add player name editing functionality in admin UI -- [x] Add admin panel links to navigation header -- [x] Add tournament update API endpoint (PUT /api/tournaments/[id]) -- [x] Consolidate delete endpoint from admin API to main tournaments API -- [x] Update database schema to add variant scoring fields (targetScore, allowTies) -- [x] Fix tie handling logic in partnership stats (ties now correctly tracked) -- [x] Fix test files for normalizedName field in Player model -- [x] Fix auth.ts to include normalizedName in Player creation -- [x] Write TODO list to repository file -- [x] Auto-create tournament when uploading matches without selecting one - -### In Progress 🔄 -- [ ] Update API routes to handle new variant scoring fields -- [ ] Update EditTournamentForm to add variant scoring controls -- [ ] Update MatchEditor to use tournament-specific target score -- [ ] Run tests and verify variant scoring implementation - -### Recently Completed ✅ -- [x] Add OpenSkill rating system support (src/lib/openskill-utils.ts) -- [x] Add Glicko2 rating system support (src/lib/glicko2-utils.ts) -- [x] Reset database and run all migrations from scratch -- [x] Regenerate Prisma client with new rating models -- [x] Update match upload page to auto-create tournament if none selected -- [x] Update all admin scripts to use PrismaPg adapter and dotenv -- [x] Fix match diagram player positioning -- [x] Add CasaOS deployment configuration and documentation -- [x] Create migration to add rating system tables (elo_ratings, glicko2_ratings, open_skill_ratings) -- [x] Add tabbed rankings page to display Elo, OpenSkill, and Glicko2 ratings - -### Backlog 📋 -- [ ] Add UI controls for variant scoring in tournament creation/edit -- [ ] Test variant tournament functionality end-to-end -- [ ] Add validation for tie scores based on tournament configuration -- [ ] Document variant tournament features - -## Recently Completed (Detailed) - -### Variant Euchre Scoring Support -- Added `targetScore` and `allowTies` fields to Event model -- Created database migration for new fields -- Fixed partnership stats tie handling (ties now increment neither wins nor losses) -- Updated Elo calculation functions to handle ties correctly (0.5 points for draw) - -### Tournament Deletion -- Consolidated delete endpoint to `/api/tournaments/[id]` -- Added options to delete matches or orphan them -- Updated DeleteTournamentButton to use consolidated endpoint - -### Player Management -- Added admin players page at `/admin/players` -- Added player name editing functionality via PATCH endpoint -- Added player merge functionality with automatic Elo recalculation -- Fixed foreign key constraint issues with elo_snapshots - -### Permissions -- Added `site_admin` role as highest privilege level -- Updated all permission functions to include site_admin support -- Fixed session cache issues by reading roles from database - -## Notes -- All 84 unit tests passing -- Database migrations applied successfully -- TypeScript compilation has pre-existing errors unrelated to our changes - -### Completed After Commit 1729dac - -#### Next.js 16 Breaking Change Fixes -- [x] Fixed `params.id` usage in all page components (must use `await params`) -- [x] Fixed `params.id` usage in all API routes (must use `await params`) -- [x] Updated client components to use `Promise<{ id: string }>` type -- [x] Added regression tests for Next.js 16 params Promise handling -- [x] Verified all 100 unit tests pass - -#### Files Updated: -- Player pages: `profile.tsx`, `schedule.tsx` -- Tournament pages: `page.tsx`, `results.tsx`, `edit.tsx`, `entry.tsx` -- API routes: `admin/players/[id]/route.ts`, `users/[id]/route.ts`, `users/[id]/role/route.ts` -- Tournament API routes: `[id]/route.ts`, `[id]/participants/route.ts`, `[id]/games/bulk/route.ts` - -#### Root Cause -Next.js 16 requires `params` to be awaited in both server components and API routes: -- Before: `const { id } = params` -- After: `const { id } = await params` - -This was not caught by the unit test suite because: -- Unit tests test individual functions in isolation -- E2E tests (Playwright) would catch this but weren't run after the upgrade diff --git a/generate_games.py b/generate_games.py deleted file mode 100644 index df38ad6..0000000 --- a/generate_games.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/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/update_partnership_stats.py b/update_partnership_stats.py deleted file mode 100644 index 7d9f82a..0000000 --- a/update_partnership_stats.py +++ /dev/null @@ -1,234 +0,0 @@ -#!/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/update_player_stats.py b/update_player_stats.py deleted file mode 100644 index b2525da..0000000 --- a/update_player_stats.py +++ /dev/null @@ -1,195 +0,0 @@ -#!/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()