1. Вводная

Недавно завершил интересный пэт-проект. Настолько интересный, что захотелось поделиться.

Это десктопная программа, которая:

  • Считывает скрин игрового стола в покере.

  • С помощью компьютерного зрения извлекает расклад, ставки и карты.

  • Рассчитывает ожидаемую выгоду (EV) каждого действия методом Монте-Карло.

  • Показывает на экране, что выгоднее сделать прямо сейчас.

Задумка родилась из идеи: «А можно ли сделать программу, которая играла бы в покер за меня и зарабатывала бы мне много тысяч денег!». Я думал: программа ведь не стрессует, не тильтует, не подвержена блефу, не принимает невыгодных решений, может быстро посчитать соотношение выигрыша к потерям, и в��обще быстро считает, поэтому в среднем всегда должна выигрывать у человека.

По наивному плану хотелось просто «напрячь» ChatGPT, подвязать пару Python-библиотек и сразу запустить в покер-рум, играть на кэш. На практике всё вышло немного сложнее, идею пришлось разложить на четыре подзадачи и пройти их по очереди:

  1. Computer Vision. Надёжно извлекать данные с экрана независимо от скина, шрифта, языка и разрешения.

  2. Монте-Карло. Научиться за секунды симулировать раздачи и выбирать решение с положительным EV.

  3. Программирование. Собрать удобный GUI, создать алгоритм анализа, собрать всё воедино.

  4. Автоматизация. Научить программу саму кликать на кнопки, имитировать человеческое поведение.

Сейчас готовы первые два пункта, третий и четвёртый - под вопросом, этический и технический порог выше, чем ожидал.

В статье покажу, как за 4 недели пройти путь от идеи до работающей MVP, чем помогли ChatGPT, Cursor и Roboflow, и какие ещё ИИ использовал. Покер здесь лишь фон, главный герой повествования: ИИ + Python + CV, способные сегодня превратить соло-разработчика в мини-R&D-команду.

2. Очень коротко

Проект делал чуть дольше месяца.

Неделя ушла на изучение покерной тематики, написание заметок, общее погружение в тему.

Неделя на написание GUI (графический юзер интерфейс), составление общей архитектуры проекта, кодовая часть.

Неделя на компьютерное зрение, подготовительные работы.

Неделя на обучение своих YOLO моделей. Получилось обучить две модели: одна для детекции стола, вторая для обнаружения карт.

И наконец, неделя на сведение всего, тестирование, фикс багов и компилирование в исполняемый файл.

3. Покер в двух словах

Покер, оказывается, очень интересная игра со своей механикой.

Цель игры - собрать сильнейшую комбинацию или заставить соперников сбросить карты.

Существует много разных видов и правил покера, но самый распространённый сегодня - это Техасский холдем.

 Техасский холдем
Техасский холдем

Правила:

  • Игрокам раздаются карты в закрытую.

  • На стол выкладываются общие карты (в открытую).

  • Идут этапы торговли (префлоп, флоп, тёрн, ривер).

  • Побеждает игрок с лучшей комбинацией или тот, кто заберёт банк без вскрытия (блеф).

В Техасском Холдеме комбинацию собирают из 2 своих карт и 5 общих на столе (всего 7 карт).

Всего в покере можно собрать 10 различных комбинаций карт. Ниже таблица комбинаций, от самой сильной (и редкой) до самой слабой.

Комбинация

Частота

Вероятность

Шанс

1

Роял-флеш

4,324

0.0032%

1 к 30 940

2

Стрит-флеш (без РФ)

37,260

0.0279%

1 к 3 589

3

Каре

224,848

0.168%

1 к 594

4

Фул-хаус

3,473,184

2.60%

1 к 38

5

Флеш (без РФ и СФ)

4,047,644

3.03%

1 к 33

6

Стрит (без РФ, СФ и Ф)

6,180,020

4.62%

1 к 22

7

Сет

6,461,620

4.83%

1 к 21

8

Две пары

31,433,400

23.5%

1 к 4

9

Одна пара

58,627,800

43.8%

1 к 2

10

Старшая карта

23,294,460

17.4%

1 к 5

-

ВСЕГО

133,784,560

100%

-

Интересное наблюдение:

  • Общее количество уникальных комбинаций 7 карт из 52 составляет

C_{52}^7 = \frac{52!}{(52 - 7)! \cdot 7!}​ = 133,784,560
  • Вероятность комбинации "Старшая карта" ниже, чем вероятность "Одна пара" или даже "Две пары" (17.4% против 43.8% и 23.5% соответственно)

Equity - это вероятность выиграть банк на вскрытии, выраженная в процентах.

