🎯 Hunter: развёртывания
Развёрнуто 2026-07-29. Бот — приватный репозиторий dicechess-hunter, закрытая половина
open-core: рукописный king-hunt оценщик (вероятность взятия короля вместо расстояния Чебышева +
дайс-взвешенная безопасность фигур). Стратегия закрыта, транспорт — публичный MIT
dicechess-bot-runtime. Про сам алгоритм см. algorithm-improvement-ideas и
why-bot-hangs-queen — терм безопасности в hunter это прямой ответ на вторую заметку.
Что и где живёт
| GCP проект | dice-chess-lab |
| Регион | europe-west3 (Франкфурт) |
| Сервис | hunter, URL https://hunter-743631906150.europe-west3.run.app |
| Образ | europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/hunter |
| Идентичность бота | lab/hunter → bot:team:lab:hunter |
| Ресурсы | --concurrency 1 --cpu 2 --memory 1Gi --timeout 60s |
Почему Франкфурт. play-api живёт на Oracle eu-frankfurt-1 (см. oracle-cloud-runbook),
и совпадение регионов минимизирует round-trip вебхука — а это напрямую защищает часы через
OVERHEAD_BUFFER_MS. Попутно выяснилось, что ONNX-бот gcp/expectimax-onnx-3 сидит в
us-central1 и платит трансатлантик на каждом ходу: потенциальное бесплатное улучшение времени,
если руки дойдут.
Почему --concurrency 1. Работа на ход упирается в процессор, делить инстанс между запросами
смысла нет, а Strategy.chooseMoves всё равно synchronized.
Runbook
Подготовка проекта (один раз):
gcloud config set project dice-chess-lab
gcloud services enable run.googleapis.com artifactregistry.googleapis.com
gcloud artifacts repositories create dicechess --repository-format=docker --location=europe-west3
gcloud auth configure-docker europe-west3-docker.pkg.devСборка и публикация образа. Только из bash — в PowerShell нет export, а BuildKit читает
переменную именно из окружения:
export GITHUB_TOKEN=$(gh auth token)
DOCKER_BUILDKIT=1 docker build --platform linux/amd64 \
--secret id=github_token,env=GITHUB_TOKEN \
-t europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/hunter:v1 .
docker push europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/hunter:v1Деплой. --allow-unauthenticated необходим: play-api должен достучаться до вебхука, а
подлинность обеспечивает HMAC-подпись внутри рантайма, не IAM:
gcloud run deploy hunter \
--image europe-west3-docker.pkg.dev/dice-chess-lab/dicechess/hunter:v1 \
--region europe-west3 --allow-unauthenticated \
--concurrency 1 --cpu 2 --memory 1Gi --timeout 60sПроверка живости до регистрации — заодно прогревает контейнер перед рукопожатием:
curl -i -X POST "https://hunter-743631906150.europe-west3.run.app/api/webhook" \
-H "Content-Type: application/json" -d '{}'Ожидается 401: маршрут есть, обработчик жив, запрос без подписи корректно отвергнут.
Регистрация. Порядок важен и решает проблему курицы-яйца сам:
# 1. Постоянная идентичность. Токен показывается ОДИН раз.
curl -X POST "https://play-api.jc.id.lv/bot/register" \
-H "Content-Type: application/json" -d '{"team":"lab","name":"hunter"}'
export BOT_TOKEN=<токен из ответа>
# 2. Вебхук. play-api сам сходит на URL с nonce и проверит эхо.
# Рукопожатие проходит БЕЗ секрета — Main это явно поддерживает.
curl -X POST "https://play-api.jc.id.lv/bot/webhook" \
-H "Authorization: Bearer $BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"url":"https://hunter-743631906150.europe-west3.run.app/api/webhook"}'
# 3. Только теперь секрет попадает в сервис (создаётся новая ревизия).
gcloud run services update hunter --region europe-west3 \
--update-env-vars DICECHESS_WEBHOOK_SECRET=<секрет из ответа>Открыть для игры людям — после этого бот появляется в каталоге на play.jc.id.lv:
curl -X POST "https://play-api.jc.id.lv/bot/open-to-humans" \
-H "Authorization: Bearer $BOT_TOKEN" -H "Content-Type: application/json" \
-d '{"description":"Hand-crafted king-hunt evaluator: ..."}'
curl -s "https://play-api.jc.id.lv/lobby/bots" | jq '.bots[] | select(.name=="hunter")'Наблюдение за работой:
gcloud beta run services logs tail hunter --region europe-west3 # tail только в beta-треке!
gcloud logging read 'resource.type=cloud_run_revision AND resource.labels.service_name=hunter AND httpRequest.requestMethod=POST' \
--limit 20 --format="table(timestamp,httpRequest.latency)" --freshness=1h⚠️ Подводные камни
Всё ниже стоило времени, так что записано именно поэтому.
gcloud run deploy --source не работает. Он собирает через Cloud Build, а Dockerfile требует
BuildKit-секрет для приватного GitHub Packages (артефакт движка). Cloud Build секреты BuildKit в
этом потоке не передаёт → sbt update падает на неразрешённой зависимости. Только локальная сборка
- push +
--image.
--platform linux/amd64 обязателен. Мак на Apple Silicon, Cloud Run на amd64. В Dockerfile
стадия сборки помечена --platform=$BUILDPLATFORM, поэтому собирается нативно быстро (байткод JVM
архитектурно нейтрален), а рантайм получается amd64 — но только если флаг передан.
export, а не VAR=val && docker. Вторая форма переменную не экспортирует, BuildKit увидит
пустой секрет. Та же грабля, что и раньше в проекте.
Wake-проба НЕ покрывает лестницу. POST /lobby/bots/{team}/{name}/wake — фича каталога,
её вызывает сайт по клику на карточку, и она честно форсирует холодный старт до того, как пойдут
часы. Планировщик лестницы её не вызывает. Значит для scale-to-zero бота на лестнице каждая партия
после простоя начинается с секунд подъёма JVM → флаги по времени → по правилу #150 бот, проигравший
по времени все партии двух подряд зеркальных пар, автоматически снимается с лестницы.
Лечение — --min-instances 1 (стоит центы за ночь, возвращается в 0 утром). Ставить только если
лестница реально играет, иначе тёплый инстанс жжёт деньги без партий.
Токен восстановлению не подлежит. И токен бота, и секрет вебхука отдаются один раз. Ротация
токена (POST /bot/token) требует текущего токена, то есть при утере не спасает. Путь
восстановления один: оператор на хосте play-api добавляет запись в PLAY_BOT_TOKENS формата
team|name|token, назначая известный токен со стороны сервера. Это правка окружения и рестарт.
gcloud run services logs tail есть только в beta. В stable-треке — только read.
Измеренное
Локальная арена (эта машина, 20 партий, контроль 1+0) против aggressive — см. arena:
| Вариант | Счёт | Флаги | p50 | max |
|---|---|---|---|---|
limit=12 | 75.0% | 0/0 | 32 мс | 228 мс |
limit=all | 75.0% | 0/0 | 538 мс | 2511 мс |
| KCP выключен | 50.0% | 0/0 | 40 мс | 232 мс |
Два вывода: дорогая фаза (вероятность взятия короля) стоит 25 процентных пунктов, а
candidateLimit=12 уже на плато — расширение до всех кандидатов не меняет ни одного решения при
17-кратной цене. Ср. насыщение K=24 у ONNX-бота в two-ply-expectimax-over-value-model.
В проде задержка примерно в 6 раз выше: медиана ~190 мс против 32 мс локально, максимум 1158 мс (похоже на прогрев JIT). Сеть почти не при чём — оба конца во Франкфурте, это разница процессоров. Для игры с человеком незаметно; для лестницы 5+3 запас всё ещё большой; для блица 1+0 запас тонкий.
Важно: добавление vCPU задержку не уменьшит — поиск однопоточный, упирается в одно ядро. Рычаги
два: опустить candidateLimit ниже 12 (нижнюю границу плато мы не искали — возможно, 6–8 дают
те же 75% вдвое быстрее) либо распараллелить вторую фазу, где кандидаты независимы.
hunter-k6 на rpi3 через туннель aurora (2026-07-29)
Второй вопрос из «Открыто» выше — нижняя граница плато candidateLimit — оказался экспериментом,
для которого нашлось идеальное железо. Замерил три хоста реальными прогонами mise run arena:timed
(тот же образ, тот же 1+0, 20 партий) перед тем как выбирать:
| Хост | Архитектура | Железо | p50 | max | Счёт |
|---|---|---|---|---|---|
| Мак (dev) | arm64 | Apple Silicon | 32 мс | 228 мс | 75.0% |
| aurora | x86_64 | 8 ядер / 22 ГБ | 80 мс | 811 мс | 75.0% |
| Cloud Run (прод) | x86_64 | 2 vCPU | ~190 мс | 1158 мс | — |
| rpi4 | arm64 | 4×A72 / 3.7 ГБ | 569 мс | 1957 мс | 75.0% |
| rpi3 | arm64 | 4×A53 / 905 МБ | 779 мс | 3021 мс | 70.0%¹ |
¹ Расхождение (14/6/0 против 15/5/0 везде остальные) — шум одной партии на 20, не эффект медленного CPU: флагов 0/0, худший ход = 5% от бюджета партии. Три других хоста сошлись на одном результате.
aurora оказалась быстрее даже Cloud Run — не Pi, а неожиданный лидер. Но именно rpi3 сейчас
не загружена вообще, поэтому туда и пошёл hunter-k6 (candidateLimit=6, а не 24 — верхняя
сторона плато уже подтверждена через limit=all = та же 75.0% при 17× цене; открытый вопрос был
только про нижнюю границу).
Сеть — не тот туннель, что я сперва предположил
На rpi4 тоже стоит cloudflare-tunnel, но это не наша инфраструктура — эксперимент с n8n,
никак не связанный с dicechess. Настоящий продовый туннель для всех сервисов — на aurora
(docker inspect → Cmd: [tunnel --no-autoupdate run], без --url — именной туннель, ingress
управляется удалённо в дашборде Cloudflare, не локальным config.yml).
rpi3 сидит в отдельной подсети 192.168.20.x (та же, где WoL не работает — но это ограничение
только L2-широковещания). Обычный TCP между aurora и rpi3 проходит без проблем
(ping/nc -zv 192.168.20.99 22 — оба ОК, TTL=63, один хоп маршрутизации). Значит расширить
существующий туннель на rpi3 — это не работа с VLAN/роутером, а всего одна запись в дашборде
Cloudflare: Public Hostname → hunter-k6.jc.id.lv → http://192.168.20.99:8091.
Развёртывание
Порт 8091, не 8080 — на rpi3 уже висит zigbee2mqtt на 8080. Память ограничена явно: у rpi3
всего 905 МБ, и -XX:MaxRAMPercentage=75.0 от неограниченного контейнера забрал бы слишком много.
# arm64-образ собирается нативно на Apple Silicon, без эмуляции
export GITHUB_TOKEN=$(gh auth token)
DOCKER_BUILDKIT=1 docker build --platform linux/arm64 \
--secret id=github_token,env=GITHUB_TOKEN -t dicechess-hunter:arm64 .
# перенос без registry — save/load через ssh-пайп
docker save dicechess-hunter:arm64 | ssh rpi3 'docker load'
ssh rpi3 'docker run -d --name hunter-k6 --restart unless-stopped --memory=512m \
-p 8091:8080 \
-e HUNTER_CANDIDATE_LIMIT=6 \
-e DICECHESS_WEBHOOK_SECRET=<секрет из шага регистрации> \
dicechess-hunter:arm64'Регистрация — тот же порядок, что и для Cloud Run (см. выше): register без URL → webhook с
публичным URL (рукопожатие БЕЗ секрета) → секрет в переменную окружения контейнера (пересоздать,
не docker update — переменные окружения живому контейнеру не добавить) → ladder/join.
Плюс, которого нет у Cloud Run: rpi3 всегда включён. Ни холодного старта, ни риска
авто-парковки от простоя (#150) — той самой проблемы, из-за которой на Cloud Run нужен
--min-instances 1. Здесь эта проблема просто не существует.
Токен и секрет не хранятся здесь — оба показываются один раз при регистрации, записаны в
менеджере паролей отдельно от этой заметки. Восстановление при утере — тот же путь, что описан
выше для lab/hunter: PLAY_BOT_TOKENS на стороне play-api.
Открыто
lab/hunter(Cloud Run,limit=12) иlab/hunter-k6(rpi3,limit=6) оба на лестнице — зеркальные пары копятся, вердикт черезGET /strength(SPRT, api#181) даст статистический ответ, держит лиk6те же 75%, что иk12, а не только один прогон на 20 партий.- Если
k6держит силу — следующий шагk4, искать границу плато точнее. - Веса необкатаны: ни один не подбирался под замер.
- Известный, не исправленный баг: при обрыве по дедлайну непереоценённые кандидаты остаются с
дешёвым баллом, а переоценённые — с полным (разные шкалы). При
limit=12/limit=6дедлайн никогда не срабатывал ни на одном хосте — риск теоретический, не наблюдённый.