You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
547 lines
16 KiB
547 lines
16 KiB
#!/usr/bin/env python3 |
|
""" |
|
Mice Game Score API |
|
FastAPI application with SQLite database for user and score management |
|
""" |
|
|
|
from fastapi import FastAPI, HTTPException, Depends |
|
from fastapi.responses import JSONResponse |
|
import sqlite3 |
|
import os |
|
from datetime import datetime |
|
from typing import List, Optional |
|
from pydantic import BaseModel |
|
import hashlib |
|
import uuid |
|
|
|
|
|
# Pydantic models for request/response |
|
class User(BaseModel): |
|
user_id: str |
|
device_id: str |
|
created_at: Optional[str] = None |
|
last_active: Optional[str] = None |
|
|
|
class Score(BaseModel): |
|
user_id: str |
|
device_id: str |
|
score: int |
|
game_completed: bool = True |
|
timestamp: Optional[str] = None |
|
|
|
class ScoreResponse(BaseModel): |
|
id: int |
|
user_id: str |
|
device_id: str |
|
score: int |
|
game_completed: bool |
|
timestamp: str |
|
|
|
class UserResponse(BaseModel): |
|
user_id: str |
|
device_id: str |
|
created_at: str |
|
last_active: str |
|
total_scores: int |
|
best_score: int |
|
|
|
|
|
# Database setup |
|
DATABASE_FILE = "mice_game.db" |
|
|
|
def init_database(): |
|
"""Initialize the SQLite database with required tables""" |
|
conn = sqlite3.connect(DATABASE_FILE) |
|
cursor = conn.cursor() |
|
|
|
# Create users table |
|
cursor.execute(''' |
|
CREATE TABLE IF NOT EXISTS users ( |
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|
user_id TEXT NOT NULL, |
|
device_id TEXT NOT NULL, |
|
created_at TEXT NOT NULL, |
|
last_active TEXT NOT NULL, |
|
UNIQUE(user_id, device_id) |
|
) |
|
''') |
|
|
|
# Create scores table |
|
cursor.execute(''' |
|
CREATE TABLE IF NOT EXISTS scores ( |
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|
user_id TEXT NOT NULL, |
|
device_id TEXT NOT NULL, |
|
score INTEGER NOT NULL, |
|
game_completed BOOLEAN NOT NULL DEFAULT 1, |
|
timestamp TEXT NOT NULL, |
|
FOREIGN KEY (user_id, device_id) REFERENCES users (user_id, device_id) |
|
) |
|
''') |
|
|
|
# Create indexes for better performance |
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_device ON users(device_id)') |
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_scores_user_device ON scores(user_id, device_id)') |
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_scores_timestamp ON scores(timestamp)') |
|
|
|
conn.commit() |
|
conn.close() |
|
|
|
def get_db_connection(): |
|
"""Get database connection""" |
|
if not os.path.exists(DATABASE_FILE): |
|
init_database() |
|
|
|
conn = sqlite3.connect(DATABASE_FILE) |
|
conn.row_factory = sqlite3.Row # Enable column access by name |
|
return conn |
|
|
|
def close_db_connection(conn): |
|
"""Close database connection""" |
|
conn.close() |
|
|
|
|
|
# FastAPI app |
|
app = FastAPI( |
|
title="Mice Game Score API", |
|
description="API for managing users and scores in the Mice game", |
|
version="1.0.0" |
|
) |
|
|
|
# Initialize database on startup |
|
@app.on_event("startup") |
|
def startup_event(): |
|
init_database() |
|
|
|
|
|
# Utility functions |
|
def validate_device_id(device_id: str) -> bool: |
|
"""Validate device ID format""" |
|
return device_id.startswith("DEV-") and len(device_id) == 12 |
|
|
|
def validate_user_id(user_id: str) -> bool: |
|
"""Validate user ID format""" |
|
return len(user_id.strip()) > 0 and len(user_id) <= 50 |
|
|
|
def user_exists(conn, user_id: str, device_id: str) -> bool: |
|
"""Check if user exists in database""" |
|
cursor = conn.cursor() |
|
cursor.execute( |
|
"SELECT 1 FROM users WHERE user_id = ? AND device_id = ?", |
|
(user_id, device_id) |
|
) |
|
return cursor.fetchone() is not None |
|
|
|
def user_id_unique_globally(conn, user_id: str) -> bool: |
|
"""Check if user_id is globally unique (across all devices)""" |
|
cursor = conn.cursor() |
|
cursor.execute("SELECT 1 FROM users WHERE user_id = ?", (user_id,)) |
|
return cursor.fetchone() is None |
|
|
|
|
|
# API Endpoints |
|
|
|
@app.get("/") |
|
def read_root(): |
|
"""API root endpoint""" |
|
return { |
|
"message": "Mice Game Score API", |
|
"version": "1.0.0", |
|
"endpoints": [ |
|
"GET /users/{device_id} - Get all users for device", |
|
"POST /signup/{device_id}/{user_id} - Register new user", |
|
"POST /score/{device_id}/{user_id} - Submit score" |
|
] |
|
} |
|
|
|
@app.post("/signup/{device_id}/{user_id}") |
|
def signup_user(device_id: str, user_id: str): |
|
""" |
|
Register a new user for a device |
|
- device_id: Device identifier (format: DEV-XXXXXXXX) |
|
- user_id: Unique user identifier |
|
""" |
|
# Validate inputs |
|
if not validate_device_id(device_id): |
|
raise HTTPException( |
|
status_code=400, |
|
detail="Invalid device_id format. Must be 'DEV-XXXXXXXX'" |
|
) |
|
|
|
if not validate_user_id(user_id): |
|
raise HTTPException( |
|
status_code=400, |
|
detail="Invalid user_id. Must be 1-50 characters long" |
|
) |
|
|
|
conn = get_db_connection() |
|
|
|
try: |
|
# Check if user_id is globally unique |
|
if not user_id_unique_globally(conn, user_id): |
|
raise HTTPException( |
|
status_code=409, |
|
detail=f"User ID '{user_id}' already exists. Choose a different user ID." |
|
) |
|
|
|
# Check if user already exists for this device |
|
if user_exists(conn, user_id, device_id): |
|
raise HTTPException( |
|
status_code=409, |
|
detail=f"User '{user_id}' already registered for device '{device_id}'" |
|
) |
|
|
|
# Register the user |
|
cursor = conn.cursor() |
|
now = datetime.now().isoformat() |
|
|
|
cursor.execute( |
|
"INSERT INTO users (user_id, device_id, created_at, last_active) VALUES (?, ?, ?, ?)", |
|
(user_id, device_id, now, now) |
|
) |
|
|
|
conn.commit() |
|
|
|
return { |
|
"success": True, |
|
"message": f"User '{user_id}' successfully registered for device '{device_id}'", |
|
"user": { |
|
"user_id": user_id, |
|
"device_id": device_id, |
|
"created_at": now |
|
} |
|
} |
|
|
|
except HTTPException: |
|
raise |
|
except Exception as e: |
|
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") |
|
finally: |
|
close_db_connection(conn) |
|
|
|
@app.post("/score/{device_id}/{user_id}") |
|
def submit_score(device_id: str, user_id: str, score_data: Score): |
|
""" |
|
Submit a score for a registered user |
|
- device_id: Device identifier |
|
- user_id: User identifier |
|
- score_data: Score information (score, game_completed) |
|
""" |
|
# Validate inputs |
|
if not validate_device_id(device_id): |
|
raise HTTPException( |
|
status_code=400, |
|
detail="Invalid device_id format. Must be 'DEV-XXXXXXXX'" |
|
) |
|
|
|
if not validate_user_id(user_id): |
|
raise HTTPException( |
|
status_code=400, |
|
detail="Invalid user_id" |
|
) |
|
|
|
if score_data.score < 0: |
|
raise HTTPException( |
|
status_code=400, |
|
detail="Score must be non-negative" |
|
) |
|
|
|
conn = get_db_connection() |
|
|
|
try: |
|
# Check if user exists and is registered for this device |
|
if not user_exists(conn, user_id, device_id): |
|
raise HTTPException( |
|
status_code=404, |
|
detail=f"User '{user_id}' not registered for device '{device_id}'. Please signup first." |
|
) |
|
|
|
# Add the score |
|
cursor = conn.cursor() |
|
timestamp = datetime.now().isoformat() |
|
|
|
cursor.execute( |
|
"INSERT INTO scores (user_id, device_id, score, game_completed, timestamp) VALUES (?, ?, ?, ?, ?)", |
|
(user_id, device_id, score_data.score, score_data.game_completed, timestamp) |
|
) |
|
|
|
# Update user's last_active timestamp |
|
cursor.execute( |
|
"UPDATE users SET last_active = ? WHERE user_id = ? AND device_id = ?", |
|
(timestamp, user_id, device_id) |
|
) |
|
|
|
conn.commit() |
|
|
|
# Get user's statistics |
|
cursor.execute( |
|
""" |
|
SELECT |
|
COUNT(*) as total_games, |
|
MAX(score) as best_score, |
|
AVG(score) as avg_score, |
|
SUM(score) as total_score |
|
FROM scores |
|
WHERE user_id = ? AND device_id = ? |
|
""", |
|
(user_id, device_id) |
|
) |
|
|
|
stats = cursor.fetchone() |
|
|
|
return { |
|
"success": True, |
|
"message": f"Score {score_data.score} submitted successfully", |
|
"score_id": cursor.lastrowid, |
|
"user_stats": { |
|
"user_id": user_id, |
|
"device_id": device_id, |
|
"total_games": stats["total_games"], |
|
"best_score": stats["best_score"], |
|
"average_score": round(stats["avg_score"], 2) if stats["avg_score"] else 0, |
|
"total_score": stats["total_score"] |
|
} |
|
} |
|
|
|
except HTTPException: |
|
raise |
|
except Exception as e: |
|
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") |
|
finally: |
|
close_db_connection(conn) |
|
|
|
@app.get("/users/{device_id}") |
|
def get_device_users(device_id: str) -> List[UserResponse]: |
|
""" |
|
Get all registered users for a specific device |
|
- device_id: Device identifier |
|
""" |
|
# Validate device_id |
|
if not validate_device_id(device_id): |
|
raise HTTPException( |
|
status_code=400, |
|
detail="Invalid device_id format. Must be 'DEV-XXXXXXXX'" |
|
) |
|
|
|
conn = get_db_connection() |
|
|
|
try: |
|
cursor = conn.cursor() |
|
|
|
# Get users with their statistics |
|
cursor.execute( |
|
""" |
|
SELECT |
|
u.user_id, |
|
u.device_id, |
|
u.created_at, |
|
u.last_active, |
|
COUNT(s.id) as total_scores, |
|
COALESCE(MAX(s.score), 0) as best_score |
|
FROM users u |
|
LEFT JOIN scores s ON u.user_id = s.user_id AND u.device_id = s.device_id |
|
WHERE u.device_id = ? |
|
GROUP BY u.user_id, u.device_id, u.created_at, u.last_active |
|
ORDER BY u.last_active DESC |
|
""", |
|
(device_id,) |
|
) |
|
|
|
users = cursor.fetchall() |
|
|
|
if not users: |
|
return [] |
|
|
|
return [ |
|
UserResponse( |
|
user_id=user["user_id"], |
|
device_id=user["device_id"], |
|
created_at=user["created_at"], |
|
last_active=user["last_active"], |
|
total_scores=user["total_scores"], |
|
best_score=user["best_score"] |
|
) |
|
for user in users |
|
] |
|
|
|
except Exception as e: |
|
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") |
|
finally: |
|
close_db_connection(conn) |
|
|
|
# Additional useful endpoints |
|
|
|
@app.get("/scores/{device_id}/{user_id}") |
|
def get_user_scores(device_id: str, user_id: str, limit: int = 10) -> List[ScoreResponse]: |
|
""" |
|
Get recent scores for a user |
|
- device_id: Device identifier |
|
- user_id: User identifier |
|
- limit: Maximum number of scores to return (default: 10) |
|
""" |
|
if not validate_device_id(device_id) or not validate_user_id(user_id): |
|
raise HTTPException(status_code=400, detail="Invalid device_id or user_id") |
|
|
|
if limit < 1 or limit > 100: |
|
raise HTTPException(status_code=400, detail="Limit must be between 1 and 100") |
|
|
|
conn = get_db_connection() |
|
|
|
try: |
|
# Check if user exists |
|
if not user_exists(conn, user_id, device_id): |
|
raise HTTPException( |
|
status_code=404, |
|
detail=f"User '{user_id}' not found for device '{device_id}'" |
|
) |
|
|
|
cursor = conn.cursor() |
|
cursor.execute( |
|
""" |
|
SELECT id, user_id, device_id, score, game_completed, timestamp |
|
FROM scores |
|
WHERE user_id = ? AND device_id = ? |
|
ORDER BY timestamp DESC |
|
LIMIT ? |
|
""", |
|
(user_id, device_id, limit) |
|
) |
|
|
|
scores = cursor.fetchall() |
|
|
|
return [ |
|
ScoreResponse( |
|
id=score["id"], |
|
user_id=score["user_id"], |
|
device_id=score["device_id"], |
|
score=score["score"], |
|
game_completed=bool(score["game_completed"]), |
|
timestamp=score["timestamp"] |
|
) |
|
for score in scores |
|
] |
|
|
|
except HTTPException: |
|
raise |
|
except Exception as e: |
|
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") |
|
finally: |
|
close_db_connection(conn) |
|
|
|
@app.get("/leaderboard/{device_id}") |
|
def get_device_leaderboard(device_id: str, limit: int = 10): |
|
""" |
|
Get leaderboard for a device (top scores) |
|
- device_id: Device identifier |
|
- limit: Maximum number of entries to return (default: 10) |
|
""" |
|
if not validate_device_id(device_id): |
|
raise HTTPException(status_code=400, detail="Invalid device_id") |
|
|
|
if limit < 1 or limit > 100: |
|
raise HTTPException(status_code=400, detail="Limit must be between 1 and 100") |
|
|
|
conn = get_db_connection() |
|
|
|
try: |
|
cursor = conn.cursor() |
|
cursor.execute( |
|
""" |
|
SELECT |
|
user_id, |
|
MAX(score) as best_score, |
|
COUNT(*) as total_games, |
|
MAX(timestamp) as last_play |
|
FROM scores |
|
WHERE device_id = ? |
|
GROUP BY user_id |
|
ORDER BY best_score DESC, last_play DESC |
|
LIMIT ? |
|
""", |
|
(device_id, limit) |
|
) |
|
|
|
leaderboard = cursor.fetchall() |
|
|
|
return [ |
|
{ |
|
"rank": idx + 1, |
|
"user_id": entry["user_id"], |
|
"best_score": entry["best_score"], |
|
"total_games": entry["total_games"], |
|
"last_play": entry["last_play"] |
|
} |
|
for idx, entry in enumerate(leaderboard) |
|
] |
|
|
|
except Exception as e: |
|
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") |
|
finally: |
|
close_db_connection(conn) |
|
|
|
|
|
@app.get("/leaderboard/global/top") |
|
def get_global_leaderboard(limit: int = 10): |
|
""" |
|
Get global leaderboard across all devices (absolute top scores) |
|
- limit: Maximum number of entries to return (default: 10) |
|
""" |
|
if limit < 1 or limit > 100: |
|
raise HTTPException(status_code=400, detail="Limit must be between 1 and 100") |
|
|
|
conn = get_db_connection() |
|
|
|
try: |
|
cursor = conn.cursor() |
|
cursor.execute( |
|
""" |
|
SELECT |
|
user_id, |
|
device_id, |
|
MAX(score) as best_score, |
|
COUNT(*) as total_games, |
|
MAX(timestamp) as last_play |
|
FROM scores |
|
GROUP BY user_id, device_id |
|
ORDER BY best_score DESC, last_play DESC |
|
LIMIT ? |
|
""", |
|
(limit,) |
|
) |
|
|
|
leaderboard = cursor.fetchall() |
|
|
|
return [ |
|
{ |
|
"rank": idx + 1, |
|
"user_id": entry["user_id"], |
|
"device_id": entry["device_id"], |
|
"best_score": entry["best_score"], |
|
"total_games": entry["total_games"], |
|
"last_play": entry["last_play"] |
|
} |
|
for idx, entry in enumerate(leaderboard) |
|
] |
|
|
|
except Exception as e: |
|
raise HTTPException(status_code=500, detail=f"Database error: {str(e)}") |
|
finally: |
|
close_db_connection(conn) |
|
|
|
|
|
if __name__ == "__main__": |
|
import uvicorn |
|
|
|
print("Starting Mice Game Score API...") |
|
print("Available endpoints:") |
|
print(" POST /signup/{device_id}/{user_id} - Register new user") |
|
print(" POST /score/{device_id}/{user_id} - Submit score") |
|
print(" GET /users/{device_id} - Get all users for device") |
|
print(" GET /scores/{device_id}/{user_id} - Get user scores") |
|
print(" GET /leaderboard/{device_id} - Get device leaderboard") |
|
print() |
|
|
|
# Initialize database |
|
init_database() |
|
|
|
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
|