EV (Expected Value) - это математическое ожидание прибыли или убытка от конкретного действия. Оно показывает, сколько мы будем выигрывать или проигрывать в среднем, если многократно повторять одно и то же решение.

Есть ещё много интересных понятий, типа споты, диапазон рук, win rate, ROI, GTO, анте, банкролл и так далее.

Но в целом, перечисленного должно быть достаточно, чтобы понять, с какой игрой мы имеем дело.

Нас же будет интересовать, как рассчитать EV каждого действия.

4. Математика

«Математика - это язык, на котором написана книга природы», Галилео Галилей.

А нам понадобится лишь пара её страниц.

Напомню, EV (Expected Value) - это математическое ожидание прибыли или убытка от конкретного действия.

В чём отличие математического ожидания и среднего арифметического?

В игре в кости математическое ожидание броска одного кубика равно 3.5, но если мы бросим кость 10 раз и получим результаты, например: [1, 6, 2, 4, 5, 3, 6, 2, 3, 4], то среднее этих бросков будет равно 3.6.

Математическое ожидание относится к ТЕОРЕТИЧЕСКОМУ среднему значению случайной величины, тогда как среднее арифметическое - это эмпирическое (РЕАЛЬНОЕ) среднее значение конкретной выборки.

Формула EV:

EV = E \times (X + Y) \ – \ (1 \ – \ E) \times X

Где:

  • X - наша ставка

  • Y - текущий банк

  • E - equity, вероятность выиграть на вскрытии, выраженная в процентах

Equity - это ключевой элемент при расчёте EV и принятии решений: коллировать, рейзить или сбрасывать.

Для расчёта equity будем использовать метод Монте-Карло.

Метод Монте-Карло - это метод оценки вероятностей путём многократного случайного моделирования исходов. Применяется, когда аналитический расчёт слишком сложен или невозможен (это наш кейс).

Если простым языком, суть Монте-Карло заключается в том, что вместо точной формулы запускается много случайных "симуляций" события, те суммируются, и по их результатам считается средний исход.

Пример кода на Python.

import random
from treys import Evaluator, Deck, Card

def monte_carlo(hero_cards: list,
				board_cards: list,
				active: int,
				n_simulations: int) -> float:

    # Создаем одну колоду и убираем из неё известные карты
    remaining_cards = []
    deck = Deck()
    used_cards = set(hero_cards + board_cards)
    remaining_cards = [c for c in deck.cards if c not in used_cards]

    wins = ties = losses = 0

    # Цикл симуляций
    for _ in range(n_simulations):

        # Перемешиваем оставшиеся карты
        random.shuffle(remaining_cards)

        # Раздаём карты противникам
        card_index = 0
        villains = []
        for _ in range(active - 1):
            villain = remaining_cards[card_index:card_index + 2]
            villains.append(villain)
            card_index += 2

        # Добираем доску до 5 карт, если у нас не Ривер
        sim_board = board_cards[:]
        cards_needed = 5 - len(sim_board)
        if cards_needed > 0:
            sim_board.extend(remaining_cards[card_index:card_index + cards_needed])

        # Оценка рук
        hero_score = EVALUATOR.evaluate(sim_board, hero_cards)

        # Находим лучшего противника
        best_villain_score = float('inf')
        for villain in villains:
            villain_score = EVALUATOR.evaluate(sim_board, villain)
            if villain_score < best_villain_score:
                best_villain_score = villain_score

        # Подсчёт результатов (в treys меньше = лучше)
        if hero_score < best_villain_score:
            wins += 1
        elif hero_score == best_villain_score:
            ties += 1
        else:
            losses += 1

    equity = (wins + 0.5 * ties) / n_simulations

    return equity
  • Модуль treys - это Python-библиотека для оценки силы руки в покере.

  • Обожаю программирование (особенно Python), что на любой случай жизни уже написана какая-нибудь библиотека.

Поэтому никакой сложной математики не получилось. За нас уже написали все необходимые классы и функции. Осталось их просто применить в нужном месте.

Чтобы функция monte_carlo() корректно всё посчитала, ей на вход нужно подать корректные данные. Можно это ручками указывать (карманные пары, карты стола, количество активных игроков), но лучше всё-таки автоматизировать.

5. Компьютерное зрение

Компьютерное зрение (Computer Vision) - это область искусственного интеллекта, которая позволяет компьютерам «видеть» и интерпретировать изображения и видео.

Если задуматься, очень крутая технология! По сути это механизм обработки картинки или видео (последовательность картинок), в которой алгоритм находит необходимый объект, добавляет его координаты, его название и свою уверенность (confidence).

