Tic-tac-toe (or noughts and crosses) is a good example project, as the game itself is pretty simple and won’t become a distraction. I’m going to try and build a version using Erlang, and more specifically Cowboy: an http server/framework.
I’m going to assume you know the basics of setting up a new project, and skip straight to the more interesting bits. The goal is to create a multi-player, websockets & html based version of tic-tac-toe. As it’s turn-based, we avoid a lot of the really thorny problems of “real-time” multi-player games.
My first step was to create a session as soon as the socket is opened:
socket.onopen = function () { socket.send('new_session') }
The backend then needs to handle this frame. I decided to add a session manager as a gen_server:
-record(state, {sessions=#{}}). handle_call(new_session, _From, State) -> SessionId = uuid:uuid_to_string(uuid:get_v4(), binary_standard), NewState = update_session(SessionId, State), {reply, {ok, SessionId}, NewState}; update_session(SessionId, State) -> Expiry = half_an_hour_from_now(), prune_expired_sessions(State#state{sessions=maps:put(SessionId, Expiry, State#state.sessions)}). half_an_hour_from_now() -> Now = calendar:universal_time(), calendar:gregorian_seconds_to_datetime(calendar:datetime_to_gregorian_seconds(Now) + (30 * 60)). prune_expired_sessions(State) -> SessionIds = maps:keys(State#state.sessions), {_, ExpiredSessions} = lists:partition(fun(S) -> session_valid(S, State#state.sessions) end, SessionIds), State#state{sessions=maps:without(ExpiredSessions, State#state.sessions)}. session_valid(SessionId, Sessions) -> case maps:find(SessionId, Sessions) of error -> false; {ok, Expires} -> Expires > calendar:universal_time() end.
In a more realistic app, you would probably want to perform some authentication here, and use a datastore of some kind; but we’ll just create a new session and return the id. We also need to add the new server to the supervision tree:
init([]) -> Procs = [ {t3_session_manager, {t3_session_manager, start_link, []}, permanent, 5000, worker, [t3_session_manager]} ], {ok, {{one_for_one, 1, 5}, Procs}}.
We can then call the server from the ws handler:
websocket_handle({text, <<"new_session">>}, Req, State) -> Resp = start_new_session(), {reply, make_frame(Resp), Req, State}; start_new_session() -> {ok, SessionId} = gen_server:call(t3_session_manager, new_session), #{type => <<"new_session">>, sessionId => SessionId}. make_frame(Msg) -> Json = jiffy:encode(Msg), {text, Json}.
We can call the server using it’s module name, rather than a pid, because it registered itself when starting:
start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
(Watch out though, this effectively makes it a singleton, which could be a choke-point in a real app). We need to handle the response on the client:
socket.onmessage = function(ev) { console.log('Received data: ' + ev.data); var msg = JSON.parse(ev.data); if (msg.type === 'new_session') { sessionId = msg.sessionId; newGameBtn.disabled = false; } }
And we’ll probably also need to be able to validate a session at some point:
handle_call({validate_session, SessionId}, _From, State) -> case session_valid(SessionId, State#state.sessions) of false -> {reply, {error, invalid_session}, State}; true -> %% sliding expiry window NewState = update_session(SessionId, State), {reply, ok, NewState} end;
You can find the full source code here. In Part 2, we’ll look at starting a new game.
Hi! Nice to see this tutorial here. Coincidentally, I just started learning by trying to build a tic-tac-toe Cowboy websockets game on 14-Jan… and today just StumbledUpon this page of yours when I was doing a search for “erlang jiffy” (yours was the 3rd result… https://lookonmyworks.co.uk/2014/07/01/json-in-a-jiffy/ ) as I needed an idea of how to use JSON in Erlang. Such a coincidence, you have a tutorial for tictactoe on 13-Jan! Cheers mate. :)
It was that or battleships :) Is your code available somewhere? I’d be interested to see what someone else came up with.
I would put it up on GitHub if I had it… but I haven’t yet! :-| Am working on it as part of my Erlang learnings… but got sidetracked with some frontend work though (at work…) Will drop you a line when I have it cheers :D