I would like to use erlang to create an online, web-based game. I thought it would make sense to start with something easy, so I chose tic-tac-toe (which seems like a popular choice here). I am implementing users, players and the rules engine as finite state machines using the OTP gen_fsm. The first step I decided to take is to get all the FSM's in order. I have one for users (i.e., someone logged in to the web site), one for players (if we decide to allow users having more than 1 player), and one for a game. I would like to know if I have some obviously poor choices in my implementation of the finite state machines. Any other un-Erlang like choices should also be pointed out.
The User
It is now mostly a placeholder. I am planning on using an erlang web framework, so the Cowboy application will handle the creation of the User processes. The module is called r_user
(because I believe there already is a user.erl module that is part of the base erlang distribution.
-module(r_user).
-behaviour(gen_server).
-export([start/1, start_link/1, init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-export([get_name/1, log_out/1]).
-export([test/0]).
-record(?MODULE, {
name = []
}).
new([Name]) ->
#?MODULE {
name = Name
}.
start(Name) ->
gen_server:start(?MODULE, [Name], []).
start_link(Name) ->
gen_server:start_link(?MODULE, [Name], []).
init([Name]) ->
process_flag(trap_exit, true),
{ok, new([Name])}.
handle_call(terminate, _From, State) ->
{stop, normal, ok, State};
handle_call(getname, _Pid, State) ->
{reply, State#?MODULE.name, State};
handle_call(Event, From, State) ->
io:format("Received unknown call ~p from ~p, state = ~p~n", [Event, From, State]),
{noreply, State}.
handle_cast(Request, State) ->
io:format("Received unknown cast ~p, state = ~p~n", [Request, State]),
{noreply, State}.
handle_info(Info, State) ->
io:format("Received unknown info ~p, state = ~p~n", [Info, State]),
{noreply, State}.
terminate(normal, State) ->
io:format("Terminated normally, state = ~p~n", [State]);
terminate(Reason, State) ->
io:format("Terminated, reason = ~p, state = ~p~n", [Reason, State]).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
get_name(Pid) ->
gen_server:call(Pid, getname).
log_out(Pid) ->
gen_server:call(Pid, terminate).
The Player
This is a gen_fsm that uses asynchronous events to manage its state. The only request that is synchronous is the get_name request. There are only 2 states, waiting_for_game
and in_game
. It currently doesn't relay anything back to the r_user
, but that should be fairly trivial to implement.
-module(player).
-behaviour(gen_fsm).
%%% Public API
%% Behavior functions
-export([start/1, start_link/1]).
%% gen_fsm callbacks
-export([
% Behaviour callbacks
init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4,
% State functions
waiting_for_game/2, in_game/2, in_game/3
]).
%% Utility functions
-export([
get_name/1, quit/1, game_quit/1, make_move/3,
test/0
]).
%%% State Record
-record(?MODULE, {
user,
game = none,
marker = none,
game_state = none
}).
%%% Public API
%% Behavior functions
start([User]) ->
gen_fsm:start(?MODULE, [User], []).
start_link([User]) ->
gen_fsm:start_link(?MODULE, [User], []).
%% Behavior callbacks
init([User]) ->
process_flag(trap_exit, true),
{ok, waiting_for_game, new([User])}.
handle_event(stop, _StateName, Data) ->
{stop, normal, Data};
handle_event(Event, StateName, Data) ->
unexpected(Event, StateName, Data),
{next_state, StateName, Data}.
handle_sync_event(getname, _From, StateName, Data) ->
% trace('User', Data#?MODULE.user, StateName),
{reply, r_user:get_name(Data#?MODULE.user), StateName, Data};
handle_sync_event(Event, _From, StateName, Data) ->
unexpected(Event, StateName, Data),
{next_state, StateName, Data}.
handle_info(Info, StateName, Data) ->
unexpected(Info, StateName, Data),
{next_state, StateName, Data}.
code_change(_OldVsn, StateName, Data, _Extra) ->
{ok, StateName, Data}.
terminate(normal, StateName, State=#?MODULE{}) ->
io:format("Player terminated normally, state name = ~p, ~p~n", [StateName, State]);
terminate(Reason, StateName, State) ->
io:format("Player terminated abnormally, reason = ~p, state name = ~p, state = ~p~n", [Reason, StateName, State]).
%% State functions
waiting_for_game({gamejoined, Game, Marker}, Data) ->
io:format("Player ~p: joined game ~p/~p~n", [r_user:get_name(Data#?MODULE.user), game:get_number(Game), Game]),
NewState = Data#?MODULE{game = Game, marker = Marker},
{next_state, in_game, NewState};
waiting_for_game(Event, State) ->
io:format("waiting_for_game: received unknown event ~p, state = ~p~n", [Event, State]),
{next_state, waiting_for_game, State}.
in_game(gamequit, Data) ->
Game = Data#?MODULE.game,
io:format("Player ~p: quit game ~p~n", [r_user:get_name(Data#?MODULE.user), Game]),
{next_state, waiting_for_game, Data#?MODULE{game = none}};
in_game({gameover, Winner}, Data) ->
Game = Data#?MODULE.game,
Result = case Winner == self() of true-> won; _ -> lost end,
io:format("~p: Game ~p over, I ~p~n", [r_user:get_name(Data#?MODULE.user), Game, Result]),
{next_state, waiting_for_game, Data#?MODULE{game = none, game_state = none, marker = none}};
in_game({yourmove, GameState}, State) ->
io:format("in_game: my move, game state ~p, state = ~p~n", [GameState, State]),
{next_state, in_game, State#?MODULE{game_state = GameState}};
in_game({makemove, Row, Column}, State) ->
Game = State#?MODULE.game,
io:format("in_game: setting row ~p, column ~p~n", [Row, Column]),
gen_fsm:send_event(Game, {playeraction, self(), {Row, Column}}), {next_state, in_game, State};
in_game(Event, State) ->
io:format("in_game: received unknown event ~p, state = ~p~n", [Event, State]), {next_state, in_game, State}.
in_game(Event, From, State) ->
io:format("in_game: received unknown event ~p from ~p, state = ~p~n", [Event, From, State]),
{next_state, in_game, State}.
%% Utility functions
quit(Player) ->
gen_fsm:send_all_state_event(Player, stop).
get_name(Player) ->
gen_fsm:sync_send_all_state_event(Player, getname).
game_quit(Player) ->
gen_fsm:send_event(Player, gamequit).
make_move(Player, Row, Column) ->
gen_fsm:send_event(Player, {makemove, Row, Column}).
%%% Private Functions
%% Helper Functions
trace(Msg, Args, State) ->
io:format("~s: ~p, state = ~p~n", [Msg, Args, State]).
unexpected(Msg, State, Data) ->
io:format("~p received unknown event ~p while in state ~p, Data = ~p~n", [self(), Msg, State, Data]).
%% Initialization
new([User]) ->
#?MODULE {
user = User,
game = none,
marker = none,
game_state = none
}.
The Game
This is as simple as I could make it. It starts in the waiting_for_players
state. Once the second player joins, it goes in the in_progress
mode. Everything is done asynchronously here as well. Once the game starts, it randomly picks the player who goes first, and notifies the player that it's their move. If someone not the active player makes a move, it prints an error message to the screen (could also send an alert to the player, but doesn't for now). If the active player tries to mark a square that isn't empty, an error message also gets printed. After a legal move is made, it updates the board state, and checks for a winner. If there is no winner, it will move to the next player and signal it's their move. If there is a winner, it signals gameover
to each player and the gen_fsm
shuts down.
I haven't implemented yet the case where the board is full and no one won, but that shouldn't be hard to do.
-module(game).
-behaviour(gen_fsm).
%%% Public API
%% Behavior functions
-export([start/1, start_link/1]).
%% gen_fsm callbacks
-export([
% Behaviour callbacks
init/1, handle_event/3, handle_sync_event/4, handle_info/3, terminate/3, code_change/4,
% State functions
waiting_for_players/2, in_progress/2, in_progress/3
]).
%% Utility functions
-export([get_number/1, add_player/2, get_active_player/1,
shut_down/1,
test/0
]).
%%% State Record
-record(?MODULE, {
number,
markers,
players = [],
active_player,
game_state
}).
%%% Public API
%% Behavior functions
start([Number]) ->
gen_fsm:start(?MODULE, [Number], []).
start_link([Number]) ->
gen_fsm:start_link(?MODULE, [Number], []).
%% Behavior callbacks
init([Number]) ->
process_flag(trap_exit, true),
{ok, waiting_for_players, new([Number])}.
handle_event(stop, _StateName, Data) ->
lists:foreach(fun player:game_quit/1, Data#?MODULE.players),
{stop, normal, Data};
handle_event(Event, StateName, Data) ->
unexpected(Event, StateName, Data),
{next_state, StateName, Data}.
handle_sync_event(getnumber, _From, StateName, Data) ->
{reply, Data#?MODULE.number, StateName, Data};
handle_sync_event(getactiveplayer, _From, StateName, Data) ->
{reply, Data#?MODULE.active_player, StateName, Data};
handle_sync_event(Event, _From, StateName, Data) ->
unexpected(Event, StateName, Data),
{next_state, StateName, Data}.
handle_info(Info, StateName, Data) ->
unexpected(Info, StateName, Data),
{next_state, StateName, Data}.
code_change(_OldVsn, StateName, Data, _Extra) ->
{ok, StateName, Data}.
terminate(normal, StateName, State=#?MODULE{}) ->
io:format("Game terminated normally, state name = ~p, ~p~n", [StateName, State]);
terminate(Reason, StateName, State) ->
io:format("Game terminated abnormally, reason = ~p, state name = ~p, state = ~p~n", [Reason, StateName, State]).
%% State functions
waiting_for_players({addplayer, Player}, Data) ->
Game = Data#?MODULE.number,
[Marker | Rest] = Data#?MODULE.markers,
io:format("Game ~p, adding player ~p~n", [Game, player:get_name(Player)]),
gen_fsm:send_event(Player, {gamejoined, self(), Marker}),
NewPlayers = [Player | Data#?MODULE.players],
PlayerCount = length(NewPlayers),
case PlayerCount of
1 -> {next_state, waiting_for_players, Data#?MODULE{players = NewPlayers, active_player = none, game_state = none, markers = Rest}};
2 ->
NewState = new_state(),
gen_fsm:send_event(Player, {yourmove, NewState}),
{next_state, in_progress, Data#?MODULE{players = NewPlayers, active_player = random:uniform(2), game_state = NewState, markers = none}}
end;
waiting_for_players(Event, State) ->
io:format("waiting_for_tournament: received unknown event ~p, state = ~p~n", [Event, State]), {next_state, waiting_for_tournament, State}.
in_progress({playeraction, Player, Action}, State) ->
io:format("in_progress: Player ~p did ~p~n", [Player, Action]),
{Row, Column} = Action,
Index = Column + (Row - 1) * 3,
CurrentState = State#?MODULE.game_state,
Current = lists:nth(Index, CurrentState),
P = State#?MODULE.active_player,
Players = State#?MODULE.players,
ActivePlayer = lists:nth(P, Players),
case Current == empty of
true ->
case Player == ActivePlayer of
true ->
NewState = lists:sublist(CurrentState, Index - 1) ++ [Player | lists:nthtail(Index, CurrentState)],
io:format("New game state:~n", []),
lists:foreach(fun (X) -> io:format(" ~p~n", [get_row(X, NewState)]) end, lists:seq(1, 3)),
Winner = check_winner(NewState),
case Winner == none of
true ->
NewActivePlayer = (P rem length(Players)) + 1,
gen_fsm:send_event(lists:nth(NewActivePlayer, Players), {yourmove, NewState}),
{next_state, in_progress, State#?MODULE{game_state = NewState, active_player = NewActivePlayer}};
false ->
io:format("Winner: ~p~n", [Winner]),
lists:foreach(fun (X) -> gen_fsm:send_event(X, {gameover, Winner}) end, Players),
{stop, normal, State#?MODULE{game_state = NewState}}
end;
_ ->
io:format("ERROR: ~p is not the active player, ~p is~n", [player:get_name(Player), player:get_name(ActivePlayer)]), {next_state, in_progress, State}
end;
_ ->
io:format("ERROR: Square ~p/~p is not empty~n", [Row, Column]), {next_state, in_progress, State}
end;
in_progress(Event, State) ->
io:format("in_progress: received unknown event ~p, state = ~p~n", [Event, State]), {next_state, in_progress, State}.
in_progress(Event, From, State) ->
io:format("in_progress: received unknown event ~p from ~p, state = ~p~n", [Event, From, State]), {next_state, in_progress, State}.
%% Utility functions
get_number(Game) ->
gen_fsm:sync_send_all_state_event(Game, getnumber).
get_active_player(Game) ->
gen_fsm:sync_send_all_state_event(Game, getactiveplayer).
add_player(Game, Player) ->
gen_fsm:send_event(Game, {addplayer, Player}).
shut_down(Game) ->
gen_fsm:send_all_state_event(Game, stop).
%%% Private Functions
new_state() ->
lists:foldl(fun(_X, Acc) -> [empty | Acc] end, [], lists:seq(1, 9)).
get_row(N, State) when N >= 1; N =< 3 ->
Offset = (N - 1) * 3,
[lists:nth(1 + Offset, State), lists:nth(2 + Offset, State), lists:nth(3 + Offset, State)];
get_row(_N, _State) ->
{error, invalid_row}.
get_col(N, State) when N >= 1, N =< 3 ->
[lists:nth(N, State), lists:nth(N + 3, State), lists:nth(N + 6, State)];
get_col(_N, _State) ->
{error, invalid_col}.
get_diag(1, State) ->
[lists:nth(1, State), lists:nth(5, State), lists:nth(9, State)];
get_diag(2, State) ->
[lists:nth(3, State), lists:nth(5, State), lists:nth(7, State)];
get_diag(_N, _State) ->
{error, invalid_diag}.
is_winner([Marker, Marker, Marker]) when Marker /= empty ->
Marker;
is_winner(_) ->
false.
check_winner(State) ->
CountEmpty = length(lists:filter(fun (X) -> X == empty end, State)),
if
CountEmpty >= 8 ->
none;
true ->
check_winner(State, row, 3)
end.
check_winner(State, row, N) when N > 1 ->
Row = get_row(N, State),
case is_winner(Row) of
false -> check_winner(State, row, N - 1);
Marker -> Marker
end;
check_winner(State, row, 1) ->
Row = get_row(1, State),
case is_winner(Row) of
false -> check_winner(State, col, 3);
Marker -> Marker
end;
check_winner(State, col, N) when N > 1 ->
Col = get_col(N, State),
case is_winner(Col) of
false -> check_winner(State, col, N - 1);
Marker -> Marker
end;
check_winner(State, col, 1) ->
Col = get_col(1, State),
case is_winner(Col) of
false -> check_winner(State, diag, 2);
Marker -> Marker
end;
check_winner(State, diag, 2) ->
Diag = get_diag(2, State),
case is_winner(Diag) of
false -> check_winner(State, diag, 1);
Marker -> Marker
end;
check_winner(State, diag, 1) ->
Diag = get_diag(1, State),
case is_winner(Diag) of
false -> none;
Marker -> Marker
end.
%% Helper Functions
trace(Msg, Args, State) ->
io:format("~s: ~p, state = ~p~n", [Msg, Args, State]).
unexpected(Msg, State, Data) ->
io:format("~p received unknown event ~p while in state ~p, Data = ~p~n", [self(), Msg, State, Data]).
%% Initialization
new([Number]) ->
#?MODULE {
number = Number,
markers = ['X', 'O']
}.