Есть множество разных способов, программ или библиотек, которые реализуют CV.

Я выбрал YOLO.

5.1 Коротко о YOLO

Segmentation, Detection,  Keypoints,  Oriented Bounding
Segmentation, Detection, Keypoints, Oriented Bounding

YOLO - это семейство моделей для обнаружения объектов в реальном времени.

Модели YOLO способны выполнять самые разные задачи по детекции объектов:

  • Classification - классификация изображений по заранее определённым классам.

  • Detection - идентификация и определение местоположения объектов на изображении.

  • Segmentation - сегментация, более сложная задача, обнаружение объектов и определение их точных границ.

  • Pose/Keypoints - обнаружение и отслеживание ключевых точек на теле человека.

  • Oriented Bounding - более точное определение объектов, вводится дополнительный угол для боксов.

Для нашей задачи, анализ покерного стола, обнаружение 52 различных карт, подойдут модели Detection.

У них есть 5 версий:

  • yolo11n.pt - nano version, 2.6 parameters, 6.5 GFLOPs

  • yolo11s.pt - small version, 9.4 parameters, 21.5 GFLOPs

  • yolo11m.pt - medium version, 20.1 parameters, 68.0 GFLOPs

  • yolo11l.pt - large version, 25.3 parameters, 86.9 GFLOPs

  • yolo11x.pt - extra version, 56.9 parameters, 194.9 GFLOPs

Parameters - общее количество параметров (весов), которые модель усваивает. Чем больше - тем выше точность, но также и выше требования к ресурсам. Parameters влияет насколько сложные зависимости может выучить модель.

GFLOPs - количество гига-флопов (миллиардов операций с плавающей точкой), необходимых на один проход одного изображения через всю модель во время обучения (или детекции). Это теоретическая мера вычислительной сложности модели. GFLOPs влияют на скорость инференса (распознавания).

Я выбрал yolo11n.pt

5.2 Коротко о Roboflow

Roboflow - это веб-сервис для аннотации и управления датасетами, который сильно упрощает процесс подготовки данных для обучения моделей компьютерного зрения.

С помощью Roboflow можно легко загружать изображения, аннотировать их, применять различные виды аугментации для увеличения объёма данных. Особенно помогает, когда ты один, а данных нужно много.

Нюансы:

Roboflow это облачная платформа, работает в браузере. Все данные хранятся на серверах. Для бизнеса это может быть чувствительно. Для пэт-проекта - норм.

На бесплатном плане можно вести до 3 проектов, в каждом - до 2000 оригинальных изображений. Аугментированные копии не считаются в лимит, так что итоговое количество файлов может быть больше.

У меня три проекта, всего разметил ~ 7к изображений (с учётом аугментаций) - в лимит пока не упёрся. Для соло-проектов и экспериментов бесплатной версии хватает.

5.3 Датасет

Датасет - это набор данных, в нашем случае это просто папка с картинками и аннотациями. Датасет это основа для обучения любой модели. Не только в задаче компьютерного зрения, но и во всех нейронных сетях, которые обучаются на данных.

Без датасета - нечему учиться. Модель не сможет "угадать", что на изображении - её надо этому научить, на примерах.

Обычно датасет делят на 3 части: train / val / test.

Название

Задача

Доля от общего датасета

train

модель на этом учится

60%-70%

val

контроль обучения

15%-25%

test

итоговая оценка качества

15%-25%

Чтобы было проще, я придумал себе аналогию с учёбой.

  • train - это уроки, домашки, работа с учебником. Модель - школьник начальных классов, учится основам математики (модель учится на train).

  • val - это проверочные работы в течение семестра. Мы - учитель, смотрим как идёт усвоение материала. Если всё плохо - меняем подход (val не даёт модели переобучиться).

  • test - это контрольные работы. Новые задачи, которых школьник не видел. По ним выставляются оценки за семестр, оценивается то, как был усвоен материал (по test строятся метрики качества).

5.4 Аугментация

Аугментация - это искусственное расширение обучающего датасета (train) с помощью различных трансформаций изображений (повороты, флипы, шум, изменение цвета и др.).

Мы берём одну картинку и создаём из неё 3-5 новых, немного изменённых.

Изменение цветокора для человека картинку не меняет, по сути и смыслу она остаётся той же самой, но для модели, это уже другой набор пикселей. Делая простые преобразования, применяя фильтры (типа как в Instagram), мы увеличиваем датасет в разы.

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

Тип аугментации

Описание

Brightness

Чуть осветляет или затемняет

Blur

Добавляет размытость

