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