О проекте
Команда: future of construction
Кейс HackAlem.ai: Спецтрек — Astana Innovations
Стадия: Доведённый продукт
Задача кейса: «Аким на 5 часов»: городской симулятор, распределяющий бюджет по 5 направлениям и 5 районам
Суть
Городской симулятор: пользователь распределяет 100 единиц бюджета на 5 мер по районам Астаны и получает Astana Quality of Life Score с объяснениями, советами и сравнением с оптимумом.
Как устроено
Детерминированный движок на TypeScript считает эффекты мер с учётом лага (эффект × (8 − лаг)/8), синергий и ограничения 0–100, затем Score = 0.7·D_avg + 0.3·min(D_d) − N_crit. Оптимизатор перебирает допустимые наборы и назначения районов, советник ищет улучшения заменой мер. /api/analyze пересчитывает результат на сервере и передаёт LLM готовые числа для объяснения; без ключа работает локальный аналитик. Есть режим городских событий с резервом бюджета, чат-консультант, карта, аккаунты на node:sqlite и Markdown-отчёт.
Ключевые решения
- детерминированная модель эффектов с лагами и синергиями
- полный перебор оптимального набора
- swap-search советник
- Zod-валидация
- grounded LLM-объяснения с локальным fallback
Метрики
Контрольный пример: Score 52.56 → 56.54 (+3.985), критические показатели 2 → 0
Особенности
Сервер не доверяет присланному клиентом Score и пересчитывает сам; оптимизатор показывает лучший набор в рамках модели; CI с typecheck, lint, Vitest, build и HTTP smoke-тестами.
Стек
Next.js 15 · React 19 · TypeScript · Tailwind CSS 4 · Zod · Recharts · Vitest · node:sqlite · GitHub Actions
Модели
OpenAI-совместимый Chat Completions (OPENAI_MODEL)
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-273c9910-future-of-construction.
README
README.mdАким на 5 часов
AI-симулятор управления городом для HackAlem. Пользователь распределяет 100 условных единиц, принимает ровно 5 решений и получает Astana Quality of Life Score с объяснением сильных сторон, рисков и компромиссов.
Модель использует предоставленный синтетический датасет: 5 условных районов Астаны, 10 показателей, 14 мероприятий. Данные, стоимость, лаги, синергии и формула фиксированы для всех пользователей. Это учебная модель, а не прогноз реальных городских показателей; условные единицы не привязаны к тенге.
Запуск с чистого клона
Требуются Node.js 24.x и npm. Для аккаунтов используется встроенный node:sqlite; отдельный сервер базы данных не нужен. В каталоге проекта выполните:
npm ci
npm run dev
Откройте http://localhost:3000. Симулятор доступен без регистрации. API-ключ для расчёта и локальных объяснений не требуется. На Windows, если PowerShell блокирует npm.ps1, используйте npm.cmd вместо npm.
Для запуска собранной версии:
npm run build
npm start
Сборка использует системные шрифты и не скачивает Google Fonts. Первая установка зависимостей требует доступа к npm registry. Версии зависимостей зафиксированы в package-lock.json.
Проверка перед демонстрацией
npm run typecheck
npm run lint
npm test
npm run build
npm run smoke
typecheck проверяет TypeScript, lint — правила Next.js, React Hooks и TypeScript. Vitest проверяет математическое ядро, валидацию, оптимизатор, советник и авторизацию. smoke запускает собранное приложение на свободном локальном порту и проверяет реальные HTTP-запросы. Он использует временную базу, завершает свой сервер и не меняет существующие аккаунты.
npm run smoke:auth: регистрация, дубликаты логинов, защита от запросов с постороннего сайта, вход, выход, сохранение профиля и перенаправления.npm run smoke:model: точный контрольный расчёт, изменение результата при изменении решений, независимость от порядка, запрет превышения и подмены бюджета, режим событий, соответствие оптимизатора движку и локальный анализ без ключа.
Для проверки интерфейса в установленном Chromium-браузере после сборки: node scripts/check-browser.mjs.
На Windows скрипт ищет Edge/Chrome; на других системах задайте путь через BROWSER_EXECUTABLE.
Он использует отдельный временный профиль, проверяет регистрацию и симуляцию на настольной и мобильной ширине и выводит пути к снимкам экрана.
Проверки TypeScript, ESLint, Vitest, сборка и HTTP smoke настроены в GitHub Actions на Node.js 24. Браузерный скрипт запускается отдельно. Проверки не обращаются к платной языковой модели. Успешный CI не подтверждает доступность внешнего AI-провайдера: его необходимо отдельно проверить с собственным ключом перед защитой.
Основной сценарий
- Оставьте режим городских событий выключенным: стартовый бюджет всех команд — 100. Кнопка «Загрузить пример из задания» сразу выбирает контрольный набор и возвращает обычный режим.
- Изучите исходные показатели районов. Значения меньше 40 считаются критическими.
- Выберите пять разных мер. Для районной меры укажите район, для общегородской район не задаётся.
- Интерфейс контролирует бюджет, повторы, несовместимости и максимум две меры одного направления. Сервер повторяет все проверки.
- Завершите сценарий. Сравните Score и показатели до/после, изучите объяснения, рекомендации и сравнение с оптимальным набором. При необходимости скачайте Markdown-отчёт.
Все пять направлений доступны: транспорт, озеленение/экология, социальная инфраструктура, безопасность, городской сервис. Согласно подробным правилам датасета, не требуется выбирать по одной мере из каждого направления: ровно пять мер и максимум две одного направления означают охват минимум трёх направлений.
Контрольный пример из задания
| ID | Мероприятие | Район | Стоимость |
|---|---|---|---|
| M7 | Школа + детсад | Нура | 24 |
| M8 | Центр семейного здоровья / поликлиника | Нура | 20 |
| M10 | Освещение и камеры Safe City | Нура | 12 |
| M12 | Единая цифровая платформа обращений | Весь город | 14 |
| M5 | Перевод частного сектора на чистое топливо | Сарыарка | 25 |
| Итого | 5 решений | 95 |
Ожидаемый результат без событий:
| Проверка | Значение |
|---|---|
| Исходный Score | 52.55768 → 52.56 |
| Итоговый Score | 56.54307 → 56.54 |
| Прирост | +3.98539 |
| Остаток бюджета | 5 |
| Критические показатели | 2 → 0 |
| Синергия | M10 + M12: B1 +2 в Нуре |
Вычисления не округляются на промежуточных шагах. При отображении прироста используется округление самого прироста, поэтому +3.99 может отличаться от разности двух уже округлённых значений на 0.01.
Сценарий защиты на 3–5 минут · Полные данные, формулы и допущения.
Как устроен расчёт
Горизонт — 8 кварталов. Эффект каждой меры уменьшается с учётом лага, синергия добавляется без уменьшения. Все изменения суммируются до ограничения показателей диапазоном 0–100.
реализованный эффект = полный эффект × (8 − лаг) / 8
D_d = сумма (вес показателя × значение в районе)
D_avg = сумма (доля населения района × D_d)
Score = 0.7 × D_avg + 0.3 × min(D_d) − N_crit
N_crit — число пар «район × показатель» со значением строго меньше 40. Остаток бюджета не даёт бонуса. Порядок решений не влияет на основной расчёт. Для невалидного набора официальный Score не вычисляется.
Оптимизатор перебирает допустимые наборы и назначения районов в фиксированном каталоге. Советник отдельно ищет улучшения заменой мер. Оптимальный Score означает лучший результат в пределах этой модели.
AI и работа без ключа
В симуляторе кнопка «Консультант» справа внизу открывает чат-навигатор. Он учитывает текущие решения, остаток бюджета, событие и показатели районов, объясняет интерфейс и предлагает кнопки перехода к разделам и направлениям каталога. Меры пользователь выбирает самостоятельно. Для чата используется тот же серверный AI-провайдер, что и для анализа; без ключа или при сбое появляется пометка «Справочный режим · без ИИ». Переписка хранится в памяти текущей страницы; в запрос входят вопрос, две последние реплики и текущий план. При изменении плана незавершённый запрос отменяется.
Числа вычисляет детерминированный движок. /api/analyze принимает решения и идентификатор события, сам пересчитывает результат на сервере и передаёт языковой модели готовые значения, дельты, вклады мер и проверенные рекомендации. Присланный клиентом Score не принимается.
Для внешнего анализа скопируйте .env.example в .env.local, заполните OPENAI_API_KEY, при необходимости — OPENAI_BASE_URL и OPENAI_MODEL, затем перезапустите сервер. Провайдер должен поддерживать используемый приложением формат Chat Completions и JSON-ответ. Ключ хранится только на сервере и не должен попадать в репозиторий.
Если ключ не задан, провайдер недоступен или ответ не прошёл проверку структуры, приложение формирует локальное объяснение на основе результата. Источник отмечается как local, ответ внешней модели — как llm. Локальный анализ работает без сети, но не является вызовом LLM. Отказ AI не меняет Score и не блокирует результат.
Дополнительные возможности и границы
- Городские события: отдельный дополнительный режим. После третьего решения возникает событие, требующее резерва 6, 8 или 10 единиц. Базовый бюджет остаётся 100; доступный для мер лимит становится 94, 92 или 90. Резерв одинаково учитывается валидатором, симуляцией, советником и оптимизатором. События не меняют сами показатели районов. Для сравнения команд используйте одинаковый режим и событие.
- Профиль: регистрация с логином и паролем, вход, выход, редактирование имени и описания. Пароли хранятся как
scrypt-хеши с индивидуальной солью, сессия передаётся вHttpOnlycookie. SQLite создаётся вdata/auth.sqlite; путь можно изменить черезAUTH_DATABASE_PATH. Для размещения нужен сервер Node.js и постоянный диск. - История и сравнение команд: локальные данные браузера. Это не общая серверная турнирная таблица, записи не синхронизируются между устройствами и не привязаны к аккаунту.
- Справочник
/admin: просмотр фиксированных данных и правил, используемых движком. Изменение исходных данных участником не входит в соревновательный режим. - Отчёт: Markdown с решениями, результатом и объяснениями. Автоматическая генерация слайдов пока не реализована.
Данные и коэффициенты взяты из задания, а не из актуальной городской статистики. Для практического планирования потребуются калибровка эффектов, проверка специалистами и оценка неопределённости. В этой версии цель — воспроизводимое сравнение учебных сценариев.
Архитектура и API
Стек: Next.js 15, React 19, TypeScript, Tailwind CSS 4, Zod, Recharts, Vitest, встроенная SQLite. Точные установленные версии — в lockfile.
src/domain/ типы, единый бюджет, веса и константы
src/data/ синтетический датасет, меры, синергии, конфликты, события
src/engine/ валидация, эффекты, Score, симуляция, оптимизатор, советник
src/ai/ внешний провайдер и локальное объяснение
src/lib/ схемы API, состояние браузера, отчёт, авторизация
src/components/ интерфейс выбора мер, карты, сравнения и профиля
src/app/ страницы и серверные API-маршруты
src/tests/ автоматические проверки
scripts/ HTTP-проверки собранного приложения
docs/ модель и сценарий демонстрации
| Маршрут | Вход | Назначение |
|---|---|---|
POST /api/simulate | { decisions, eventId? } | Проверить набор и вычислить результат |
POST /api/analyze | { decisions, eventId? } | Пересчитать и объяснить результат, вернуть источник llm/local |
POST /api/consultant | { message, decisions, eventId?, history?, hasResult? } | Ответить о сайте и текущем плане, вернуть разрешённые переходы и источник llm/local |
POST /api/optimize | { eventId? } | Найти лучший набор для данного режима |
POST /api/auth/register, /api/auth/login, /api/auth/logout | Данные аккаунта / без тела для выхода | Управление сессией |
GET /api/profile, PATCH /api/profile | Для PATCH: { displayName, bio } | Просмотр и изменение профиля |
Районное решение: { "measureId": "M7", "scope": "district", "districtId": "nura" }. Общегородское: { "measureId": "M12", "scope": "city" }. Для обычного режима eventId опускается или равен null.
Ошибочная структура запроса возвращает HTTP 400, нарушение правил набора — 422. Дополнительные поля, например budget или finalScore, отклоняются. Сервер использует общий каталог, а не цены и эффекты из браузера.
Следующие шаги
Серверное хранение сценариев и общая таблица команд; импорт версионированных наборов данных для отдельных соревнований; калибровка модели и диапазоны неопределённости; генерация презентаций. Эти возможности не заявляются как уже реализованные.
Авторы коммитов
История коммитов
- 2026-09-23 17:30Update frontendakill93
- 2026-09-23 17:54Polish account pages and simulator entryAnubizze
- 2026-09-23 17:47new pravkaaaaaaaaaaaa finalAnubizze
- 2026-09-23 16:35new pravka4Anubizze
- 2026-09-23 15:55pravka3Anubizze
- 2026-09-23 15:10Add spend analysis, advisor, and leaderboard updatesAnubizze
- 2026-09-23 14:27Redesign: budget game UI and hackathon READMEAnubizze
- 2026-09-23 14:05Add Akim 5 hours city simulator.Anubizze
Структура репозитория (123 файлов)
.env.example .github/workflows/ci.yml .gitignore README.md docs/demo.md docs/model.md eslint.config.mjs next-env.d.ts next.config.ts package-lock.json package.json postcss.config.mjs scripts/check-auth.mjs scripts/check-browser.mjs scripts/check-model.mjs src/ai/analyst.ts src/ai/consultant.ts src/ai/grounding.ts src/ai/localAnalyst.ts src/ai/prompts.ts src/ai/provider.ts src/app/admin/page.tsx src/app/api/analyze/route.ts src/app/api/auth/[action]/route.ts src/app/api/consultant/route.ts src/app/api/optimize/route.ts src/app/api/profile/route.ts src/app/api/simulate/route.ts src/app/globals.css src/app/icon.svg src/app/layout.tsx src/app/login/page.tsx src/app/map/page.tsx src/app/page.tsx src/app/profile/page.tsx src/app/register/page.tsx src/app/results/page.tsx src/app/simulator/page.tsx src/components/admin/AdminConsole.tsx src/components/auth/AccountLink.tsx src/components/auth/AccountShell.tsx src/components/auth/AuthForm.tsx src/components/auth/ProfileForm.tsx src/components/dashboard/AppHeader.tsx src/components/dashboard/SimulatorApp.tsx src/components/districts/DistrictBoard.tsx src/components/landing/CityIllustration.tsx src/components/landing/LandingPage.tsx src/components/layout/SiteFooter.tsx src/components/layout/SiteHeader.tsx src/components/map/MapExplorer.tsx src/components/map/YandexMap.tsx src/components/map/map.css src/components/measures/MeasureCatalog.tsx src/components/results/ResultsApp.tsx src/components/simulation/DecisionPanel.tsx src/components/simulation/PreviewPanel.tsx src/components/spend/BudgetBar.tsx src/components/spend/Consultant.tsx src/components/spend/DistrictStrip.tsx src/components/spend/MeasureTile.tsx src/components/spend/Receipt.tsx src/components/spend/ResultPanel.tsx src/components/spend/SpendGame.tsx src/components/spend/measureIcons.ts src/components/spend/measureVisuals.ts src/components/spend/simulator.css src/components/ui/badge.tsx src/components/ui/button.tsx src/components/ui/progress.tsx src/data/districts.ts src/data/events.ts src/data/examples.ts src/data/incompatibilities.ts src/data/mapPlaces.ts src/data/measures.ts src/data/synergies.ts src/domain/constants.ts src/domain/types.ts src/engine/advisor.ts src/engine/effects.ts src/engine/fastScore.ts src/engine/optimizer.ts src/engine/scoring.ts src/engine/simulation.ts src/engine/validator.ts src/lib/analysisClient.ts src/lib/auth-http.ts src/lib/auth-schema.ts src/lib/auth-store.ts src/lib/consultant.ts src/lib/current-user.ts src/lib/decisionNarrative.ts src/lib/eligibility.ts src/lib/leaderboard.ts src/lib/mapWorkspace.ts src/lib/money.ts src/lib/report.ts src/lib/scenario-http.ts src/lib/schemas.ts src/lib/session.ts src/lib/utils.ts src/tests/advisor.test.ts src/tests/ai.test.ts src/tests/analysisNarrative.test.ts src/tests/analyzeApi.test.ts src/tests/auth.test.ts src/tests/consultant.test.ts src/tests/consultantApi.test.ts src/tests/decisionNarrative.test.ts src/tests/fixtures.ts src/tests/leaderboard.test.ts src/tests/mapWorkspace.test.ts src/tests/modelContract.test.ts src/tests/optimizeApi.test.ts src/tests/optimizer.test.ts src/tests/scenarioApi.test.ts src/tests/scoring.test.ts src/tests/session.test.ts src/tests/simulation.test.ts src/tests/validator.test.ts tsconfig.json vitest.config.mts
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.