Poker game logic with a gen_fsm stack

Joel Reymont joelr1@REDACTED
Mon Apr 25 13:08:53 CEST 2005


Folks,

I have clearly distinct stages in my poker game logic. First I need to
wait for enough players to join the game, then I need to pick two players
to post the blind bets, etc. 

I tried to stick all the stages into one big gen_fsm but that approach
proved to be somewhat unwieldy. I considered selective receive suggested
by Joe as well as Ulf's plain_fsm. Then I found Vance's LAPD protocol
stack design at http://www.erlang.org/ml-archive/erlang-questions/200501/
msg00035.html and used it as a base for a design of my own. I hope
someone will find it useful.

I implement each stage as a separate gen_fsm callback module. This keeps
the  modules small, allows me to test them separately and reuse them for
different variants of poker. The super-FSM/dispatcher interacts with the
players and forwards messages to the callback modules. 

The dispatcher is a behavior module that simulates a gen_fsm just like in
Vance's LADP mux gen_fsm. The callback modules then do 

-behaviour(cardgame).

and use cardgame:send_event(...), etc. in place of gen_fsm:send_event(...).

The dispatcher is initialized with a stack of callback modules. For
example, I might pass it the wait_for_players module followed by the
blinds module, the card dealing module, etc. The dispatcher saves a copy
of the module list and then pops the stack to get the first module. It
goes like this:

init([SeatCount, Limit, Modules]) when number(SeatCount);list(Modules) ->
    process_flag(trap_exit, true),
    {Module, Args} = hd(Modules),
    {ok, Game} = game:start(SeatCount, Limit),
    case Module:init([Game|Args]) of
	{ok, State, Data} ->
	    Ctx = #context {
	      game = Game,
	      modules = Modules,
	      stack = Modules,
	      state = State,
	      data = Data
	     },
	    {ok, state, Ctx};
    ...

Callback modules should finish with:

1. {stop, {normal, exit}, ...} 

Will cause the dispatcher to exit.

1. {stop, {normal, restart}, ...}

Will cause the dispatcher to restart by resetting the module stack to the
original list, popping the head module and initializing it.

2. {stop, {normal, Result}, ...};

This will tell the dispatcher gen_fsm to take Result and use it to kick
off the next module in the stack.

As per the docs gen_fsm will consider any reason other than "normal" an
error. The dispatcher captures the {stop, Reason, ...} of the callback
module and processes it like this:

state(Event, Ctx) ->
    {Module, _} = hd(Ctx#context.stack),
    State = Ctx#context.state,
    case Module:State(Event, Ctx#context.data) of
	{next_state, NextState, NewData} ->
	    NewCtx = Ctx#context {
		       state = NextState,
		       data = NewData
		      },
	    {next_state, state, NewCtx};

	{next_state, NextState, NewData, Timeout} ->
	    NewCtx = Ctx#context {
		       state = NextState,
		       data = NewData
		      },
	    {next_state, state, NewCtx, Timeout};

	{stop, Reason, NewData} ->
	    stop(Ctx, Reason, NewData);

	Other ->
	    Other
    end.

and then like this:

%% stop card game

stop(Ctx, {normal, exit}, Data) ->    
    stop(Ctx, normal, Data);

%% terminate current module
%% and restart at the top

stop(Ctx, {normal, restart}, Data) ->    
    {Module, _} = hd(Ctx#context.stack),
    State = Ctx#context.state,
    Module:terminate({normal, restart}, State, Data),
    io:format("cardgame:stop/restart: Module=~w, State=~w~n",
	     [Module, State]),
    start_next_module(Ctx, Ctx#context.modules, nil);

%% terminate current module 
%% and start the next one

stop(Ctx, {normal, Result}, Data) ->    
    {Module, _} = hd(Ctx#context.stack),
    State = Ctx#context.state,
    Module:terminate({normal, restart}, State, Data),
    io:format("cardgame:stop/continue: got ~w~n", [Result]),
    start_next_module(Ctx, Ctx#context.stack, Result);

%% stop cardgame

stop(Ctx, Reason, Data) ->
    io:format("cardgame:stop: stopping with reason ~w~n", [Reason]),
    NewCtx = Ctx#context {
	       data = Data
	      },
    {stop, Reason, NewCtx}.

The end result of the blinds gen_fsm callback module would be a tuple of
small blind, big blind and button. These woudl be used in the next module
in the stack. To pass this information to the next module in the stack I
use a "kick off" event like this when starting the first module:

start(SeatCount, Limit, Modules) when number(SeatCount);list(Modules) ->
    {X, Pid} = gen_fsm:start(?MODULE, [SeatCount, Limit, Modules], []),
    kick_off(Pid),
    {X, Pid}.

and I just send the event to self() (the dispatcher) when popping the
next module off the stack. The event is then forwarded to the module that
I just popped. 

Apart from implementing the game logic as a gen_fsm I also implemented a
game gen_server. This server manages table seats, adds players to a game,
etc. etc. The gen_server does the heavy lifting and is passed as the
first argument when each module is initialized.

init([SeatCount, Limit, Modules]) when number(SeatCount);list(Modules) ->
    process_flag(trap_exit, true),
    {Module, Args} = hd(Modules),
    {ok, Game} = game:start(SeatCount, Limit),
    case Module:init([Game|Args]) of

To summarize, I can now mix and match various small and well-tested poker
logic gen_fsms to implement different poker variants. If Game X does not
need the blinds then I just don't pass the module to the dispatcher.

I think the same approach can be used to implement sub-FSMs by replacing
the module at the top of the stack with a different gen_fsm when
appropriate. The "current" callback module could change depending on
where the player is when implementing an adventure game, for example.

Your comments and suggestions are welcome.

    Thanks, Joel

-- 
http://wagerlabs.com/tech





More information about the erlang-questions mailing list