Flip

Отзеркаливание по горизонтали

Rotation

Поворот на угол (обычно ±10°)

Noise

Добавляет пиксельный шум

Cutout

Закрашивает случайный фрагмент

  • Аугментация применяется на уже размеченный датасет.

  • Один тип аугментации добавляет ровно одно дополнительное изображение ко всем изображениям изначального датасета.

Побочные эффекты:

  • Слишком жёсткие искажения (фильтров) могут запутать модель.

  • Слишком большое раздувание датасета почти одинаковыми сценами может замедлить обучение.

  • Утечки, это когда одинаковые или почти одинаковые картинки попадают и в train, и в val/test - это приводит к завышенным метрикам и ложному восприятию качества модели.

На Free плане в Roboflow возможно применить только два типа аугментации на выбор, то есть увеличить датасет в 3 раза. Этого вполне достаточно. Аугментацию можно реализовать и с помощью кода.

Также в коде можно реализовать динамическую аугментацию. Это та же Аугментация, только применяется случайным образом во время каждой эпохи обучения. Файлы картинок не сохраняются, трансформации генерируются на лету.

Из плюсов:

  • Гораздо большее разнообразие

  • Требуется меньше дополнительного места памяти

  • Меньше риск утечек

  • Лучше предотвращает переобучение

Минусы:

  • Требует дополнительной поддержки (больше кода)

  • Требуется большее время на обучение, в среднем на ~10-15%

5.5 Обучение

Какое слово использовать, ДОобучение, ПЕРЕобучение, или просто обучение?, в чём разница?

Давайте сразу со сложного, ПЕРЕобучение (overfitting) - это ошибка. Это когда модель "зазубрила" тренировочные примеры, но не может обобщать. В аналогии со школой: ученик вызубрил таблицу умножения до 10, но пример 11 × 11 - уже не осилит.

Обучение (training) - это процесс, в котором нейросеть учится распознавать паттерны в данных. Это базовый термин, он универсален.

ДОобучение (fine-tuning / transfer learning) - это обучение на уже обученной модели. Например, YOLO уже умеет распознавать людей, котиков, автобусы (всего ~35 классов), но мы хотим, чтобы она распознавала покерные карты - мы её дообучаем на нашем датасете.

 статье я использую слово "обучение", так как мы используем базовую модель YOLO только как начальную архитектуру (пустая сетка), ее предобученые классы нам не нужны. Так привычнее на слух, и меньше путаницы.

Чтобы скачать базовую модель, переходим на сайт Ultralytics YOLOv11 - выбираем нужную архитектуру (например, yolo11n.pt - nano-версия), скачиваем в��са.

Далее команда для запуска обучения через терминал:CUDA_VISIBLE_DEVICES=0 yolo detect train \
model=yolo11n.pt \
data=datasets/poker_dataset/data.yaml \
epochs=50 \
imgsz=640 \
batch=8 \
patience=10 \
lr0=0.005 \
name=poker_train

Пояснение ключей:

  • CUDA_VISIBLE_DEVICES=0 - принудительное использование GPU (если есть несколько, или вдруг будет обучаться на CPU).

  • model= - путь до базовой YOLO-модели (.pt), можно выбрать потяжелее yolo11m.pt

  • data= - путь к YAML-файлу, в нём описываются пути до папок train, val, test и список классов

  • epochs= - кол-во эпох, обычно от 20 до 200, зависит от размера датасета.

  • imgsz= - размер изображений, можно увеличить до 768 или 960(чем лучше качество, тем выше нагрузка на VRAM)

  • batch= - размер батча, влияет на скорость обучения, нагружает VRAM, допустимые значения 2, 4, 8, 16, 32 (у меня 4ГБ - ставил значение 8)

  • patience= - параметр ранней остановки, если метрика валидации не улучшается в течение N эпох, обучение автоматически прерывается (по умолчанию: patience=50)

  • lr0= - начальная скорость обучения, learning rate, рабочие значения 0.001-0.01

  • name= - название эксперимента, создастся папка в runs/detect/...

Запуск детекции в Python.

from ultralytics import YOLO

model = YOLO("yolo11n.pt")
results = model("image.jpg")

results[0].show()           # Показать картинку с боксами
results[0].boxes.xyxy       # Координаты бокса (x1, y1, x2, y2)
results[0].probs.top1       # Класс с максимальной вероятностью
  • координаты боксов записываются как координаты (x_1, y_1) левой верхней точки квадрата, и координаты (x_2, y_2) правой нижней

  • двух точек достаточно, чтобы однозначно воссоздать прямоугольник

Сколько времени займёт обучение, сильно зависит от:

  • размера модели

  • размера изображений

  • количества изображений

  • размера батча

  • мощности видеокарты

У меня обучение yolo11n.ptimgsz=640epochs=40, на 200 изображений заняло ~15–20 минут на RTX 2050. Это была модель для детекции объектов на покерном столе. В ней был�� всего 8 классов.

Для детекции игральных карт, нужна была модель с 52 классами (52 игральные карты), обучал yolo11n.ptimgsz=768epochs=80, на 7598 изображений - вышло ~6 часов.

Чем больше классов, тем больше нужно картинок. Если грубо, то на один класс должно получаться 100+ выделенных боксов. Это условно, лучше корректироваться по метрикам.

Также, в Roboflow есть особая очень полезная функция - автосплит. То есть выставляете желаемое соотношение train/val/test, а он уже по имеющимся данным старается всё разделить. При этом алгоритм не просто картинки по папкам разделяет в заданном соотношении, но и внутри каждого класса старается сплитовать с заданным соотношением (по количеству выделенных боксов).

"Старается", это значит что точно разделить данные по заданному соотношению не получится. Сложно объяснить понятным языком, но это математически практически невозможно, поэтому алгоритм стремится к самому близкому соответствию нашего запроса (у меня было 70/15/15), а дальше как получится.

После тренировки будет создана папка runs/detect/poker_train/weight/... В ней будут две модели best.pt (лучшая модель) и last.pt (последняя модель) полученные во время обучения. Копируем best.pt в свой проект. Это и есть то самое ценное что мы создавали, веса модели которые будут творить магию.

Теперь можем использовать их в каком-нибудь скрипте для детекции объектов по скриншотам.

5.6 Метрики

 "Когда код работает слишком хорошо"
"Когда код работает слишком хорошо"

Отлично! Теперь надо оценить качество обученных нами моделей. Это, по-моему, очень важный момент. Мы должны понять что она умеет, на чём ошибается, какие у неё вообще оценки.

После обучения первой модели в корне будет автоматически создана папка runs/detect/.., в которой будет папка проекта, название которого мы указывали при обучении. В ней самая важная папка это, конечно, weight/.., в ней веса готовой, обученной модели (в прошлом блоке обсуждали).

Но также там будет ещё куча всяких папок, файлов и отчётов. Расскажу про самые важные (на мой взгляд).

results.png и results.csv - это одно и то же, просто в разных форматах. В визуальном как картин��а с графиками, и просто таблица с данными (на которых строились эти графики) с классическим .csv расширением.

 Для примера, мой results.png, для общей модели детекции игрового стола.
Для примера, мой results.png, для общей модели детекции игрового стола.

График

Измеряет

Как читать

train/box_loss, val/box_loss

Ошибка при предсказании координат боксов

Чем ниже - тем лучше. Должна постепенно убывать. Скачки на val допустимы, но не сильные.

train/cls_loss, val/cls_loss

Ошибка при классификации объектов

Чем ниже - тем лучше. Если val растёт при убывающем train, возможно переобучение.

train/dfl_loss, val/dfl_loss

DFL (Distribution Focal Loss), уточняет границы боксов

Аналогично box_loss - стремимся к снижению.

metrics/precision(B)

Точность: из всех предсказанных - сколько верных

Стремимся к 1. Если низкий - модель путает классы. Важно смотреть вместе с Recall.

metrics/recall(B)

Полнота: из всех верных - сколько найдено

Стремимся к 1. Если низкий - модель что-то пропускает.

metrics/mAP50(B)

Средняя точность (mAP) при IoU=50%

Это главный показатель качества модели. Ближе к 1 - отлично.

metrics/mAP50-95(B)

mAP усреднённый по IoU от 50% до 95%

Более строгая метрика. Всё выше 0.7 - уже хорошо.

Recall

Recall (полнота) - насколько модель хорошо может предсказать все наши объекты.
Показывает количество правильно предсказанных объектов среди всех ИСТИННЫХ объектов.

Распределение от 0 до 100%, чем выше процент, тем лучше

Precision

Precision (точность) - показывает количество правильно предсказанных объектов среди всех ПРЕДСКАЗАННЫХ объектов.

Распределение от 0 до 100%, чем выше процент, тем лучше

