Brace yourselves… a long post is coming.
It took me too long to write this blog post and the main reason is that it encompasses many many different topics. This will be a blog post about
A long time ago, when I was a little kid, I started programming in QBasic and, as many of you might know, QBasic came with two little games in it. One of them was Nibbles
Fast-forward 25 years… and you’ll find me at Erlang Solutions trying to come up with the subject for our next Hackathon with the help of Roberto Romero and Hernán Rivas Acosta. We wanted a multiplayer game that could be watched on a single screen. Of course we came up with serpents!
Every year at Erlang Solutions we all take one day off from our projects and we spend it all together celebrating what we love the most: programming. In 2014 we ran a “build your app in just one day” competition with awesome results. For 2015’s edition, we decided to come up with a multi-player game where the organisers set up the server and the teams developed the clients. The code that I will be showing through the rest of this blog post, it all comes from the game server that Hernán, Roberto and myself developed.
The main goal of serpents was of course to be used for the Hackathon. But I had a hidden agenda for it as well: Since this project was going to be open-sourced on github, I wanted to use it as a case to show how we work. I wanted to be able to get people on IRC, erlang-questions, reddit, stackoverflow, etc. that asks questions like “hey! how do you build a web-server in Erlang?” and tell them “look here. This is how you do it!”. With that in mind, we faced this project as if it was a client project and, turns out, our clients (our fellow devs) were in fact one of the toughest clients we have ever faced.
So, how did we build a multiplayer server for a serpent’s game? We started by dividing it into 3 main components:
For the web page we used plain HTML and some Javascript (you can tell how that is not really interesting to me at least :P). What is interesting is the real-time part. We implemented that one (as we usually do) over SSE. During a game, the website looked like this:
From the server perspective, we implemented the RESTful API using cowboy on trails and we documented it with swagger. It ended up looking like this:
On the other side, we decided to give client devs 2 options. They could just use the same RESTful API and SSE connection to communicate with the server, or they could use our HDP protocol.
HDP is a blazingly fast protocol that runs on UDP. It was developed by Hernán and documented in kafka style for all to use. It’s specialised for serpents, but it can be replicated in other projects with ease.
Internally, as you can see on github, the code is divided in multiple folders and multiple modules that we could test individually.
One one side, we had the basic building blocks of the game: the rules (in spts_core
) implemented as a gen_fsm
per game and its entities (represented as Data Types or Models, each one with its own module and opaque type). You can see our approach in spts_serpents
:
%%% @doc Serpent model-module(spts_serpents).-author('[email protected]').-type status() :: alive | dead.-type name() :: binary().-opaque serpent() :: #{ name => name() , numeric_id => pos_integer() , token => binary() , body => [spts_games:position()] , direction => spts_games:direction() , food => pos_integer() , status => status() }.-export_type([serpent/0, status/0, name/0]).-export([new/6]).-export([ name/1 , numeric_id/1 , direction/1 , direction/2 , to_json/1 , to_json/2 , to_binary/2 % ... ]).-spec name(serpent()) -> name().name(#{name := Name}) -> Name.-spec direction( serpent(), spts_games:direction()) -> serpent().direction(Serpent, Direction) -> Serpent#{direction := Direction}.% ...
We also had multiple cowboy handlers for the different endpoints of our API. All of them were built using mixer to “inherit” functions from the spts_base_handler
.
%%% @doc /games/:game_id/serpents handler-module(spts_serpents_handler).-author('[email protected]').-include_lib("mixer/include/mixer.hrl").-mixin([{ spts_base_handler , [ init/3 , rest_init/2 , allowed_methods/2 , content_types_accepted/2 , content_types_provided/2 , resource_exists/2 ] }]).-export([ handle_post/2 , trails/0 ]).-type state() :: spts_base_handler:state().-behaviour(trails_handler).-spec trails() -> trails:trails().trails() -> Metadata = #{ post => #{ tags => ["serpents"] , description => "Adds a serpent to a game" , consumes => ["application/json"] , produces => ["application/json"] , parameters => [ spts_web:param(request_body) , spts_web:param(game_id) ] } }, Path = "/api/games/:game_id/serpents", Opts = #{path => Path}, [trails:trail(Path, ?MODULE, Opts, Metadata)].% ...
To report events from the core to either the SSE or the HDP clients, we used gen_event
but, to ease our lives we implemented a simple gen_event
handler that, in turn, required only one function to be implemented in your client :notify(pid(), event())
. We called it [spts_gen_event_handler](https://github.com/inaka/serpents/blob/master/src/spts_gen_event_handler.erl)
. We used that same handler to write tests for the core without depending on any type of client.
For HDP, we wrote an asynchronous UDP listener which starts a gen_server
for each client that connects (there are no actual connections in UDP). The simplicity of that comes from the fact that we abstracted all the protocol parsing/validation logic to its own module: spts_hdp
. That module in itself is a great example of the power of Erlang’s bit syntax. It showcases list-comprehensions, combined with binary-comprehensions and a lot of amazing binary pattern-match expressions. My favourite:
%% @doc Parses a list of serpent diffs.%% Each one includes the serpent id and the%% length of its body, followed by each of%% the body cells.parse_diff_serpents(0, Next, Acc) -> {lists:reverse(Acc), Next};parse_diff_serpents( N, << SerpentId:?UINT , BodyLength:?USHORT , Body1:BodyLength/binary , Body2:BodyLength/binary , Next/binary >>, Acc) -> Body = <<Body1/binary, Body2/binary>>, Serpent = #{ id => SerpentId , body => [ {Row, Col} || <<Row:?UCHAR, Col:?UCHAR>> <= Body] }, parse_diff_serpents(N-1, Next, [Serpent|Acc]).
And, to create all this, we used our usual approach: TDD. We worked with common_test
for that and you can see the somewhat long list of test suites we implemented here. We’ve written many blog posts about TDD, code-coverage and even meta-testing so I’ll not go over it again here. Just keep in mind that if you need examples of those things, you have plenty around here.
And if you are asking yourself are there any client examples out there? Yes, they are here. I’ve developed all of them myself and you can create your own either starting from scratch or using spts_cli.
We ended up complying with both of my goals. On the one hand, I can say we had an extraordinary Hackathon with lots of people happily playing while coding. On the other hand, now we have a great example of how we build stuff at Erlang Solutions to show the world. And you can enjoy it, too! just do…
$ git clone https://github.com/inaka/serpents.git$ cd serpents$ make$ _rel/serpents/bin/serpents start
…and open http://localhost:8585 in your browser to start playing!
And, of course, as I did with Nibbles when I was just a kid, the funniest part starts when you start modifying the code to make the serpents fatter, thinner, randomly drunk after eating a poisoned fruit…
Oh, man! I have to implement that! Bye all, 9-year-old me has something important to do…
Originally published at www.erlang-solutions.com on April 26, 2018.
This blog post was originally written for inaka.net on 13 November 2015.