exec( "CREATE TABLE IF NOT EXISTS tetris_scores ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, player_name VARCHAR(24) 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, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_score (score DESC, lines_cleared DESC), INDEX idx_created_at (created_at DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ); $ready = true; } function tetrisFetchLeaderboard(int $limit = 10): array { tetrisEnsureSchema(); $stmt = db()->prepare( 'SELECT id, player_name, score, lines_cleared, level_reached, duration_seconds, created_at FROM tetris_scores ORDER BY score DESC, lines_cleared DESC, duration_seconds ASC, id ASC LIMIT :limit' ); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } function tetrisFetchRecent(int $limit = 8): array { tetrisEnsureSchema(); $stmt = db()->prepare( 'SELECT id, player_name, score, lines_cleared, level_reached, duration_seconds, created_at FROM tetris_scores ORDER BY created_at DESC, id DESC LIMIT :limit' ); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } function tetrisFetchScoreById(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 tetrisFetchBestScore(): ?array { $scores = tetrisFetchLeaderboard(1); return $scores[0] ?? null; } function tetrisSaveScore(array $input): int { tetrisEnsureSchema(); $name = trim((string) ($input['player_name'] ?? '')); if ($name === '') { throw new InvalidArgumentException('Enter your name to save the run.'); } $nameLength = function_exists('mb_strlen') ? mb_strlen($name) : strlen($name); if ($nameLength > 24) { throw new InvalidArgumentException('Name must be 24 characters or fewer.'); } if (!preg_match('/^[\p{L}\p{N} _.-]+$/u', $name)) { throw new InvalidArgumentException('Use letters, numbers, spaces, dots, dashes, or underscores only.'); } $score = max(0, min(999999, (int) ($input['score'] ?? 0))); $lines = max(0, min(9999, (int) ($input['lines_cleared'] ?? 0))); $level = max(1, min(999, (int) ($input['level_reached'] ?? 1))); $duration = max(0, min(86400, (int) ($input['duration_seconds'] ?? 0))); if ($score === 0 && $lines === 0) { throw new InvalidArgumentException('Finish a run before saving to the leaderboard.'); } $stmt = db()->prepare( 'INSERT INTO tetris_scores (player_name, score, lines_cleared, level_reached, duration_seconds) VALUES (:player_name, :score, :lines_cleared, :level_reached, :duration_seconds)' ); $stmt->bindValue(':player_name', $name, 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); $stmt->execute(); return (int) db()->lastInsertId(); }