В этой статье мы рассмотрим создание симулятора покера. Так как правила покера немного отличаются между собой, то в качестве правил для симуляции мы возьмем правила Holdem No Limit Poker с сайта PokerStars. На основе симулятора мы сделаем две игры — игра компьютера с живым игроком и просто игра компьютерных игроков между собой. Первая игра нам понадобится для тестирования.

 

Интерфейсы

Создадим два интерфейса — Ilogic и IEventSimulation. Первый интерфейс нужен для того, чтобы унифицировать вызов различных логик. То есть у нас имеется один интерфейс, который реализует различные логики, и нам не нужно беспокоиться о хранении различных логик — мы храним только массив интерфейсов ILogic и вызываем метод этого интерфейса. У данного интерфейса есть только один метод — int getAnswer(float p, float totalBet, float curBet, float pot, int betting, int minRaise), он возвращает 0, когда нужно сбросить (fold), 1 при принятии ставки (call) и 2 при увеличении ставки (raise). Немного упростим модель — при рейзе не будем выставлять значение ставки, а просто увеличим ставку на минимально возможное значение.

Рассмотрим параметры этого метода: p — вероятность выигрыша (про нее читай в статье «Натягиваем сетевые poker room’ы» в июньском ] [ или на диске к этому номеру), totalBet — все поставленные игроком деньги за игру, curBet — текущее количество денег, которое нужно поставить, pot — размер банка, betting — номер круга торговли, minRaise — минимальное количество денег, на которое нужно повысить ставку при рейзе. Второй интерфейс нужен для создания различных оболочек для симулятора. В нашем случае будет две оболочки — для игры компьютерных игроков с человеком и для игры компьютерных игроков между собой. В интерфейсе IEventSimulation определены методы, которые позволяют сообщать оболочке обо всех изменениях в игре.

Перечислим эти методы: changeBoardCard(int[] board) — метод вызывается при изменении карт на столе, changePot(int pot) вызывается при изменении размера банка, changeMoneyOfPlayers(int[] money)вызывается при изменении количества денег игроков, postDillerMessage(String message)вызывается при отправке сообщений дилера, changeDillerPosition(int posOfDealer)вызывается при изменении позиции дилера, changePlayerStatus(int player, int status, int[] hand) вызывается при изменении статуса игрока.

 

Схема симулятора

Как известно, правила покера неоднородны и склонны друг от друга отличаться. Например, в круге торговли. Так, по правилам с сайта PokerStars после первого круга торговли первым ходит активный игрок слева от дилера, а по другим правилам первым ходит игрок слева от игрока, который ходил первым на прошлом круге торговли. В симуляторе реализованы правила Holdem Poker с сайта PokerStars. По размеру ставок будем делать не NoLimit и не Limit, а кое-что свое — ограничим размер рейза текущей ставкой.

Всего логик семь: AggressiveLogic (разыгрывает даже слабые руки), CautiousLogic (разыгрывает только сильные руки), RationalLogic (действует рационально), RaiseLogic (все время повышает ставку), CallLogic (все время поддерживает ставку), FoldLogic (все время сбрасывает), RandomLogic (случайно ходит).

AggressiveLogic, CautiousLogic и RationalLogic используют в принятии решений формулу p*pot = win и сравнивает win со своими ставками. Иначе говоря, использует формулу, которую мы обсуждали в прошлой статье (если хочешь освежить память — вставь в свою ЭВМ диск к этому журналу и зачитай ее). Единственное, что — CautiousLogic уменьшает вероятность, чтобы разыгрывать меньше рук, а AggressiveLogic увеличивает, чтобы разыгрывать больше. Оболочка HoldemForm рисует форму на swing’e и реализует два интерфейса — IEventSimulation и ILogic. Первый интерфейс нужен для того, чтобы отображать на форме все события симуляции — раздачу карт, сообщения дилера, изменения состояний игроков и т.д. Второй интерфейс мы создаем, чтобы пользователь мог сообщать симулятору свои действия — Fold, Call или Raise. Форма отображает все карты игроков и вероятности их выигрыша, поэтому она не подходит для честной игры с компьютером, но зато идеально подходит для отладки симулятора.