mAP50

  • mAP , model Average Precision (средняя точность модели) - насколько хорошо модель находит и правильно классифицирует объекты на изображениях.

  • IoU Intersection over Union (Пересечение на объединение) - это насколько сильно пересекаются реальный бокс и предсказанный (0% - не пересекаются, 100% - полное совпадение)

  • mAP@50 означает, что два объекта (истинный и предсказанный) считаются совпадающими, если перекрытие IoU ≥ 50%

  • mAP@50 = 0.9, это значит, что модель почти всегда находит и классифицирует объекты правильно (с ≥50% перекрытием)

  • mAP@50-95 - это усреднённое значение mAP, рассчитанное по 10 разным порогам IoU: от 0.50 до 0.95 с шагом 0.05 - то есть усреднение mAP@50mAP@55mAP@60...mAP@95 (cтрогая метрика, часто используется для оценки в соревнованиях или в продакшене; особенно важна в реальных задачах: медицина, автопилоты)

  • mAP@50-95 = 0.7 - это очень хороший результат, особенно если много мелких объектов.

F1

Также очень важным графиком на мой взгляд является кривая F1, в папке будет именоваться как F1_curve.png

Кривая F1-уверенности (F1-Confidence Curve) для всех классов.
Кривая F1-уверенности (F1-Confidence Curve) для всех классов.

F1 - гармоническое среднее. Объединяет в себе recall и precision. Более сбалансированная и точная метрика.

  • Так же как и recall и precision распределяется от 0 до 1

  • Наилучший порог - это тот, при котором достигается максимум F1 по всем классам

  • В нашем случае (на графике): F1 = 0.99 при Confidence = 0.433

Confidence

Confidence Threshold (порог уверенности) - это число от 0 до 1, которое говорит, насколько модель должна быть уверена, чтобы мы поверили её предсказанию. Это то число, которое ставится у обнаруженной рамки. На всех графиках, все метрики ставятся в сравнение именно с Confidence Threshold.

Итого

Эти метрики могут показаться сложными, если вы не сталкивались с ними ранее. Главное, на что стоит обращать внимание:

  • Уверенное снижение всех loss-метрик

  • Высокие показатели recall и precision

  • Рост и стабилизация mAP50 и mAP50-95

На что также обратить внимание:

  • Если loss падает, а метрики остаются на месте, возможно, модель переобучается.

  • Если метрики сильно колеблются, возможно недостаточно данных или модель слишком сложная.

5.7 Модели

В итоге я обучил две YOLO‑модели. Работают они в связке.

Пример работы моделей: total_model и cards_model
Пример работы моделей: total_model и cards_model

Первая - общая модель (total_model)
Обрабатывает весь скрин игрового стола: ищет кнопки, карты, стек, пот, дилер фишку и т.д.

Вторая - карточная модель (cards_model)
Используется точечно, только на кропах, чтобы точно определить номинал и масть конкретной карты (например: Qh, 9d, As).

Сначала я подаю скриншот в total_model, она находит все важные объекты - например, выделяет область hero_card (hero - так в покере называют игрока). Потом эта вырезанная область отправляется во вторую модель - cards_model, которая уже конкретно говорит, какие именно карты там лежат.

С total_model всё шло гладко - классов было немного (всего 7), а вот с cards_model пришлось попотеть. 52 класса (все возможные игральные карты), ~7500 размеченных изображений, много правок и дублирующей разметки - но в итоге всё собрал, разметил и обучил. В результате, довольно стабильно распознаёт карты даже на замыленных и затемнённых скринах.

По метрикам получилось так:

Model

Class

train / val / test

Precision

Recall

mAP@50

mAP@50–95

total_model

7

150 / 25 / 25

0.991

0.997

0.995

0.822

cards_model

52

5059 / 1283 / 1256

0.986

0.983

0.991

0.866

cards_model даже при большом числе классов (52 карты) уверенно держит mAP@50-95 выше 0.85, что для такой задачи - очень хороший результат.

Обе модели - на базе YOLOv11n. Получились лёгкие и быстрые, запускаются локально на ноуте с RTX 2050.

6. Код и инструменты

Проект получился объёмный, но структура у него простая и модульная. Использовал разные инструменты, которые сильно помогали: IDE Cursor, ChatGPT, различный ИИ, рабо��а в терминале.

Всё собирал под Windows, локально, скомпилировал в один исполняемый файл.

6.1 IDE Cursor

Мне кажется на сегодня (на июль 2025), не найдётся ни одного человека (айтишника), кто не слышал бы про Cursor.

Всю кодовую часть проекта писал в Cursor - это форк от VS Code, только с очень круто интегрированным ИИ. Те же горячие клавиши, расширения, плагины, немного другая тема, но главное - ИИ, он встроен прямо в редактор, не как плагин, а как полноценный помощник.

ИИ в Cursor подключается от основных провайдеров. В настройках можно выбрать, какие именно модели использовать.

