П/ВИН

PokerBrain: как мы настраивали тесты на MacBook

·8 мин чтения

Когда начинаешь тестировать новый проект на чужой машине — особенно на MacBook, где Python из коробки это системный 3.9 без прав записи — всё идёт не так, как планировалось. Именно так и вышло с PokerBrain: локально на сервере всё работало, 203 теста зелёные, архив собран. Но стоило открыть терминал на MacBook Air — и начался квест.

В этой статье расскажу, через какие грабли мы прошли, как раздавали сборку без доступа к GitHub, почему pynput крашил Python на ARM и как в итоге всё заработало.

Что такое PokerBrain

PokerBrain — это Python-приложение, которое работает как оверлей поверх покерного клиента (WPT Global и аналоги). Оно делает скриншот стола, передаёт изображение в Claude Vision, получает распознанное состояние игры — карты, пот, ставки, позиции — и выдаёт рекомендацию: fold, call или raise с обоснованием.

Под капотом:

  • Vision-only recognition через Anthropic Claude (claude-3-5-sonnet) — без Tesseract OCR, без шаблонного матчинга
  • Стратегический движок с pot odds, Monte Carlo equity (10 000 симуляций), ICM pressure для турниров
  • PyQt6 оверлей поверх экрана, always-on-top
  • Калибровка — пользователь выделяет область покерного стола, дальше Vision сам находит регионы

Проект приватный, без публичного GitHub. Это само по себе создало первую проблему при тестировании на MacBook коллеги.

Проблема 1: нет доступа к GitHub, нет git, нет pip

Стандартный онбординг для разработчика:

git clone https://github.com/org/pokerbrain.git
cd pokerbrain
pip install -e ".[dev,gui,eval]"
pytest tests/ -v

На MacBook Air с чистой macOS это сломалось сразу в нескольких местах одновременно:

zsh: no such file or directory: repo-url
cd: no such file or directory: pokerbrain
zsh: command not found: pip

Репозиторий приватный — без настроенного SSH-ключа или токена GitHub не скачать. Homebrew не установлен. pip не найден, потому что macOS по умолчанию даёт только pip3 от системного Python 3.9.

Решение оказалось простым: поднять HTTP-сервер на VPS и раздавать ZIP-архив проекта напрямую.

# На сервере — собираем архив через git archive (без мусора и venv)
git archive --format=zip --output=/tmp/pokerbrain-latest.zip HEAD
 
# Запускаем простой HTTP-сервер
python3 -m http.server 9090 --directory /tmp &

На MacBook — просто curl:

cd ~/Downloads
curl -O http://[SERVER_IP]:9090/pokerbrain-latest.zip
mkdir pokerbrain && cd pokerbrain
unzip ../pokerbrain-latest.zip

Важный момент: первая попытка упаковки через zip -r затянула в архив виртуальное окружение и вложила папку внутрь папки, из-за чего распаковка давала неправильную структуру. git archive решает оба вопроса — берёт только то, что в репозитории, и без лишней вложенности.

Документация git archive

Также открыли порт на файрволе — изначально он был закрыт и curl просто висел без ответа. Проверка простая:

# С клиентской машины
curl -v telnet://[SERVER_IP]:9090

Проблема 2: старый pip не поддерживает pyproject.toml в editable-режиме

После распаковки попытка установить зависимости дала:

ERROR: File "setup.py" or "setup.cfg" not found.
(A "pyproject.toml" file was found, but editable mode currently requires
a setuptools-based build.)
WARNING: You are using pip version 21.2.4

Проект использует pyproject.toml как единственный файл конфигурации — без setup.py. Это современный стандарт PEP 517 и PEP 660. Поддержка editable install через pyproject.toml появилась в pip 21.3+.

Но корень проблемы глубже: системный Python на macOS это 3.9, а проект требует 3.11+. Homebrew не установлен. Решение — скачать официальный .pkg установщик с python.org:

https://www.python.org/ftp/python/3.11.9/python-3.11.9-macos11.pkg

После установки Python 3.11 через .pkg появляется отдельный бинарник python3.11 с собственным pip, который уже знает про pyproject.toml:

python3.11 -m pip install -e ".[dev,gui,eval]"
python3.11 -m pytest tests/ -v