HoldemConsole просто выводит все сообщения дилера на экран.

 

Порядок симуляции

Для начала — небольшой алгоритм. Итак:

  1. Поставить большой и малый блайнды;
  2. Раздать карты игрокам (Пре флоп);
  3. Провести круг торговли;
  4. Положить три карты на стол (Флоп);
  5. Провести круг торговли;
  6. Положить четвертую карту на стол (Терн);
  7. Провести круг торговли;
  8. Положить пятую карту на стол (Ривер);
  9. Провести круг торговли;
  10. Открыть карты и определить выигрышную комбинацию.

Соответственно, после каждого круга торговли нужно проверять, не остался ли в игре только один игрок. Если да, то весь банк уходит ему.
Количество игроков, которые будут играть, равно девяти. Во время игры их может стать меньше, но в начале их будет именно девять. Это сделано в целях упрощения симуляции — не надо заботиться о длинах массивов.

 

Код симулятора

Определимся с тем, что должен знать симулятор. Во-первых, симулятор должен иметь следующие данные об игроках: их деньги (moneyOfPlayers), карты (handOfPlayers) и их состояние (в игре или вышли) — stateOfPlayers. Во-вторых, должен знать позицию дилера (posOfDealer), количество денег в банке (pot), размер большого блайнда (bigBlind) и текущие карты на столе (board). Для работы логики принятия решений нужно также запоминать, сколько денег положил в банк каждый из игроков за текущую игру(totalBet). И самое главное — симулятор должен знать, что за игроки играют за столом, то есть у него должен быть список всех игроков (playersList).

Методы, нужные для симуляции: trade(int betting) — метод торговли, startGame() — главный метод, в котором происходит игра, int getSinglePlayer() — если в игре остался один игрок, то метод вернет индекс этого игрока, int getActivePlayer() — количество активных игроков в игре. К этим методам добавляются несколько setметодов для изменения значений по умолчанию — setBigBlind(int bigBlind), setRoundCount(int roundCount). Можно сделать метод по изменению количества денег перед игрой, но я считаю, это не критично, ведь, в конце концов, это симулятор для тестирования алгоритмов, а там не важно, сколько денег у игроков в начале игры.

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

 

Startgame

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

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

x = (x + 1) % 9;

Эта массивная конструкция представляет собой всего лишь циклическое увеличение значения переменной x от 0 до 8-9. В данном случае оно означает количество игроков за столом.

 

Trade

Входной параметр в методе, которой проводит круг торговли — номер круга торговли. Это 1 (пре-флоп), 2 (флоп), 3 (терн), 4 (ривер). В начале метода проверяем на первый круг торговли, и если да, то находим позиции малого и большого блайндов и кладем деньги в банк. Далее происходит сам круг торговли.

Непосредственно перед ходом каждого игрока проверяются следующие параметры: больше ли одного игрока в игре, может ли текущий игрок играть. При повторном круге торговли проверяется, не равна ли ставка текущего игрока максимальной ставке (то есть нужно ли игроку еще вкладывать деньги в банк), и есть ли у игрока вообще деньги.
Проверки перед началом торговли

// увеличиваем номер текущего игрока
curPlayer = (curPlayer + 1) % 9;
if (getSinglePlayer() != -1) {
break;
}
if (stateOfPlayers[curPlayer] == false) {
continue;
}
if ((repeatTrade == true) && (betOfPlayers[curPlayer]
== maxBet)) {
continue;
}
if (moneyOfPlayers[curPlayer] == 0) {
continue;
}

После этих проверок можно переходить непосредственно к определению текущего хода игрока. Для этого вычисляем вероятность выигрыша игрока на основе его карт, карт на столе и количества игроков и вызываем метод интерфейса ILogic для определения хода игрока:

Вызов методов расчета вероятности и принятия решений

