Skip to content

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

poker_server

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 3

GET /health — проверка живости.

POST /register — старт по команде оператора

Клиент вызывает один раз при запуске (после выбора областей на экране, до начала слежения за столом) — регистрируется (тот же лимит --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 целиком.

POST /hands без --clients

Просто сохраняет раздачу в 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-ом, и чтения могут разойтись: сервер берёт самый частый непустой вариант и при разногласии печатает об этом строку — молчаливый выбор тут менял бы победителя раунда.

POST /hands с --clients N — сравнение рук

Раунд — последняя присланная рука от каждого из 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 — номер раздачи из чата, общий для всех клиентов за столом: по нему журналы трёх машин сводятся в одну картину.

stats.py

python3 stats.py                        # все сессии сразу, с разбивкой по дням
python3 stats.py --last                 # только последняя сессия
python3 stats.py logs/20260910_003300   # конкретная сессия (или отдельный файл)

Раздачи сортируются по времени приёма, каталоги сессий — по имени, то есть тоже по времени.

Отвечает на четыре вопроса:

  1. Сколько зарабатывает каждое место и сколько уходит на блайнды. Три клиента за одним столом платят три комплекта блайндов, а разыгрывают одну раздачу из трёх — окупается ли это, видно тут.
  2. Где течёт: чистый результат по классам решений (улица + действие + корзина эквити). Правило, которое стабильно в минусе, всплывает здесь раньше, чем в общем балансе. Раздача засчитывается каждому своему решению, поэтому суммы пересекаются — это указатель, а не бухгалтерия.
  3. Чем реально ставят на этом лимите: что показывают на вскрытии после ставок разного размера. Это калибровка для infer_opponent_range в клиенте — сейчас её проценты взяты из общих соображений.
  4. Как часто пул сбрасывает на ставку. Если почти никогда — блефовать тут нечем, и вэлью-беты надо делать крупнее.

Проверка: 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.

top_hand напрямую

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}

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages