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)”