float p=logic.getProbabilityOfWin(
handOfPlayers[curPlayer], board,getActivePlayers());
int action=playersList.get(curPlayer).getAction(p,
totalBet[curPlayer] + betOfPlayers[curPlayer],
maxBet-betOfPlayers[curPlayer],pot,betting,
maxBet==0?bigBlind:maxBet);

Метод расчета вероятности вызывается со следующими параметрами: текущие карты игрока, карты на столе и количество активных (тех, кто не сбросил карты) игроков в игре.
Первый параметр в методе getAction — вероятность выигрыша; второй — сумма всех поставленных денег за прошлые круги торговли и поставленных денег на текущем круге торговли; третий параметр — то количество денег, которое нужно поставить игроку, чтобы уравнять ставки, то есть разность между максимальной ставкой на текущем круге торговли и текущей ставкой игрока; четвертый параметр — размер банка; пятый — номер круга торговли, шестой — минимальное количество денег, которое нужно поставить при рейзе. Здесь мы приняли его как значение максимальной ставки за текущий круг торговли или, если эта ставка равна нулю, размер большого блайнда. После получения действия от игрока (переменная action) выполняем это действие.

Алгоритм таков: если игрок сделал fold, то делаем его неактивным; если call, то сначала проверяем, может ли он поставить деньги, или сразу идет all-in, а потом выполняем требуемое действие, то есть или ставим часть денег, или ставим все, что есть; если raise, то сначала уравниваем ставку игрока до максимальной ставки, а потом ставим оставшуюся часть денег, требуемую для рейза. В общем случае при ставке следует проверять, есть ли требуемая сумма на счету у игрока, если нет, то ставим все оставшиеся деньги (all-in). После проведения ставок всех игроков проверяем, все ли игроки поставили одинаковое количество денег. Если кто-то не поставил, и у него при этом еще есть деньги, проводим повторный круг торговли.

 

Симуляция

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

Добавление игроков для игры в HoldemForm

List<ILogic> playersList=new ArrayList<ILogic>();
playersList.add(frame);
playersList.add(new FoldLogic());
playersList.add(new CautiousLogic());
playersList.add(new CallLogic());
playersList.add(new RationalLogic());
playersList.add(new AggressiveLogic());
playersList.add(new CautiousLogic());
playersList.add(new AggressiveLogic());
playersList.add(new RaiseLogic());

Во второй строчке мы добавляем в качестве игрока текущую форму, это означает, что все методы принятия решений для первого игрока будут вызываться из этой формы. Он, в свою очередь, будет спрашивать пользователя, что делать — fold, call или raise. Для начала проведем игру между человеком и компьютерными игроками, проверим работу правил симуляции — как раздаются карты, как ходят игроки, как происходит смена дилера. Я ошибок не нашел, но они наверняка там есть. Поэтому если ты что-то нашел, или у тебя будут предложения по улучшению программы, пиши мне на timreset@mail.ru

Перечислю некоторые неточности в симуляции, чтобы знать, где можно доделать симулятор:

  1. Фиксированное количество игроков. Можно сделать от двух до десяти.
  2. В HoldemForm не отображается фишка дилера, хотя метод changeDillerPosition при смене дилера вызывается. Нужно добавить на форму возле игрока-дилера пометку.
  3. Фиксированное количество денег в начале игры. Можно сделать изменение этого значения перед игрой.
  4. Только целые значения большого и малого блайнов. Сделать тип float для них. Int был выбран только из-за произ водительности… и то, наверное, это спорный выбор.
  5. Неправильный выбор минимального значения ставки при рейзе. Сделать вычисление минимального рейза по правилам. Ссылка на них есть в статье.
  6. Фиксированное увеличение ставки при рейзе. Сделать значение рейза динамическим — от минимального значения до максимального.
  7. При открытии карт, если есть игроки с одинаковыми картами, выигрывает только первый игрок. Можно это исправить, чтобы выигрыш делился поровну между игроками. Хотя эта ошибка будет повторяться нечасто (все-таки вероятность того, что у игроков будут две одинаковые по силе комбинации, мала), лучше все же реализовать ее по правилам. После того, как был протестирован алгоритм симуляции, запустим несколько десятков раундов в оболочке HoldemConsole.