Я обычно оставляю три: последнюю от OpenAI, последнюю версию Sonnet, и последнюю гугловскую (Gemini).

Подробно все возможности описывать не буду, лучше попробовать самим. Интерфейс интуитивный, если с VS Code знакомы - разберётесь быстро.

Перечислю только основные режимы работы с ИИ:

  1. Agent - ИИ имеет полный доступ к файлам проекта, может вносить изменения, пушить, комитить.

  2. Ask - режим вопрос-ответ, ИИ видит файлы, может предлагать правки, но код сам не трогает. Обычно в нём работаю - сам читаю, сам решаю, что принять.

  3. Manual - полностью ручной режим, ИИ не смотрит код. Если нужно просто пообщаться или решить отдельную задачу. Сам ни разу не использовал.

Также есть дополнительные возможности:

  • Tab-комплит - ИИ дописывает код на лету. Главное успевать читать, что он предлагает.

  • Ask to Code - можно выделить кусок кода и задать вопрос напрямую в редакторе, без переключений в чат.

  • Ask to Terminal - то же самое, но с логами из терминала. Очень удобно для дебага!

Если умеешь кодить - режим Agent лучше не использовать.
Он может внести изменения, которые не сразу заметишь, а потом полдня будешь разбираться, почему всё сломалось.

Итого

Если шарите хоть немного в коде, и есть интересная идея - Cursor сильно сэкономит время.

6.2 ChatGPT + Sonnet

"Когда закончится весь этот зоопарк?"
"Когда закончится весь этот зоопарк?"

Даже близко не буду пытаться объяснять текущие нейминги ИИ. Зоопарк у всех!

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

Выработал простое правило - использую самую последнюю стабильную версию. Обычно этого вполне достаточно.

У OpenAI пользуюсь только gpt-4o. Она быстрее, дешевле, и на сегодняшний день самая универсальная.

Для кода почти всегда использую Sonnet, последние версии (3.7 / 4.0). Они аккуратнее пишут код, меньше галлюцинируют.

В Cursor можно выбрать конкретную модель, которая будет подключена к чату. Это удобно, можно работать в разных вкладках с разными ИИ, сравнивать их поведение. Иногда открывал отдельный чат с Gemini, чисто для сравнения, когда отлаживал пайплайн и хотел посмотреть, как разные модели решают одну и ту же задачу.

Отдельно веду чаты в ChatGPT, там удобнее тексты писать, наговаривать мысли, вести проект в целом.

Начинал проект с общей идеи. Задал промт для анализа нескольким ИИ с режимами thinking (Grok, DeepSeek, ChatGPT). Ответил на уточняющие вопросы (кто спросил), получил большие ресерчи по теме. Сохранил в текстовые файлы. В ChatGPT создал отдельный изолированный проект, подгрузил туда мнение "экспертов", далее в новом чате, с полученной информацией выстраивал поэтапный план. Разбил на подзадачи, составил заметки по темам.

6.3 Структура

Структура у меня вышла простая, модульная.

В самом начале, вместе с o4-mini-high, определил идею, общую структуру, что мне нужно, какие классы и функции создать, как их примерно связать. Зафиксировал всё в текстовом файле.

В дальнейшем ссылался на него, указывая другим ИИ для контекста.

В процессе немного корректировал, изменял.

Project_09_Poker/
├── src/
│   ├── __init__.py
│   ├── gui/
│   │   └── gui.py                # графический интерфейс приложения
│   ├── pokerlogic/
│   │   ├── __init__.py
│   │   ├── best_action.py        # основная логика расчёта оптимального действия
│   │   └── available_actions.py  # определение доступных действий
│   ├── cv/
│   │   ├── __init__.py
│   │   ├── detect.py             # YOLO детектор
│   │   ├── ocr.py                # распознавание текста
│   │   └── parser.py             # парсинг результатов
│   └── config.py                 # конфигурация
├── models/                       # YOLO модели
├── tests/                        # тесты
├── app.py                        # точка входа
├── .gitignore
├── LICENSE
├── requirements.txt
└── README.md

Весь код из проекта в моём репозитории на GitHub.

6.4 GUI

GUI, графический юзер интерфейс.

Этот вопрос полностью отдал на аутсорс Cursor + Sonnet. Работал в режиме Agent.

Весь GUI писал Sonnet. Я просто формулировал задачу, передавал текстовый файл с описанием проекта и общей структурой, потом читал, фильтровал, кое-что отклонял, что-то менял, копировал, вставлял.

Вот здесь полностью использовал режим Agent, классический вайбкодинг. Визуал не моя область, было лень вникать. Управился за пару дней.

