false, 'error' => 'Method not allowed.'], 405); } $raw = file_get_contents('php://input'); $payload = json_decode($raw ?: '[]', true); if (!is_array($payload)) { jsonResponse(['success' => false, 'error' => 'Invalid JSON payload.'], 400); } $action = (string) ($payload['action'] ?? ''); if ($action === 'save_score') { saveScore($payload); } if ($action === 'list_scores') { listScores((int) ($payload['limit'] ?? 10)); } jsonResponse(['success' => false, 'error' => 'Unknown action.'], 422); } catch (Throwable $exception) { error_log('Scoreboard API error: ' . $exception->getMessage()); jsonResponse(['success' => false, 'error' => 'Scoreboard is temporarily unavailable.'], 500); } function saveScore(array $payload): void { $playerName = normalizePlayerName((string) ($payload['player_name'] ?? '')); $score = max(0, (int) ($payload['score'] ?? 0)); $linesCleared = max(0, (int) ($payload['lines'] ?? 0)); $level = max(1, (int) ($payload['level'] ?? 1)); $piecesPlaced = max(0, (int) ($payload['pieces_placed'] ?? 0)); $durationSeconds = max(0, (int) ($payload['duration_seconds'] ?? 0)); $roomCode = normalizeRoomCode((string) ($payload['room_code'] ?? '')); $mode = $roomCode !== '' ? 'multiplayer' : 'solo'; $pdo = db(); $insert = $pdo->prepare( 'INSERT INTO tetris_scores (player_name, score, lines_cleared, level, pieces_placed, duration_seconds, mode, room_code) ' . 'VALUES (:player_name, :score, :lines_cleared, :level, :pieces_placed, :duration_seconds, :mode, :room_code)' ); $insert->bindValue(':player_name', $playerName, PDO::PARAM_STR); $insert->bindValue(':score', $score, PDO::PARAM_INT); $insert->bindValue(':lines_cleared', $linesCleared, PDO::PARAM_INT); $insert->bindValue(':level', $level, PDO::PARAM_INT); $insert->bindValue(':pieces_placed', $piecesPlaced, PDO::PARAM_INT); $insert->bindValue(':duration_seconds', $durationSeconds, PDO::PARAM_INT); $insert->bindValue(':mode', $mode, PDO::PARAM_STR); if ($roomCode === '') { $insert->bindValue(':room_code', null, PDO::PARAM_NULL); } else { $insert->bindValue(':room_code', $roomCode, PDO::PARAM_STR); } $insert->execute(); $scoreId = (int) $pdo->lastInsertId(); $placementQuery = $pdo->prepare( 'SELECT COUNT(*) FROM tetris_scores WHERE ' . 'score > :score ' . 'OR (score = :score AND lines_cleared > :lines_cleared) ' . 'OR (score = :score AND lines_cleared = :lines_cleared AND level > :level) ' . 'OR (score = :score AND lines_cleared = :lines_cleared AND level = :level AND duration_seconds < :duration_seconds) ' . 'OR (score = :score AND lines_cleared = :lines_cleared AND level = :level AND duration_seconds = :duration_seconds AND id < :score_id)' ); $placementQuery->bindValue(':score', $score, PDO::PARAM_INT); $placementQuery->bindValue(':lines_cleared', $linesCleared, PDO::PARAM_INT); $placementQuery->bindValue(':level', $level, PDO::PARAM_INT); $placementQuery->bindValue(':duration_seconds', $durationSeconds, PDO::PARAM_INT); $placementQuery->bindValue(':score_id', $scoreId, PDO::PARAM_INT); $placementQuery->execute(); $placement = ((int) $placementQuery->fetchColumn()) + 1; jsonResponse([ 'success' => true, 'entry' => [ 'id' => $scoreId, 'player_name' => $playerName, 'score' => $score, 'lines' => $linesCleared, 'level' => $level, 'pieces_placed' => $piecesPlaced, 'duration_seconds' => $durationSeconds, 'mode' => $mode, 'room_code' => $roomCode !== '' ? $roomCode : null, ], 'placement' => $placement, ]); } function listScores(int $limit = 10): void { $limit = max(1, min(25, $limit)); $pdo = db(); $stmt = $pdo->prepare( 'SELECT id, player_name, score, lines_cleared, level, pieces_placed, duration_seconds, mode, room_code, created_at ' . 'FROM tetris_scores ' . 'ORDER BY score DESC, lines_cleared DESC, level DESC, duration_seconds ASC, id ASC ' . 'LIMIT :limit' ); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); $scores = []; foreach ($stmt->fetchAll() as $row) { $scores[] = [ 'id' => (int) $row['id'], 'player_name' => (string) $row['player_name'], 'score' => (int) $row['score'], 'lines' => (int) $row['lines_cleared'], 'level' => (int) $row['level'], 'pieces_placed' => (int) $row['pieces_placed'], 'duration_seconds' => (int) $row['duration_seconds'], 'mode' => (string) $row['mode'], 'room_code' => $row['room_code'] !== null ? (string) $row['room_code'] : null, 'created_at' => (string) $row['created_at'], ]; } jsonResponse([ 'success' => true, 'scores' => $scores, 'limit' => $limit, ]); } function normalizePlayerName(string $value): string { $value = trim(preg_replace('/\s+/', ' ', $value) ?? ''); $value = preg_replace('/[^\p{L}\p{N} ._\-]/u', '', $value) ?? ''; if ($value === '') { return 'Player'; } if (function_exists('mb_substr')) { return mb_substr($value, 0, 48); } return substr($value, 0, 48); } function normalizeRoomCode(string $value): string { $value = strtoupper(trim($value)); $value = preg_replace('/[^A-Z0-9]/', '', $value) ?? ''; return substr($value, 0, 8); } function jsonResponse(array $payload, int $status = 200): void { http_response_code($status); echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; }