Игроки там распределены следующим образом:

Добавление игроков для игры в HoldemConsole

playersList.add(new RationalLogic());
playersList.add(new FoldLogic());
playersList.add(new CautiousLogic());
playersList.add(new CallLogic());
playersList.add(new CautiousLogic());
playersList.add(new AggressiveLogic());
playersList.add(new RandomLogic());
playersList.add(new AggressiveLogic());
playersList.add(new RaiseLogic());

50 раундов на моем ноуте выполнялись около 15 минут. В принципе, приемлемое значение. Количество денег после 49 раундов следующее (начальное количество у всех одинаково — $750):

  • 1-й — $580
  • 2-й — $590
  • 3-й — $570
  • 4-й — $2220
  • 5-й — $570
  • 6-й — $680
  • 7-й — $0
  • 8-й — $750
  • 9-й — $790

После второй симуляции:

  • 1-й — $570
  • 2-й — $560
  • 3-й — $580
  • 4-й — $2450
  • 5-й — $590
  • 6-й — $1110
  • 7-й — $0
  • 8-й — $890
  • 9-й — $0
 

Вывод

Теперь самое главное — интерпретация результатов. Выше можно заметить, что самый успешный игрок — CallLogic, за ним следует RaiseLogic (в первом случае) и AggressiveLogic (во втором случае). Почему так? Ведь самый оптимальный алгоритм у нас — это RationalLogic и, по идее, он должен всех обыгрывать? Да, это так, но на данном этапе этот алгоритм не учитывает одной важной составляющей — истории рук, то есть того, как ходят остальные игроки при тех или иных картах и текущих ходах игроков. А ведь история рук позволяет узнать, что значат ходы игроков — блефуют ли они (то есть колируют и рейзят со слабыми руками) или у них действительно сильные карты. Больше информации об игроках, по теореме покера, приводит к лучшим ходам. Так как он это не учиты вает, а основывается только на ставках и размере банка, то получается, что он много рук не разыгрывает, а сбрасывает. В отличие от других игроков — CallLogic, RaiseLogic и AggressiveLogic.

Они же разыгрывают большой диапазон рук, то есть блефуют. Кстати, хотел бы сделать небольшое замечание к своей прошлой статье. В условии определения действия вместо SB нужно использовать minRaise, где minRaise — минимальный размер ставки, который нужно сделать при рейзе. Он равен последней ставке игрока, который ходил до нас. В общем, твори, дорабатывай логику и обязательно пиши нам письма, ведь именно благодаря твоим отзывам — от критичных и даже агрессивных до позитивных и даже благодарных 🙂 — мы приняли решение и дальше развивать тему кодинга покерных ботов. Если все пойдет нормально, то в следующей статье мы реализуем взаимодействие с клиентом покер-рума — считывание информации и нажимание на кнопки.

Метод для определения хода пользователя

public int getAction(float p, float totalBet, float
curBet, float pot, int betting, int minRaise) {
if (curBet == 0) {
btnCall.setText("Check");
} else {
btnCall.setText("Call " +
String.valueOf(curBet));
}
btnCall.setVisible(true);
btnFold.setVisible(true);
btnRiase.setVisible(true);
btnRiase.setText("Raise " +
String.valueOf(curBet + minRaise));
frame.repaint();
action = -1;
while (action == -1) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
frame.repaint();
}
btnCall.setVisible(false);
btnFold.setVisible(false);
btnRiase.setVisible(false);
frame.repaint();
return action;
}

 

Сайты по теме

Правила покера:

Фундаментальная теорема покера: http://poker-wiki.ru/poker/Фундаментальная_теорема_покера
Вики по покеру: http://poker-wiki.ru/

 

Links

Оставить мнение

Check Also

Windows 10 против шифровальщиков. Как устроена защита в обновленной Windows 10

Этой осенью Windows 10 обновилась до версии 1709 с кодовым названием Fall Creators Update …