39321-vm/api/scoreboard.php
2026-03-25 17:19:10 +00:00

174 lines
6.2 KiB
PHP

<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../db/migrate.php';
run_migrations();
try {
$method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
if ($method === 'GET') {
listScores((int) ($_GET['limit'] ?? 10));
}
if ($method !== 'POST') {
jsonResponse(['success' => 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;
}