Тесты прошли — 207 собрано, 204 passed, 3 failed. Три упавших теста оказались ожидаемыми:

  • test_capture_returns_none_without_display — написан для headless Linux CI, на MacBook есть дисплей, capture работает
  • Два теста на UI ожидали русский язык интерфейса, а на MacBook стоял английский по умолчанию

Все тесты стратегического движка — pot odds, equity, preflop ranges, ICM — зелёные.

Документация Python для macOS

Проблема 3: pynput крашит Python на Apple Silicon

Приложение запустилось, оверлей появился — но при первом нажатии глобального хоткея (Cmd+Shift+P) Python падал с crash report:

Process: Python [9345]
Code Type: ARM-64 (Native)
...
Thread 0 crashed with ARM Thread State (64-bit)

Краш в pynput.keyboard.Listener — вызов TSMGetInputSourceProperty из фонового потока. На Apple Silicon macOS жёстко требует, чтобы операции с UI и keyboard API выполнялись из main thread. Pynput запускает listener в отдельном потоке — отсюда SIGTRAP на уровне C, который try/except не ловит.

Первая попытка исправления — заменить pynput на QShortcut из PyQt6:

from PyQt6.QtGui import QShortcut, QKeySequence
 
shortcut_analyze = QShortcut(QKeySequence("Ctrl+Shift+P"), self)
shortcut_analyze.activated.connect(self.run_analysis)

QShortcut работает в Qt event loop — main thread, никаких крашей. Но обнаружился второй недостаток: QShortcut срабатывает только когда окно приложения в фокусе. Для покерного оверлея это бесполезно — фокус всегда на покерном клиенте.

Окончательное решение — использовать macOS нативный NSEvent для глобального мониторинга клавиш:

import objc
from AppKit import NSEvent, NSKeyDownMask
 
def setup_global_hotkeys(self):
    mask = NSKeyDownMask
    NSEvent.addGlobalMonitorForEventsMatchingMask_handler_(
        mask,
        self._handle_key_event
    )
 
def _handle_key_event(self, event):
    flags = event.modifierFlags()
    keycode = event.keyCode()
    # Ctrl+Shift+P = keycode 35
    if keycode == 35 and (flags & 0x40000) and (flags & 0x20000):
        self.run_analysis()

NSEvent работает через Cocoa, вызывается из run loop — никаких конфликтов с macOS thread policy. После этого хоткеи заработали стабильно.

Документация PyQt6 QShortcut

Проблема 4: «FOLD — no card detected» сразу после запуска

Приложение запустилось, хоткеи работают, нажимаем анализ — и всегда получаем «FOLD — no card detected». Никаких карт, никакого пота, пустой стол в глазах PokerBrain.

Причина понятна: приложение не знает, где на экране искать покерный стол. В коде стоят дефолтные координаты, рассчитанные под конкретное разрешение и расположение окна. На WPT Global стол каждый раз открывается в новом месте экрана, в произвольном окне.

Решение — виджет выбора области экрана при старте. Пользователь вручную выделяет прямоугольник вокруг покерного стола, координаты сохраняются, и дальше Vision делает скриншот именно этой области:

class RegionSelector(QWidget):
    region_selected = pyqtSignal(QRect)
 
    def __init__(self):
        super().__init__()
        self.setWindowFlags(
            Qt.WindowType.FramelessWindowHint |
            Qt.WindowType.WindowStaysOnTopHint |
            Qt.WindowType.Tool
        )
        self.setWindowOpacity(0.3)
        self.showFullScreen()
        self.origin = None
        self.selection = QRect()
 
    def mousePressEvent(self, event):
        self.origin = event.pos()
 
    def mouseMoveEvent(self, event):
        self.selection = QRect(self.origin, event.pos()).normalized()
        self.update()
 
    def mouseReleaseEvent(self, event):
        self.region_selected.emit(self.selection)
        self.close()
 
    def paintEvent(self, event):
        painter = QPainter(self)
        painter.fillRect(self.selection, QColor(0, 120, 215, 80))
        painter.setPen(QPen(QColor(0, 120, 215), 2))
        painter.drawRect(self.selection)

