In Part 1 we dealt with session management, now we’ll take a look at starting a new game.
First, we send a message from the client:
newGameBtn.onclick = function() {
var msg = JSON.stringify({type: 'new_game', sessionId: sessionId});
send(msg);
newGameBtn.disabled = true;
clearBoard();
updateStatus('Waiting to join game...');
};
and handle that on the other side of the websocket:
websocket_handle({text, Json}, Req, State) ->
Msg = jiffy:decode(Json, [return_maps]),
Resp = validate_session(Msg, fun() ->
Type = maps:get(<<"type">>, Msg),
handle_message(Type, Msg)
end),
{reply, make_frame(Resp), Req, State};
validate_session(Msg, Fun) ->
SessionId = maps:get(<<"sessionId">>, Msg),
case gen_server:call(t3_session_manager, {validate_session, SessionId}) of
ok -> Fun();
invalid_session -> #{type => <<"error">>, msg=> <<"invalid_session">>}
end.
handle_message(<<"new_game">>, Msg) ->
SessionId = maps:get(<<"sessionId">>, Msg),
start_new_game(SessionId);
start_new_game(_SessionId) ->
Res = try
gen_server:call(t3_match_maker, {find_game}, 30000)
catch
exit:{timeout,_} -> timeout
end,
case Res of
{ok, GameId} -> #{type => <<"new_game">>, id => GameId};
timeout -> #{type => <<"no_game_available">>}
end.
We decode the message, and validate the session; then we need to find a game, a task we delegate to the “match maker”, another gen_server. It’s possible a game won’t be found in time, so we need to handle timeouts too.
The match maker is pretty simple, like the session manager, but might be more interesting in a real app:
handle_call({find_game}, From, State) ->
case find_game(From, State) of
{ok, GameId, NewState} -> {reply, {ok, GameId}, NewState};
{wait, NewState} -> {noreply, NewState}
end;
find_game(From, #state{waiting=[]}) ->
{wait, #state{waiting=[From]}};
find_game({P2,_}, #state{waiting=[From|Rest]}) ->
GameId = uuid:uuid_to_string(uuid:get_v4(), binary_standard),
{P1,_} = From,
{ok, _Pid} = supervisor:start_child(t3_game_sup, [{P1, P2, GameId}]),
gen_server:reply(From, {ok, GameId}),
{ok, GameId, #state{waiting=Rest}}.
If someone is waiting, then we start a new game; otherwise, the pid of the websocket gets pushed onto the queue (this should probably be a more stable reference, like a user id, and we would look up the pid. We also don’t handle the case that a queued socket is closed, or times out waiting).
We need to add two more items to our supervision tree:
init([]) ->
Procs = [
{t3_session_manager, {t3_session_manager, start_link, []}, permanent, 5000, worker, [t3_session_manager]},
{t3_match_maker, {t3_match_maker, start_link, []}, permanent, 5000, worker, [t3_match_maker]},
{t3_game_sup, {t3_game_sup, start_link, []}, permanent, 5000, supervisor, [t3_game_sup]}
],
{ok, {{one_for_one, 1, 5}, Procs}}.
The game supervisor is also very simple:
init([]) ->
Procs = [{t3_game_fsm, {t3_game_fsm, start_link, []}, transient, 5000, worker, [t3_game_fsm]}],
{ok, {{simple_one_for_one, 5, 10}, Procs}}.
The important thing to note is the “simple one-for-one” restart strategy, where children are added dynamically, on demand.
We’ll look at the game FSM in more detail in Part 3, but when started it notifies both players:
init(Args) ->
io:format("New game started: ~p~n", [Args]),
[{P1, P2, GameId}] = Args,
true = gproc:reg({n, l, GameId}),
State = #state{p1 = P1, p2 = P2},
P1 ! {your_turn, State#state.board},
P2 ! {wait, State#state.board},
{ok, p1_turn, State}.
Finally, we need to handle some new messages on the client:
} else if (msg.type === 'new_game') {
gameId = msg.id;
updateStatus('New game!');
} else if (msg.type === 'your_turn') {
updateStatus('Your turn!');
updateBoard(msg.data);
enableBoard();
} else if (msg.type === 'wait') {
updateBoard(msg.data);
updateStatus('Waiting for other player...');
}
2 thoughts on “Building a Tic Tac Toe game with Cowboy & websockets (Part 2)”