Не так давно я стал увлекаться покером, а помимо покера я занимаюсь компьютерным зрением и решил, почему бы не совместить приятное с полезным, и сделал распознавание объектов, которые находятся на покерном столе и добавил немного аналитики на основании которой я мог бы принимать решения о своих ходах. Кому интересно, что у меня получилось, добро пожаловать под кат!
Общее функционирование программы
Сразу оговорюсь, что в качестве рума я сделал выбор в пользу PokerStars и выбрал самую популярную разновидность покера — техасский холдем. Функционирование программы заключается в том, что запускается бесконечный цикл, который считывает определённую область экрана, в которой находится покерный стол. Когда наступает наш(героя) ход, выскакивает или обновляется окошко со следующей информацией:
какие карты сейчас у нас на руках;
карты, которые сейчас на столе;
общий банк;
эквити;
о позиции и о ставке каждого игрока.
Визуально это выглядит следующим образом:
Определение хода героя
Сразу же под картами героя есть небольшая область, которая может быть либо чёрная, либо серая:
Если данная область горит серым — ход наш, в противном случае — ход соперника. Так как у нас изображение статично, то мы вырезаем по координатам данную область и работаем с ней, а далее с помощью функции inRange()
, которая используется для детектирования пикселей изображения, которые находятся в определённом диапазоне цветов, передав туда вырезанное изображение, определить по количеству белых пикселей на бинарном изображении, которое нам вернула данная функция, наш ход либо нет:
res_img = self.img[self.cfg['hero_step_define']['y_0']:self.cfg['hero_step_define']['y_1'],
self.cfg['hero_step_define']['x_0']:self.cfg['hero_step_define']['x_1']]
hsv_img = cv2.cvtColor(res_img, cv2.COLOR_BGR2HSV_FULL)
mask = cv2.inRange(hsv_img, np.array(self.cfg['hero_step_define']['lower_gray_color']),
np.array(self.cfg['hero_step_define']['upper_gray_color']))
count_of_white_pixels = cv2.countNonZero(mask)
return True if count_of_white_pixels > self.cfg['hero_step_define']['min_white_pixels'] else False
Детектирование карт
Что ж, после того как мы определили, что ход наш, надо бы распознать карты героя и те, которые на столе. Для этого предлагаю опять воспользоваться тем, что изображение статично и вырезать, а далее бинаризовать области с картами. В результате для такого изображения с картами:
получается следующее бинарное изображение:
После находим внешние контуры значений и мастей с помощью функции findContours()
, которые в последующем передаём в функцию boundingRect()
, которая возвращает ограничительные рамки каждого контура. Чудненько, теперь у нас есть боксы всех карт, но как нам понять, что у нас на карте, к примеру, туз червей? Для этого я нашёл и обрезал вручную каждое значение и каждую масть, и поместил данные изображения в специальную папку как эталонные изображения. После этого считаем среднеквадратическую ошибку между каждым эталонным и боксом карты изображениями:
err = np.sum((img.astype("float") - benchmark_img.astype("float")) ** 2)
err /= float(img.shape[0] * img.shape[1])
Для какого эталонного изображения ошибка получилась меньше всего, то и имя изображения присваиваем боксу. Всё просто:)
Определение банка и ставки игрока. Нахождение фишки дилера
Для определения банка мы будем работать с шаблонным изображением такого вида:
Шаблонное изображение и изображение всего стола мы передаём в функцию matchTemplate()
, про которую я писал в одной из своих прошлых статей , которая одним из параметров возвращает координаты левого верхнего угла шаблонного изображения на изображении всего стола. Зная данные координаты, мы можем, отступив на константное значение вправо, найти цифры банка. Далее, по знакомой схеме, находим контуры и боксы каждой цифры, которые в последующем сравниваем с эталонным, только уже цифры, изображением, и считаем среднеквадратическую ошибку. Всю эту же махинацию, описанную в этом разделе, за исключением поиска шаблонного изображения, проворачиваем и со ставками каждого игрока, где координаты ставок прописаны в конфиг файле.
Фишка дилера в покере — обязательный атрибут, определяет очерёдность действий и торга всех участников игры. Если вы должны действовать одним из первых, то вы находитесь в ранней позиции. Если вы сидите в поздней позиции, то ваша очередь хода наступает одной из последних. Для 6-max стола, а мы именно такой и рассматриваем, позиции распределяются следующим образом:
Для определения, кто диллер, мы также берём шаблонное изображение, только уже такого вида:
Находим координаты верхнего левого угла данного изображения на изображении стола и используя формулу расстояния между двумя точками на плоскости, где вторые x и у координаты — это заранее прописанные в конфиг файле координаты центра игрока, определяем к кому ближе находится данная кнопка, тот и будет её владельцем:)
Распознавание свободных мест и игроков, которые отсутствуют
Так часто бывает, что за столом вместо 6 игроков сидит 5, тогда свободное место помечается подобным образом:
А под ником игрока, который на данный момент отсутствует, появляется следующая надпись:
Для выявления присутствия таких игроков, берём данные изображения в качестве шаблонных и изображение стола, и опять же подаём на вход функции matchTemplate()
, но только теперь возвращаем не координаты, а вероятность насколько два изображения похожи между собой. Если вероятность, допустим, между первым изображением и изображение стола большая, значит у нас за столом отсутствует игрок.
Расчёт эквити
Эквити — это вероятность на победу у конкретной руки против двух конкретных карт или диапазона соперника. Математически эквити вычисляется как отношение количества возможных выигрышных комбинаций к общему количеству возможных комбинаций. На Python данный алгоритм можно реализовать с помощью библиотеки eval7(которая в данном случае помогает оценить насколько сильная рука) следующим образом:
win_count = 0
for _ in range(iters):
np.random.shuffle(deck)
num_remaining = 5 - len(table_cards)
draw = deck[:num_remaining+2]
opp_hole = draw[:2]
remaining_comm = draw[2:]
player_hand = hero_cards + table_cards + remaining_comm
opp_hand = opp_hole + table_cards + remaining_comm
player_strength = eval7.evaluate(player_hand)
opp_strength = eval7.evaluate(opp_hand)
if player_strength > opp_strength:
win_count += 1
win_prob = (win_count / iters) * 100
Заключение
Я много стал читать литературы по покеру и в дальнейшем собираюсь добавить много интересной аналитики, которая должна быть полезна. Если кто‑то хочет поучаствовать в проекте или у кого‑то есть идеи по его развитию — пишите! Исходный код как всегда доступен на github. Всем хорошего дня!
Комментарии (24)
Napaste
19.05.2023 05:18Забавное применение CV.
Вообще некоторые румы (в т.ч. Poker Stars) позволяют использовать программы сбора и отображения статистики за столом. Вариантов различных статистических показателей там тысячи (VPIP, PFR и 3bet и прочие).
Самое интересное, что подобные программы подключаются прямо к клиенту и получают почти всю информацию, которую получает в визуальном представлении игрок. На основе подобного интерфейса подключения явно где-то есть "читы" для покера, позволяющие играть с максимальным equity.
А вот для румов, которые к данным стола доступа не дают, ваше решение было бы интересно. Таких румов довольно много и среди них есть очень популярные (тот же PokerOK)
wadik69 Автор
19.05.2023 05:18Вообще некоторые румы (в т.ч. Poker Stars) позволяют использовать программы сбора и отображения статистики за столом
Не знал, но догадывался про существование таких программ. Они же платные?
А вот для румов, которые к данным стола доступа не дают, ваше решение было бы интересно
Я это учитывал, поэтому написал абстрактный класс, чтобы в дальнейшем была возможность написать решение для других румов
w00dLAN
19.05.2023 05:18+1Лет 5 назад тоже увлекался техасским холдемом и тоже на покер старс. Тоже писал помощника по расчёту эквити. Так как с компьютерным зрением не дружил, то использовал такой подход.
Переход хода к игроку активировал цветовую кнопку и я просто контролировал цвет пикселя в определённой точке (для точности можно в 2...3 местах). Смена цвета из серого в синий (вроде синий) означал переход хода ко мне.
Фишки дилера всегда стоят в определённых местах. Тот же принцип, либо в определённой точке цвет сукна, либо цвет фишки диллера.
Карты, сначала отсеивал цвет масти, красный или чёрный, потом попиксельно сравнивал с набранными шаблонами мастей и цифр/букв.
Ставки и банк, тоже по шаблону цифр, нарезал скрин на цифры и через маску прогонял.
Дальше, прикрутил управление мышкой, написал алгоритм работы и в автомате тыкал на клавиши.
Дальше тестов дело не пошло, т.к. на маленьких ставках играют не "по науке". Поскольку для тех же американцев потерять пару центов не проблема, они иногда (на маленьких ставках ВСЕГДА) делают нелогичные ставки и математические алгоритмы прописанные в книгах не работают. И получаешь, что можно рандомно играть и будет также. А на больших ставках не тестировал, там, как говорят, играют уже по учебникам (опять же большинство).
kalbas
19.05.2023 05:18+3Забавный факт, но почти всегда можно угадать, что код написан не обычным разрабом, а млщиком по таким вот строкам:
return True if count_of_white_pixels > self.cfg['hero_step_define']['min_white_pixels'] else False
Zara6502
лет 10 назад думал о таком, но победила лень, забавно что попалась ваша статья, приятно что кто-то тоже додумался до такого. Интересен результат - увеличивает ли это шансы на победу?
wadik69 Автор
На данный момент из аналитики имеется только эквити, если добавить ещё пару вкусностей, то вполне увеличит:)
Zara6502
я собственно от электронного покера ушел потому, что, как мне показалось, площадки занимаются подтасовкой результатов. То есть вы никогда не узнаете какие карты и кому раздаются и не жульничает ли площадка. То есть формально элемент случайности выпадения карт подкручен в пользу организатора. Иначе пропадает смысл раздавать по 50-300 уе бонусов. Я деньги свои не тратил, играл всегда по 1 центу и 50 уе все проиграл, я правда не машинным зрением а экселем пользовался, но за всё время хорошая карта пришла всего 2 раза (а это 5000 партий), что статистически выглядит странно. Именно поэтому мне интересен ваш результат, так как там тоже не лохи сидят же.
wadik69 Автор
Думаю, что крупные румы не грешат такими вещами, но то, что Вы говорите, что хорошая карта выпала два раза из такого количества раздач - тут стоит задуматься)
А что за рум?
Zara6502
PokerStars
wadik69 Автор
Хммм...
w00dLAN
Площадка не жульничает. Это же казино. Они берут с вас процент и им этого за глаза. Просто на маленьких ставках не работают "научные методы", т.к. потери небольшие и всё играют наугад. Хотите больше стабильности и науки - надо играть в ставки от 10 баксов за блайнд.
Zara6502
1) как бы кто не играл, но тот кто пользуется наукой будет иметь преимущество, но и карты должны в любом случае приходить, а когда карт нет, то чем играть?
2) я не помню чтобы с меня брали какой-то процент, просто делаешь ставку и если выиграл то взял приз, если проиграл, то потерял ставку. Значит площадке удобно если выигрывать будет их бот. Это реализовать несложно, а проверить невозможно.
3) Я играл только из спортивного интереса, я никогда не трачу деньги на азартное, ну или если трачу, то всегда к этому отношусь как к потере. За последние лет 20 кажется раза два покупал за 100р какую-то жилищную лотерею, но я уверен на 1000000% что это лохотрон, это был довесок к подарку родственникам.
wadik69 Автор
Всё-таки, если рум честный, то вероятность появления хорошей карты у всех одинакова)
Zara6502
именно поэтому я сделал вывод что играя в электронном виде не стоит вопрос честности - это только вопрос веры.
LeSick
по второму пункту – не совсем так. давно не заходил на PS, но на сколько помню, там с бай-ина маленький процент «диллеру» идет. Это в турнирах. Там банк не равен сумме байинов. В кэше не помню точно…
А что значит для вас хорошая карта на 5000 раздач? Так-то даже карманные тузы не гарантия выигрыша
Zara6502
Хорошая карта это не их номинал, а те которые приводят к выигрышу. Покер он же прямой как лом и случайный на 100%, когда вы не сидите за столом в контакте с другими игроками там полностью пропадает часть игры. Поэтому работает только теория вероятности.