Здравствуй.
Меня очень привлекает спортивное программирование, а ещё я люблю покер. Поэтому я решил убить сразу двух зайцев, запустив турнир покерных ботов.
В отличие от других подобных мероприятий, программа участника может быть реализована в виде веб-сервиса, с которым «крупье» будет общаться через HTTP.
Изначальная идея проекта принадлежит моему другу (он не с Хабра). Первый раз она прозвучала примерно так:
Я считаю, что нет ничего дурного в том, чтобы боты играли в покер-румах. Хотя, может быть, честнее было бы организовать специальный покерный клуб, где роботы играли бы только друг с другом. Вот я бы с удовольствием написал такого бота. А ты?
Не помню, что я ответил. Мне интереснее было именно сделать такой сервис. Challenge accepted ;)
Движок
На забавной картинке чуть выше обозначены основные компоненты системы и их связи друг с другом. Отдельного комментария требует часть, связанная с покерным движком. Подходящий готовый движок мне найти не удалось, поэтому я написал небольшой (~1000 строк) python-пакет, который реализует логику покерного крупье и общения с ботами. К пакету прилагается простой unix-style интерфейс для генерации покерных раздач и их непосредственного проведения.
Движок ничего «не знает» про логику турнира, базу данных пользователей и т.п. Он выступает исключительно в роли «крупье» и возвращает результат своей работы в виде XML, содержащего конечное состояние данной раздачи.
Такая реализация даёт возможность использовать один и тот же движок как внутри покер-рума, так и на компьютере пользователя во время разработки/тестирования бота (то, что я называю SDK). Это гарантирует, что бот будет находится в одних и тех же условиях на тест-лабе пользователя и во время реальной турнирной игры, а также сокращает дублирование кода.
Исходный код движка опубликован на Гитхаб (лицензия MIT). Для использования в виде SDK под Windows собирается (с помощью py2exe) exe-шник с включенным python-интерпретатором. Под Linux и Mac SDK поставляется в исходных текстах и использует интерпретатор, установленный (по умолчанию) в операционной системе.
В текущей реализации движок поддерживает только одну версию покера — «Texas hold'em» с фиксированными ставками. Судя по википедии, это одна из самых популярных покерных игр в мире.
Я не стал делать SDK в виде веб-сервиса, чтобы пользователь мог запускать систему полностью локально. Таким образом можно написать сотни регрессионных тестов на бота и прогонять их очень быстро, хоть после каждого изменения. Причём для реализации такого сценария не нужно патчить исходники или писать собственный интерфейс к движку — всё работает из коробки «by design».
Сами тесты пишутся легко и приятно благодаря stateless API.
API
Если у меня есть выбор, я почти всегда делаю stateless API. В данном случае этот выбор был не столь очевиден, т.к. необходимо на каждом ходу передавать боту всю историю раздачи (это минимально-необходимая информация для принятия решения).
В текущий момент эта информация (я называю её «состоянием» раздачи) передаётся боту в формате XML с помощью HTTP POST запроса. В худшем случае этот XML может весить до 4КБ. Много это или мало — судить вам, но это та цена, которую я готов заплатить ради упрощения процесса разработки бота и снижения порога на вход для новых участников.
Разработка является действительно довольно несложной — участнику нужно просто написать программу, которая парсит некоторый XML, анализирует описанную в нём ситуацию и принимает определённое решение (пасовать, поддержать, повысить, пойти ва-банк и т.д.).
Схему XML-документа пришлось разработать самостоятельно.
Пример
<?xml version="1.0"?>
<game>
<table button="2">
<player sit="0" name="vbo" stack="80" in_stack="100"/>
<player sit="2" name="lenny" stack="80" in_stack="100"/>
<player sit="5" name="psycho" stack="90" in_stack="100"/>
</table>
<posts>
<post amount="10" player="psycho" type="small_blind"/>
<post amount="20" player="vbo" type="big_blind"/>
</posts>
<betting>
<round name="preflop">
<action amount="20" player="lenny" type="call"/>
<action amount="10" player="psycho" type="fold"/>
<action amount="20" player="vbo" type="check"/>
</round>
<round name="flop">
<action amount="0" player="vbo" type="check"/>
<action amount="0" player="lenny" type="check"/>
</round>
<round name="turn"/>
<round name="river"/>
</betting>
<community>
<card rank="Q" suit="H"/>
<card rank="A" suit="H"/>
<card rank="7" suit="S"/>
<card rank="T" suit="S"/>
</community>
</game>
Данная схема мне кажется довольно простой для понимания человеком (в том числе за счёт лёгкой избыточности, например, //player/@stack) и удобной для анализа роботом. В дополнение я планирую в ближайшее время сделать JSON-версию схемы — кому как нравится.
Для внутреннего представления раздачи движок использует расширенную версию схемы, где присутствует колода, для каждого бота указан его транспорт (URL и т.д.), карманные карты и т.д. Конечное состояние раздачи также включает в себя ноду «showdown», которая даёт понять, какой игрок выиграл и какие у него были карты (если ему пришлось вскрываться). В текущей реализации, конечное состояние раздачи видит только автор бота, когда просматривает статистику игр на сайте покер-рума.
Вся остальная информация передаётся (в случае HTTP-транспорта) в отдельных полях POST-запроса. Это сделано для того, чтобы можно было написать простого бота вообще без парсинга XML. К тому-же, такая реализация делает более удобным написание тестов — тестовый бот может вообще не парсить XML и действовать, например, рандомно или следуя простейшему алгоритму (в духе, «check/call any» или «bet/raise good cards»).
Для этого «крупье» также передаёт боту список действий, которые вообще возможно совершить в данной ситуации. Если бот совершает действие, которого нет в этом списке — его ответ рассматривается, как fold, а текст ответа (обычно, это stack trace исключения) записывается в логи на стороне покер-рума. На данный момент, я сам читаю эти логи и пишу участнику email, если бот ломается. Разумеется, в самое ближайшее время данный процесс будет автоматизирован ;)
Турнир
В состязаниях по спортивному программированию меня всегда очень расстраивает следующее:
- Они не вовремя начинаются.
Например, в последние выходные перед запуском большого проекта на работе. - Они заканчиваются в самый интересный момент.
Например, когда я придумал гениальную идею, которая сделает мою программу ну совсем потрясающей.
Поэтому, я с самого начала решил делать основное состязание «бесконечным». Представьте себе реальный он-лайн покерный клуб для людей. Туда можно прийти в любой момент, закинуть деньжат, сесть за стол и отлично провести время. Можно совершенствовать свои навыки и садится за столы с более высокими ставками, а можно просто развлекаться игрой «на фантики» в свободное время.
Разумеется, сразу делать нечто подобное, только для роботов, я не стал — было бы очень обидно, например, после шести месяцев разработки осознать, что у сервиса слишком высокий порог на вход и я играю там в гордом одиночестве.
Поэтому для начала я запустил нечто вроде sandbox'а — турнира покерных ботов, к которому можно присоединиться в любой момент и получить представление об уровне своей программы на фоне других участников.
В начале каждого дня покер-рум составляет рейтинг участников турнира, на основе суммы «денег», которые им удалось выиграть (или пришлось проиграть) за предыдущие сутки. После этого баланс каждого пользователя устанавливается в ноль и соревнование начинается с начала. В рамках соревнования покер-рум старается обеспечить, чтобы все боты постоянно играли, причём, по возможности, с различными соперниками. В текущей реализации для этого используются следующие правила:
- Если бот свободен, он сразу садится за первый попавшийся стол. При этом приоритет отдаётся столам, за которыми бот давно (или никогда) не сидел и столам с наименьшим количеством игроков.
- Бот встаёт из-за стола, как только проиграется или сыграет за ним более N раздач. Последнее правило применяется, чтобы равные соперники не играли друг с другом до бесконечности.
- Покер-рум поддерживает «оптимальное» число открытых столов — чтобы всем хватало и ещё оставались места, куда можно пересесть после факапа. Такое число линейно зависит от кол-ва активных игроков.
Алгоритмы, реализующие эти правила (на схеме это «Tournament worker») работают параллельно, асинхронно и совершенно независимо. Таким образом обеспечивается равномерность нагрузки на машину и постоянство кол-ва раздач на единицу времени. Также такая архитектура делает систему более масштабируемой.
Worker'ы, как и сайт, реализованы на PHP (у меня здесь больше опыта, чем в python'е) и работают с PostgreSQL базой данных. Для обеспечения параллельной обработки очереди столов используется pg_try_advisory_lock при выборке кандидатов. Не ручаюсь, что этот способ самый верный, т.ч. буду рад обсудить данный вопрос в комментариях.
Планы
Я пока не вижу смысла выкладывать исходники покер-рума в открытый доступ — код получился не слишком сложным и слишком привязанным к проекту. Вместо этого я сосредоточусь на улучшении «движка» (например, добавлю поддержку безлимитного покера), реализации API для «обучения» ботов (т.е. для получения результатов прошедших игр), а так же на разработке нового типа игры, в котором участники через отдельное API смогут сами управлять рассадкой за столы и выходить из-за стола по мере необходимости. В общем, смогут вести себя почти, как люди.
Также, я планирую сократить порог на вход для начинающих (или просто далёких от веб) программистов — дать возможность разрабатывать бота прямо в браузере на языке вроде CoffeeScript и тестировать его через специальный веб-интерфейс. Было бы просто замечательно сделать также некоторый визуальный редактор алгоритма. Конечно, он не обязательно должен давать абсолютные возможности, но это позволит пользоваться сервисом даже не-программистам, что очень полезно. На данный момент у меня нет идей, как сделать такой редактор, т.ч. если у вас они есть, пожалуйста, отпишитесь в комментариях.
Проект делается «just for fun», т.ч. сроки реализации всех этих «корованов» публиковать бесполезно. Судя по git log текущую реализацию я начал делать в начале февраля. Из этого можно сделать выводы о моей скорости.
Если читателю интересно поиграть — бета версия покер-рума работает здесь. А на youtube можно посмотреть короткое видео о том, как сделать простейшего бота на PHP, протестировать его и зарегистрировать в турнире.
Автор: vbo