Files
Learn_System/plans/quantik-game/phase-1-shell-first-level.md
Maxim Dolgolyov 351251d652 @
feat(quantik-game): фаза 1 — оболочка игры + физ-уровень + прогресс (MVP)

Страница /quantik монтирует уровень-спеку в SimEngine (игровой режим: HUD из
Ф0 + слайдеры закона + play/reset), на победу шлёт результат и показывает
экран успеха (звёзды/время/попытки, inline SVG). Уровень phys-artillery-1
как данные (levels.js): гравитация + запуск тела из угла/скорости, портал,
бонус-звезда. Бэкенд: миграция 076 game_progress (UNIQUE user+level),
/api/game/progress (GET свой / POST upsert best time/stars, attempts++,
auth-only, валидация входа), клиент LS.gameProgress*, пункт сайдбара.
game.test.js 13/13; npm test 251 pass/8 baseline; lint:routes 0.
Уровень проверен на реальном интеграторе (311 выигрышных комбо, 31 на 3★).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@
2026-06-13 15:31:25 +03:00

9.4 KiB
Raw Permalink Blame History

Phase 1: Оболочка игры + 1 физ-уровень + прогресс (MVP)

Status: Done (reviewed — PASS, committed) Parent plan: PLAN.md Domain: fullstack

Objective

Сквозной играбельный срез: страница /quantik грузит уровень-спеку, монтирует движок в «игровом режиме» (управление = слайдеры закона + кнопка «Запуск»), на победу шлёт результат на сервер, показывает экран успеха со звёздами/временем. Прогресс сохраняется в БД. Первый уровень — «Артиллерия Квантика»: угол+скорость, попасть в портал, собрать звезду.

Tasks

  • Task 1: Миграция 076_game_progress.sql game_progress: id, user_id, level_id TEXT, best_time_ms INTEGER, best_stars INTEGER, attempts INTEGER, completed_at. UNIQUE(user_id, level_id).
  • Task 2: Контроллер gameController.js + роутер game.js, смонтирован в server.js (после /api/custom-sims). GET /api/game/progress (свой прогресс), POST /api/game/progress {level_id, time_ms, stars} (upsert: min time / max stars; attempts++). auth-only; валидация входа.
  • Task 3: Клиент LS.gameProgressList() / LS.gameProgressSubmit(levelId, {time_ms, stars}) в js/api.js.
  • Task 4: Уровень как ДАННЫЕ: frontend/js/game/levels.js (window.QuantikLevels), встроенная спека phys-artillery-1 (physics gravity + body launch + goal + 1 star + portal). Источник уровней зафиксирован в CONTEXT.md (встроенные данные сейчас; custom_sims в Ф5).
  • Task 5: frontend/quantik.html + frontend/js/game/quantik-game.js: доступ всем авторизованным (LS.initPage()); подключает _sim_expr.js+_sim_engine.js тем же путём, что lab/sim-builder. Монтирует уровень, onGoal → submit + экран успеха.
  • Task 6: «Игровой режим» — HUD из Ф0 включается сам наличием goal; управление = слайдеры params движка + кнопки play/reset (встроены в inst.el). Редакторских панелей нет.
  • Task 7: Экран успеха (DOM-оверлей страницы): звёзды (inline SVG), время, попытки, «Ещё раз» (inst.reset) / «Дальше» (disabled-заглушка для MVP). Без эмодзи.
  • Task 8: Пункт сайдбара js/sidebar.js/quantik в группе practice (icon rocket), видим всем. isActive('/quantik') подсветка работает на clean URL.
  • Task 9: Тест backend/tests/game.test.js (паттерн lab-links.test.js): submit создаёт строку, лучший перезаписывает / худший нет, attempts++, per-user, требует auth (401), валидирует вход (400). 13/13 PASS.

Files to Modify/Create

  • backend/src/db/migrations/0NN_game_progress.sql — таблица прогресса.
  • backend/src/controllers/gameController.js, backend/src/routes/game.js — API.
  • backend/src/server.js — монтаж роутера.
  • frontend/quantik.html, frontend/js/game/quantik-game.js, frontend/js/game/levels.js — клиент+уровень.
  • frontend/js/api.jsLS.gameProgress*.
  • frontend/js/sidebar.js — пункт меню.
  • backend/tests/game.test.js — тест.

