Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
翻译准备中
尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
Локальный аналог игры GeoGuessr для Санкт-Петербурга. Вас «высаживают» в случайную точку города — нужно осмотреться в панораме улиц и угадать на карте, где это. 1. Игра выбирает случайную точку в Петербурге и показывает панораму улиц Яндекса в этом месте. 2. Осмотритесь и отметьте на карте, где, по вашему мнению, вы находитесь. 3. Чем ближе к реальной точке — тем больше очков (максимум 5000 за раунд). 4. Игра состоит из 5 раундов, максимальный счёт — 25000. Для центральных режимов точки генерируются вокруг центра с разным разбросом; «Весь город» использует точную административную геометрию:查看完整 README ↗
Локальный аналог игры GeoGuessr для Санкт-Петербурга. Вас «высаживают» в случайную точку города — нужно осмотреться в панораме улиц и угадать на карте, где это.
Для центральных режимов точки генерируются вокруг центра с разным разбросом; «Весь город» использует точную административную геометрию:
| Режим | Охват | Описание |
|---|---|---|
| 🏛️ Центр | ~1.5 км | Исторический центр |
| 🏙️ Средняя | ~3 км | Центр и ближайшие районы |
| 🌆 Весь город | Границы 18 районов | Вся административная территория Петербурга |
Вместо линейной шкалы можно выбрать один из 18 административных районов. Тогда сервер генерирует и повторно проверяет все точки внутри его реальной границы, а найденная панорама за границей автоматически заменяется до начала раунда. Карта районов строится из локального GeoJSON и не обращается к OSM, Overpass или другому GIS API во время игры.
Карта ответа открывается на заранее подобранном виде городской застройки
района. Центр и масштаб постоянны для района и не зависят от точек партии;
они не ограничивают перемещение карты или территорию генерации. Настройки
хранятся в DISTRICT_MAP_VIEWS в static/js/maps.js. Для районов с несколькими
городами начальным ориентиром служит одно из городских ядер: например,
Сестрорецк в Курортном и Петергоф в Петродворцовом.
Дополнительно есть режим 🚷 «не сходя с места»: осматриваться можно, ходить по стрелкам — нельзя (шаг откатывается). Такие игры помечаются бейджем в таблице лидеров.
Режимы не сравнимы напрямую (в «Центре» угадывать проще), поэтому в таблице лидеров у каждой записи показан её режим, а саму таблицу можно отфильтровать по территории и периоду. Фильтр «Район» открывает результаты одного из 18 районов. После игры таблица сразу показывает топ сыгранного режима или района, после вызова дня — дневной топ; с главной открывается общий рейтинг.
На старте можно ограничить время на раунд (1 или 3 минуты). Когда время выходит, ответ отправляется автоматически; если точка не отмечена — раунд приносит 0 очков. Лимит проверяется и на сервере: опоздавший ответ не засчитывается. Отсчёт начинается только после появления панорамы, поэтому медленная сеть не съедает игровое время. Игры с таймером помечены в таблице лидеров бейджем ⏱.
После завершения игры кнопка «Бросить вызов другу» копирует ссылку вида
/?challenge=TOKEN. Открывший её игрок сыграет те же самые 5 точек с теми
же настройками, а в конце увидит сравнение своего счёта со счётом автора.
Один общий набор из 5 точек на день (день — по петербургскому времени) для всех игроков и отдельный топ «сегодня». Одна попытка в день; недоигранный вызов можно продолжить. Кнопка на стартовом экране показывает, сколько человек уже сыграло сегодня.
С 20 октября по 1 ноября по МСК из-за логотипа на стартовом экране время от времени выглядывает Медный всадник, а сам логотип на это время глитчем переключается в «ночной» вариант. Клик по нему или по логотипу — волна на весь экран, и стартовый экран переходит в «Ночь Медного всадника»: палитра обложки поэмы, легенда и короткие правила вместо подзаголовка, таймер только 1 или 3 минуты (вызов дня и «Без лимита» недоступны). Повторный клик возвращает обычную игру. Режим не запоминается: каждый визит всадника находят заново.
В раунде вместо сухого отсчёта к игроку приближается тёмный памятник с горящими глазами, панорама чёрно-белая, в последние 10 секунд стучит сердце. Не успел отметить точку — «Попался!» и 0 очков; отмеченная к сроку точка засчитывается как обычно. Правила и счёт те же, что у игры с таймером, поэтому в таблице лидеров такие партии помечены обычным бейджем ⏱. Логотип и копирайты Яндекса не перекрываются и остаются цветными — этого требуют условия API.
Звук — только эффекты, без музыки, и только в этом режиме. Волна входа —
шум воды и удар бронзы. В раунде слышно «тяжело-звонкое скаканье по
потрясённой мостовой»: галоп сначала далёкий, глухой и редкий, к концу отсчёта
громче, звонче и чаще, в последние 10 секунд с ним стучит сердце; ответил
вовремя — галоп стихает, не успел — удар, гул бронзы и храп коня. Звук включён
по умолчанию (вход в режим — это клик, а браузеры разрешают звук только после
жеста), круглая кнопка в раунде выключает его, и выбор запоминается на
устройстве. Обычная игра звук не получает и файлы не скачивает. Записи —
CC0 с Freesound, источники в static/audio/halloween/README.md.
HALLOWEEN_MODE=on включает режим вне сезона (например, для проверки на DEV),
off — выключает; без переменной режим следует календарю. Как удалить режим
целиком, написано в docstring halloween.py.
Каждая сыгранная панорама сохраняется в пул проверенных точек. Когда в пуле накопилось хотя бы 15 подходящих мест, в обычной игре вероятность взять одно из них — 70%, а оставшиеся 30% раундов исследуют новые случайные места. Эта доля не уменьшается по мере дальнейшего роста пула. Если у новой точки съёмки действительно нет, после одной проверки игра быстро восстанавливает раунд точкой из пула; сетевой сбой повторяет ту же точку и не помечает её плохой. Вызов дня при достаточном пуле состоит только из уже проверенных мест, чтобы общий для всех набор не менялся из-за пропуска.
После десяти неудачных замен поиск приостанавливается. «Продолжить поиск» открывает следующую серию в том же раунде: территория, очки и сыгранные раунды сохраняются. Таймер начнётся после появления панорамы; менять уже показанное место нельзя. «К настройкам» возвращает к выбору параметров новой игры.
Пул поддерживает себя сам: если панорама у точки перестала находиться в трёх
разных играх подряд (Яндекс иногда убирает съёмку) — точка выбывает. Одна игра
записывает точке не больше одной неудачи.
А владелец может модерировать пул руками на странице /admin (карта всех
точек, ссылка на панораму, удаление «некрасивых» мест) — доступ по
ADMIN_KEY из окружения.
По накопленным парам «догадка → ответ» считается фактическая сложность каждой точки пула (средний промах игроков) — на экране результата показывается «это место сложнее N% точек города».
Сайт устанавливается на телефон как приложение (манифест + иконки). На мобильных карта — сворачиваемый «нижний лист»: панораму можно смотреть на весь экран, не сбрасывая позицию. Пока игрок смотрит результат, приложение один раз ищет панораму следующего раунда и прогревает несколько её самых лёгких тайлов. Скрытый панорамный Player для этого не создаётся, поэтому предзагрузка не расходует второй WebGL-контекст и заметно меньше нагружает телефон.
cd geoguessr-spb
python3 -m venv venv
source venv/bin/activate # На Windows: venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
Отредактируйте .env:
SECRET_KEY — секретный ключ Flask (python3 -c "import secrets; print(secrets.token_hex(32))")YANDEX_MAPS_API_KEY — API-ключ Яндекс.Карт (developer.tech.yandex.ru)SESSION_COOKIE_SECURE — false для локальной разработки по http (иначе cookie сессии не сохранится)ADMIN_KEY — (необязательно) ключ страницы модерации пула /admin (там же — сводка: игры, доигрываемость)METRIKA_ID — (необязательно) счётчик Яндекс Метрики с целями game_start/game_complete/daily_start/challenge_accept/challenge_share/halloween_startFLASK_DEBUG — true только для локальной отладкиFAVICON_VARIANT — dev для экспериментальной иконки; без переменной используется production-иконкаHALLOWEEN_MODE — (необязательно) on/off принудительно включает или выключает хэллоуинский режим; без переменной — по календарю (20.10–1.11 по МСК)DATABASE_URL — (необязательно) URI базы; по умолчанию SQLite-файл в instance/python app.py
Откройте в браузере: http://localhost:5000
Схема БД создаётся и обновляется автоматически при старте (Alembic-миграции через Flask-Migrate). Базы, созданные старыми версиями приложения, подхватываются и мигрируются без потери данных.
Продовая схема не запускает Flask через mod_wsgi: Apache завершает TLS и
проксирует запросы в Gunicorn на 127.0.0.1:8000. Production-vhost хранится в
deploy/apache/geoguessr.conf, а конфигурация
dev-сервера — в
deploy/apache/geoguessr-dev.conf.
Необходимые модули Apache: proxy, proxy_http, ssl, headers, rewrite
и http2. Сертификат Certbot должен использовать Apache как authenticator и
installer; после настройки это проверяется командой certbot renew --dry-run.
Рабочая версия живёт на защищённом HTTP Basic Auth домене
dev.geoguessr.spb.ru и соответствует постоянной ветке dev. Релиз
выполняется pull request’ом dev → main; GitHub Actions только запускает
тесты. Production самостоятельно проверяет публичный origin/main по
systemd-таймеру и применяет новый commit после merge.
Одноразовая настройка GitHub и production, а также обычный рабочий цикл
описаны в deploy/DEPLOYMENT.md.
Схема управляется Flask-Migrate (Alembic). По умолчанию миграции применяются
автоматически при старте приложения (AUTO_MIGRATE=true). Для ручного
управления:
AUTO_MIGRATE=false venv/bin/flask --app app.py db upgrade # применить миграции
Новую миграцию генерируйте на временной БД, а не на рабочей: с
AUTO_MIGRATE=true любая команда flask сначала мигрирует БД из
DATABASE_URL. Порядок действий описан в AGENTS.md, раздел 8.
При multi-worker запуске gunicorn без --preload отключите автомиграции
(AUTO_MIGRATE=false) и применяйте их отдельным шагом деплоя.
pip install -r requirements-dev.txt
pytest
Тесты используют отдельную временную БД (через DATABASE_URL) и не трогают
рабочую базу. Покрыты: расчёт расстояния и очков, валидация координат,
генерация точек, идемпотентные повторы запросов, полный проход игры, пул
проверенных точек, таймер, челленджи, вызов дня, статистика сложности, режимы
и лимиты.
E2E-тесты (tests/e2e/) гоняют полный игровой цикл в реальном Chromium
через Playwright с управляемой заглушкой Яндекс.Карт — без сети и ключей. В
том числе проверяются медленный prefetch, отсутствие двойного Player,
пропавшее покрытие, временный сетевой сбой и быстро закрытый модальный Player:
python -m playwright install chromium # один раз
pytest tests/e2e/
Всё вместе автоматически запускается в GitHub Actions для каждого push в
dev и pull request’ов в main.
geoguessr-spb/
├── app.py # Flask-приложение: конфигурация, миграции, HTTP-роуты
├── game_logic.py # Чистая игровая логика: константы, расчёты, валидация
├── districts.py # Загрузка районов, point-in-polygon и генерация точек
├── pool.py # Пул проверенных точек с панорамами
├── daily.py # Общий набор точек дня по МСК
├── stats.py # Сложность мест по накопленным ответам
├── halloween.py # Временный сезонный режим: окно Хэллоуина и HALLOWEEN_MODE
├── models.py # Модели БД (SQLAlchemy)
├── migrations/ # Alembic-миграции схемы БД (Flask-Migrate)
├── requirements.txt # Прод-зависимости
├── requirements-dev.txt# Зависимости для тестов
├── data/
│ ├── spb_districts.geojson # Canonical границы 18 районов (OSM, ODbL)
│ └── README.md # Provenance, relation versions и attribution
├── tools/
│ ├── update_spb_districts.py # Воспроизводимое обновление GeoJSON
│ └── favicon_gen.py # Генерация наборов иконок dev/ и prod/
├── pytest.ini # Конфиг pytest
├── .github/workflows/ # CI: push dev и PR в main
├── static/
│ ├── css/style.css # Стили интерфейса
│ ├── js/main.js # Экраны и жизненный цикл игры
│ ├── js/panorama.js # Поиск, Player и лёгкий прогрев панорам
│ ├── js/maps.js # Карты выбора, результата и финала
│ ├── js/api.js # Таймауты, повторы и обёртки HTTP API
│ ├── js/state.js # Состояние клиента и поколения загрузок
│ ├── js/districts.js # SVG-selector административного района
│ ├── js/sdk.js # Восстановление не загрузившегося SDK карт
│ ├── js/halloween.js # Хэллоуинский режим и погоня (временно)
│ ├── js/halloween-sound.js # Звук погони на Web Audio (временно)
│ ├── img/halloween/ # Всадник и «ночной» логотип для режима (временно)
│ ├── audio/halloween/ # CC0-звуки погони и их источники (временно)
│ └── favicons/ # Иконки приложения: отдельные наборы dev/ и prod/
├── templates/
│ ├── index.html # Главная страница
│ └── admin.html # Карта модерации пула (/admin)
└── tests/ # Backend, миграции и браузерные E2E
DATABASE_URL можно заменить на PostgreSQL и т.п.)Для SQLite приложение включает WAL, busy_timeout и проверку соединений: это
снижает число редких ошибок блокировки при работе нескольких gunicorn worker'ов.
| Метод | URL | Описание |
|---|---|---|
| GET | /api/districts/geometry | Локальный canonical GeoJSON для SVG-selector |
| POST | /api/game/start | Начать игру; первая локация уже находится в ответе |
| GET | /api/game/location | Координаты текущего раунда для поиска панорамы |
| POST | /api/game/ready | Зафиксировать появление панорамы и получить дедлайн таймера; в игре с таймером обязателен до ответа |
| POST | /api/game/validate_panorama | Проверить фактическую панораму до показа: границы района/города, расстояние, отсутствие повторов, а для известного места — что съёмка не сместилась |
| POST | /api/game/skip_location | Сменить точку; location_version делает сетевой повтор идемпотентным |
| POST | /api/game/continue_search | Разрешить ещё 10 замен до появления панорамы; обязательны round_id, location_version, search_batch; повтор не открывает лишнюю серию |
| POST | /api/game/guess | Идемпотентно отправить догадку с round_id; возвращает следующую локацию |
| POST | /api/game/set_address | Best-effort сохранить асинхронно найденный адрес |
| POST | /api/game/panorama_metric | Записать время поиска/появления и статус панорамы |
| GET | /api/game/results | Результаты игры: раунды с координатами, сравнение челленджа |
| GET | /api/challenge/<token> | Информация о челлендже для приглашённого игрока |
| GET | /api/player/stats?name= | Статистика игрока: игр, лучший и средний счёт |
| GET | /api/daily | Инфо о вызове дня (сыграл ли, сколько игроков сегодня) |
| GET | /api/daily/leaderboard | Топ сегодняшнего вызова дня |
| GET | /api/leaderboard |
Версия правил проверки панорам сохраняется вместе с игрой. Начатые до обновления партии и их челленджи сохраняют прежние правила границ и повторов; новые партии используют актуальные проверки. Перед началом новой игры из старой открытой вкладки сервер может попросить обновить страницу.
round_id, поэтому поздний ответ
старого раунда не портит следующий./ready) замены запрещены. Создание игр, замены точек и чтение лидерборда
ограничены по частоте (Flask-Limiter)./ready; ответ, пришедший позже лимита и 20 с запаса, получает
0 очков. Время показа и ответа хранится у каждого раунда, поэтому скорость
ответов можно разобрать и позже.Границы этих мер: браузер неизбежно получает координату, возле которой открывает панораму, поэтому они обеспечивают согласованные данные и отсекают ошибки клиента, но не делают подделку результата невозможной. Подозрительные результаты автоматически не скрываются — ложное срабатывание на честном игроке обошлось бы дороже.
Геометрия в data/spb_districts.geojson произведена из OpenStreetMap и
распространяется отдельно на условиях ODbL 1.0: © OpenStreetMap contributors.
Полный provenance и требования attribution описаны в data/README.md.
Таблица лидеров (?difficulty=center|medium|hard|district, для района обязателен district_id; ?period=week|month|all) |
| GET | /api/admin/stats | Сводка игр, пула и загрузки панорам (X-Admin-Key) |
| GET/DELETE | /api/admin/points[/<id>] | Модерация пула точек (заголовок X-Admin-Key) |
| GET | /healthz | Для deploy: 200, если БД отвечает и её ревизия Alembic совпадает с кодом |