Files
socoban/php/api.php
T
Alex Cube 4ac392a9ab Add favicon; fix two server bugs found deploying to socoban.08h.ru
- Favicon: 🚀 emoji via inline SVG data URI, both sokoban.html and php/index.php
- api.php: mb_substr() -> substr() (mbstring not enabled on the server)
- api.php: MariaDB rejects LIMIT directly inside NOT IN(...) (error 1235) —
  wrap the top-5 subquery in an extra derived-table SELECT
Verified live against the deployed MySQL/MariaDB DB: whoami, set_name,
submit (insert + dedup + trim-to-5), top all round-trip correctly.
2026-08-10 15:29:14 +03:00

124 lines
5.0 KiB
PHP

<?php
// api.php — весь бэкенд одним файлом с роутингом по ?action=. Никакого
// фреймворка не нужно на 4 маленьких эндпоинта.
require __DIR__ . '/db.php';
header('Content-Type: application/json; charset=utf-8');
$pdo = get_db();
$action = $_GET['action'] ?? '';
function read_json_body(): array {
$data = json_decode(file_get_contents('php://input'), true);
return is_array($data) ? $data : [];
}
function top5(PDO $pdo, string $level): array {
$stmt = $pdo->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(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 по числу ходов на уровень. MariaDB не даёт
// LIMIT прямо внутри NOT IN(...) (ошибка 1235) — оборачиваем
// подзапрос ещё одним SELECT (derived table), это разрешено.
$pdo->prepare('
DELETE FROM solutions WHERE level_name = ? AND id NOT IN (
SELECT id FROM (
SELECT id FROM solutions WHERE level_name = ? ORDER BY move_count ASC LIMIT 5
) AS keep_ids
)
')->execute([$level, $level]);
echo json_encode(top5($pdo, $level));
break;
}
default:
http_response_code(404);
echo json_encode(['error' => 'unknown action']);
}