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