AAYMA
RU
Войти

Аким на 5 часов

Городской симулятор: пользователь распределяет 100 единиц бюджета на 5 мер по районам Астаны и получает Astana Quality of Life Score с объяснениями…

Доведённый продукт
Команда «future of construction» 7💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: 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-провайдера: его необходимо отдельно проверить с собственным ключом перед защитой.

Основной сценарий

  1. Оставьте режим городских событий выключенным: стартовый бюджет всех команд — 100. Кнопка «Загрузить пример из задания» сразу выбирает контрольный набор и возвращает обычный режим.
  2. Изучите исходные показатели районов. Значения меньше 40 считаются критическими.
  3. Выберите пять разных мер. Для районной меры укажите район, для общегородской район не задаётся.
  4. Интерфейс контролирует бюджет, повторы, несовместимости и максимум две меры одного направления. Сервер повторяет все проверки.
  5. Завершите сценарий. Сравните Score и показатели до/после, изучите объяснения, рекомендации и сравнение с оптимальным набором. При необходимости скачайте Markdown-отчёт.

Все пять направлений доступны: транспорт, озеленение/экология, социальная инфраструктура, безопасность, городской сервис. Согласно подробным правилам датасета, не требуется выбирать по одной мере из каждого направления: ровно пять мер и максимум две одного направления означают охват минимум трёх направлений.

Контрольный пример из задания

IDМероприятиеРайонСтоимость
M7Школа + детсадНура24
M8Центр семейного здоровья / поликлиникаНура20
M10Освещение и камеры Safe CityНура12
M12Единая цифровая платформа обращенийВесь город14
M5Перевод частного сектора на чистое топливоСарыарка25
Итого5 решений95

Ожидаемый результат без событий:

ПроверкаЗначение
Исходный Score52.55768 → 52.56
Итоговый Score56.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-хеши с индивидуальной солью, сессия передаётся в HttpOnly cookie. 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, отклоняются. Сервер использует общий каталог, а не цены и эффекты из браузера.

Следующие шаги

Серверное хранение сценариев и общая таблица команд; импорт версионированных наборов данных для отдельных соревнований; калибровка модели и диапазоны неопределённости; генерация презентаций. Эти возможности не заявляются как уже реализованные.

Звёзды
⭐ 0
Форки
0
Язык
TypeScript
Последний push
в прошлом месяце
Коммиты
8
Авторы
2
Файлы
123
Ветки
1

Авторы коммитов

Anubizze 7akill93 1

История коммитов

  • 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 комментариев

Войдите, чтобы комментировать

Пока нет комментариев — будьте первым.