О проекте
Команда: mittens
Кейс HackAlem.ai: Спецтрек — Astana Innovations
Стадия: Доведённый продукт
Задача кейса: «Аким на 5 часов»: городской симулятор, распределяющий бюджет по 5 направлениям и 5 районам
Суть
Городской симулятор Астаны: игрок выбирает 5 инициатив из 14 в пределах бюджета 100 для 5 районов, видит изменения показателей и итоговый Score на карте 2ГИС, AI поясняет результат.
Как устроено
У каждого района 10 показателей в 5 направлениях. Инициативы из каталога проходят серверную валидацию: бюджет, не больше 2 мер одного направления, несовместимые пары. Детерминированный движок считает I' = clip(I + Σэффект×(8−лаг)/8 + синергии) и Score = 0.7×средневзвешенный D + 0.3×минимальный D − N. OpenAI gpt-4.1-mini только объясняет готовый расчёт (до 50 слов) и на результат не влияет.
Ключевые решения
- детерминированная модель эффектов с лагами и синергиями
- взвешенный Score с учётом слабейшего района
- исчерпывающий оптимизатор кампании
- 2GIS MapGL и Places API
- polygon-clipping для границ районов
Особенности
В Score входят средневзвешенный индекс, слабейший район и штраф за критичные показатели. Сервер пересчитывает весь набор с исходного состояния, поэтому повторная отправка не удваивает эффект. Есть жюри-сценарий с ожидаемыми числами (Score 52.56 → 56.54).
Стек
TypeScript · React 19 · TanStack Start · Vite · 2GIS MapGL · Cloudflare Workers · Node.js test runner
Модели
gpt-4.1-mini
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-9f94a26d-mittens.
README
README.mdАким на 5 часов
Интерактивный симулятор управления Астаной: пять решений, ограниченный бюджет и наглядные последствия для районов.
Проект помогает разобраться, как выбор городских инициатив влияет на транспорт, экологию, социальную инфраструктуру, безопасность и городские сервисы. Игрок выступает в роли акима: распределяет ресурсы и сравнивает результат со стартовым состоянием города. Решение предназначено для демонстрации жюри хакатона, знакомства с городским управлением и обсуждения решений с городскими командами и аналитиками.
Это игровой прототип на синтетических данных, а не прогноз развития Астаны. Карта и границы загружаются из 2ГИС; показатели районов, доли населения, стоимость и эффекты инициатив заданы в коде. AI объясняет готовые расчёты, но не определяет результат.
Демонстрация

