Add PHP server version: leaderboard, identity, menu screens, lore
- php/: MySQL-backed (via config.php, gitignored) top-5 solutions per level, cookie identity with rename, server-side progress for "Продолжить" - Menu screens: Играть / Продолжить / Предыстория / Настройки, plus placeholder start/end screens for a future intro/outro - Lore screen: EXO:GRID backstory text (6 epochs, no gameplay changes yet) - levels.js/solver.js copied in as-is
This commit is contained in:
+119
@@ -0,0 +1,119 @@
|
||||
<?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(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']);
|
||||
}
|
||||
Reference in New Issue
Block a user