Написание покерного бота.
Внимание материал носит чисто ознакомительный характер, и автор не несет ответственности за закрытие аккаунтов покерными румами. По законам стран создание и использование ботов не запрещено, однако по правилам покерных румов они запрещены.
В данной статье полных исходных кодов не будет, только теоретически что и как можно
использовать, и некоторые куски функций. Если ты интересуешься этим, тебе не составит собрать все в единую картину и написать свое. Так же не буду учить стратегиям игры, про термины или стратегии а так же правила можно в интернете найти много информации.
Немного истории.
Начал играть около 5 лет назад, за это время отыграно более 1 миллиона рук. В основном это No Limit Holdem (нелимитированный холдем) за короткими столами (от 2 до 6 человек за столом), есть опыт игры в Омаху, HU NL Holdem. Игра вроде как хобби и отдых. И вот как то с друзьями появилась идея написать бота, первая идея была написать под лимит холдем, и когда наполовину бот был написан, приняли закон запрещающий играть американцам в покер, в итоге с лимитом закинули бота. Первая версия была написано на нейронной сети с распознаванием образов, что это значит – обучили бота распознавать по снимкам с экрана карты, но это сами понимаете неточный
метод, но при достаточно хорошем обучении распознавал с точностью 98-99%.Далее версии ботов уже работали на прямую с окнами румов, используя их ресурсы., а так же в тандеме с программами для анализа и сбора статистики игры.
Покер румы не спят.
Практически во всех покер румах есть та или иная защита и система обнаружения покерных ботов. Рассмотрим часть из них. Долгая игра – когда человек слишком долго
играет, становится подозрительным, поэтому в некоторых румах могут появляться окошки с вопросами. Как защита не делать долгих сессий игры. Действия за столом – нажатие кнопок, выбор окон, действий не должно происходить не передвигая мышь, в свернутом окне. Траектории мыши лучше задать немного нелинейно, нажатия в разные места кнопок, скорость движения тоже не моментальная. Сканы запущенных процессов и скриншоты экрана – защита как говорил выше, не играть в свернутых окнах, процесс бота называть не покер-бот , и тому подобное, не оставлять окно бота развернутым на экране, имя процесса менять спустя какое то время (например через х минут перезапустить бота с новым именем процесса).
Схема покер бота.
Схему можно разделить на 3 части:
- Блок 1 - блок взаимодействия с клиентом для игры в покер
- Блок 2 - блок принятия решения
- Блок 3 - блок сбора статистики, на мой взгляд лучше использовать сторонний софт, например PokerTracker3, отключив при этом вывод статистики на экран.
Далее рассмотрим каждый блок более подробно.
Блок 1 - блок взаимодействия с клиентом для игры в покер.
Этот блок служит для сбора информации на игровом столе и передачи в блок принятия
решения, а так же получив ответ с решением выполнить то или иное действие как Fold, Raise, Call или All-In. Теперь рассмотрим этот блок. Часть этой части это взаимодействие с главным окном
программы, это такие действия как выбор лимита, выбор стола за которым будем играть, это тоже большая часть, но на ней не будем останавливаться. Рассмотрим более подробно часть уже со столом. Сначала нам нужно найти хэндлы всех игровых открытых столов (окон), можно это сделать при помощи функции EnumWindows
Функция EnumWindows перечисляет все окна верхнего уровня на экране, передавая дескриптор каждого окна, в свою очередь, в определяемую программой функцию повторного вызова. EnumWindows действует до тех пор, пока последнее окно верхнего уровня не будет перечислено, или пока функция повторного вызова не возвратит значение ЛОЖЬ (FALSE).
Синтаксис
BOOL EnumWindows
(
WNDENUMPROC lpEnumFunc, // указатель на функцию обратного вызова
LPARAM lParam // определяемое программой значение
);
Параметры
lpEnumFunc
Указывает на определяемую программой функцию повторного вызова. Для получения дополнительной информации, см. функцию повторного вызова EnumWindowsProc.
lParam
Устанавливает 32-разрядное, определяемое программой значение, которое будет передано в функцию повторного вызова.
Возвращаемые значения
Если функция завершилась успешно, возвращается значение отличное от нуля. Если функция потерпела неудачу, возвращаемое значение - ноль.
Если функция отработала успешно, мы можем получить имя окна GetWindowText, и проанализировав его понять нужное или нет окно, обычно в заголовке окна присутствует ваш ник, название покер рума и лимит. Далее не мешало бы хэндлы всех найденных окон сохранить, для дальнейшего использования, без повторного поиска, поиск нужно будет делать лишь в случае открытия нового окна.
Теперь когда мы имеем хэндл игрового окна мы можем вытягивать информацию о столе. Большая часть информации хранится в дилерском окне . Из него мы можем узнать все
участников за столом, кто зашел, кто вышел со стола, кто сделал какое действие, карты на столе и свои карты. Для этого нам понадобится парсер текста, как его писать рассказывать не буду, это отдельная тема, главное что идея ясна J. Но перед парсером нам нужно сначала найти элементы окна, и среди них найти дилерское окошко. Очень часто дилерское окно это класс производный от Internet Explorer_Server , чтобы его найти воспользуемся функцией EnumChildWindows, а затем GetClassName.
EnumChildWindows перечисляет
дочерние окна, которые принадлежат определенному родительскому окну, в свою
очередь, передавая дескриптор каждого дочернего окна в функцию повторного
вызова, определяемую программой. Функция EnumChildWindows работает до тех пор, пока не будет
перечислено последнее дочернее окно или функция повторного вызова не возвратит
значение ЛОЖЬ (FALSE).
Синтаксис
<i>BOOL EnumChildWindows<o:p></o:p></i>
<i>(<o:p></o:p></i>
<i>HWND hWndParent, // дескриптор родительского окна<o:p></o:p></i>
<i>WNDENUMPROC lpEnumFunc, // указатель на функцию обратного вызова<o:p></o:p></i>
<i>LPARAM lParam // значение, определяемое программой<o:p></o:p></i>
<i>);</i><o:p></o:p>
Параметры
hWndParent
Идентифицирует родительское окно, чьи дочерние окна должны перечисляться.
lpEnumFunc
Указывает на определяемую программой функцию повторного вызова. Для получения дополнительной информации относительно функции повторного вызова, см. функцию повторного вызова EnumChildProc.
lParam
Устанавливает 32-разрядное, определяемое программой значение, которое будет передано в функцию повторного вызова.
Возвращаемые значения
Если функция завершилась успешно, возвращается значение отличное от нуля. Если функция потерпела неудачу, возвращаемое значение - ноль.
Теперь можно приступать и к парсингу текста. Наш ход или нет можно анализировать исходя из того какие кнопки доступны нам в данный момент. Это можно сделать как через ресурсы так и и просто по точкам на экране в этом окне. Далее если наш ход то передаем известные данные в блок принятия решения. При получении ответа делаем соответствующее действие. В этой части еще не рассмотрено как нажимать на кнопки, но это думаю и сами осилите .
Блок 2 - блок принятия решения
Этот блок, сердце всего бота, т.к. от него зависит насколько он успешен в игре, насколько правильные принимает решения. В журнале Хакер 137 от 06.2010 рассматривался пример полностью опирающийся на теорию вероятности и это хорошо, но чтобы получить достаточно достоверный результат надо провести достаточно много
итераций. Чтобы играть достаточно уверенно против противника мы должны знать его диапазон и как он играет(это кусок привязан к статистике), так же есть определенные стратегии игры, зависящие от количества денег у вас, от количества игроков за столом, и самой стратегии игры. От всего этого зависит стартовый диапазон рук, и действия на последующих улицах, например человек играющий по стратегии коротких стеков (SSS) редко делает какие то ходы на ривере (когда выложена 5 карта на стол),т.к. к этому моменту он часто уже в Аллине (пошел в ва-банк). Оценка ситуаций так же идет в подсчете аутов, ауты это количество карт, которое улучшит наше положение на последующих улицах, отсюда вытекаю такие комбинации как стрит дро, стрит в дырку, флэшь дро и т.д. Дро это тоже комбинация и в зависимости от количества аутов имеет цену.И имея определенную комбинацию, даже в данный момент не победную, мы уже строим линию игры. Благодаря этим дополнительным данным мы уже можем принимать решения в зависимости от выбранной стратегии, если добавить поверх еще статистику игрока они будут еще точнее, а при добавлении полного
просчета эту выборку еще можно дополнить. Надеюсь донес свою мысль о сокращении выборок и принятия решений, все это складывается на личном опыте игры и
принятия решения,т.к. в несколько предложений не расскажешь теории игры в покер, которая издается во многих томах, о разных ситуациях, который бесчисленное множество, хотя можно и выделить по основным группам…
Сначала нам надо узнать ситуацию на столе, и узнать какая комбинация у нас имеется. Вот пример кода определения комбинации(переписал более понятно, но может не
оптимально):
// функция для заполнения массива карт, вовращает порядковый номер карты
int CardToNumber(char Card)
{
if ((Card>'1')&&(Card<='9'))return (Card-48);
if (Card=='T')return 10;
if (Card=='J')return 11;
if (Card=='Q')return 12;
if (Card=='K')return 13;
if (Card=='A')return 14;
return -1;
}
// функция для получения бита, соотвествующего масти карты
int MastToNumber(char mast)
{
if (mast=='h')return 1;
if (mast=='d')return 2;
if (mast=='c')return 4;
if (mast=='s')return 8;
return 0;
}
// функции расчета комбинации
int Hand(char *MyHand_,char *CardsTable_)
{
int
flush_,flush; //0-ничего 1-флэшь 2-флэшь дро 3-бэедорфлэшьдро 4-флэшь на столе(nothing cards)
int cards[15][4]; //i-карты j1-кол-во карт j2-масти bits 1,2,3,4-h,d,c,s j3-0/1 - 1-наша карта 0-не наша(на столе)
int straight; //0-ничего, 1-стрит, 2-открытый стрит(2-х сторонний) 3-гатшот 4-натс стрит на столе 5-стрит на столе
int readyhand; //0-не готовая , 1-готовая комбинация
int i,j, maxi,flag_,flag,flmax,top,pair_,pair,three,four,over,set_,treeps,quad,num,kicker;
// заполнение массива с картами исходя из входящих данных
for(i=1;i<=14;i++)
for(j=1;j<=3;j++)cards[i][j]=0;
cards[CardToNumber(MyHand_[0])][1]++;
cards[CardToNumber(MyHand_[0])][2]=cards[CardToNumber(MyHand_[0])][2]| 2*MastToNumber(MyHand_[1]));
cards[CardToNumber(MyHand_[0])][2]=cards[CardToNumber(MyHand_[0])][2]|(32*MastToNumber(MyHand_[1]));
cards[CardToNumber(MyHand_[0])][3]=1;
cards[CardToNumber(MyHand_[3])][1]++;
cards[CardToNumber(MyHand_[3])][2]=cards[CardToNumber(MyHand_[3])][2]|(2*MastToNumber(MyHand_[4]));
cards[CardToNumber(MyHand_[3])][2]=cards[CardToNumber(MyHand_[3])][2]|(32*MastToNumber(MyHand_[4]));
cards[CardToNumber(MyHand_[3])][3]=cards[CardToNumber(MyHand_[3])][3]+1;
i=0;
while(i<strlen(CardsTable_))
{
cards[CardToNumber(CardsTable_[i])][1]++;
cards[CardToNumber(CardsTable_[i])][2]=cards[CardToNumber(CardsTable_[i])][2]|(2*MastToNumber(CardsTable_[i+1]));
i=i+3;
}
// вычисление стритов, и на сколько хороший стрит
num=0;
i=1;
maxi=0;
flag=0;
flmax=0;
while(i<15)
{
if((cards[i][1]>0)&&(i>1))
{
num++;
if (cards[i][3]==1)flag=i;
}
else
{
if (maxi<num)
{
maxi=num;
flmax=flag;
}
num=0;
flag=0;
}
if(i==1){
if (cards[14][1]>0)
{
num++;
if (cards[i][3]==1)flag=i;
}
else
{
if (maxi<num)
{
maxi=num;
flmax=flag;
}
num=0;
flag=0;
}
}
i++;
}
if(maxi<num){maxi=num;flmax=flag;}
num=0;
i=0;
flag=0;
if(flmax>=maxi)
{
while(i<maxi)
{
if((cards[flmax-i][3]>0)&&(flmax-i!=1))flag++;
if((cards[14][3]>0)&&((flmax-i)==1))flag++;
i++;
}
}
straight=0;
if((maxi>=5)&&(flmax==14)&&(flag==0)) straight=4;
if((maxi>=5)&&(flmax<14)&&(flag==0)) straight=5;
if((maxi>=5)&&(flag>0)) straight=1;
if((maxi==4)&&(flag>0)&&(flmax<14)) straight=2;
if((maxi==4)&&(flag>0)&&(flmax==14)) straight=3;
if((maxi==4)&&(flag>0)&&(flmax==4)) straight=3;
// вычисление флэшей
flush_=0;
j=2;
while(j<=16)
{
flush=0;
flag=0;
i=2;
num=0;
flmax=0;
flag_=0;
while(i<15){
if ((cards[i][2]& j)==j) { flmax=i;num++;}
if ((cards[i][2]& (j*16))==j*16) { flag_++;flag=i;}
i++;
}
if((num==5)&&(flag==0)) flush=4;
if((num>=5)&&(flmax==14)&&(flag>=13)) flush=1;
if((num>=5)&&(flmax==14)&&((cards[13][2]& j)==j)&&(flag==12)) flush=1;
if((num>=5)&&(flmax==14)&&((cards[13][2]& j)==j)&&((cards[12][2]& j)==j)&&(flag==11)) flush=1;
if((num>=5)&&(flmax==14)&&((cards[13][2]& j)==j)&&((cards[12][2]& j)==j)&&((cards[11][2]& j)==j)&&(flag==10)) flush=1;
if((num>=5)&&(flmax==14)&&((cards[13][2]& j)==j)&&((cards[12][2]& j)==j)&&((cards[11][2]& j)==j)&&((cards[10][2]& j)==j)) flush=1;
if((num==5)&&(flag_==2)) flush=1;
if((num==4)&&(flag>0)) flush=2;
if((num==3)&&(flag==2)) flush=3;
if((flush>flush_)&&(flush!=0))flush_=flush;
j*=2;
}
flush=flush_;
// вычисление пары, овер пары, сета, трипса, квада, кикера
pair=0;three=0;four=0;
top=0;over=0;set_=0;treeps=0;quad=0;pair_=0;
i=2;
while(i<15)
{
if ((cards[i][1]>=1)&&(cards[i][3]==0)){ top=0;over=0;}
if ((cards[i][1]==1)&&(cards[i][3]==0)){ top=0;over=0;}
if ((cards[i][1]==2)&&(cards[i][3]==0)){ pair++;}
if ((cards[i][1]==2)&&(cards[i][3]==1)){ pair_++;pair++;top=1;}
if ((cards[i][1]==2)&&(cards[i][3]==2)){ pair_++;pair++;over=1;}
if (cards[i][1]==3){ three++;}
if (cards[i][1]==4){ four++;}
if ((cards[i][1]==3)&&(cards[i][3]==1)) treeps=1;
if ((cards[i][1]==3)&&(cards[i][3]==2)) set_=1;
if ((cards[i][1]==4)&&(cards[i][3]>0)) quad=1;
if ((cards[i][1]==4)&&(cards[14][3]>0)) quad=1;
i++;
}
kicker=0;
if (top>0)
for(i=2;i<=14;i++){
if ((cards[i][1]==1)&&(cards[i][3]==1)) kicker=i;
}
// 'Стрит:и тип стрита '- straight
// 'Флеш:и тип флэша '- flush
// 'К-во пар: '- pair
// 'К-во пар у нас: '- pair_
// 'Топ пара: '- top
// 'Кикер' - kicker
// 'Овер пара: '- over
// '3 карты: '- three
// 'Трипс:(2 карты на столе 1 у нас) '- treeps
// 'Сет: (2 у нас 1 на столе)'- Set_
// '4 карты: '- four
// 'Каре: '- quad
………
}
В итоге в соответствующих переменных мы имеем данные о текущем положении дел. При чем заметьте понятия как фул хауз нету, это производная из пара и трипс или сет. Тут еще нужен кусок который определяет готовность руки, т.е. готовая, не готовая, полуготовая. Например такой код:
…
//готовность руки
readyhand=0;
if ((pair==1)&&((top==1)||(over==1))) readyhand=1;
if ((pair==1)&&(top==1)&&(kicker<11)&&(over==0)) readyhand=0;
if ((pair>=2)&&((top==1)||(over==1))) readyhand=1;
if ((pair==2)&&(top==1)&&(kicker<11)&&(over==0)) readyhand=0;
if ((pair==2)&&((top==0)||(over==0))&&(pair_==2)) readyhand=1;
if ((pair>2)&&((top==0)||(over==0))) readyhand=0;
if ((three==1)&&(top==0)&&(over==0)&&(pair_==0)) readyhand=0;
if (treeps==1) readyhand=1;
if (set_==1) readyhand=1;
if (straight==5) readyhand=0;
if (straight==4) readyhand=1;
if ((straight==2)&&(readyhand==0)) readyhand=2;
if (straight==1) readyhand=1;
if (flush==4) readyhand=0;
if ((flush==2)&&(readyhand==0)) readyhand=2;
if (flush==1) readyhand=1;
if ((three==1)&&((top==1)||(over==1))) readyhand=1;
if ((three==1)&&(top==0)&&(over==0)&&(pair_>0)) readyhand=1;
if ((three==1)&&(top==0)&&(over==0)&&(pair_==0)&&(pair>0)) readyhand=0; //фулл на столе
if ((treeps==1)&&(pair>0)) readyhand=1;
if ((set_==1)&&(pair>0)) readyhand=1;
if (quad==1) readyhand=1;
…
Исходя из этих данных идем к части, которая получает статистику об оппонентах, расчеты о шансах банка и другие необходимые, принимаем решение о действии и переходим к первому блоку – выполнить действие.
Блок 3 - блок сбора статистики
Это немаловажный блок, помогающий в принятии решений, вы можете как сами собирать статистику, как воспользоваться сторонними программами. Я опишу пример запроса к PokerTracker3, программа для сбора и анализа статистики игры, параметров в программе более 100, но нам хотя бы для примера достаточно и нескольких основных. В PokerTracker3 база хранится в Postges базе. Пример запроса:
SELECT
sites.site_abbrev,
p.player_name,
COUNT(hhps.id_player) AS hands,
AVG (CASE WHEN flg_vpip THEN 1 ELSE
0 END)*100 AS vpip,
AVG (CASE WHEN cnt_p_raise >= 1
THEN 1 ELSE 0 END)*100 AS pfr,
AVG (CASE WHEN flg_steal_att THEN 1
WHEN flg_steal_opp THEN 0 END)*100 AS ats,
AVG (CASE WHEN flg_sb_steal_fold
THEN 1 WHEN flg_blind_def_opp and flg_blind_s THEN 0 END)*100 AS fsbtos,
AVG (CASE WHEN flg_bb_steal_fold
THEN 1 WHEN flg_blind_def_opp and flg_blind_b THEN 0 END)*100 AS fbbtos,
AVG (CASE WHEN
enum_f_cbet_action='F' THEN 1 WHEN flg_f_cbet_def_opp THEN 0 END)*100 AS fcbetf
FROM lookup_sites AS sites INNER
JOIN (
player AS p INNER JOIN
holdem_hand_player_statistics AS
hhps
ON (p.player_name = 'ник аппонента')and(p.id_player
= hhps.id_player)
)
ON p.id_site = sites.id_site
GROUP BY
sites.site_abbrev,p.player_name
ORDER BY sites.site_abbrev DESC;
На выходе получим по определенному игроку выборку, сайтов на которых такой ник имеется в базе, можно сократить по нашему сайту только, количество рук оппонента (нужна для точности показаний), VPIP(% рук с которыми заходит в игру), PFR (preflop raise –префлоп рейз ), ATS (attemptto steel – сколько крадет блайндов на префлопе
),CBET (c-bet on flop – ставка в продолжение на флопе),BBS (big blind steel –кража большого блайнда ),SBS (small blind steel – кража малого блайнда). Эти параметры в основном используются при расширенной стратегии коротких стеков, для полных стеков этих параметров мало, но в качестве примера как их получить хватит. Для чего все эти параметры нужны и как их использовать советую почитать соответствующие ресурсы.
После того как мы получили дополнительную статистику переходим ко второму блоку и корректируем наше решение об игре.
В заключение.
Если ты заинтересовался этой тематикой, то я дал тебе мысли для размышлений и начала
написания бота, дальше сам изучай материалы по соответствующим темам и развивай своего бота. Надеюсь у меня получилось донести информацию до тебя, т.к. красиво писать не очень умею , а мыслей о чем рассказать хотелось много, много осталось за кадром этой статьи.
На написание этой статьи подвинули статьи из журнала Хакер 137 от 06.2010 «Натягиваем сетевые poker room’ы» и Хакер 139 от 08.2010 «Симуляция покерного оргазма». Так как с автором я не полностью согласен и тему считаю полностью не раскрытой и хотелось поделиться своим мнением.
Вот немного скриншотов с того что может получится у тебя:
В рубрике: Программирование » Свои разработки » Заметки » Софт
Теги: poker bot написание покер бот
Вы можете следить за комментариями к этой записи поRSS
Оставьте комментарий