Смотреть исходное видео в формате MP4
Что реализовано
- Интерактивная карта с выбором пяти игровых районов: Есиль, Алматы, Сарыарка, Байконур и Нура; перемещение, масштабирование, подписи и отображение индекса района.
- Переключение дневной и тёмной темы карты и интерфейса. Выбор темы сохраняется в браузере и не сбрасывает раунд.
- По 10 показателей на район в пяти направлениях. Значения находятся в диапазоне 0–100, где больше — лучше. Панель статистики показывает исходное состояние и изменения.
- Каталог из 14 инициатив: 10 районных и 4 общегородские. Среди них — автобусные полосы, школа и детский сад, чистое топливо, освещение, обновление сетей и цифровая платформа обращений.
- Раунд из ровно 5 уникальных инициатив при бюджете 100 условных единиц. Допускается не более двух инициатив одного направления; проверяются несовместимые сочетания.
- Применение решений по одному или несколькими за отправку, редактирование черновика и начало нового раунда. Уже применённые решения учитываются в следующих ходах.
- Детерминированный расчёт с задержкой эффекта, синергиями, отрицательными последствиями отдельных мер и ограничением значений показателей.
- Результаты каждого хода и итоговый отчёт: Score до и после, изменения направлений по городу и критичные показатели.
- Необязательное объяснение на русском через OpenAI после хода и завершения раунда, до 50 слов на объяснение. Повторный запрос текста не меняет бюджет и расчёт.
- Обработка ошибок карты, сервера и AI; автоматические проверки модели и вспомогательной логики интерфейса.
Как работает решение
- Игрок открывает приложение и выбирает район на карте, по подписи или кнопке района.
- Изучает показатели и выбирает инициативы в категориях. Районная мера относится к выбранному району; инициативы вкладки «Весь город» затрагивают все пять районов.
- Выбранные меры попадают в черновик и резервируют бюджет. До отправки их можно удалить в разделе «План».
- Нажатие «Применить» отправляет на сервер весь накопленный набор решений. Сервер проверяет ограничения и пересчитывает результат от исходного состояния — повторная отправка не удваивает расходы или эффекты.
- Карта и статистика обновляются. Результат хода показывает изменения последней отправки, а статистика района — изменения от начала раунда. AI-текст запрашивается отдельно и не задерживает численный результат.
- После пяти применённых инициатив доступен итоговый Score и отчёт раунда. Если все пять отправлены одним пакетом, отчёт открывается кнопкой «Итоги раунда»; при поэтапном завершении — автоматически.
Правила расчёта
Для каждого показателя суммируются эффекты выбранных мер с учётом лага на фиксированном горизонте 8, затем добавляются предусмотренные синергии:
I' = clip(I + Σ(эффект × (8 − лаг) / 8) + синергии, 0, 100)
D = Σ(вес показателя × значение показателя)
Score = 0.7 × средневзвешенный D + 0.3 × минимальный D − N
Здесь D — индекс района; средневзвешенный индекс использует игровые доли населения; N — количество показателей ниже 40 во всех районах. Веса показателей заданы в src/simulator/scoring.ts. Таким образом, оценка учитывает средний результат, положение слабейшего района и критичные значения. Итоговый Score выдаётся только для полного набора из пяти мер.
Лимит двух мер относится к исходному направлению, даже если мера отображается во вкладке «Весь город». Одну и ту же меру нельзя повторить, в том числе в другом районе. Несовместимы M1 и M3 в одном раунде, а также M4 и M7 либо M5 и M13 в одном районе.
Технологии
| Слой | Технологии и назначение |
|---|---|
| Языки | TypeScript, JavaScript, CSS; JSON для каталога |
| Интерфейс | React 19, Lucide React для иконок |
| Приложение и сервер | TanStack Start, TanStack Router; серверные функции в одном приложении |
| Сборка | Vite 8, плагин React, TypeScript 6 |
| Карта | @2gis/mapgl, MapGL JS API и Places API 2ГИС |
| Геометрия | wellknown для WKT, polygon-clipping для объединения границ |
| AI | OpenAI SDK, Responses API; по умолчанию gpt-4.1-mini, модель можно изменить через OPENAI_MODEL |
| Проверки | Встроенный тестовый раннер Node.js, tsx, проверка типов TypeScript |
| Развёртывание | Cloudflare Workers, @cloudflare/vite-plugin, Wrangler |
Версии зависимостей зафиксированы в package-lock.json. База данных и отдельный сервис расчётов не используются.
Архитектура проекта
Браузер: React + карта 2ГИС
├─ MapGL / Places API → карта и границы районов
├─ simulateCity → валидация → движок симуляции → показатели и Score
└─ explainCityActions → рассчитанные изменения → OpenAI → пояснение
Фронтенд и серверные функции собираются как одно приложение TanStack Start. Сервер проверяет идентификаторы, районы, бюджет, повторы и конфликты, не принимая клиентские числа за источник истины. Расчётный модуль не зависит от доступности карты или AI.
| Путь | Содержимое |
|---|---|
src/routes/index.tsx | Основной экран, состояние раунда, черновик и отправка решений |
src/components/ | Карта, карточки инициатив, статистика, результаты хода, итоговый отчёт и AI-текст |
src/lib/ | Загрузка и объединение границ, представление показателей, проверки выбора и форматирование |
src/server/simulateCity.ts | Серверные функции исходных данных, расчёта и запроса пояснения |
src/server/analysis-context.ts | Контекст последнего хода и полного раунда для AI |
src/server/analysis.server.ts | Серверный вызов OpenAI и обработка ошибок |
src/simulator/ | Исходные показатели, меры, валидация, формулы, синергии и разбивка результата |
data/campaigns.json | Русские названия, стоимость, категории и правила выбора для интерфейса |
tests/, src/**/*.test.ts | Автоматические проверки |
vite.config.ts, wrangler.jsonc | Сборка и конфигурация Cloudflare Workers |
Подробнее о расчётном модуле — в документации симулятора. Описание продукта и интерфейса — в PRODUCT.md и DESIGN.md.
Установка и локальный запуск
Требования
- Node.js 22.12+ и npm. В
.nvmrcуказана ветка 20; если используете её, Vite 8 требует минимум 20.19.0. - Ключ 2ГИС с доступом к MapGL и Places API, включая поля
items.geometry.centroidиitems.geometry.selection, для полноценной карты. - Интернет для карты и необязательного AI-анализа.
- Серверный ключ OpenAI — только если нужны текстовые объяснения.
Локальная разработка
В корне полученной копии репозитория выполните:
npm ci
cp .env.example .env.local
Если .env.local уже существует, отредактируйте его, сохранив нужные значения. Заполните переменные:
VITE_2GIS_MAP_KEY=[скрыто]
VITE_2GIS_MAP_STYLE_ID=
OPENAI_API_KEY= sk-svc…[ключ скрыт]
# Необязательно; значение по умолчанию показано ниже:
OPENAI_MODEL=gpt-4.1-mini
| Переменная | Назначение |
|---|---|
VITE_2GIS_MAP_KEY | Браузерный ключ карты и границ; ограничьте разрешённые домены в настройках 2ГИС, включая локальный адрес http://localhost:3000 |
VITE_2GIS_MAP_STYLE_ID | Необязательный UUID опубликованного стиля 2ГИС; без него используются встроенные дневной и ночной стили, по умолчанию — ночной |
OPENAI_API_KEY | Необязательный серверный ключ; без него расчёт работает, интерфейс сообщает о недоступности AI |
OPENAI_MODEL | Необязательное имя модели OpenAI |
VITE_-переменные доступны браузеру и встраиваются при сборке. Ключ OpenAI должен оставаться серверным, без префикса VITE_. Локальные файлы окружения исключены из Git. После изменения настроек перезапустите сервер.
npm run dev
Откройте http://localhost:3000. Фронтенд и backend работают вместе; отдельный сервер на порту 8000 не нужен.
Сборка и локальный просмотр
npm run build
npm run preview
preview запускает собранное приложение с локальной средой Workers; адрес выводится в терминале.
Как проверить решение
Сценарий для жюри
Начните новый раунд и последовательно примените следующие меры. После каждого незавершающего хода нажимайте «Следующее действие».
| Шаг | Район / вкладка | Инициатива | Стоимость |
|---|---|---|---|
| 1 | Нура → социальная сфера | M7: Школа и детский сад | 24 |
| 2 | Нура → социальная сфера | M8: Центр семейного здоровья | 20 |
| 3 | Нура → безопасность | M10: Освещение и камеры | 12 |
| 4 | Весь город | M12: Цифровая платформа обращений | 14 |
| 5 | Сарыарка → экология | M5: Чистое топливо для частного сектора | 25 |
Ожидаемый результат по модели, до округления интерфейсом:
- Применено 5 из 5 решений; потрачено 95, осталось 5.
- Исходный Score: 52.55768; итоговый: 56.54307; прирост: 3.98539.
- Количество показателей ниже 40 снижается с 2 до 0.
- M10 и M12 дают предусмотренную синергию безопасности в Нуре.
- При настроенном OpenAI появляется пояснение; отсутствие ключа не мешает завершить раунд и увидеть числа.
Этот набор хранится в src/simulator/example.ts. Его можно воспроизвести без карты и внешних API:
node --import tsx --input-type=module -e 'import { simulate } from "./src/simulator/simulate.ts"; import { EXAMPLE_SCENARIO } from "./src/simulator/example.ts"; const r = simulate(EXAMPLE_SCENARIO); console.log({ baseline: r.baselineScore, final: r.finalScore, delta: r.scoreDelta, budget: r.budget });'
Для проверки ограничений начните новый раунд, выберите M1 и попробуйте добавить M3: сочетание должно быть недоступно. Можно также проверить, что третья мера одного направления заблокирована, а «Обновить отчёт» повторяет только AI-запрос, сохраняя расчёт.
Автоматические проверки
npm test
npx tsc --noEmit
npm run build
Тесты проверяют исходные показатели, каталог, веса, эффекты, лаги, синергии, ограничения, частичные отправки, точные результаты сценариев, геометрию объединения районов и вспомогательную логику интерфейса. Проверки AI используют подмену клиента; успешные тесты не подтверждают доступность внешних API.
В среде, где запрещено создание IPC-сокета CLI tsx, те же тесты можно запустить напрямую:
node --import tsx --test tests/*.test.mjs src/server/*.test.ts src/simulator/*.test.ts
Данные и интеграции
Игровые данные. Источник стартовых значений и долей населения — src/simulator/districts.ts; эффектов, лагов, синергий и несовместимостей — src/simulator/measures.ts; весов — src/simulator/scoring.ts. Названия и определения показателей находятся в src/simulator/presentation.ts. Это локальные заданные таблицы, без загрузки актуальной городской статистики.
2ГИС. MapGL отображает географическую основу. Places API (https://catalog.api.2gis.com/3.0/items) возвращает геометрию районов для города с ID 9570771978420226. WKT преобразуется в полигоны. Территории Алматы и Сарайшыка геометрически объединяются в один игровой район Алматы с одной долей населения и набором показателей. Это условность сценария, а не утверждение об административном делении.
OpenAI. Сервер передаёт рассчитанные результаты и контекст изменений в Responses API. Модель получает инструкцию объяснять существующие числа, не придумывая прогнозов; ответ ограничивается 50 словами. Запрос выполняется с store: false, тайм-аутом 20 секунд и без автоматических повторов. Ошибка возвращается как статус недоступного пояснения.
Ограничения текущей версии
- MVP ориентирован прежде всего на настольный экран; интерфейс русскоязычный.
- Доступен один сценарий Астаны, пять игровых районов, 14 фиксированных мер и раунд из пяти решений. Редактора сценариев в интерфейсе нет.
- Модель использует заданные эффекты и фиксированный горизонт, не учитывает реальные динамические данные и не валидирована как инструмент городского прогнозирования. Название проекта не означает наличие пятичасового таймера.
- Нет авторизации, базы данных, сохранения раундов между перезагрузками и многопользовательского режима. Состояние раунда хранится в памяти вкладки; в localStorage сохраняется только выбранная тема.
- Применённые меры нельзя отменить отдельно: можно удалить черновик или начать новый раунд.
- Полноценная карта зависит от сети, доступности 2ГИС и разрешений ключа. Геометрия не заменяет синтетические показатели.
- AI необязателен и зависит от доступности API и выбранной модели. Текст не является источником числового результата.
Deployed-версия
Подтверждённая ссылка на работающую публичную версию в репозитории не указана. Для демонстрации жюри используйте локальный запуск по инструкции выше: http://localhost:3000.
Авторы коммитов
История коммитов
- 2026-09-23 17:58Update README.mdDegr1s
- 2026-09-23 17:56Merge latest theme changes and finalize local setup documentationcloudlybtw
- 2026-09-23 17:54Keep setup instructions focused on local developmentcloudlybtw
- 2026-09-23 17:53Configure Cloudflare Workers deploymentcloudlybtw
- 2026-09-23 17:53Document project in Russian and add demo recordingcloudlybtw
- 2026-09-23 17:53Add dark city map and persistent top-right day-night theme toggleNurssk
- 2026-09-23 17:44Add construction loading animation, report retry and simpler round summaryNurssk
- 2026-09-23 17:35Merge origin/main and reconcile concise GPT analysis instructionsNurssk
Все коммиты (24)
- 2026-09-23 17:34Polish city HUD and add step feedback, round summary and concise GPT explanationsNurssk
- 2026-09-23 17:27rechecked the prompt for the open aiNocs782
- 2026-09-23 16:47Add bottom action dock, incremental rounds and district indicator trendsNurssk
- 2026-09-23 16:26Add deterministic exhaustive campaign optimizercloudlybtw
- 2026-09-23 16:04Add district dropdowns to campaign selectioncloudlybtw
- 2026-09-23 15:55some backend logic extendedNocs782
- 2026-09-23 15:47Redesign city HUD with blue accents and a light fullscreen mapNurssk
- 2026-09-23 15:26Install backend SDK dependency and include server testsNurssk
- 2026-09-23 15:25Merge remote-tracking branch 'origin/main'Nurssk
- 2026-09-23 15:25Group initiative selection by development categoryNurssk
- 2026-09-23 15:23some backend logicNocs782
- 2026-09-23 15:12Merge origin/main with real 2GIS map and six-district selectionNurssk
- 2026-09-23 15:08Preserve local Astana simulator with 2GIS district mapNurssk
- 2026-09-23 14:58feat: list selectioncloudlybtw
- 2026-09-23 14:40Add 2GIS district command map UINurssk
- 2026-09-23 14:31Add TanStack simulation frontend skeletonNurssk
Структура репозитория (64 файлов)
.cta.json .env.example .gitignore .nvmrc DESIGN.md PRODUCT.md README.md data/campaigns.json hack-alem.gif hack-alem.mp4 package-lock.json package.json src/components/AIExplanation.tsx src/components/ActionResult.tsx src/components/AstanaMap.tsx src/components/IndicatorStatistics.tsx src/components/InitiativeCard.tsx src/components/RoundSummary.tsx src/components/ThemeToggle.tsx src/lib/action-feedback.ts src/lib/analysis-text.ts src/lib/astana-districts.ts src/lib/indicator-stats.ts src/lib/initiative-tabs.ts src/lib/map-style.ts src/lib/round-feedback.ts src/lib/scenario-districts.ts src/lib/selection.mjs src/routeTree.gen.ts src/router.tsx src/routes/__root.tsx src/routes/index.tsx src/server/analysis-context.test.ts src/server/analysis-context.ts src/server/analysis.server.ts src/server/analysis.test.ts src/server/simulateCity.ts src/simulator/README.md src/simulator/breakdown.test.ts src/simulator/breakdown.ts src/simulator/districts.ts src/simulator/example.ts src/simulator/measures.ts src/simulator/presentation.ts src/simulator/scoring.ts src/simulator/simulate.test.ts src/simulator/simulate.ts src/simulator/types.ts src/simulator/validator.ts src/styles.css tests/action-feedback.test.mjs tests/analysis-text.test.mjs tests/indicator-stats.test.mjs tests/initiative-card.test.mjs tests/map-style.test.mjs tests/progress.test.mjs tests/reference-dataset.test.mjs tests/round-feedback.test.mjs tests/scenario-districts.test.mjs tests/selection.test.mjs tsconfig.json tsr.config.json vite.config.ts wrangler.jsonc
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.