В итоге получилось:

  • Управляющая панель, через которую запускается весь процесс.

  • Пользователь вручную выделяет область экрана, нажимает кнопку «Анализ» - бот делает скриншот, детектирует нужные элементы (карты, банк, to call и т. д.), извлекает тексты, собирает JSON и передаёт в best_action().

  • Результат анализа сразу выводится: стадия игры, карты, стек, банк, действия с рассчитанным EV. Сверху отдельно показывается строка с наиболее выгодным действием. Всё логируется с таймингами.

  • Режим автоанализа - процесс запускается в цикле, пока не нажата кнопка «Стоп». Автоклик пока не реализован, маскировка под человека тоже.

  • В конце кнопка «Завершить» корректно закрывает все процессы и очищает временные файлы.

7. Что получилось

Изначальный план был:

  1. Создать алгоритм, моделировать симуляции раздач и рассчитывать оптимальные действия с максимальным EV.

  2. Извлекать все необходимые данные с экрана, передать в алгоритм, объединить в удобный интерфейс.

  3. Реализовать имитацию человеческого поведения, чтобы обходить античиты.

  4. Сделать программу, работающую полностью в автономном режиме.

С первыми двумя пунктами я справился. Логику выбора лучшего действия прописал за день. Монте-Карло оказалось не таким сложным - основное время ушло на доводку функции расчётов. Компьютерное зрение также заняло не много времени, около недели, разобрался быстро.

Насчет имитации человеческого поведения, у меня были очень интересные задумки, хотел создать свое вероятностные распределения, с использованием некоторых статистических методов. Очень интересный математический прием хотел применить.

Однако после изучения вопроса античитов, понял, что не хочу лезть в эту серую зону. Античиты устроены иначе, не в моей зоне компетенций.

К тому же, после завершения рабочего портотипа, провел повторный анализ рабочего процесса, осознал масштаб дальнейших работ, понял - сделать полностью автоматический бот в одиночку, я либо не смогу, либо это займёт слишком много времени. Следом отпал и четвертый пункт про автономный режим.

Poker Bot в действии
Poker Bot в действии

В итоге получилась MVP программа, которая анализирует изображения с редкими ошибками в детекции, выдаёт корректные EV (хотя иногда ломается из-за неверного распознавания). Скорость инфиренса, 4-6 секунд через интерпретатор и 5–8 секунд через скомпилированный .exe файл. Основное время съедает моделирование симуляций Монте-Карло. GUI простой, но рабочий (я не дизайнер, делал чтобы просто работало).

Программа, в каждый момент игры, дает подсказку, показывает все возможные доступные действия, считает их EV.

Основная цель программы, повысить математическое ожидание выигрыша. Матожидание это теоретическое ожидаемое среднее количество побед в будущем.

Проверить реальную эффективность не получилось, так как для достоверной статистической оценки, нужно сыграть большое количество игр, желательно чтобы это делали разные люди, на протяжении длительного времени. Ресурсы (время, деньги, человеческие) не позволили провести такую оценку.

Возможные улучшения:

  • Детекция:
    Можно дообучить модели, попробовать более тяжёлые архитектуры, расширить датасет, поиграть с гиперпараметрами, прогнать на 200-300 эпох, используя облачные мощности. Выжать ещё 5-7% по mAP@50-95 и свести ошибки к минимуму. Это реально.

  • Математика:
    Добавить логику диапазонов рук, учесть fold equity, поднять количество симуляций. Добавить в расчетах Байесовский классификатор. Всё это заметно улучшит точность EV.

  • Код:
    Оптимизировать циклы, разделить громоздкие модули, всё это сократит время инференса. А если смотреть в долгую, можно вообще всё переписать на JavaScript или C++: при всей моей любви к Python, он хорош для быстрых прототипов, но не для оптимальных и стабильных решений.

  • Дополнительные функции:
    Если всё-таки двигаться к изначальной идее, нужно дописать БД для хранения истории по сессии, добавить функции имитации человеческих паттернов, и продумать автоплей.

Вывод

От идеи до рабочего прототипа прошло чуть больше четырёх недель. Я делал всё сам: от структуры проекта и математики до моделей распознавания и интерфейса. ИИ-инструменты помогали на каждом шаге: где-то ускоряли работу, где-то предлагали варианты решений, но направлять и дорабатывать всё равно приходилось вручную.

В итоге у меня получился MVP, который действительно работает: видит карты, понимает ситуацию на столе, считает EV и подсказывает действие.

Для личного проекта, сделанного в одиночку, это считаю это отличный результат!