174 lines
6.2 KiB
PHP
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;
|
|
}
|