HTTP-приёмник раздач от poker_client/hand_tracker.py. С флагом
--clients N сравнивает руки всех N клиентов через top_hand (перенесён
из ../gto/top_hand) и отвечает каждому вердиктом.
Только stdlib — venv не нужен, pip install ничего не требует.
poker_server/
├── hand_server.py # HTTP-сервер (http.server), сохраняет раздачи в server_data/<client_id>.jsonl
└── lib/
└── strategy/
├── cards.py, evaluator.py # копии из gto/ (нужны top_hand отдельно от poker_client)
└── top_hand/
└── top_hand.py # pick_best_hand(hands, board=None, ...)
Ранг+масть одной буквой, "T" для десятки: "Th", "As", "9c" — формат
lib/strategy/cards.py (parse_card), тот же, что печатает
poker_client/hand_tracker.py в поле "hand". Единый формат везде в
проекте.
python3 hand_server.py --port 8765
# сравнение рук: сессия ровно на N клиентов
python3 hand_server.py --port 8765 --clients 3GET /health — проверка живости.
Клиент вызывает один раз при запуске (после выбора областей на экране, до
начала слежения за столом) — регистрируется (тот же лимит --clients N,
что и в /hands, 403 сверх лимита) и блокируется без таймаута. В
консоли сервера наберите start и Enter — все, кто сейчас ждёт на
/register, получают {"status": "start"} одновременно и клиенты
приступают к работе. Гарантирует, что все клиенты стартуют синхронно, а
не как только сами успели настроиться.
Тело запроса может нести необязательный "nickname" (в hand_tracker.py
берётся из --nick):
{"client_id": "a1b2c3...", "nickname": "luhar"}Сервер запоминает client_id -> nickname и подставляет ник вместо
client_id в "others" победителю раунда (см. ниже) — так победителю не
нужно самому сопоставлять чужие client_id с игроками за столом. Для
клиента без --nick в "others" по-прежнему используется его client_id
целиком.
Просто сохраняет раздачу в server_data/<client_id>.jsonl и отвечает
{"status": "ok"} — сравнение рук не включено (без N неизвестно, скольких
клиентов ждать).
Тело /hands может нести необязательный "board" — уже открытые карты
стола в той же нотации:
{"client_id": "a1b2c3...", "hand_number": 7, "ts": "14:51:24",
"hand": ["8s", "9s"], "board": ["5h", "6d", "7c"], "bomb_pot": true}Шлёт его клиент с флагом --bomb-pot: на «взрывном банке» флоп известен
до всяких ставок, и сравнивать руки без него неправильно — готовый стрит
проигрывает паре тузов, потому что сравнение идёт по двум картам. С бордом
pick_best_hand(board=...) даёт верный ответ (проверено: 8♠9♠ против
A♥A♦ на 5♥6♦7♣ — побеждает стрит).
Борд у всех клиентов один, но читается OCR-ом, и чтения могут разойтись: сервер берёт самый частый непустой вариант и при разногласии печатает об этом строку — молчаливый выбор тут менял бы победителя раунда.
Раунд — последняя присланная рука от каждого из N зарегистрированных клиентов (без сверки по времени/номеру раздачи — у каждого клиента свой независимый счётчик). Запрос блокируется (long-poll), пока не наберутся все N, и тогда все получают ответ на свой же POST одновременно:
- у кого рука лучшая (по
top_hand.pick_best_hand):Ключи в{"verdict": "top_hand", "round": 0, "hand": ["Ah", "As"], "others": {"luhar": ["Kd", "Kc"], "<client_id2>": ["7h", "2d"]}}"others"— ники клиентов, кому в этом раунде уйдётfold(или ихclient_idцеликом, если ник не передавался на/register). - у остальных:
{"verdict": "fold", "round": 0, "hand": ["Kd", "Kc"]}
Клиент шлёт сюда POST /hand_log — по одной записи на КАЖДУЮ раздачу,
даже когда рука не топовая и когда все сфолдили. Записи копятся в
server_data/logs/ГГГГММДД_ЧЧММСС/<client_id>.jsonl — по подкаталогу
на каждый запуск сервера, чтобы соседние сессии не смешивались; имена так
сортируются по времени сами собой. Сервер добавляет к записи только
received_at. В консоли это видно строкой:
[журнал] zov71: раздача 2854198650 -> lose, решений 2
Внутри записи сырые данные: все события чата этой раздачи по порядку,
каждое решение советника с числами, по которым оно принято (банк,
доплата, эквити, требуемое эквити, живые оппоненты, что нажали), стек до
и после, исход. hand_id — номер раздачи из чата, общий для всех
клиентов за столом: по нему журналы трёх машин сводятся в одну картину.
python3 stats.py # все сессии сразу, с разбивкой по дням
python3 stats.py --last # только последняя сессия
python3 stats.py logs/20260910_003300 # конкретная сессия (или отдельный файл)Раздачи сортируются по времени приёма, каталоги сессий — по имени, то есть тоже по времени.
Отвечает на четыре вопроса:
- Сколько зарабатывает каждое место и сколько уходит на блайнды. Три клиента за одним столом платят три комплекта блайндов, а разыгрывают одну раздачу из трёх — окупается ли это, видно тут.
- Где течёт: чистый результат по классам решений (улица + действие + корзина эквити). Правило, которое стабильно в минусе, всплывает здесь раньше, чем в общем балансе. Раздача засчитывается каждому своему решению, поэтому суммы пересекаются — это указатель, а не бухгалтерия.
- Чем реально ставят на этом лимите: что показывают на вскрытии
после ставок разного размера. Это калибровка для
infer_opponent_rangeв клиенте — сейчас её проценты взяты из общих соображений. - Как часто пул сбрасывает на ставку. Если почти никогда — блефовать тут нечем, и вэлью-беты надо делать крупнее.
Проверка: python3 tests/check_hand_log.py — поднимает сервер, шлёт
запись, читает файл и прогоняет по нему stats.py.
Раунд ждёт не всех зарегистрированных клиентов, а живых. Если рука от
кого-то не пришла за --round-timeout (5 сек по умолчанию — живые
клиенты присылают руки практически одновременно, ждать дольше нечего),
раунд всё равно разыгрывается теми, кто прислал, а молчуну засчитывается
пропуск:
[молчит] chechnyapoker пропустил раунд (1 из 2) — раунд доигран без него
[выбыл] chechnyapoker молчит 2 раунда подряд (по 5с) — дальше играем без него (2 в игре)
После DROP_AFTER_MISSES (2) пропусков подряд клиента перестают ждать —
остальные играют в обычном темпе. «Выбыл» тут не приговор: пропасть на
две-три раздачи и вернуться — нормально, любая присланная рука возвращает
клиента в строй:
[вернулся] chechnyapoker снова в игре (3 в игре)
Почему два пропуска, а не один: клиенты идут не строго в ногу, и рука,
пришедшая на полсекунды позже чужого раунда, открывает следующий — в нём
остальные ещё не участвовали, хотя живее некуда. Один пропуск такое не
отличает, два — уже молчание. Раньше на месте всего этого был ответ
{"status": "timeout"} (HTTP 202), и одна упавшая машина вешала всех
остальных на каждой раздаче.
Проверка — tests/check_dropout.py (поднимает настоящий сервер на
свободном порту с таймаутом 2 сек):
python3 tests/check_dropout.py(N+1)-й новый client_id получает 403 client limit reached.
from lib.strategy.top_hand.top_hand import pick_best_hand
from lib.strategy.cards import parse_cards
result = pick_best_hand([parse_cards("AhAs"), parse_cards("KdKc")])
# {'best_index': 0, 'win_rate': [...], 'trials': ..., 'deterministic': False}