167 lines
5.9 KiB
PHP
167 lines
5.9 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/../db/config.php';
|
|
|
|
function tetrisEnsureSchema(): void
|
|
{
|
|
static $ready = false;
|
|
if ($ready) {
|
|
return;
|
|
}
|
|
|
|
db()->exec(
|
|
"CREATE TABLE IF NOT EXISTS tetris_scores (
|
|
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
player_name VARCHAR(32) NOT NULL,
|
|
score INT UNSIGNED NOT NULL DEFAULT 0,
|
|
lines_cleared INT UNSIGNED NOT NULL DEFAULT 0,
|
|
level_reached INT UNSIGNED NOT NULL DEFAULT 1,
|
|
duration_seconds INT UNSIGNED NOT NULL DEFAULT 0,
|
|
client_signature VARCHAR(64) DEFAULT NULL,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
INDEX idx_score_order (score DESC, lines_cleared DESC, level_reached DESC, duration_seconds ASC)
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"
|
|
);
|
|
|
|
$ready = true;
|
|
}
|
|
|
|
function tetrisNormalizePlayerName(string $name): string
|
|
{
|
|
$name = trim(preg_replace('/\s+/', ' ', $name) ?? '');
|
|
return function_exists('mb_substr') ? mb_substr($name, 0, 32) : substr($name, 0, 32);
|
|
}
|
|
|
|
function tetrisFetchTopScores(int $limit = 10): array
|
|
{
|
|
tetrisEnsureSchema();
|
|
$limit = max(1, min(100, $limit));
|
|
$stmt = db()->query(
|
|
"SELECT id, player_name, score, lines_cleared, level_reached, duration_seconds, created_at
|
|
FROM tetris_scores
|
|
ORDER BY score DESC, lines_cleared DESC, level_reached DESC, duration_seconds ASC, id ASC
|
|
LIMIT {$limit}"
|
|
);
|
|
|
|
return $stmt->fetchAll() ?: [];
|
|
}
|
|
|
|
function tetrisFetchRecentScores(int $limit = 8): array
|
|
{
|
|
tetrisEnsureSchema();
|
|
$limit = max(1, min(100, $limit));
|
|
$stmt = db()->query(
|
|
"SELECT id, player_name, score, lines_cleared, level_reached, duration_seconds, created_at
|
|
FROM tetris_scores
|
|
ORDER BY id DESC
|
|
LIMIT {$limit}"
|
|
);
|
|
|
|
return $stmt->fetchAll() ?: [];
|
|
}
|
|
|
|
function tetrisFetchScore(int $id): ?array
|
|
{
|
|
tetrisEnsureSchema();
|
|
$stmt = db()->prepare(
|
|
'SELECT id, player_name, score, lines_cleared, level_reached, duration_seconds, created_at
|
|
FROM tetris_scores
|
|
WHERE id = :id
|
|
LIMIT 1'
|
|
);
|
|
$stmt->bindValue(':id', $id, PDO::PARAM_INT);
|
|
$stmt->execute();
|
|
$score = $stmt->fetch();
|
|
|
|
return $score ?: null;
|
|
}
|
|
|
|
function tetrisFetchScoreRank(int $id): ?int
|
|
{
|
|
$score = tetrisFetchScore($id);
|
|
if (!$score) {
|
|
return null;
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'SELECT COUNT(*) + 1 AS score_rank
|
|
FROM tetris_scores
|
|
WHERE score > :score
|
|
OR (score = :score AND lines_cleared > :lines)
|
|
OR (score = :score AND lines_cleared = :lines AND level_reached > :level)
|
|
OR (score = :score AND lines_cleared = :lines AND level_reached = :level AND duration_seconds < :duration)
|
|
OR (score = :score AND lines_cleared = :lines AND level_reached = :level AND duration_seconds = :duration AND id < :id)'
|
|
);
|
|
$stmt->bindValue(':score', (int) $score['score'], PDO::PARAM_INT);
|
|
$stmt->bindValue(':lines', (int) $score['lines_cleared'], PDO::PARAM_INT);
|
|
$stmt->bindValue(':level', (int) $score['level_reached'], PDO::PARAM_INT);
|
|
$stmt->bindValue(':duration', (int) $score['duration_seconds'], PDO::PARAM_INT);
|
|
$stmt->bindValue(':id', (int) $score['id'], PDO::PARAM_INT);
|
|
$stmt->execute();
|
|
|
|
return (int) ($stmt->fetchColumn() ?: 1);
|
|
}
|
|
|
|
function tetrisInsertScore(array $input): array
|
|
{
|
|
tetrisEnsureSchema();
|
|
|
|
$playerName = tetrisNormalizePlayerName((string) ($input['player_name'] ?? ''));
|
|
$score = (int) ($input['score'] ?? 0);
|
|
$lines = (int) ($input['lines_cleared'] ?? 0);
|
|
$level = (int) ($input['level_reached'] ?? 1);
|
|
$duration = (int) ($input['duration_seconds'] ?? 0);
|
|
$clientSignature = trim((string) ($input['client_signature'] ?? ''));
|
|
|
|
$playerLength = function_exists('mb_strlen') ? mb_strlen($playerName) : strlen($playerName);
|
|
|
|
if ($playerName === '' || $playerLength < 2) {
|
|
throw new InvalidArgumentException('Enter a player name with at least 2 characters.');
|
|
}
|
|
|
|
if (!preg_match('/^[\p{L}\p{N} ._\-]+$/u', $playerName)) {
|
|
throw new InvalidArgumentException('Use letters, numbers, spaces, dots, dashes, or underscores in the player name.');
|
|
}
|
|
|
|
if ($score < 0 || $score > 9999999) {
|
|
throw new InvalidArgumentException('Score is outside the allowed range.');
|
|
}
|
|
|
|
if ($lines < 0 || $lines > 9999) {
|
|
throw new InvalidArgumentException('Lines cleared is outside the allowed range.');
|
|
}
|
|
|
|
if ($level < 1 || $level > 999) {
|
|
throw new InvalidArgumentException('Level is outside the allowed range.');
|
|
}
|
|
|
|
if ($duration < 0 || $duration > 86400) {
|
|
throw new InvalidArgumentException('Duration is outside the allowed range.');
|
|
}
|
|
|
|
if ($score === 0 && $lines === 0) {
|
|
throw new InvalidArgumentException('Play a round before submitting a score.');
|
|
}
|
|
|
|
$stmt = db()->prepare(
|
|
'INSERT INTO tetris_scores (player_name, score, lines_cleared, level_reached, duration_seconds, client_signature)
|
|
VALUES (:player_name, :score, :lines_cleared, :level_reached, :duration_seconds, :client_signature)'
|
|
);
|
|
$stmt->bindValue(':player_name', $playerName, PDO::PARAM_STR);
|
|
$stmt->bindValue(':score', $score, PDO::PARAM_INT);
|
|
$stmt->bindValue(':lines_cleared', $lines, PDO::PARAM_INT);
|
|
$stmt->bindValue(':level_reached', $level, PDO::PARAM_INT);
|
|
$stmt->bindValue(':duration_seconds', $duration, PDO::PARAM_INT);
|
|
$safeSignature = $clientSignature !== '' ? (function_exists('mb_substr') ? mb_substr($clientSignature, 0, 64) : substr($clientSignature, 0, 64)) : null;
|
|
$stmt->bindValue(':client_signature', $safeSignature, $safeSignature !== null ? PDO::PARAM_STR : PDO::PARAM_NULL);
|
|
$stmt->execute();
|
|
|
|
$id = (int) db()->lastInsertId();
|
|
|
|
return [
|
|
'score' => tetrisFetchScore($id),
|
|
'rank' => tetrisFetchScoreRank($id),
|
|
];
|
|
}
|