Poker bots and nested state machines /Structured Network Programming reloaded/
Joel Reymont
joelr1@REDACTED
Sun Jan 1 17:27:21 CET 2006
I went through Ulf Wiger's EUC presentation on Structured Network
Programming and realized that I had to tackle something similar in my
recent project. I would like to share my design and solicit feedback.
I find my approach very easy to implement and use and Uffe's a bit
complicated. I would appreciate comments on the merits of the our
approaches among other things.
The goal of my project is to be able to thoroughly test a poker
server using poker bots. Each poker bot is to be scripted and
excercise different parts of the server by talking the poker protocol
consisting of 150+ binary messages. The poker server itself is
written in C++ and runs on Windows.
Easy scripting is an essential requirement since customer's QA techs
are not programmers but need to be able to write the bots. Another
key requirement is to be able to launch at least 4,000 poker bots
from a single machine.
It's worth noting that I started with Haskell and by the time I gave
up 10-11 weeks later I was still troubleshooting core issues in my
application. It took me 1 week to rewrite the app in Erlang. It's the
end of that week and I'm already way past the state of the Haskell
version. The Erlang code is about 1/3 of the Haskell code. I used the
same approach to event handling in both apps.
Each poker bot is a state machine. I managed to bang out wrappers to
the 150+ packets rather quickly and elegantly (see http://
www.erlang.org/ml-archive/erlang-questions/200512/msg00279.html) and
it took me about 2 days. Implementing the state machine proved to be
more troublesome.
My initial thinking was to give the customer a library of packet
wrappers and the means to send and receive them. This failed
miserably. Writing a robust poker bot state machine proved too
complicated for the QA techs. I was then asked to provide a library
of "code snippets" or LEGO blocks to assemble a poker bot with.
This is a sample poker bot script that joins the lobby. Each poker
bot script is a module with a single exported function. The module
name is meant to be given to the controller which calls script/0 and
passes it the user id, password and any other required arguments.
-module(gotolobby).
-export([script/3]).
-include("records.hrl").
script(Bot, _, connected)
when is_record(Bot, bot) ->
%% common script initialization
Bot1 = snippets:start_script(Bot),
Bot2 = snippets:go_to_lobby(Bot1, [...]),
%% set trace level
Bot3 = bot:traceat(Bot2, 100),
bot:trace(Bot3, 10, "Kicking off"),
%% mark event as processed
{eat, Bot3};
script(Bot, _, {joined_table, 0}) ->
bot:trace(Bot, 10, "We are in the lobby"),
timer:sleep(3000), % milliseconds
%% stop the bot
stop;
script(Bot, _, Event) ->
bot:trace(Bot, 100, "Skipping: ~w", [Event]),
{skip, Bot}.
I control the event loop and dispatch events by calling the script
fun. Each script fun is a dispatcher or state machine with 3 possible
return values: {eat, State}, {skip, State} and stop. The controller
keeps a stack of dispatchers and events are propagated from the top
of the stack to the bottom.
If a dispatcher in the stack returns {skip, State} then the event is
propagated to the next dispatcher and the new bot state is assumed to
be State. On {eat, State} the dispatch sequence is complete and the
controller goes on to the next iteration of the event loop. On stop
the bot is stopped. If no dispatcher has eaten the event by the time
the bottom of the stack is reached then the controller throws out the
event.
Dispatchers can be pushed on to the stack and popped from it. The
first dispatcher that's pushed onto the stack is the bot script
itself. It always stays there. I do not try to guard against extra
stack pops, my assumption is that a qualified person will be writing
the snippets and will match each push with a pop. Bot writers do not
need to use push/pop at all.
%%% Push a handler. Desc describes the handler (string, atom, etc.)
%%% and is only used for debugging.
push(Bot, Desc, Fun, Args)
when is_record(Bot, bot),
is_function(Fun),
is_list(Args) ->
Dispatchers = Bot#bot.dispatchers,
Bot1 = Bot#bot {
dispatchers = [{Desc, Fun, Args}|Dispatchers]
},
Bot1.
%%% Pop a handler
pop(Bot)
when is_record(Bot, bot) ->
%% drop the head
[_|Dispatchers] = Bot#bot.dispatchers,
Bot1 = Bot#bot {
dispatchers = Dispatchers
},
Bot1.
The reusable blocks of code (snippets, LEGO blocks) are implemented
as dispatchers that are pushed onto the stack. By pushing snippets to
the top of the stack they get to look at each event before the bot
and decide whether to propagate the event or eat it.
The controller event loop looks like this:
%%% Dispatch event
run(_, {keep_going, Bot})
when is_record(Bot, bot) ->
receive
{tcp, _, <<Packet/binary>>} ->
Event = unpickle(Bot, Packet),
run(Bot, handle(Bot, Event));
{script, Event} ->
run(Bot, handle(Bot, Event));
Any ->
run(Bot, handle(Bot, Any))
end;
%%% Handle event
handle(Bot, Event)
when is_record(Bot, bot) ->
%% let dispatchers in the stack take a look
handle(Bot, Bot#bot.dispatchers, Event).
handle(Bot, [], _)
when is_record(Bot, bot) ->
%% bottom of the dispatcher stack reached,
%% throw out the event and keep going
{keep_going, Bot};
handle(Bot, [{_, Fun, Args}|Rest], Event)
when is_record(Bot, bot) ->
case Fun(Bot, Args, Event) of
{skip, Bot1} ->
handle(Bot1, Rest, Event);
{eat, Bot1 } ->
{keep_going, Bot1};
stop ->
stop;
Other ->
trace(Bot, 85, "handle: Unknown event: ~p~n", [Other]),
erlang:error(unknown_event)
end.
Consider this section of the example bot code:
script(Bot, _, connected)
when is_record(Bot, bot) ->
Bot1 = snippets:start_script(Bot),
Bot2 = snippets:go_to_lobby(Bot1, [...]),
Bot3 = bot:traceat(Bot2, 100),
bot:trace(Bot3, 10, "Kicking off"),
{eat, Bot3};
snippets:start_script/1 above pushes another handler onto the stack
that handles common events that are used in all poker bots such as
updating the number of players at the table. snippets:go_to_lobby/2
is a good snippet to look at.
%%% Go to lobby. The lobby is table# 0
go_to_lobby(Bot, AffId) ->
go_to_table(Bot, 0, AffId).
%%% Join table
go_to_table(Bot, 0, AffId)
when is_record(Bot, bot),
is_list(AffId) ->
Bot1 = bot:push(Bot, "go_to_table",
fun go_to_table/3, [0, AffId]),
Cmd = #cl_connect_game {
...
},
bot:send(Bot1, lobby, Cmd),
Bot1;
The fun above is meant to be invoked from poker bot scripts and
pushes the go_to_table/3 dispatcher to the top of the stack. It also
sends the "connect to table" packet to the poker server.
go_to_table(Bot, Table, AffId)
when is_record(Bot, bot),
is_number(Table),
is_list(AffId) ->
Bot1 = bot:push(Bot, "go_to_table",
fun go_to_table/3, [Table, AffId]),
bot:reconnect(Bot1, Table);
Joining a particular table is more complicated since separate
connections to the poker server need to be maintained for the lobby
and for each each table. Not my design, so just take it for
granted ;-). bot:reconnect/2 closes the existing table connection and
opens a new one.
go_to_table(Bot, [Table, AffId], table_connected) ->
Cmd = #cl_connect_game {
...
},
bot:send(Bot, table, Cmd),
{eat, Bot};
The table_connected event is posted once the table connection has
been established, i.e. the custom SSL handshake has been completed.
More on that below. The lobby connection goes through the encryption
handshake when the bot starts so we only need to send the "connect to
game" packet and there's no waiting for the table_connected event.
go_to_table(Bot, [Table, _], #srv_connect_game_ok{}) ->
bot:post(Bot, {joined_table, Table}),
Bot1 = bot:pop(Bot),
{eat, Bot1};
The table is considered connected when "srv_connect_game_ok" is
received. That's when I post the {joined_table, Table} event and pop
myself from the dispatcher stack.
Take a look at the sample script at the beginning of the message. My
nested "go to lobby" state machine is completely transparent to the
user. It even looks like a synchronous call /Bot2 =
snippets:go_to_lobby(Bot1, [...]/.
go_to_table(Bot, _, _) ->
{skip, Bot}.
If I don't recognize the event then I just skip it which lets
dispatchers further down in the stack take a look at it. This is a
crucial ability. Two server connections (lobby and table) can be open
at the same time so I can be receiving lobby messages while trying to
connect to a table.
Opening a new table connection requires going through a custom SSL
handshake with the server. This eventually boils down to the
following piece of code
connect(Bot, Args) ->
%% connect to the script server to get a client handle
UtilSock = Bot#bot.util_sock,
?match(ok, gen_udp:send(UtilSock, ?SRV_IP, ?SRV_PORT, <<?
SRV_CONNECT>>)),
%% push handler
Bot1 = bot:push(Bot, "handshake", fun handshake/3, Args),
%% start script server connection timer
bot:start_timer(Bot1, script_server_connect, 5000).
I'm using a separate UDP server to keep track of SSL descriptors
since I could not figure out a way to do this with the built-in
Erlang SSL. I need to capture each packet in the standard SSL
handshake and add my own header before sending it out. This lets me
selectively use SSL encryption on the packets that require it and
lessens the load on the server and network connections.
Notice the bot:push/4 above. The state machine (dispatcher) stack is
3-deep at this point. The encryption handshake state machine is at
the top, followed by the "connect to game" state machine and the
poker bot state machine (gotolobby:script/3).
Finally, I start a timer above to make sure that the encryption state
machine does not run forever.
Thanks, Joel
--
http://wagerlabs.com/
More information about the erlang-questions
mailing list