Let's find the fastest beamer!
BeamOlympics is an Erlang app to help you check your Erlang-Fu. You can also play with friends in a local network.
If you want to generate a release by hand, follow the steps below. If you would like to start with a pre-generated release, just download the latest one and jump to the Start section below.
As stated in rebar.config, BeamOlympics server only compiles with Erlang/OTP19+.
Before you can start playing, you have to install the BeamOlympics server. To do so, clone this repo and use rebar3 to generate a release for it.
$ git clone https://github.com/inaka/beam_olympics.git
$ rebar3 release
If needed, between those 2 steps you can change your node name in vm.args to your convenience.
To start the BeamOlympics server you you run…
$ $REL_PATH/beam_olympics/bin/beam_olympics start
Whre $REL_PATH
would be _build/default/rel
if you just generated the release with rebar3 or .
if you downloaded a pre-compiled release from github.
That will boot up an Erlang node in your computer with the name specified by vm.args.
Inside the node started in the previous step, a gen_server will be running. That server will act as the point of contact for clients to play the game.
Every interaction will be accomplished through gen_server
calls. For example, to check the overall game statistics, from a client node a caller can evaluate the following Elixir code:
GenServer.call({:bo_server, server_node}, :stats)
where server_node
is a variable bound to the server node name (or a function that returns it).
- To prevent fraud, users must always post solutions from the node in which they signed up. The node might crash/be closed, but when restarted it has to be called with the same name, otherwise submissions will be rejected.
- There is no undo, if you skipped a task you can't choose to submit a solution for it later.
- Each task has it's own associated score. Points are assigned this way:
- Each time a task is correctly solved,
score
points are added to the user - Each time a task is skipped,
score / 2
points are deduced from the user
- Each time a task is correctly solved,
- After skipping or completing a task, the user will either receive a new task or just the atom
the_end
indicating that the game is over
You can find the complete list of commands/calls with their possible answers below. Specs are written as functions although in reality the input is the call sent to the server and the output is the possible responses the server may return.
Using this call, the users register themselves in the game and receive their initial task.
{signup, PlayerName :: binary()} -> {ok, Task} | {error, conflict}.
Task :: #{ name := module()
, desc := binary()
, spec := Spec
, score := pos_integer()
}.
Spec :: #{ input := [binary()]
, output := binary()
}.
{:signup, player_name} :: {:ok, task} | {:error, :conflict}
@type player_name :: String.t
@type task :: %{
name: module,
desc: String.t,
spec: spec,
score: pos_integer
}
@type spec :: %{
input: [String.t],
output: String.t
}
- The server will return
conflict
if the player is already registered. - The spec describes the function required to complete the task.
input
indicates the number and types of the parameters,output
indicates the type of result it should produce.
gen_server:call({bo_server, ServerNode}, {signup, <<"Player">>}).
GenServer.call({:bo_server, server_node}, {:signup, "Player"})
Retrieves the current task for the player.
{task, PlayerName :: binary()} ->
{ok, Task} | {error, ended | forbidden | notfound}
{:task, player_name} :: {:ok, task} | {:error, :ended | :forbidden | :notfound}
- The server will return
ended
if the game is over for the player and therefore there is no current task - The server will return
forbidden
if the player is calling from a node that's not the one from which they signed up in the first place - The server will return
notfound
if the player doesn't exist
gen_server:call({bo_server, ServerNode}, {task, <<"Player">>}).
GenServer.call({:bo_server, server_node}, {:task, "Player"})
Submits a solution for the current task.
{submit, PlayerName :: binary(), Solution :: any()} ->
{ok, Task}
| the_end
| {error, invalid | timeout | ended | forbidden | notfound}
| {failures, [term(), ...]}
{:submit, player_name, term} ::
{:ok, task} | :the_end |
{:error, :invalid | :timeout | :ended | :forbidden | :notfound} |
{:failures, [term,...]}
- The server will return
ended
if the game is over for the player and therefore there is no current task - The server will return
forbidden
if the player is calling from a node that's not the one from which they signed up in the first place - The server will return
notfound
if the player doesn't exist - The server will return
timeout
if the evaluation of the tests took longer than expected - The server will return
invalid
if the arity of the provided function doesn't match the arity expected by the task or if the input is not even a function - The server will return
the_end
if the game is over for the player (i.e. there are no more tasks to complete/skip) - The server will return
failures
with a list of failure descriptions if there are tests for which the provided function fails
gen_server:call(
{bo_server, ServerNode}, {submit, <<"Player">>, fun() -> something end}).
GenServer.call(
{:bo_server, server_node}, {:submit, "Player", fn() -> :something end})
Skips the current task.
{skip, PlayerName :: binary()} ->
{ok, Task} | the_end | {error, ended | forbidden | notfound}
{:skip, player_name} ::
{:ok, task} | :the_end | {:error, :ended | :forbidden | :notfound}
- The server will return
ended
if the game is over for the player and therefore there is no current task - The server will return
forbidden
if the player is calling from a node that's not the one from which they signed up in the first place - The server will return
notfound
if the player doesn't exist - The server will return
the_end
if the game is over for the player (i.e. there are no more tasks to complete/skip)
gen_server:call({bo_server, ServerNode}, {skip, <<"Player">>}).
GenServer.call({:bo_server, server_node}, {:skip, "Player"})
Retrieves the current score for the player.
{score, PlayerName :: binary()} -> {ok, integer()} | {error, forbidden | notfound}
{:score, player_name} :: {:ok, integer} | {:error, :forbidden | :notfound}
- The server will return
forbidden
if the player is calling from a node that's not the one from which they signed up in the first place - The server will return
notfound
if the player doesn't exist
gen_server:call({bo_server, ServerNode}, {score, <<"Player">>}).
GenServer.call({:bo_server, server_node}, {:score, "Player"})
Retrieves the current game stats.
stats -> #{ tasks := pos_integer()
, players := [PlayerStats]
}.
PlayerStats :: #{ name := binary()
, done := non_neg_integer()
, score := integer()
}.
:stats :: stats
@type stats :: %{
tasks: pos_integer,
players: [player_stats]
}
@type player_stats :: %{
name: String.t,
done: non_neg_integer,
score: integer
}
tasks
will be the total number of tasks in the gamedone
will be the number of tasks already completed/skipped by the player
gen_server:call({bo_server, ServerNode}, stats).
GenServer.call({:bo_server, server_node}, :stats)
If you find any bugs or have a problem while using this library, please open an issue in this repo (or a pull request :)).
And you can check all of our open-source projects at inaka.github.io.