После выбора области координаты передаются в ScreenCapture, который делает скриншот именно этого региона и отправляет в Vision API. Калибровка больше не требует ручного ввода пикселей.

Финальный флоу тестирования на MacBook

После всех исправлений процесс тестирования выглядит так:

# Скачать актуальную сборку
cd ~/Downloads
rm -rf pokerbrain pokerbrain-latest.zip
curl -O http://[SERVER_IP]:9090/pokerbrain-latest.zip
mkdir pokerbrain && cd pokerbrain
unzip ../pokerbrain-latest.zip
 
# Установить зависимости (только первый раз)
python3.11 -m pip install -e ".[dev,gui,eval]"
 
# Запустить тесты
python3.11 -m pytest tests/ -v
 
# Запустить приложение
export ANTHROPIC_API_KEY="[ВАШ_КЛЮЧ]"
python3.11 -m pokerbrain

При запуске приложение показывает диалог настроек (тип игры, формат, стиль), потом предлагает выделить область стола. После выбора — оверлей готов к работе.

Результат прогона тестов после всех исправлений:

207 items collected
204 passed, 3 failed (expected environment differences)
Duration: 12.4s

Стратегический движок: 100% тестов зелёные. Vision и UI тесты — с поправкой на среду.

Документация Anthropic Claude API Документация PyQt6

Эволюция архитектуры за сессию

Что интересно — параллельно с отладкой тестирования шла активная работа над самим проектом. Git-история показывает, что за эти ~15 часов было сделано:

  • Vision-only recognition — убрали Tesseract OCR и шаблонный матчинг карт, перешли полностью на Claude Vision. Это упростило зависимости (больше не нужен tesseract в системе) и улучшило качество распознавания
  • Verifier pass — второй независимый вызов Vision для валидации распознанных данных (hole cards, board, ставки)
  • Постфлоп улучшения — защита от value-raise без готовой руки, детектирование флэш/стрит бордов, слабый кикер
  • LLM router — все API-вызовы через единый роутер, можно легко переключать модели
  • Launcher.command файл для запуска одним кликом, с автообновлением
# Пример из strategy/postflop.py
def calculate_pot_odds(bet_to_call: float, pot: float) -> float:
    """Возвращает минимальный equity для безубыточного колла."""
    if pot + bet_to_call == 0:
        return 0.0
    return (bet_to_call / (pot + bet_to_call)) * 100
 
def get_postflop_action(equity: float, pot_odds: float, hand_strength: str) -> str:
    if equity > pot_odds * 1.5:
        return "RAISE"
    elif equity > pot_odds:
        return "CALL"
    else:
        return "FOLD"

Выводы

Главный урок этой сессии — разрыв между «работает у меня» и «работает у тестировщика» может быть огромным, даже в простом Python-проекте. MacBook с системным Python 3.9, без Homebrew, без доступа к приватному репо — это реальная среда реального человека, и её нужно учитывать заранее.

Раздача сборок через простой HTTP-сервер оказалась неожиданно удобным решением для приватных проектов на этапе ранних тестов. Не нужно настраивать CI/CD, не нужен Docker, не нужен доступ к GitHub — curl и unzip справляются. Единственное условие — следить за тем, как собирается архив: git archive вместо ручного zip -r решает сразу несколько проблем со структурой.

pynput — популярный выбор для глобальных хоткеев в Python, но на Apple Silicon он ведёт себя непредсказуемо. Краш через SIGTRAP не ловится стандартными средствами Python. Если пишете десктопное приложение под macOS с глобальными хоткеями — сразу смотрите в сторону нативного NSEvent через pyobjc. Это немного больше кода, но зато стабильно.

Три упавших теста из 207 при переносе на новую среду — это хороший результат. Но важно понимать природу каждого падения: тест написан под CI без дисплея, или реальный баг? Документировать ожидаемые различия между средами стоит прямо в коде тестов через pytest.mark.skipif или в комментарии рядом с тестом.

Калибровка через выбор области экрана — правильное UX-решение для любого приложения, работающего с конкретными координатами чужого окна. WPT Global, PokerStars, GGPoker — у каждого клиента свой размер окна, разное расположение элементов. Хардкодить координаты бессмысленно; пусть пользователь покажет приложению, где смотреть.

Паша Вин
Паша Вин

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.