4
\$\begingroup\$

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']
    }.
\$\endgroup\$
2
\$\begingroup\$

I read your code carefully, and I must say it is easy to read. I have some remarks to make though, sorted from the most to the least important (at least from my point of view :o).

The first one is about the definition of roles and startup phase. In my opinion you should clarify first how the system works: creation of a game, a user, how he connects etc. Your system works, but everything must be done manually, and you have 2 processes for the player which roles aren't very clear, and which store in their state useless information - I think you could work with the player only, storing the game and its name. In my opinion you should have one registered server whose job is to register the users, create games on demand etc. If I have time I will propose you something. You should use the OTP supervisor behavior to manage the processes (and application to startup the whole thing).

You have used process_flag(trap_exit, true) in all your processes. You should not do this. There are very few reason why a process die. The most common are your bugs, and trapping exit will not help you to debug or live with them. Then there are some environment issues (memory, disk, OS, other application...) and it will not help in this case either. Especially in your case because you do not analyze the potential error messages. This feature is generally reserved to supervisors. In your code you should add some defensive measure to prevent the bad input from external world (early crash, adding some test or try/catch + user interaction...)

You use case in a somewhat wrong way. I consider that 3 intricate cases is not a good idea in Erlang. And there is a bug in your code maybe due to that: you forgot that the result of the main case must return something like {next_state,StateName,State}. In 2 branches, it ends on io:format(...) that returns ok and causes a crash of the fsm.

You don't take advantage of the very powerful Erlang pattern matching. It is frequent to write more clauses of one function, each of them with limited code and cases. It is a good usage since it allows to write he code for different use cases without modifying the previous version, decode in the clause most of the variable you will need, and in general it gives very compact, but still readable, code.

You used a list to store the state of the game. For a small and fixed size variable I prefer to use a tuple, like this it is clear that the size will not change. The list could be a good choice if the players can choose the size of the board at the beginning.

Last there is a bug using the function random:uniform/1. This function uses a seed to generate pseudo random sequences. The seed is valid at process level (in fact the last value is stored in the process dictionary, which is a "side effect", but linked to the expected behavior). If you don't initialize it with something like random:seed(erlang:now()), the function will always return the same sequence.


Here I propose you a new version of your code (I removed r_user). This version checks most of the erroneous cases and warns the player. I didn't enhance the startup, I'll do it if I find time. It is followed by an example of game performed on 3 different VMs connected in a cluster.

The player:

-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
]).

%%% State Record

-record(?MODULE, {
      user,
      game = none
     }).


%% Behavior functions

start(User) ->
    gen_fsm:start(?MODULE, [User], []).

start_link(User) ->
    gen_fsm:start_link(?MODULE, [User], []).

%% Behavior callbacks

