prepare(' SELECT s.move_count, s.source, s.created_at, u.name FROM solutions s JOIN users u ON u.id = s.user_id WHERE s.level_name = ? ORDER BY s.move_count ASC LIMIT 5 '); $stmt->execute([$level]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } switch ($action) { case 'whoami': { $u = current_user($pdo); echo json_encode($u ? ['name' => $u['name'], 'lastLevel' => (int)$u['last_level']] : ['name' => null, 'lastLevel' => 0]); break; } case 'save_progress': { $u = current_user($pdo); if (!$u) { http_response_code(401); echo json_encode(['error' => 'no user']); break; } $body = read_json_body(); $level = (int)($body['level'] ?? -1); if ($level < 0) { http_response_code(400); echo json_encode(['error' => 'bad level']); break; } $pdo->prepare('UPDATE users SET last_level = ? WHERE id = ?')->execute([$level, $u['id']]); echo json_encode(['ok' => true]); break; } case 'set_name': { $body = read_json_body(); $name = trim(mb_substr((string)($body['name'] ?? ''), 0, 40)); if ($name === '') { http_response_code(400); echo json_encode(['error' => 'empty name']); break; } $u = current_user($pdo); if ($u) { $pdo->prepare('UPDATE users SET name = ? WHERE id = ?')->execute([$name, $u['id']]); } else { $token = bin2hex(random_bytes(16)); $pdo->prepare('INSERT INTO users (token, name, created_at) VALUES (?, ?, ?)') ->execute([$token, $name, time()]); setcookie('sokoban_token', $token, time() + 60*60*24*365, '/', '', false, true); } echo json_encode(['name' => $name]); break; } case 'top': { $level = (string)($_GET['level'] ?? ''); echo json_encode($level === '' ? [] : top5($pdo, $level)); break; } case 'best': { $level = (string)($_GET['level'] ?? ''); $stmt = $pdo->prepare('SELECT moves FROM solutions WHERE level_name = ? ORDER BY move_count ASC LIMIT 1'); $stmt->execute([$level]); $row = $stmt->fetch(PDO::FETCH_ASSOC); echo json_encode($row ? ['moves' => json_decode($row['moves'])] : null); break; } case 'submit': { $u = current_user($pdo); if (!$u) { http_response_code(401); echo json_encode(['error' => 'no user']); break; } $body = read_json_body(); $level = (string)($body['level'] ?? ''); $moves = $body['moves'] ?? null; $source = ($body['source'] ?? 'player') === 'solver' ? 'solver' : 'player'; if ($level === '' || !is_array($moves) || !count($moves) || count($moves) > 20000) { http_response_code(400); echo json_encode(['error' => 'bad payload']); break; } // каждый ход — пара из {-1,0,1}, ничего больше сюда залететь не должно foreach ($moves as $m) { if (!is_array($m) || count($m) !== 2 || !in_array($m[0], [-1,0,1], true) || !in_array($m[1], [-1,0,1], true)) { http_response_code(400); echo json_encode(['error' => 'bad move']); break 2; } } $moveKey = md5(implode('|', array_map(function($m) { return $m[0] . ',' . $m[1]; }, $moves))); try { $pdo->prepare('INSERT INTO solutions (level_name, user_id, moves, move_count, move_key, source, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)') ->execute([$level, $u['id'], json_encode($moves), count($moves), $moveKey, $source, time()]); } catch (PDOException $e) { // UNIQUE(level_name, move_key) — та же последовательность уже есть, не ошибка } // держим только топ-5 по числу ходов на уровень $pdo->prepare(' DELETE FROM solutions WHERE level_name = ? AND id NOT IN ( SELECT id FROM solutions WHERE level_name = ? ORDER BY move_count ASC LIMIT 5 ) ')->execute([$level, $level]); echo json_encode(top5($pdo, $level)); break; } default: http_response_code(404); echo json_encode(['error' => 'unknown action']); }