'use strict'; /* Game progress ("Квантик — Законы Мира", Фаза 1). * * Прогресс игрока по уровням. Уровень = спека SimForge с блоком goal; * идентифицируется строковым level_id. На победу клиент шлёт результат * (time_ms, stars); сервер делает upsert, сохраняя ЛУЧШИЙ результат * (минимальное время, максимум звёзд) и инкрементируя attempts. * * Стиль следует customSimController / studentMaterialsController: * node:sqlite db.prepare, auth-only (роутер ставит authMiddleware), * валидация входа без исполнения, статусы 400. */ const db = require('../db/db'); const MAX_LEVEL_ID = 120; // длина level_id (TEXT) const MAX_TIME_MS = 24 * 60 * 60 * 1000; // санитарный потолок: сутки в мс /* Целое неотрицательное число (отвергаем NaN/Infinity/дробь/отрицательное). */ function isNonNegInt(v) { return typeof v === 'number' && Number.isInteger(v) && v >= 0; } /* GET /api/game/progress — прогресс текущего пользователя по всем уровням. */ function listProgress(req, res) { const uid = req.user.id; const rows = db.prepare(` SELECT level_id, best_time_ms, best_stars, attempts, completed_at FROM game_progress WHERE user_id = ? ORDER BY completed_at DESC, id DESC `).all(uid); res.json({ progress: rows }); } /* POST /api/game/progress body: { level_id, time_ms, stars } * Upsert: сохраняем ЛУЧШИЙ результат (min time_ms, max stars); attempts++. * Валидация: level_id строка ≤120; time_ms/stars — неотрицательные целые; * stars 0..3. БЕЗ исполнения чего-либо. */ function submitProgress(req, res) { const uid = req.user.id; const b = req.body || {}; const levelId = typeof b.level_id === 'string' ? b.level_id.trim() : ''; if (!levelId) return res.status(400).json({ error: 'level_id обязателен' }); if (levelId.length > MAX_LEVEL_ID) { return res.status(400).json({ error: `level_id длиннее ${MAX_LEVEL_ID} символов` }); } const timeMs = b.time_ms; const stars = b.stars; if (!isNonNegInt(timeMs)) return res.status(400).json({ error: 'time_ms должно быть неотрицательным целым' }); if (timeMs > MAX_TIME_MS) return res.status(400).json({ error: 'time_ms вне допустимого диапазона' }); if (!isNonNegInt(stars)) return res.status(400).json({ error: 'stars должно быть неотрицательным целым' }); if (stars > 3) return res.status(400).json({ error: 'stars вне диапазона 0..3' }); const existing = db.prepare( 'SELECT id, best_time_ms, best_stars FROM game_progress WHERE user_id = ? AND level_id = ?' ).get(uid, levelId); if (!existing) { db.prepare(` INSERT INTO game_progress (user_id, level_id, best_time_ms, best_stars, attempts) VALUES (?, ?, ?, ?, 1) `).run(uid, levelId, timeMs, stars); } else { // Лучшее время = минимум (null трактуем как «нет результата»); лучшие звёзды = максимум. const bestTime = (existing.best_time_ms == null) ? timeMs : Math.min(existing.best_time_ms, timeMs); const bestStars = Math.max(existing.best_stars || 0, stars); db.prepare(` UPDATE game_progress SET best_time_ms = ?, best_stars = ?, attempts = attempts + 1 WHERE id = ? `).run(bestTime, bestStars, existing.id); } const row = db.prepare( 'SELECT level_id, best_time_ms, best_stars, attempts, completed_at FROM game_progress WHERE user_id = ? AND level_id = ?' ).get(uid, levelId); res.json({ ok: true, progress: row }); } module.exports = { listProgress, submitProgress };