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