init([User]) ->
    {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) ->
    {reply, 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}, Data) ->
    io:format("Player ~s: joined game ~p/~p~n", [Data#?MODULE.user, game:get_number(Game), Game]),
    NewState = Data#?MODULE{game = Game},
    {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 ~s: quit game ~p~n", [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 of 
        winner -> "you won"; 
        looser -> "you loose";
        par -> "there is no winner"
    end,
    io:format("~s: ~p Game over, ~s~n", [Data#?MODULE.user, Game, Result]),
    {next_state, waiting_for_game, Data#?MODULE{game = none}};
in_game({yourmove, Game}, State) ->
    print_play(State#?MODULE.user,Game),
    {next_state, in_game, State};
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(gotmove, State) ->
    io:format("movement accepted~n"),
    {next_state, in_game, State};
in_game({badaction, Action}, State) ->
    io:format("invalid movement ~p~n",[Action]),
    {next_state, in_game, State};
in_game(notYourTurn, State) ->
    io:format("Please ~s wait for your turn~n",[State#?MODULE.user]),
    {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
print_play(P,G) ->
    io:format("~s in_game: it is your turn to play~n~n", [P]),
    Me = self(),
    L = [sign(X,Me) || X <- tuple_to_list(G)],
    print_game(L).

print_game([A,B,C|R]) ->
    io:format("+-+-+-+~n|~c|~c|~c|~n",[A,B,C]),
    print_game(R);
print_game([]) ->
    io:format("+-+-+-+~n~n",[]).

sign(0,_) -> 32;
sign(X,X) -> $X;
sign(_,_) -> $O.

%% Helper Functions

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 = lists:flatten(io_lib:format("~s",[User])),
     game = none
    }.

The game:

-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
]).

%%% State Record

-record(?MODULE, {
    number,
    players = [],
    active_player,
    game = {0,0,0,0,0,0,0,0,0},
    turn =0
}).

%%% Public API

%% Behavior functions

start(Number) ->
    gen_fsm:start(?MODULE, [Number], []).

start_link(Number) ->
    gen_fsm:start_link(?MODULE, [Number], []).

%% Behavior callbacks

init([Number]) ->
    random:seed(erlang:now()),
    {ok, waiting_for_players, #?MODULE {number = Number}}.

handle_event(stop, _StateName, Data) ->
    {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,
    io:format("Game ~p, adding player ~p~n", [Game, player:get_name(Player)]),
    NewPlayers = [Player | Data#?MODULE.players],
    PlayerCount = length(NewPlayers),
    gen_fsm:send_event(Player, {gamejoined, self()}),
    AP = maybe_play(PlayerCount,NewPlayers,Data#?MODULE.game),
    {next_state, waiting_for_players(PlayerCount), Data#?MODULE{players = NewPlayers, active_player = AP}};
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, {Row, Column}}, State = #?MODULE{active_player = Player, game = Game , players =  Players, turn = Turn}) ->
    io:format("in_progress: Player ~p did ~p~n", [Player, {Row, Column}]),
    Index = index(Column,Row),
    case is_valid_action(Index,Game) of
        true ->
            _NewGame = setelement(Index,Game,Player),
            _CheckWinner = check_winner(_NewGame,Turn+1),
            in_progress_valid_action(_CheckWinner, Turn+1, Player, new_player(Players,Player), _NewGame, State);
        _ ->
            in_progress_invalid_action(Player,{Row,Column},State)
    end;
in_progress({playeraction, Player, _}, State) ->
    gen_fsm:send_event(Player,notYourTurn),
    {next_state, in_progress,State};

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
in_progress_valid_action(none, NewTurn, OldP, NewP, Game, State) ->
    gen_fsm:send_event(NewP, {yourmove, Game}),
    gen_fsm:send_event(OldP, gotmove),
    {next_state, in_progress, State#?MODULE{game = Game, active_player = NewP, turn = NewTurn}};
in_progress_valid_action(par, _, OldP, NewP, _, State) ->
    gen_fsm:send_event(NewP, {gameover, par}),
    gen_fsm:send_event(OldP, {gameover, par}),
    {stop,normal,State};
in_progress_valid_action(Win, _, _, _, _, State = #?MODULE{players = Players}) ->
    gen_fsm:send_event(Win, {gameover, winner}),
    gen_fsm:send_event(new_player(Players,Win), {gameover, looser}),
    {stop,normal,State}.

in_progress_invalid_action(Player,Action,State) ->
    gen_fsm:send_event(Player, {badaction, Action}),
    {next_state, in_progress,State}.

index(C, R) when ((C =:= 1) orelse (C =:= 2) orelse (C =:= 3)) andalso
                 ((R =:= 1) orelse (R =:= 2) orelse (R =:= 3)) ->
    C + (R - 1) * 3;
index(_,_) -> error.

is_valid_action(I,Game) when is_integer(I) ->
    element(I,Game) == 0;
is_valid_action(_,_) -> false.

new_player([P1,P2],P1) -> P2;
new_player([P1,_],_) -> P1.

maybe_play(1,_,_) -> none;
maybe_play(2,[P1,P2],Game) ->
    S = random:uniform(2),
    P = case S of
        1 -> P1;
        2 -> P2
    end,
    yourmove(P,Game),
    P.

yourmove(P,Game) -> 
    gen_fsm:send_event(P, {yourmove, Game}).

waiting_for_players(1) -> waiting_for_players;
waiting_for_players(2) -> in_progress.


check_winner({X,X,X,_,_,_,_,_,_},_) when X =/= 0 -> X;
check_winner({_,_,_,X,X,X,_,_,_},_) when X =/= 0 -> X;
check_winner({_,_,_,_,_,_,X,X,X},_) when X =/= 0 -> X;
check_winner({X,_,_,X,_,_,X,_,_},_) when X =/= 0 -> X;
check_winner({_,X,_,_,X,_,_,X,_},_) when X =/= 0 -> X;
check_winner({_,_,X,_,_,X,_,_,X},_) when X =/= 0 -> X;
check_winner({X,_,_,_,X,_,_,_,X},_) when X =/= 0 -> X;
check_winner({_,_,X,_,X,_,X,_,_},_) when X =/= 0 -> X;
check_winner(_,9) -> par;
check_winner(_,_) -> none.

%% Helper Functions

unexpected(Msg, State, Data) ->
    io:format("~p received unknown event ~p while in state ~p, Data = ~p~n", [self(), Msg, State, Data]).

The game shell:

(game@homepc)1> net_adm:ping(p2@homepc).
pong
(game@homepc)2> 
(game@homepc)2> net_adm:ping(p1@homepc).
pong
(game@homepc)3> 
(game@homepc)3> {ok,G} = game:start(5582).
{ok,<0.50.0>}
(game@homepc)4> 
(game@homepc)4> {shell,p2@homepc} ! G. 
<0.50.0>
(game@homepc)5> 
(game@homepc)5> 
(game@homepc)5> {shell,p1@homepc} ! G. 
<0.50.0>
(game@homepc)6> 
(game@homepc)6> 
Game 5582, adding player "Bob"
Game 5582, adding player "Joe"
in_progress: Player <7169.52.0> did {2,2}
in_progress: Player <6080.52.0> did {2,2}
in_progress: Player <6080.52.0> did {2,8}
in_progress: Player <6080.52.0> did {1.0,1}
in_progress: Player <6080.52.0> did {1,1}
in_progress: Player <7169.52.0> did {1,3}
in_progress: Player <6080.52.0> did {3,1}
in_progress: Player <7169.52.0> did {2,1}
in_progress: Player <6080.52.0> did {3,3}
in_progress: Player <7169.52.0> did {2,3}
Game terminated normally, state name = in_progress, {game,5582,
                                                     [<6080.52.0>,<7169.52.0>],
                                                     <7169.52.0>,
                                                     {<6080.52.0>,0,
                                                      <7169.52.0>,<7169.52.0>,
                                                      <7169.52.0>,0,
                                                      <6080.52.0>,0,
                                                      <6080.52.0>},
                                                     6}
(game@homepc)6>

Player 1 shell:

(p1@homepc)1> register(shell,self()).  
true
(p1@homepc)2>  
(p1@homepc)2> 
(p1@homepc)2> G = receive M -> M end.
<5980.50.0>
(p1@homepc)3> 
(p1@homepc)3> {ok,P1} = player:start("Bob").
{ok,<0.52.0>}
(p1@homepc)4> 
(p1@homepc)4> game:add_player(G, P1).
ok
(p1@homepc)5> 
Player Bob: joined game 5582/<5980.50.0>
Bob in_game: it is your turn to play

+-+-+-+            
| | | |
+-+-+-+            
| | | |
+-+-+-+            
| | | |
+-+-+-+            

(p1@homepc)5> player:make_move(P1, 2, 2).
in_game: setting row 2, column 2
ok
movement accepted  
Bob in_game: it is your turn to play

+-+-+-+            
|O| | |
+-+-+-+            
| |X| |
+-+-+-+            
| | | |
+-+-+-+            

(p1@homepc)6> player:make_move(P1, 1, 3).
in_game: setting row 1, column 3
ok
movement accepted  
Bob in_game: it is your turn to play

+-+-+-+            
|O| |X|
+-+-+-+            
| |X| |
+-+-+-+            
|O| | |
+-+-+-+            

(p1@homepc)7> player:make_move(P1, 2, 1).
in_game: setting row 2, column 1
ok
movement accepted  
Bob in_game: it is your turn to play

+-+-+-+            
|O| |X|
+-+-+-+            
|X|X| |
+-+-+-+            
|O| |O|
+-+-+-+            

(p1@homepc)8> player:make_move(P1, 2, 3).
in_game: setting row 2, column 3
ok
Bob: <5980.50.0> Game over, you won
(p1@homepc)9>

Player 2 shell:

(p2@homepc)1> register(shell,self()).  
true
(p2@homepc)2>  
(p2@homepc)2> 
(p2@homepc)2> G = receive M -> M end.
<5980.50.0>
(p2@homepc)3> 
(p2@homepc)3> {ok,P2} = player:start("Joe").
{ok,<0.52.0>}
(p2@homepc)4> 
(p2@homepc)4> game:add_player(G, P2).
ok
(p2@homepc)5> 
Player Joe: joined game 5582/<5980.50.0>
(p2@homepc)5> player:make_move(P2, 2, 2).
in_game: setting row 2, column 2
ok
Please Joe wait for your turn
Joe in_game: it is your turn to play

+-+-+-+            
| | | |
+-+-+-+            
| |O| |
+-+-+-+            
| | | |
+-+-+-+            

(p2@homepc)6> player:make_move(P2, 2, 2).
in_game: setting row 2, column 2
ok
invalid movement {2,2}
(p2@homepc)7> player:make_move(P2, 2, 8).
in_game: setting row 2, column 8
ok
invalid movement {2,8}
(p2@homepc)8> player:make_move(P2, 1.0 , 1).
in_game: setting row 1.0, column 1
ok
invalid movement {1.0,1}
(p2@homepc)9> player:make_move(P2, 1, 1).   
in_game: setting row 1, column 1
ok
movement accepted   
Joe in_game: it is your turn to play

+-+-+-+             
|X| |O|
+-+-+-+             
| |O| |
+-+-+-+             
| | | |
+-+-+-+             

(p2@homepc)10> player:make_move(P2, 3, 1).
in_game: setting row 3, column 1
ok
movement accepted   
Joe in_game: it is your turn to play

+-+-+-+             
|X| |O|
+-+-+-+             
|O|O| |
+-+-+-+             
|X| | |
+-+-+-+             

(p2@homepc)11> player:make_move(P2, 3, 3).   
in_game: setting row 3, column 3
ok
movement accepted   
Joe: <5980.50.0> Game over, you loose
(p2@homepc)12>
\$\endgroup\$
  • \$\begingroup\$ I actually started work on a different, much more involved project, and I think I make much better use of pattern matching there. I haven't read through your new version yet, will try to get to it soon. The first comment I have is that I am seeding the RNG with the same number on purpose so the behavior of the tests is reproducible. In my other project, I am using eunit so the testing is much better isolated from the code. \$\endgroup\$ – Paschover Feb 13 '15 at 14:17
  • \$\begingroup\$ Regarding user vs. player processes, this was done with an eye towards allowing people to play from different platforms. For example, someone logging in from a phone, from a tablet, from a PC, from a game console, etc. You would have different processes to handle all of these, and they would all end up working through the player process. \$\endgroup\$ – Paschover Feb 13 '15 at 14:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.