О проекте
Команда: SGL
Кейс HackAlem.ai: Спецтрек — Astana Innovations
Стадия: Рабочий MVP
Задача кейса: «Аким на 5 часов»: городской симулятор, распределяющий бюджет по 5 направлениям и 5 районам
Суть
Городской симулятор Астаны на реальной карте. Игрок выбирает меры (M1–M14) по районам в пределах бюджета, модель считает Score через 8 игровых кварталов, результаты показаны в режиме «До / После».
Как устроено
Бэкенд на Python (city_simulator.py, city_game.py, city_events.py) хранит каталог мер с лагами эффектов и считает 10 показателей районов и итоговый Score. Также есть «Пульс» с синтетическими обращениями и советник (game_ai.py). Фронтенд на ES modules с MapLibre GL JS и OpenFreeMap: полигоны 5 районов заданы вручную, символическая геометрия проектов, 2D/3D, SVG-карта как резерв. План можно импортировать и экспортировать в JSON.
Ключевые решения
- симуляция по кварталам с лагами мер
- каталог мер M1–M14
- Score качества жизни
- альтернативный сценарий
- советник по приоритетам
Особенности
Сделано без npm и сборки, MapLibre лежит локально в CSP-сборке. Каждой мере соответствует визуальное изменение на карте (коридоры ЛРТ, парки, узлы Safe City) с детерминированными позициями. Есть смоук-тесты браузера через собственный CDP-клиент.
Стек
Python · HTML/CSS/ES modules · MapLibre GL JS · OpenFreeMap · GitHub Actions
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-c65acf53-sgl.
README
README.mdАким на 5 часов
Лёгкий симулятор управления Астаной: реальная карта города, пять решений и результат через два года. Python 3.10+ · обычные HTML/CSS/ES modules · без npm и сборки.
Запуск
Запускайте команды из корня проекта, где находятся app.py и папка static/.
Для основного приложения достаточно Python 3.10+: устанавливать зависимости из
requirements.txt или выполнять npm install не нужно.
macOS / Linux:
python3 app.py
Windows · PowerShell:
py -3 app.py
Откройте http://127.0.0.1:8000. Держите терминал открытым; Ctrl+C
останавливает сервер. Если порт занят, запустите python3 app.py --port 8001
(py -3 app.py --port 8001 на Windows) и откройте адрес с портом 8001.
После обновления сервер нужно перезапустить: список статических маршрутов изменился.
Карта использует интернет. Для расчётов, выбора проектов и резервной схемы интернет не нужен.
Быстрая демонстрация
В меню ··· выберите «Демо-план», затем нажмите «Запустить симуляцию».
План из example_plan.json содержит пять мер стоимостью 95 у.е.:
M7, M8 и M10 для Нуры, M12 для всего города и M5 для Сарыарки.
После восьми игровых кварталов Score меняется с 52,56 до 56,54.
В разделе «Итоги» сравните состояние «До / После» и откройте альтернативный сценарий.
Интерфейс
- Карта занимает весь экран. Сверху — качество жизни, оставшийся бюджет и количество решений.
- Карта: наведение показывает район и оценку; клик открывает небольшую карточку с тремя приоритетными проблемами. Все десять показателей доступны по явному раскрытию.
- Решения: фильтр направлений, назначение района, компактный список выбранных проектов, валидация и запуск. На старте район не выбран.
- Пульс: компактная история синтетических обращений, запусков проектов и проверенных реакций Q8. Новые события сверху; счётчик отмечает непрочитанное, открытие панели отмечает её события прочитанными.
- Итоги: изменение оценки, основные результаты и переключатель «До / После». Он меняет игровые слои и показатели, а не реальные здания или дороги на базовой карте.
- В Q8 карта остаётся открытой: небольшой блок завершения показывает Score, «До / После» и «Показать изменения». Полная аналитика открывается через «Итоги».
- Альтернативный сценарий, реакции жителей и пресс-конференция находятся в раскрываемых блоках.
- Советник: приоритет по данным города или выбранного района, состав частичного плана, незатронутая проблема и наибольший итоговый прирост. «Показать» фокусирует район, «Почему?» раскрывает данные, «Что можно сделать?» показывает только меры каталога с положительным эффектом на нужный показатель.
- Одна основная панель открыта одновременно. На телефоне она становится нижним листом.
- Темы меняют интерфейс, базовую карту и игровые слои. Предпочтение хранится в
akim-theme-v1; при первом запуске используется системная тема. - Черновик плана по-прежнему сохраняется в
akim-map-plan-v1. Импорт, экспорт, демо-план и сброс доступны через меню···. - Кнопка выбора района в группе управления картой доступна с клавиатуры. Escape закрывает контекстную панель; анимация учитывает
prefers-reduced-motion.
Карта и география
MapLibre GL JS 5.6.2, локальная CSP-сборка и отдельный worker, находятся в
static/vendor/maplibre/. Лицензия поставляется рядом. Зависимость зафиксирована;
приложение не загружает JavaScript с CDN. Восстановить эти файлы можно командой:
python tools/fetch_map_assets.py
Провайдер — OpenFreeMap, векторные данные OpenStreetMap. Настройки камеры,
URL светлого/тёмного стиля и поля высоты зданий изолированы в static/map-provider.js.
Стили Positron/Dark дополняются палитрой из CSS-переменных --map-*.
Атрибуция провайдера остаётся видимой. Google Maps и извлечённые данные Google не используются.
static/map-adapter.js владеет единственным экземпляром MapLibre. Переключение тем,
видов и 2D/3D не пересоздаёт карту. После загрузки нового стиля адаптер восстанавливает
игровые источники, слои и выделение из текущего состояния. При 3D меняются наклон
камеры и слой экструзии. Объёмные здания видны на близком масштабе (от zoom 13);
используется доступная высота провайдера, отсутствующая высота не выдумывается.
Границы районов временные и приблизительные. В репозитории не было проверенных
административных полигонов. static/game-geography.js содержит вручную заданные
городские сектора GeoJSON со стабильными ID и точными backendName для пяти
районов модели. Это не официальная административная карта. Позже этот файл можно
заменить проверенным лицензированным набором без изменения симулятора.
Позиции обращений и проектов — представительные точки районов, не адреса жителей и не настоящие строительные площадки. Городская мера показывается одним отдельным маркером, чтобы не перегружать карту. Проблемы кластеризуются при отдалении. Небольшие области вокруг активных проектов — условная визуализация, не географическая оценка зоны воздействия.
Симулятор не зависит от провайдера: базовая карта, игровые полигоны и численная модель
разделены. При смене провайдера обновите также точный список разрешённых источников
в CSP в app.py; общие разрешения * не нужны.
Визуальные изменения города (фаза 3)
static/project-geometry.js — чистая модель визуализации: тип проекта, стабильная
репрезентативная позиция, кешированная символическая геометрия и состояния
planned / active / completed. Это визуальные этапы, не подтверждение
существования или завершения реального объекта. Названия, сроки и затрагиваемые
показатели берутся из серверного каталога; формулы и события Пульса не изменены.
static/project-visualization.js содержит ProjectVisualizationLayer. Он владеет
GeoJSON-источником sim-projects, слоями линий, полигонов, кругов и низких экструзий,
а также максимум пятью доступными с клавиатуры кнопками проектов. Адаптер карты
передаёт план, квартал, подтверждённый результат, ветку, режим сравнения и тему.
При смене стиля слои восстанавливаются в том же экземпляре MapLibre.
| Мера | Символическое изменение |
|---|---|
| M1 · Автобусные полосы | Пунктирный коридор становится сплошной транспортной линией с узлами |
| M2 · Умные светофоры | Пять редких контрольных узлов и перекрестий по районам |
| M3 · ЛРТ | Двойной условный рельсовый коридор со станциями |
| M4 · Парк | Зелёный полигон, дорожка, девять постепенно появляющихся крон |
| M5 · Чистое топливо | Условная штриховая зона и синтетическая дымка с меньшей непрозрачностью после активации |
| M6 · Озеленение | Небольшие зелёные участки и по три кроны в каждом районе |
| M7 · Школа и детсад | Учебный корпус и двор; низкая символическая экструзия в 3D |
| M8 · Центр здоровья | Крестообразный медицинский корпус; низкая экструзия в 3D |
| M9 · Спорт-хаб | Площадка с разметкой |
| M10 · Safe City | Условный освещённый коридор и четыре световых узла |
| M11 · Переходы | Две зоны безопасности и поперечная разметка |
| M12 · Платформа обращений | Пять сервисных узлов и пунктирные цифровые связи; без выдуманного здания |
| M13 · Коммунальные сети | Разветвлённая сервисная сеть с узлами |
| M14 · Аварийные бригады | Пять сервисных узлов с условными областями действия |
Позиция определяется только ID проекта и районом: четырнадцать разных постоянных слотов вокруг районного центра. Порядок выбора, удаление других мер, перезагрузка и ветка не сдвигают проект. Городские меры имеют одну главную кнопку и распределённые небольшие эффекты. Нет случайных координат, поиска настоящего адреса или изменения данных базовой карты. Метка «СИМ», подсказка и карточка явно обозначают условность.
До lag + 1 проект полупрозрачен, линии пунктирные. При активации меняются линии,
заливки и свет, геометрия появляется за 750 мс. Анимация использует feature-state,
без пересоздания источника на каждом кадре; непрерывного пульсирования нет.
prefers-reduced-motion отключает переходы. В Q8 проекты остаются видны.
В 2D корпуса показаны контурами/заливками, в 3D — условными низкими объёмами.
Высоты и размеры символические, не инженерные или географические измерения.
Наведение/фокус показывает проект, район и срок работы, выделяет связанные текущие проблемы без изменения их серьёзности. Клик использует существующую контекстную карточку. После Q8 она показывает реальные суммарные дельты плана, явно не выдавая их за индивидуальный вклад выбранной меры. До Q8 численных изменений там нет.
«До» убирает все проектные вмешательства и показывает исходные проблемы/Score. «После» возвращает все вмешательства и финальные проблемы/Score. Сам basemap остаётся тем же. «Показать изменения» включает отдельный лёгкий слой-приглушение под игровыми объектами и усиливает их подсветку; повторное нажатие восстанавливает обычный вид. Ни второй карты, ни отдельного 3D-движка, ни новой постоянной панели нет.
Если сложный слой не устанавливается, остаются простые интерактивные кнопки проектов; расчёт и воспроизведение продолжаются. Повторная загрузка стиля восстанавливает нативные эффекты. Резервная SVG-карта сохраняет прежние упрощённые значки, без новых экструзий. На очень мелком масштабе соседние символы могут сближаться; районный масштаб раскрывает геометрию. Нет настоящих трасс, точных площадок, измеренного загрязнения, транспортного потока или детальной архитектуры.
Сеть и резервный режим
Если стиль или тайлы недоступны, интерфейс показывает компактное сообщение с повторной загрузкой и переходом на схему. Выбор проектов, черновик и симуляция продолжают работать. При недоступном стиле остаются локальные игровые слои, если WebGL поддерживается. Тайлы, подписи и высоты полностью офлайн не поставляются.
Старый static/city-map.js сохранён без изменения как временный SVG-рендерер.
Можно открыть http://127.0.0.1:8000/?legacyMap=1 или нажать «Открыть схему»
при ошибке. Это схематичная резервная карта; переключатель реального 3D в ней отключён.
«Повторить» возвращает к реальной карте без потери плана.
Время и расчёты
city_simulator.py и правила не изменены. Каждый расчёт начинается с исходных значений.
- Ровно 5 уникальных мероприятий, стоимость ≤ 100, не более 2 одного направления.
- M1/M3 несовместимы везде; M4/M7 и M5/M13 — в одном районе.
- Эффект:
base_effect × (8 - lag) / 8. - Городская мера применяется полностью к каждому району.
- Синергии фиксированы и не масштабируются лагом.
- После суммирования значения ограничиваются
[0, 100]. D_d = sum(weight × indicator);D_avg = sum(population_share × D_d).N_crit— количество пар «район / показатель» строго ниже 40.Score = 0.7 × D_avg + 0.3 × min(D_d) - N_crit.
Квартальная шкала — визуальное воспроизведение. Проект активируется в квартале
lag + 1 (например, M8 с лагом 3 — в Q4). На экране может появиться сообщение о
запуске и измениться маркер. Численные оценки остаются исходными до завершения Q8,
когда интерфейс применяет окончательный ответ Python. Интерполяции Q1–Q7 нет.
Контрольный план: M7, M8, M10 в Нуре; M12 для города; M5 в Сарыарке.
| Величина | До | После |
|---|---|---|
| D_avg | 56.8624 | 58.0776 |
| Минимальный балл района | 49.18 | 52.9625 |
| N_crit | 2 | 0 |
| Quality of Life | 52.55768 | 56.54307 |
Стоимость 95, остаток 5, прирост 3.98539. В UI числа округляются до двух знаков.
python city_simulator.py --self-check
python city_simulator.py --plan example_plan.json
python city_simulator.py --plan example_plan.json --prompt
from city_simulator import CitySimulator, PlanValidationError
sim = CitySimulator()
try:
result = sim.simulate(sim.example_plan())
prompt = sim.build_report_prompt(result)
except PlanValidationError as error:
print(error.to_dict())
Формат плана — массив объектов {"measure_id": "M7", "district": "Нура"}.
Для городской меры district отсутствует или равен null. Строка «город» запрещена.
Существующая интеграция AI
В этом этапе AI backend и логика альтернатив не изменялись. Ключи остаются на сервере.
$env:OPENAI_API_KEY = 'ваш-ключ'
$env:LLM_MODEL = 'идентификатор-модели'
$env:LLM_BASE_URL = 'https://api.openai.com/v1'
python app.py
Также поддерживается LLM_API_KEY с приоритетом над OPENAI_API_KEY. .env
автоматически не загружается. Провайдер должен поддерживать Chat Completions с
response_format: json_schema. Без настроек или при ошибках используются локальные
тексты. AI меняет только повествование; числа и связи с районами вычисляет Python.
Пресс-конференция не влияет на Score. Пульс и советник используют отдельный строгий
контракт event_wording.py; прежние narrative/report API сохранены для совместимости.
Пульс города: события и текущие проблемы
city_events.py получает исходную матрицу или результат CitySimulator и создаёт:
- Текущие проблемы: все пары с показателем ниже 60. ID вида
nura:S1постоянен. Приоритет — меньшее значение, затем больший вес, затем стабильный ID. Карта и карточка района показывают до трёх приоритетов на район; на обзорной карте работает существующая кластеризация MapLibre. Нормализовавшийся показатель исчезает из активных проблем, история сохраняется. - Историю: Q0 — до двух слабых показателей каждого района; начало работы каждой
выбранной меры — в
lag + 1; Q8 — ненулевые итоговые дельты и незатронутые ведущие проблемы. Дельты берутся после clip, связи включают участников синергий. ID события:run_id:branch:тип:объект. Повторная генерация с тем же контекстом идемпотентна. У альтернативы собственные run ID, события и состояние прочтения.
Пороги: <40 critical, <50 high, <60 attention, >=60 normal. Например,
медицина Нуры 35 → 43.75 улучшилась, но проблема сохраняется. Порог 60 — правило
представления проблем, он не меняет формулу Score или порог N_crit = 40.
Числовой модели Q1–Q7 нет. Во время воспроизведения карта, приоритеты и Score сохраняют исходные значения. Событие активации содержит каталог и ожидаемую область действия, без промежуточных чисел. Только завершение Q8 публикует финальные факты. Переключатель «До» показывает исходные показатели; историю завершённого сценария он не переписывает.
Каждое событие сразу имеет локальный текст. Дополнительный фоновый запрос AI
пакетно обогащает формулировки, не блокируя запуск или воспроизведение.
Сервер повторно вычисляет факты из плана: клиентские значения не принимаются.
Ответ AI содержит только event_id и message; оболочка партии — items.
Сервер возвращает клиенту также source, run_id и branch.
Строгая граница AI: свободную прозу невозможно гарантированно проверить на
выдуманные факты одним blacklist. Поэтому Python формирует несколько допустимых
естественных формулировок (allowed_messages), а AI выбирает дословную формулировку.
Любые добавленные числа, адреса, неподтверждённые утверждения, метаданные или
неверная схема приводят к локальному fallback. Это ограничивает разнообразие
текста, но обеспечивает требование не придумывать состояние города.
LRU-кеш на 512 формулировок использует семантические факты и версию промпта,
без run ID и ветки. Доступ потокобезопасен, одинаковые параллельные запросы
сериализуются только в AI-пути. Ошибки не кешируются. Кеш, истории и прочтение
не переживают перезапуск/перезагрузку; черновик плана сохраняется как прежде.
static/city-pulse.js замораживает факты, хранит тексты отдельным слоем и проверяет
run/branch/event ID. app.js дополнительно проверяет ревизию плана и переключения
ветки, прежде чем применить асинхронный ответ.
API и файлы
POST-контракты сохранены: /api/validate, /api/simulate, /api/alternative,
/api/compare, /api/report, /api/ai/appeals, /api/ai/result, /api/ai/feedback.
Сервер по умолчанию локальный. Дополнения совместимы с прежними полями:
| Маршрут | Дополнение |
|---|---|
GET /api/catalog | city: исходный пакет событий и проблем |
POST /api/simulate | city: run ID, main, исходный/финальный контекст и события |
POST /api/alternative | city: тот же пакет для рассчитанной альтернативы |
POST /api/events/context | {plan, run_id?, branch?, stage?}; baseline по умолчанию, допускает частичный план; final требует валидных пяти мер |
POST /api/events/wording | Те же входы плюс необязательный event_ids; возвращает только патчи формулировок |
Пакет city содержит run_id, branch, events, initial, final (null до расчёта).
Контексты содержат issues, map_issue_ids, advisor, district_advisors.
Допустимые ветки: main, alternative; этапы: baseline, final.
| Файл | Ответственность |
|---|---|
static/index.html | Плавающая оболочка, панели и доступные элементы управления |
static/style.css | Общие компоненты, две палитры, мобильный вид |
static/app.js | Существующее состояние плана, валидация, UI, воспроизведение |
static/city-pulse.js | Неизменяемые факты, публикация событий, прочтение, слой формулировок |
static/project-geometry.js | Визуальные типы всех 14 мер, детерминированное размещение, кеш геометрии |
static/project-visualization.js | Нативные игровые слои MapLibre, активация, сравнение, выделение и fallback |
static/theme.js | Раннее применение и сохранение темы |
static/map-adapter.js | MapLibre, игровые слои, взаимодействие, ошибка/повтор/схема |
static/map-provider.js | Провайдер, стили, поля зданий, начальная камера |
static/game-geography.js | Приблизительные игровые GeoJSON-полигоны и точки |
static/city-map.js | Сохранённый SVG-рендерер и иконки |
city_simulator.py | Неизменённые правила и расчёт |
city_events.py | Детерминированные проблемы, события, связи мер и советник |
event_wording.py | Строгий AI-контракт, fallback и кеш |
city_game.py, game_ai.py, presentation.py, reports.py | Неизменённые альтернативы, обращения и AI |
app.py | HTTP API, разрешённые статические ресурсы и CSP |
Проверки
python -m unittest -v
node tools/test_city_pulse.mjs
node tools/test_project_visualization.mjs
Исходная проверка до изменений (23 сентября 2026): 37/38 проходят.
Существующий test_alternative_is_one_valid_replacement_with_real_score использует
точное сравнение float: 0.6624899999999982 != 0.6624899999999911.
Этот тест и вычисления намеренно не изменены. Перед фазой 2 было 38/39 успешных
проверок. Добавлены проверки событий, строгих формулировок, кеша и новых API;
текущий итог — 53/54 проходят с тем же исходным падением, без новых регрессий.
Для browser smoke нужны Chrome и Node 22+ только в окружении проверки.
Пакеты npm и websocket-client не требуются. Используйте отдельный профиль:
тест сбрасывает его черновик и тему, сохраняет скриншоты в .browser-check/.
Терминал 1:
python app.py --port 8001
Терминал 2 (PowerShell, из корня проекта):
$testProfile = Join-Path (Get-Location) '.browser-check/chrome'
Start-Process 'C:\Program Files\Google\Chrome\Application\chrome.exe' -WindowStyle Hidden -ArgumentList @(
'--headless=new', '--remote-debugging-port=9222', '--enable-unsafe-swiftshader',
"--user-data-dir=$testProfile", '--no-first-run', 'http://127.0.0.1:8001'
)
python tools/check_browser.py http://127.0.0.1:8001 9222
node tools/check_city_pulse.mjs http://127.0.0.1:8001 9222
node tools/check_project_visualization.mjs http://127.0.0.1:8001 9222
Проверяются реальная карта, отсутствие открытой панели при старте, выбор района, раскрытие метрик, ручной план, бюджет, конфликты, защита от устаревшей валидации, Q1–Q8 без промежуточного Score, финал 56.54, «До / После», альтернативы, импорт/экспорт, светлая/тёмная темы и 2D/3D на одном экземпляре карты, мобильный вид, клавиатура, reduced motion, искусственная блокировка тайлов, схема и восстановление. Проверка нормальной карты требует доступности OpenFreeMap. Скриншоты и тестовый профиль не включаются в Git.
Phase-2 browser-проверка дополнительно кликает реальный маркер проблемы на canvas, проверяет карточку Нуры, фильтр конкретных мер, советника и его действия, живую публикацию/прочтение событий, отсутствие чисел в активациях, историю обеих веток, остаточную проблему после улучшения и отложенный AI-ответ старого плана. AI-транспорт в проверках подменён; реальные платные запросы для проверки не нужны.
Phase-3 проверки покрывают стабильные позиции и отсутствие совпадающих слотов, все четырнадцать типов, этапы и лаги, городские эффекты, кеш, сравнение, разделение веток и fallback. Browser-проверка проходит три валидных плана, проверяет реальные нативные слои, совпадение кнопок с географическими якорями, карточки/дельты, визуальное резюме, light/dark × 2D/3D на одной карте, reduced motion, искусственный отказ слоя и восстановление. Скриншоты демо — 1440×900 и 1920×1080.
Авторы коммитов
История коммитов
- 2026-09-23 17:58docs: clarify app launch and demoArsen
- 2026-09-23 17:35feat: add project geometry and visualization layersmidjeff
- 2026-09-23 17:10Add integration tests, UI smoke checks, and styling updatesAdam
- 2026-09-23 17:10feat: add small CDP client and behavioral smoke test for Chromemidjeff
- 2026-09-23 17:10feat: Enhance event handling and wording managementmidjeff
- 2026-09-23 17:10Add initial implementation of the City Simulator MVP and README documentationmidjeff
Структура репозитория (36 файлов)
.env.example .github/workflows/tests.yml .gitignore README.md app.py city_events.py city_game.py city_simulator.py event_wording.py example_plan.json game_ai.py presentation.py reports.py static/app.js static/city-map.js static/city-pulse.js static/game-geography.js static/index.html static/map-adapter.js static/map-provider.js static/project-geometry.js static/project-visualization.js static/style.css static/theme.js test_app.py test_city_events.py test_city_game.py test_city_simulator.py tools/browser-client.mjs tools/check_browser.mjs tools/check_browser.py tools/check_city_pulse.mjs tools/check_project_visualization.mjs tools/fetch_map_assets.py tools/test_city_pulse.mjs tools/test_project_visualization.mjs
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.