Acceptance Criteria

  • /quantik грузится, монтирует уровень, цель видна; «Запуск» проигрывает физику.
  • Попадание в портал (+звезда) → экран успеха с временем/звёздами; результат записан в game_progress.
  • Повторный худший результат не перезаписывает лучший; attempts растёт.
  • npm run migrate применяет миграцию; npm test зелёный (+ новый тест); lint:routes baseline 0.

Notes

  • Маршрутизация /js/game/*: помнить гочу sim-builder — /js мапится на корневой js/, а файлы лежат во frontend/js/game/ → отдаются через express.static(frontendDir). Не трогать server.js static.
  • Роуты :id прикрыть authMiddleware на уровне роутера (lint:routes baseline 0).
  • Время — из getResult().timeMs (Ф0).

Review Checklist

  • Все задачи; конвенции (ownership/auth как studentMaterials/customSim); без эмодзи/eval
  • Миграция применяется; API безопасен; тест зелёный; lint baseline 0; existing тесты не сломаны

Handoff to Next Phase

Реестр уровней (форма данных)

frontend/js/game/levels.jswindow.QuantikLevels:

  • QuantikLevels.list() → массив записей уровней (копия); QuantikLevels.get(id) → одна запись или null; QuantikLevels.LEVELS — сырой массив.
  • Запись уровня: { id, title, subject?, hint?, spec }. id == level_id для API прогресса. spec — обычная спека SimForge с верхнеуровневым блоком goal (Ф0). Сейчас один уровень phys-artillery-1.
  • Добавить уровень = добавить запись в LEVELS (или, в Ф5, подгрузить опубликованные custom_sims cat='game' и смержить в реестр — той же формы записи). Источник = данные, не код.
  • Уровень мог бы прийти и из custom_sims (cat='game'): spec уже валидируется сервером (validateSpec пропускает goal/game). Реестр в Ф2/Ф5 может стать асинхронным (загрузка + слияние со встроенными).

Контракт API прогресса

  • GET /api/game/progress (auth) → { progress: [ { level_id, best_time_ms, best_stars, attempts, completed_at } ] } — все уровни текущего игрока.
  • POST /api/game/progress (auth) body { level_id, time_ms, stars }{ ok:true, progress:{...одна строка...} }. Upsert: best_time_ms=min, best_stars=max, attempts++. Валидация: level_id строка ≤120; time_ms/stars неотрицательные целые; stars 0..3 (иначе 400).
  • Клиент: LS.gameProgressList(), LS.gameProgressSubmit(levelId, { time_ms, stars }).
  • Таблица game_progress — миграция 076, UNIQUE(user_id, level_id), user_id ON DELETE CASCADE.
  • На Ф6 (лидерборд) — этой таблицы достаточно для «лучшее время по уровню»; агрегаты по классу — JOIN на class_members.

Где живёт экран успеха / как монтируется уровень

  • Монтаж: QuantikGame.start({ host, level })SimEngine.mount(host, level.spec) → возвращает inst. «Игровой режим» включается САМ (HUD появляется, т.к. в спеке есть goal). Управление — слайдеры params + play/reset движка (внутри inst.el).
  • Победа: inst.onGoal(res => …) (Ф0; срабатывает 1 раз). В колбэке: LS.gameProgressSubmit(level.id, { time_ms: res.timeMs, stars: res.stars.got }) (best-effort, .catch офлайн) + экран успеха.
  • Экран успеха = DOM-оверлей .qg-overlay, добавляется в host (=#qg-stage), QuantikGame.buildSuccessOverlay(state) строит карточку (звёзды inline SVG, время/звёзды/попытки, кнопки). «Ещё раз» → убрать оверлей + inst.reset(). «Дальше» — disabled-заглушка (нет следующего уровня в MVP); Ф2 (карта/мир) активирует её переходом к следующему узлу.
  • CSS оверлея — в <style> quantik.html (.qg-*). Ф2 переиспользует buildSuccessOverlay (можно расширить параметром «следующий уровень»).

Гочи для Ф2

  • inst.onGoal срабатывает 1 раз и делает pause(). Перезапуск — inst.reset() (это И физика, И attempts++). Не звать play() в onGoal-колбэке.
  • res.timeMs — мировое время (детерминизм), не wallclock. res.stars.got/res.stars.total — счётчики звёзд.
  • Страница не разрушает inst явно при навигации; Ф2 при смене уровня без перезагрузки должна вызвать inst.destroy() перед монтированием нового (или перезагружать ?level=).
  • Сайдбар-пункт /quantik видим ВСЕМ (без hidden), в отличие от teacher-only /sim-builder.