39305-vm/lib/tetris_store.php
Flatlogic Bot 9191afc91a 0
2026-03-25 12:13:13 +00:00

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),
];
}