Skip to content

Latest commit

 

History

47 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Таблица Шульте

Тренажёр внимания и периферийного зрения: таблицы генерирует сервер, каждая попытка сохраняется вместе с покадровым разбором нажатий, статистика считается в Postgres.

client/   Vue 3 + Vite + Tailwind 4   — гексагональная архитектура (порты и адаптеры)
server/   NestJS + Prisma + Postgres  — генерация таблиц, подсчёт результатов, агрегаты

Запуск

docker compose up -d                 # Postgres на localhost:5433

cd server
cp .env.example .env                 # при необходимости поправьте DATABASE_URL/PORT
npm install
npx prisma migrate dev               # схема + Prisma Client
npm run dev                          # API на http://localhost:3333/api

cd ../client
npm install
npm run dev                          # http://localhost:5173, /api проксируется на сервер

Если бэкенд не поднят, приложение само переключается на локальный адаптер: играть можно, результаты лягут в localStorage, в шапке появится значок «офлайн».

Игрок

Регистрации нет. При первом запросе сервер заводит анонимного пользователя и кладёт его id в httpOnly-cookie (schulte_uid, год жизни). Все сессии и статистика привязаны к ней. Имя для общего рейтинга задаётся на странице статистики.

Режимы

Режим Что ищем Максимальный размер
CLASSIC числа 1…N² по возрастанию 9×9
REVERSE числа N²…1 по убыванию 9×9
GORBOV чёрные вверх и красные вниз попеременно 9×9
LETTERS буквы русского алфавита 5×5 (длина алфавита)

Плюс режим фиксации взгляда: точка в центре, таблица проходится «боковым зрением».

API

Все ответы — JSON, cookie обязательна (credentials: 'include').

Метод Путь Назначение
GET /api/me профиль анонимной сессии
PATCH /api/me имя для рейтинга
GET /api/games/catalog режимы и допустимые размеры
POST /api/games старт: сервер генерирует сетку и запоминает время
GET /api/games/:id сессия с разбором нажатий
POST /api/games/:id/finish нажатия игрока → итог считает сервер
POST /api/games/:id/abandon пометить сессию брошенной
GET /api/stats/summary сводка + разбивка по режимам и размерам
GET /api/stats/sessions история попыток с пагинацией
GET /api/stats/progress точки для графика + рекорд на каждый момент
GET /api/stats/daily активность по дням и текущая серия
GET /api/stats/leaderboard лучшие результаты по таблице
DELETE /api/stats очистить свою статистику

Клиент присылает только факты — { cellId, atMs }. Верное нажатие, точность, реакции и итоговое время считает сервер, сверяя длительность со своими часами.

Архитектура клиента

Не FSD, а порты и адаптеры: правила игры не знают ни про Vue, ни про сеть.

src/
  domain/          правила: SessionRun, генерация сетки, подсчёт итога, агрегации
  application/     порты (GameGateway, StatsGateway, ProfileGateway) и сценарии
  infrastructure/  адаптеры портов: http/ (NestJS) и local/ (localStorage)
  presentation/    Vue: страницы, компоненты, composables, форматтеры
  app/             сборка приложения: DI-контейнер, роутер, стили

app/providers/container.ts подставляет в сценарии тот адаптер, который сейчас активен, через прокси — поэтому переключение «сервер ↔ офлайн» происходит на лету и не требует менять ни одну строчку в домене или в компонентах.

Схема данных

User → GameSession → Tap. В сессии лежит сгенерированная сетка и правильный порядок обхода, поэтому сервер может пересчитать результат независимо от клиента, а Tap хранит каждое нажатие с реакцией — из них строится разбор прохождения.

Деплой

Всё живёт на бесплатных тарифах: статика и API — на Vercel, база — на Neon. API собран одной serverless-функцией на том же домене, что и фронт, поэтому httpOnly-cookie работает без sameSite=none и CORS не участвует.

api/server.js   точка входа Vercel: реэкспорт server/dist/serverless.js
vercel.json     сборка обеих частей и маршруты: /api/** на функцию,
                остальное — на index.html для history-режима vue-router

Маршрут /api/** задан правилом в vercel.json явно: catch-all по имени файла (api/[...path].js) на Vercel ловил только один сегмент пути, и всё вида /api/games/catalog отбрасывалось с NOT_FOUND, не дойдя до функции.

1. Neon

Создать проект, в Connection Details взять две строки подключения:

Переменная Какую строку брать
DATABASE_URL pooled — с -pooler в хосте
DIRECT_URL direct — без -pooler

Пул нужен потому, что каждая холодная функция открывает своё соединение; миграции же через пул не проходят и идут по прямому адресу.

2. Vercel

  1. Add New Project → репозиторий, Root Directory оставить корнем.
  2. Environment Variables: DATABASE_URL, DIRECT_URL и COOKIE_SECURE=true.
  3. Deploy.

vercel-build в server/package.json прогоняет prisma generate, prisma migrate deploy и nest build — миграции накатываются на каждый деплой, отдельного шага не требуется.

Особенности бесплатных тарифов

Первый запрос после простоя ждёт холодного старта функции (~1-2 с), Neon дополнительно будит базу (~0.5 с). Дальше всё отвечает как обычно.

About

Таблица Шульте

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages