Current section
Files
Jump to
Current section
Files
src/throwdown_arena.erl
-module(throwdown_arena).
-behaviour(gen_server).
-type arena_name() :: atom() | string() | binary().
-type arena_mode() :: 'waiting' | 'playing'.
-record(state, {
name :: arena_name(),
mode :: arena_mode(),
rules :: function(),
players = #{} :: map(),
game_state = #{} :: map()
}).
-export([
start_link/1,
start_link/2,
child_spec/1,
register_player/3,
submit_choice/3,
done/1,
default_rules/2
]).
-export([
init/1,
handle_cast/2,
handle_call/3,
handle_info/2,
terminate/2,
code_change/3
]).
%% public API
start_link(Name) ->
start_link(Name, fun default_rules/2).
start_link(Name, Rules) when is_function(Rules) ->
gen_server:start_link(?MODULE, [Name, Rules], []).
child_spec(Name) ->
#{id => Name,
start => {throwdown_arena, start_link, []},
restart => temporary,
shutdown => 2000,
type => worker,
modules => [throwdown_arena]}.
default_rules(rock, rock) -> tie;
default_rules(rock, paper) -> loss;
default_rules(rock, scissors) -> win;
default_rules(rock, lizard) -> win;
default_rules(rock, spock) -> loss;
default_rules(paper, paper) -> tie;
default_rules(paper, rock) -> win;
default_rules(paper, scissors) -> loss;
default_rules(paper, lizard) -> loss;
default_rules(paper, spock) -> win;
default_rules(scissors, scissors) -> tie;
default_rules(scissors, rock) -> loss;
default_rules(scissors, paper) -> win;
default_rules(scissors, lizard) -> win;
default_rules(scissors, spock) -> loss;
default_rules(lizard, lizard) -> tie;
default_rules(lizard, rock) -> loss;
default_rules(lizard, paper) -> win;
default_rules(lizard, scissors) -> loss;
default_rules(lizard, spock) -> win;
default_rules(spock, spock) -> tie;
default_rules(spock, rock) -> win;
default_rules(spock, paper) -> loss;
default_rules(spock, scissors) -> win;
default_rules(spock, lizard) -> loss.
-spec register_player( Arena :: pid(),
Name :: binary(),
PlayerPid :: pid() ) -> ok.
register_player(Arena, Name, PlayerPid) ->
gen_server:call(Arena, {register, Name, PlayerPid}).
submit_choice(Arena, Name, Pick) ->
gen_server:call(Arena, {choice, {Name, Pick}}).
done(Arena) ->
gen_server:call(Arena, done).
%% gen_server callback
init([Name, Rules]) ->
Choices = throwdown:get_env(choices, [rock, paper, scissors, lizard, spock]),
GState = #{ choices => Choices, current => ordsets:new(), results => [] },
{ok, #state{ mode = waiting, name = Name, rules = Rules, game_state = GState }}.
handle_cast(_Cast, State) ->
{noreply, State}.
handle_call(done, _From, State = #state{ players = P, game_state = G } ) ->
{Reply, NewState} = case maps:size(P) of
X when X < 2 ->
{{error, not_enough_players}, State};
_ ->
play_round(P, G),
{ok, State#state{mode = playing}}
end,
{reply, Reply, NewState};
handle_call({choice, _C}, _From, State = #state{ mode = waiting }) ->
{reply, {error, cannot_select}, State};
handle_call({choice, C}, _From, State = #state{ mode = playing, players = P, game_state = G }) ->
Current = maps:get(current, G),
NewCurrent = ordsets:add_element(C, Current),
NewG = maps:put(current, NewCurrent, G),
NewMode = case ordsets:size(NewCurrent) == maps:size(P) of
true ->
self() ! start_round,
evaluation;
false ->
playing
end,
{reply, ok, State#state{ mode = NewMode, game_state = NewG }};
handle_call({register, _Name, _PlayerPid}, _From, State = #state{ mode = playing }) ->
{reply, {error, cannot_register}, State};
handle_call({register, Name, PlayerPid}, _From, State = #state{ mode = waiting,
players = P }) ->
NewP = maps:put(Name, PlayerPid, P),
{reply, ok, State#state{ players = NewP }};
handle_call(_Call, _From, State) ->
{reply, dieeeeee, State}.
handle_info(start_round, State = #state{ mode = evaluation, rules = R, players = P, game_state = G }) ->
Current = ordsets:to_list(maps:get(current, G)),
Results = evaluate_choices(R, Current, Current, []),
G1 = maps:put(current, ordsets:new(), G),
R0 = maps:get(results, G),
NewG = maps:put(results, [ Current | R0 ], G1),
NewP = remove_players(Results, P),
case maps:size(NewP) of
0 ->
{stop, no_players_remain, State#state{ mode = stop, players = NewP, game_state = NewG}};
1 ->
{stop, {winner, NewP}, State#state{ mode = stop, players = NewP, game_state = NewG}};
_ ->
play_round(NewP, NewG),
{noreply, State#state{ mode = playing, players = NewP, game_state = NewG }}
end;
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%% private
play_round(P, G) ->
maps:map(fun(_Name, Pid) ->
throwdown_player:play(Pid, G)
end,
P).
remove_players([], P) -> P;
remove_players([ {loss, {Name, _Pick}} | Tail], P) ->
PlayPid = maps:get(Name, P),
throwdown_player:leave(PlayPid),
remove_players(Tail, maps:remove(Name, P));
remove_players([ _H | T ], P) ->
remove_players(T, P).
evaluate_choices(_Rules, [], _Picks, Acc) -> Acc;
evaluate_choices(Rules, [ H | Rest ], All, Acc) ->
Picks = All -- [H],
Outcome = case versus(Rules, H, Picks, undefined) of
tie -> {tie, H};
win -> {win, H};
loss -> {loss, H}
end,
evaluate_choices(Rules, Rest, All, [ Outcome | Acc ]).
versus(_Rules, _Player, [], Result) -> Result;
versus(Rules, {_NameA, PlayA} = A, [ {_NameB, PlayB} | T ], _LastResult) ->
case Rules(PlayA, PlayB) of
loss ->
loss;
Result ->
versus(Rules, A, T, Result)
end.
-ifdef(TEST).
-endif.