aboutsummaryrefslogtreecommitdiff
path: root/learn-you-some-erlang/processquest
diff options
context:
space:
mode:
authorTrygve Laugstøl <trygvis@inamo.no>2024-02-23 07:08:18 +0100
committerTrygve Laugstøl <trygvis@inamo.no>2024-02-23 07:08:18 +0100
commit5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e (patch)
tree982ca2e7f9ac4e8c350dfb5c4f60bcfdfff5afaf /learn-you-some-erlang/processquest
parent05ae56e5e89abf2993f84e6d52b250131f247c35 (diff)
downloaderlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.tar.gz
erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.tar.bz2
erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.tar.xz
erlang-workshop-5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e.zip
wip
Diffstat (limited to 'learn-you-some-erlang/processquest')
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/Emakefile2
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/.this-file-intentionally-left-blank0
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/processquest.app7
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/include/.this-file-intentionally-left-blank0
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/priv/.this-file-intentionally-left-blank0
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_enemy.erl17
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_events.erl49
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_market.erl77
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_player.erl175
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_stats.erl19
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_sup.erl31
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_supersup.erl28
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/processquest.erl39
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_enemy_tests.erl33
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_handler.erl24
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_tests.erl55
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_market_tests.erl15
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_player_tests.erl303
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_stats_tests.erl28
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/processquest_tests.erl49
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/Emakefile2
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/.this-file-intentionally-left-blank0
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.app8
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.appup9
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/include/.this-file-intentionally-left-blank0
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/priv/.this-file-intentionally-left-blank0
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_enemy.erl32
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_events.erl53
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_market.erl77
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_player.erl216
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_quest.erl15
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_stats.erl19
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_sup.erl31
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_supersup.erl28
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/processquest.erl39
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_enemy_tests.erl33
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_handler.erl24
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_tests.erl55
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_market_tests.erl15
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_player_tests.erl395
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_quest_tests.erl30
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_stats_tests.erl28
-rw-r--r--learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/processquest_tests.erl49
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/Emakefile2
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/ebin/regis.app7
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis.erl29
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_server.erl98
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_sup.erl18
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_server_tests.erl120
-rw-r--r--learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_tests.erl18
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile2
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app11
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl13
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl25
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl153
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl32
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl58
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/Emakefile2
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.app11
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.appup4
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv.erl13
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_pq_events.erl25
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl154
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_sup.erl32
-rw-r--r--learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_trans.erl58
-rw-r--r--learn-you-some-erlang/processquest/processquest-1.0.0.config21
-rw-r--r--learn-you-some-erlang/processquest/processquest-1.1.0.config21
-rw-r--r--learn-you-some-erlang/processquest/rel/.this-directory-must-be-tracked0
68 files changed, 3036 insertions, 0 deletions
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/Emakefile b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/Emakefile
new file mode 100644
index 0000000..b203ea3
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/Emakefile
@@ -0,0 +1,2 @@
+{["src/*", "test/*"],
+ [{i,"include"}, {outdir, "ebin"}]}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/.this-file-intentionally-left-blank
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/processquest.app b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/processquest.app
new file mode 100644
index 0000000..abda4b2
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/processquest.app
@@ -0,0 +1,7 @@
+{application, processquest,
+ [{description, "Game inspired by the Progress Quest game (http://progressquest.com)"},
+ {vsn, "1.0.0"},
+ {mod, {processquest, []}},
+ {registered, [pq_supersup]},
+ {modules, [processquest, pq_stats, pq_enemy, pq_events, pq_player]},
+ {applications, [stdlib, kernel, regis, crypto]}]}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/include/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/include/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/include/.this-file-intentionally-left-blank
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/priv/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/priv/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/priv/.this-file-intentionally-left-blank
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_enemy.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_enemy.erl
new file mode 100644
index 0000000..6f8e9b3
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_enemy.erl
@@ -0,0 +1,17 @@
+%% Gives random enemies
+-module(pq_enemy).
+-export([fetch/0]).
+
+fetch() ->
+ L = enemies(),
+ lists:nth(random:uniform(length(L)), L).
+
+enemies() ->
+ [{<<"Ant">>, [{drop, {<<"Ant Egg">>, 1}}, {experience, 1}]},
+ {<<"Wildcat">>, [{drop, {<<"Pelt">>, 1}}, {experience, 1}]},
+ {<<"Pig">>, [{drop, {<<"Bacon">>, 1}}, {experience, 1}]},
+ {<<"Wild Pig">>, [{drop, {<<"Tasty Ribs">>, 2}}, {experience, 1}]},
+ {<<"Goblin">>, [{drop, {<<"Goblin hair">>, 1}}, {experience, 2}]},
+ {<<"Robot">>, [{drop, {<<"Chunks of Metal">>, 3}}, {experience, 2}]}].
+
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_events.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_events.erl
new file mode 100644
index 0000000..324c432
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_events.erl
@@ -0,0 +1,49 @@
+%%% Wrapper module for the event manager of ProgressQuest players.
+%%% It adds a few functions to wrap the events and sleep on the right
+%%% scale on behalf of the pq_player process.
+-module(pq_events).
+-export([killed/3, location/3, lvl_up/5, buy/4, sell/3]).
+-export([start_link/1, stop/1, add_handler/3, delete_handler/3, notify/2]).
+
+start_link(Name) ->
+ {ok, Pid} = gen_event:start_link(),
+ ok = regis:register({events, Name}, Pid),
+ {ok, Pid}.
+
+stop(Name) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:stop(ManagerPid).
+
+add_handler(Name, Handler, Args) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:add_handler(ManagerPid, Handler, Args).
+
+delete_handler(Name, Handler, Args) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:delete_handler(ManagerPid, Handler, Args).
+
+notify(Name, Msg) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:notify(ManagerPid, Msg).
+
+killed(Name, Enemy = {_EnemyName, _Props}, Time) ->
+ notify(Name, {Name, killed, Time*2, Enemy}),
+ timer:sleep(Time*2).
+
+location(Name, Place, Time) ->
+ notify(Name, {Name, heading, Time, Place}),
+ timer:sleep(Time).
+
+lvl_up(Name, NewStats, NewLvl, NewExp, _Time) ->
+ notify(Name, {Name, lvl_up, 0, NewStats, NewLvl, NewExp}),
+ ok.
+
+buy(Name, Slot, Item, Time) ->
+ T = round(Time/2),
+ notify(Name, {Name, buy, T, Slot, Item}),
+ timer:sleep(T).
+
+sell(Name, Item, Time) ->
+ T = round(Time/5),
+ notify(Name, {Name, sell, T, Item}),
+ timer:sleep(Time).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_market.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_market.erl
new file mode 100644
index 0000000..241e9cd
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_market.erl
@@ -0,0 +1,77 @@
+%%% Can be used to obtain weapons and pieces of equipment of various types
+%%% to be equipped by the hero. The standard format is:
+%%% {Name, LevelModifier, Level, Price}.
+-module(pq_market).
+-export([helmet/2, weapon/2, shield/2, armor/2]).
+
+weapon(CombinedLvl, Money) ->
+ L = [
+ {<<"plastic knife">>, -1, 1, 2},
+ {<<"plastic knife">>, 0, 1, 3},
+ {<<"plastic knife">>, 1, 1, 5},
+ {<<"metal spoon">>, -1, 4, 3},
+ {<<"metal spoon">>, 0, 4, 4},
+ {<<"butter knife">>, -1, 6, 5},
+ {<<"butter knife">>, 0, 6, 7},
+ {<<"butter knife">>, 1, 6, 9},
+ {<<"machete">>, -1, 9, 15},
+ {<<"machete">>, 0, 9, 20},
+ {<<"machete">>, 1, 9, 25},
+ {<<"broad sword">>, -1, 12, 23},
+ {<<"broad sword">>, 0, 12, 30},
+ {<<"broad sword">>, 1, 12, 38},
+ {<<"lance">>, -1, 15, 32},
+ {<<"lance">>, 0, 15, 44},
+ {<<"lance">>, 1, 15, 57},
+ {<<"pistol">>, -1, 25, 95},
+ {<<"pistol">>, 0, 25, 105},
+ {<<"pistol">>, 1, 25, 155},
+ {<<"submachine gun">>, -1, 40, 200},
+ {<<"submachine gun">>, 0, 40, 245},
+ {<<"submachine gun">>, 1, 40, 365}
+ ],
+ first_match(fun(W = {_, Modifier, Lvl, Price}) ->
+ if Modifier+Lvl > CombinedLvl, Price =< Money -> W;
+ true -> continue
+ end
+ end, L).
+
+helmet(CombinedLvl, Money) -> pick_material(CombinedLvl, Money).
+shield(CombinedLvl, Money) -> pick_material(CombinedLvl, Money).
+armor(CombinedLvl, Money) -> pick_material(CombinedLvl, Money).
+
+pick_material(CombinedLvl, Money) ->
+ L = materials(),
+ first_match(fun(W = {_, Modifier, Lvl, Price}) ->
+ if Modifier+Lvl > CombinedLvl, Price =< Money -> W;
+ true -> continue
+ end
+ end, L).
+
+
+first_match(_, []) -> undefined;
+first_match(F, [H|T]) ->
+ case F(H) of
+ continue -> first_match(F,T);
+ Val -> Val
+ end.
+
+materials() ->
+ [{<<"wool">>, 0, 1, 25},
+ {<<"pleather">>, 0, 2, 45},
+ {<<"pleather">>, 1, 2, 50},
+ {<<"pleather">>, 2, 2, 65},
+ {<<"leather">>, -2, 7, 30},
+ {<<"leather">>, -1, 7, 35},
+ {<<"leather">>, 0, 7, 45},
+ {<<"leather">>, 2, 7, 65},
+ {<<"chain mail">>, -2, 12, 70},
+ {<<"chain mail">>, 0, 12, 85},
+ {<<"chain mail">>, 1, 12, 95},
+ {<<"chain mail">>, 2, 12, 105},
+ {<<"plate mail">>, -2, 17, 90},
+ {<<"plate mail">>, -1, 17, 95},
+ {<<"plate mail">>, 0, 17, 105},
+ {<<"plate mail">>, 1, 17, 115},
+ {<<"plate mail">>, 2, 17, 135}].
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_player.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_player.erl
new file mode 100644
index 0000000..b304f69
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_player.erl
@@ -0,0 +1,175 @@
+%%% The core of ProcessQuest -- the player FSM,
+%%% acting for each of the players in the game.
+%%%
+%%% The FSM actually depends on no external events and only sends messages
+%%% to itself to prompt state changes. This is somewhat unusual as far as
+%%% gen_fsm usages go, but it's pretty useful when needing to work with
+%%% a standalone process.
+-module(pq_player).
+-behaviour(gen_fsm).
+-export([start_link/2]).
+-export([init/1, market/2, killing/2, handle_event/3, handle_sync_event/4,
+ handle_info/3, terminate/3, code_change/4]).
+
+-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1,
+ equip=[], money=0, loot=[], bought=[], time=0}).
+
+
+%%% Possible states & events
+%%
+% sell buy
+% / | | \
+% \ ^ ^ /
+% [market]<--,
+% | |
+% done buying |
+% | bag full
+% v /
+% [killing fields]
+% / V V |
+% \ / | |
+% kill lvl up
+
+start_link(Name, Opts) ->
+ gen_fsm:start_link(?MODULE, {Name, Opts}, []).
+
+init({Name, Opts}) ->
+ %% Properly seeding stuff. If not doing this, the random module will
+ %% seed it based on a slightly unique time value. However, when starting
+ %% many processes at about the same time, the seeds can be very close
+ %% and give barely random results. The crypto:rand_bytes/1 function
+ %% allows for much better seeding.
+ <<A:32, B:32, C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ %% The first event, to start the FSM
+ gen_fsm:send_event(self(), kill),
+ case regis:register(Name, self()) of
+ {error, _} ->
+ {stop, name_taken};
+ ok ->
+ %% Use proplists with default values to let the user configure
+ %% all parts of the FSM's state by using the Opts proplist.
+ S = #state{
+ name=Name,
+ stats=proplists:get_value(stats, Opts, pq_stats:initial_roll()),
+ exp=proplists:get_value(exp, Opts, 0),
+ lvlexp=proplists:get_value(lvlexp, Opts, 1000),
+ lvl=proplists:get_value(lvl, Opts, 1),
+ equip=proplists:get_value(equip, Opts, []),
+ money=proplists:get_value(money, Opts, 0),
+ loot=proplists:get_value(loot, Opts, []),
+ bought=proplists:get_value(bought, Opts, []),
+ time=proplists:get_value(time, Opts, 0)
+ },
+ {ok, market, S}
+ end.
+
+%% Done selling. Switch to the event where we head to the killing fields
+market(sell, S = #state{loot=[]}) ->
+ gen_fsm:send_event(self(), buy),
+ {next_state, market, S};
+%% Selling an Item we have looted to the market, for whatever value it has
+market(sell, S = #state{loot=[H={_X,Val}|T], money=M}) ->
+ pq_events:sell(S#state.name, H, S#state.time),
+ gen_fsm:send_event(self(), sell),
+ {next_state, market, S#state{loot=T, money=M+Val}};
+%% When done selling, buy items with your money
+market(buy, S = #state{equip=Equip, money=Money, bought=Bought}) ->
+ %% we have slots of equipment. It's useless to buy the same
+ %% kind of item time after time, so we must select one we haven't observed yet
+ case next_slot(Equip, Bought) of
+ undefined ->
+ %% when no slot is found, go to the killing field
+ gen_fsm:send_event(self(), kill),
+ {next_state, market, S#state{bought=[]}};
+ OldItem = {Slot, {_Name, Modifier, Lvl, _Price}} ->
+ %% Replace the item by a slightly better one if possible.
+ case pq_market:Slot(Modifier+Lvl, Money) of
+ undefined ->
+ market(buy, S#state{bought=[Slot|Bought]});
+ NewItem = {_, _, _, Price} ->
+ pq_events:buy(S#state.name, Slot, NewItem, S#state.time),
+ gen_fsm:send_event(self(), buy),
+ NewEquip = [{Slot, NewItem} | Equip -- [OldItem]],
+ {next_state, market, S#state{equip=NewEquip,
+ money=Money-Price,
+ bought=[Slot|Bought]}}
+ end
+ end;
+%% Heading to the killing field. State only useful as a state transition.
+market(kill, S) ->
+ pq_events:location(S#state.name, killing, S#state.time),
+ gen_fsm:send_event(self(), kill),
+ {next_state, killing, S}.
+
+%% Killing an enemy on the killing field. Taking its drop and keeping it
+%% in our loot.
+killing(kill, S = #state{loot=Loot, stats=Stats, exp=Exp, lvlexp=LvlExp}) ->
+ MaxSize = proplists:get_value(strength, Stats)*2,
+ {EnemyName, Props} = pq_enemy:fetch(),
+ pq_events:killed(S#state.name, {EnemyName, Props}, S#state.time),
+ Drop = {_N, _V} = proplists:get_value(drop, Props),
+ KillExp = proplists:get_value(experience, Props),
+ NewLoot = [Drop|Loot],
+ if length(NewLoot) =:= MaxSize ->
+ gen_fsm:send_event(self(), market);
+ Exp+KillExp >= LvlExp ->
+ gen_fsm:send_event(self(), lvl_up);
+ true ->
+ gen_fsm:send_event(self(), kill)
+ end,
+ {next_state, killing, S#state{loot=NewLoot, exp=Exp+KillExp}};
+%% If we just leveled up, the stats get updated before we keep killing.
+killing(lvl_up, S = #state{stats=Stats, lvl=Lvl, lvlexp=LvlExp}) ->
+ NewStats = [{charisma, proplists:get_value(charisma, Stats)+pq_stats:roll()},
+ {constitution, proplists:get_value(constitution, Stats)+pq_stats:roll()},
+ {dexterity, proplists:get_value(dexterity, Stats)+pq_stats:roll()},
+ {intelligence, proplists:get_value(intelligence, Stats)+pq_stats:roll()},
+ {strength, proplists:get_value(strength, Stats)+pq_stats:roll()},
+ {wisdom, proplists:get_value(wisdom, Stats)+pq_stats:roll()}],
+ gen_fsm:send_event(self(), kill),
+ pq_events:lvl_up(S#state.name, NewStats, Lvl+1, LvlExp*2, S#state.time),
+ {next_state, killing, S#state{stats=NewStats, lvl=Lvl+1, lvlexp=LvlExp*2}};
+%% Heading to the market state transition
+killing(market, S) ->
+ pq_events:location(S#state.name, market, S#state.time),
+ gen_fsm:send_event(self(), sell),
+ {next_state, market, S}.
+
+handle_event(_Event, StateName, State) ->
+ {next_state, StateName, State}.
+
+handle_sync_event(_Event, _From, StateName, State) ->
+ {next_state, StateName, State}.
+
+handle_info(_Event, StateName, State) ->
+ {next_state, StateName, State}.
+
+terminate(_Reason, _StateName, _State) ->
+ ok.
+
+code_change(_OldVsn, StateName, State, _Extra) ->
+ {next_state, StateName, State}.
+
+%%%%%%%%%%%%%%%
+%%% PRIVATE %%%
+%%%%%%%%%%%%%%%
+
+%% Picks a slot based on what has been seen so far, combined with the
+%% current weakest item.
+next_slot(Equip, Bought) ->
+ L = expand(Equip),
+ case lists:sort([{Mod+Lvl, Entry} || Entry = {Slot, {_, Mod, Lvl, _}} <- L,
+ not lists:member(Slot, Bought)]) of
+ [] -> undefined;
+ [{_, Entry}|_] -> Entry
+ end.
+
+expand(L) ->
+ [expand_field(armor, L),
+ expand_field(helmet, L),
+ expand_field(shield, L),
+ expand_field(weapon, L)].
+
+expand_field(F, L) ->
+ {F, proplists:get_value(F, L, {undefined,0,0,0})}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_stats.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_stats.erl
new file mode 100644
index 0000000..379f5e9
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_stats.erl
@@ -0,0 +1,19 @@
+%%% Rolls dice to generate statistics or level increases for a character
+-module(pq_stats).
+-export([initial_roll/0, roll/0]).
+
+%% First roll, when setting the stats up for the first time
+initial_roll() ->
+ [{charisma, roll(3)},
+ {constitution, roll(3)},
+ {dexterity, roll(3)},
+ {intelligence, roll(3)},
+ {strength, roll(3)},
+ {wisdom, roll(3)}].
+
+%% Rolls a single die. Used when leveling up
+roll() -> roll(1).
+
+%% Rolls Num 6-faced dice
+roll(Num) ->
+ lists:sum([random:uniform(6) || _ <- lists:seq(1,Num)]).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_sup.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_sup.erl
new file mode 100644
index 0000000..de89a9a
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_sup.erl
@@ -0,0 +1,31 @@
+%%% Supervisor for each player. Goes over a pair of a
+%%% gen_fsm (pq_player) and gen_event (pq_events).
+-module(pq_sup).
+-behaviour(supervisor).
+-export([start_link/2]).
+-export([init/1]).
+
+
+start_link(Name, Info) ->
+ supervisor:start_link(?MODULE, {Name,Info}).
+
+%% The name is passed to the events so that
+%% it can register itself as {events, Name} into the
+%% 'regis' regsitry app.
+%% Same for pq_player, which also gets the info.
+%%
+%% It is important that pq_events is started before
+%% pq_player, otherwise we might create race conditions
+%% when starting a player and then quickly generating events to
+%% an event manager that doesn't exist.
+init({Name, Info}) ->
+ {ok,
+ {{one_for_all, 2, 3600},
+ [{events,
+ {pq_events, start_link, [Name]},
+ permanent, 5000, worker, [dynamic]},
+ {player,
+ {pq_player, start_link, [Name, Info]},
+ permanent, 2000, worker, [pq_player]}]}}.
+
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_supersup.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_supersup.erl
new file mode 100644
index 0000000..577bbed
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_supersup.erl
@@ -0,0 +1,28 @@
+%%% pq_supersup is the ProcessQuest top-level supervisor.
+%%% It sits over many pq_sup instances, allowing to have
+%%% a truckload of different players running at once.
+-module(pq_supersup).
+-behaviour(supervisor).
+-export([start_link/0, start_player/2, stop_player/1]).
+-export([init/1]).
+
+%% We register it so that it's guaranteed to be unique
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% Using a SOFO strategy because we get to have many
+%% supervisees of the same type.
+init([]) ->
+ {ok,
+ {{simple_one_for_one, 1, 60000},
+ [{sup,
+ {pq_sup, start_link, []},
+ permanent, infinity, supervisor, [pq_sup]}]}}.
+
+%% Starts an individual player
+start_player(Name, Info) ->
+ supervisor:start_child(?MODULE, [Name, Info]).
+
+%% Stops a player.
+stop_player(Name) ->
+ supervisor:terminate_child(?MODULE, regis:whereis(Name)).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/processquest.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/processquest.erl
new file mode 100644
index 0000000..469dcb0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/processquest.erl
@@ -0,0 +1,39 @@
+%%% ProcessQuest's main wrapping module.
+%%% Start ProcessQuest by calling application:start(processquest).
+%%% Create a player by calling processquest:start_player(Name, Info).
+%%% - Name is any term to identify the player
+%%% - Info is additional information to configure the player. Consult
+%%% the pq_player module for more info.
+%%%
+%%% You can subscribe to the player events by calling
+%%% processquest:subscribe(Name, Handler, Args).
+%%% The handler is a regular event handler. See test/pq_events_handler.erl.
+-module(processquest).
+-behaviour(application).
+-export([start/2, stop/1]).
+-export([start_player/2, stop_player/1, subscribe/3]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% APPLICATION CALLBACKS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+start(normal, []) ->
+ pq_supersup:start_link().
+
+stop(_) -> ok.
+
+%%%%%%%%%%%%%%%%%%%%%%
+%%% USER INTERFACE %%%
+%%%%%%%%%%%%%%%%%%%%%%
+
+%% Starts a player
+start_player(Name, Info) ->
+ pq_supersup:start_player(Name, Info).
+
+%% Stops a player
+stop_player(Name) ->
+ pq_supersup:stop_player(Name).
+
+%% Subscribe to user events
+subscribe(Name, Handler, Args) ->
+ pq_events:add_handler(Name, Handler, Args).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_enemy_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_enemy_tests.erl
new file mode 100644
index 0000000..14742fa
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_enemy_tests.erl
@@ -0,0 +1,33 @@
+-module(pq_enemy_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+is_random_test_() ->
+ F = fun(Parent, Ref) -> fun() ->
+ <<A:32,B:32,C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ Entries = [pq_enemy:fetch() || _ <- lists:seq(1,100)],
+ Parent ! {Ref, Entries}
+ end end,
+ Refs = [begin
+ Ref = make_ref(),
+ spawn_link(F(self(), Ref)),
+ Ref
+ end || _ <- lists:seq(1,3)],
+ [A,B,C] = [receive
+ {Ref, X} -> X
+ end || Ref <- Refs],
+ [?_assert(A =/= B),
+ ?_assert(A =/= C),
+ ?_assert(B =/= C)].
+
+format_test_() ->
+ [[?_assertMatch({_Name, [{drop, {_DropName, _DropVal}},
+ {experience, _Exp}]}, pq_enemy:fetch())
+ || _ <- lists:seq(1,10)],
+ begin
+ {Name, [{drop, {Drop, Val}}, {experience, Exp}]} = pq_enemy:fetch(),
+ [?_assert(is_binary(Name)),
+ ?_assert(is_binary(Drop)),
+ ?_assert(is_integer(Val)),
+ ?_assert(is_integer(Exp))]
+ end].
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_handler.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_handler.erl
new file mode 100644
index 0000000..a02df4c
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_handler.erl
@@ -0,0 +1,24 @@
+%% A fake event handler used for tests
+-module(pq_events_handler).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+init(Parent) -> {ok, Parent}.
+
+handle_event(E, Pid) ->
+ Pid ! E,
+ {ok, Pid}.
+
+handle_call(Req, Pid) ->
+ Pid ! Req,
+ {ok, ok, Pid}.
+
+handle_info(E, Pid) ->
+ Pid ! E,
+ {ok, Pid}.
+
+terminate(_, _) -> ok.
+
+code_change(_OldVsn, Pid, _Extra) ->
+ {ok, Pid}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_tests.erl
new file mode 100644
index 0000000..3a327b7
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_tests.erl
@@ -0,0 +1,55 @@
+-module(pq_events_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}).
+-define(setup(T), ?setup(make_ref(), T)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+events_start_stop_reg_test_() ->
+ {"The event handler can be reached, started and stopped by using the "
+ "player's name",
+ ?setup(can_contact)}.
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start(Name) ->
+ application:start(regis),
+ {ok, Pid} = pq_events:start_link(Name),
+ unlink(Pid),
+ Name.
+
+stop(Name) ->
+ pq_events:stop(Name).
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+can_contact(Name) ->
+ ok = pq_events:add_handler(Name, pq_events_handler, self()),
+ pq_events:notify(Name, hello),
+ L1 = flush(),
+ pq_events:delete_handler(Name, pq_events_handler, []),
+ pq_events:notify(Name, hello),
+ L2 = flush(),
+ [?_assertEqual([hello], L1),
+ ?_assertEqual([], L2)].
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+flush() ->
+ receive
+ X -> [X | flush1()]
+ after 300 ->
+ []
+ end.
+
+flush1() ->
+ receive
+ X -> [X | flush1()]
+ after 0 ->
+ []
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_market_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_market_tests.erl
new file mode 100644
index 0000000..d689f67
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_market_tests.erl
@@ -0,0 +1,15 @@
+-module(pq_market_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+best_smallest_weapon_test_() ->
+ [?_assertMatch({<<"plastic knife">>, 0, 1, 3}, pq_market:weapon(0, 5)),
+ ?_assertMatch({<<"plastic knife">>, 1, 1, 5}, pq_market:weapon(1, 100)),
+ ?_assertMatch(undefined, pq_market:weapon(0,0)),
+ ?_assertMatch(undefined, pq_market:weapon(50000,100000000000000000000))].
+
+best_smallest_gear_test_() ->
+ [[?_assertMatch({<<"wool">>, 0, 1, 25}, pq_market:F(0, 35)),
+ ?_assertMatch({<<"pleather">>, 0, 2, 45}, pq_market:F(1, 100)),
+ ?_assertMatch(undefined, pq_market:F(0,0)),
+ ?_assertMatch(undefined, pq_market:F(50000,100000000000000000000))]
+ || F <- [helmet, shield, armor]].
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_player_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_player_tests.erl
new file mode 100644
index 0000000..bbdbf3d
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_player_tests.erl
@@ -0,0 +1,303 @@
+-module(pq_player_tests).
+-include_lib("eunit/include/eunit.hrl").
+-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1, % copied from pq_player.erl
+ equip=[], money=0, loot=[], bought=[], time=0}).
+
+-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}).
+-define(setup(T), ?setup(make_ref(), T)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+new_player_test_() ->
+ [{"A player holds its own name in its state",
+ ?setup(initial_name)},
+ {"A new player has stats randomly rolled",
+ ?setup(initial_roll)},
+ {"A new player becomes a registered process",
+ ?setup(initial_register)},
+ {"A player has a counter for experience and a counter for "
+ "the next level (which is higher than the current exp)",
+ ?setup(initial_lvl)},
+ {"A player has a basic equipment (empty) when first going",
+ ?setup(initial_equipment)},
+ {"A new player has no money",
+ ?setup(initial_money)},
+ {"A new player has no loot",
+ ?setup(initial_loot)},
+ {"The state of a player can be overriden using the info "
+ "arguments to the init function",
+ ?setup(override_init)}].
+
+market_test_() ->
+ [{"A player with N items will sell all of them to the market "
+ "and end with equivalent money, before switching to the "
+ "buying state",
+ ?setup(sell_all)},
+ {"A player with nearly infinite money will buy items available "
+ "for his money, higher than his level",
+ ?setup(buy_items)},
+ {"A player with no money or no items available for the price "
+ "range leaves for the killing fields.",
+ ?setup(buy_none)},
+ {"Receiving the kill message just forwards to the killing state",
+ ?setup(to_killing)}].
+
+killing_fields_test_() ->
+ [{"Kill enemies until the loot limit is hit. Loot is 2x Strength",
+ ?setup(loot_limit)},
+ {"Killing enemies raises XP until someone levels up",
+ ?setup(kill_xp)},
+ {"Leveling up boosts stats. The sum is higher by at least as "
+ "many points as there are fields, but not more than 6 times. "
+ "Moreover, the rolling is random.",
+ ?setup(lvl_stats)},
+ {"receiving the market message just forwards to the market state",
+ ?setup(to_market)}].
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start(Name) ->
+ application:start(crypto),
+ application:start(regis),
+ Pid = spawn(fun() -> timer:sleep(infinity) end),
+ regis:register({events, Name}, Pid),
+ Name.
+
+stop(Name) ->
+ exit(regis:whereis({events, Name}), kill),
+ application:stop(regis),
+ application:stop(crypto).
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+
+%% Initial State
+initial_name(Name) ->
+ {ok, market, S} = pq_player:init({Name, []}),
+ M = read_event(),
+ [?_assertEqual(Name, S#state.name),
+ ?_assertEqual(M, kill)].
+
+initial_roll(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertMatch([{charisma,_}, {constitution, _}, {dexterity, _},
+ {intelligence, _}, {strength, _}, {wisdom, _}],
+ lists:sort(S#state.stats)).
+
+initial_register(Ref) ->
+ {ok, _, _} = pq_player:init({Ref, []}),
+ _ = read_event(),
+ Pid = regis:whereis(Ref),
+ Ret = pq_player:init({Ref, []}),
+ _ = read_event(),
+ [?_assert(undefined =/= Pid),
+ ?_assert(is_pid(Pid)),
+ ?_assertEqual({stop, name_taken}, Ret)].
+
+initial_lvl(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ [?_assert(is_integer(S#state.lvlexp)),
+ ?_assert(S#state.lvlexp > 0),
+ ?_assert(S#state.exp =:= 0)]. % start at 0 exp
+
+initial_equipment(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ [?_assert(is_list(S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(armor, S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(helmet, S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(weapon, S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(shield, S#state.equip))].
+
+initial_money(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertEqual(0, S#state.money).
+
+initial_loot(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertEqual([], S#state.loot).
+
+override_init(Name) ->
+ {ok, _, Partial} = pq_player:init({Name, [
+ {stats, [{charisma,1}, {constitution,1}, {dexterity,1},
+ {intelligence,1}, {strength,1}, {wisdom,1}]},
+ {lvlexp,1}]}),
+ regis:unregister(Name),
+ _ = read_event(),
+ {ok, _, Complete} = pq_player:init({Name, [
+ {stats, [{charisma,1}, {constitution,1}, {dexterity,1},
+ {intelligence,1}, {strength,1}, {wisdom,1}]},
+ {exp, 1}, {lvlexp,1}, {lvl,9},
+ {equip, [{weapon,{<<"plastic knife">>, -1, 1, 2}}]},
+ {money,1}, {loot, [{<<"Bacon">>, 1}]}, {bought, [helmet]}
+ ]}),
+ _ = read_event(),
+ [?_assertMatch(#state{stats=[{_,1},{_,1},{_,1},{_,1},{_,1},{_,1}],
+ lvlexp=1, lvl=1, exp=0},
+ Partial),
+ ?_assertMatch(#state{stats=[{_,1},{_,1},{_,1},{_,1},{_,1},{_,1}],
+ exp=1, lvlexp=1, lvl=9, equip=[{weapon,_}],
+ money=1, loot=[{_,_}], bought=[_]},
+ Complete)].
+
+%% Market
+sell_all(Name) ->
+ undefined = read_event(),
+ Loot = [proplists:get_value(drop, element(2, pq_enemy:fetch()))
+ || _ <- lists:seq(1,5)],
+ {[Sum1, Sum2, Sum3, Sum4, Sum5], _} = lists:mapfoldl(
+ fun(X, Sum) -> {X+Sum, X+Sum} end,
+ 0,
+ [Val || {_, Val} <- Loot]
+ ),
+ %undefined = read_event(),
+ S0 = #state{name=Name, loot=Loot, money=0, lvl=1},
+ {next_state, market, S1} = pq_player:market(sell, S0),
+ M1 = read_event(),
+ {next_state, market, S2} = pq_player:market(sell, S1),
+ M2 = read_event(),
+ {next_state, market, S3} = pq_player:market(sell, S2),
+ M3 = read_event(),
+ {next_state, market, S4} = pq_player:market(sell, S3),
+ M4 = read_event(),
+ {next_state, market, S5} = pq_player:market(sell, S4),
+ M5 = read_event(),
+ {next_state, market, S6} = pq_player:market(sell, S5),
+ M6 = read_event(),
+ [?_assertMatch(#state{money=Sum1, loot=[_,_,_,_]}, S1),
+ ?_assertMatch(#state{money=Sum2, loot=[_,_,_]}, S2),
+ ?_assertMatch(#state{money=Sum3, loot=[_,_]}, S3),
+ ?_assertMatch(#state{money=Sum4, loot=[_]}, S4),
+ ?_assertMatch(#state{money=Sum5, loot=[]}, S5),
+ ?_assertMatch(#state{money=Sum5, loot=[]}, S6),
+ ?_assertEqual([sell, sell, sell, sell, sell, buy],
+ [M1,M2,M3,M4,M5,M6])].
+
+buy_items(Name) ->
+ %% 4 different pieces of equipment to buy
+ S0 = #state{name=Name, equip=[], money=999999999999},
+ {next_state, market, S1} = pq_player:market(buy, S0),
+ M1 = read_event(),
+ {next_state, market, S2} = pq_player:market(buy, S1),
+ M2 = read_event(),
+ {next_state, market, S3} = pq_player:market(buy, S2),
+ M3 = read_event(),
+ {next_state, market, S4} = pq_player:market(buy, S3),
+ M4 = read_event(),
+ %% All slots bought. Implicit requirement: not buying for the
+ %% same slot twice.
+ {next_state, market, S5} = pq_player:market(buy, S4),
+ M5 = read_event(),
+ [?_assertEqual([S5#state.money, S4#state.money, S3#state.money,
+ S2#state.money, S1#state.money, S0#state.money],
+ lists:sort([S5#state.money, S4#state.money, S3#state.money,
+ S2#state.money, S1#state.money, S0#state.money])),
+ ?_assertEqual([1,2,3,4,4],
+ [length(L) || L <- [S1#state.equip, S2#state.equip,
+ S3#state.equip, S4#state.equip,
+ S5#state.equip]]),
+ ?_assertEqual([buy, buy, buy, buy, kill],
+ [M1, M2, M3, M4, M5])].
+
+buy_none(Name) ->
+ S0 = #state{name=Name, equip=[], money=0},
+ %% one try per part of the equipment
+ {next_state, market, S1} = pq_player:market(buy, S0),
+ _ = read_event(),
+ {next_state, market, S2} = pq_player:market(buy, S1),
+ _ = read_event(),
+ {next_state, market, S3} = pq_player:market(buy, S2),
+ _ = read_event(),
+ {next_state, market, S4} = pq_player:market(buy, S3),
+ M = read_event(),
+ [?_assertEqual(S0, S4),
+ ?_assertEqual(kill, M)].
+
+to_killing(Name) ->
+ S = #state{name=Name},
+ Res = pq_player:market(kill, S),
+ M = read_event(),
+ [?_assertMatch({next_state, killing, S}, Res),
+ ?_assertEqual(kill, M)].
+
+%% Killing fields tests
+loot_limit(Name) ->
+ S0 = #state{name=Name, stats=[{strength, 2}], loot=[]},
+ {next_state, killing, S1 = #state{loot=L1}} = pq_player:killing(kill, S0),
+ M1 = read_event(),
+ {next_state, killing, S2 = #state{loot=L2}} = pq_player:killing(kill, S1),
+ M2 = read_event(),
+ {next_state, killing, S3 = #state{loot=L3}} = pq_player:killing(kill, S2),
+ M3 = read_event(),
+ {next_state, killing, #state{loot=L4}} = pq_player:killing(kill, S3),
+ M4 = read_event(),
+ %% Group identical drops with a counter?
+ [?_assertEqual([1,2,3,4], [length(L) || L <- [L1, L2, L3, L4]]),
+ ?_assertEqual([kill, kill, kill, market], [M1, M2, M3, M4])].
+
+kill_xp(Name) ->
+ S0 = #state{name=Name, stats=[{strength, 999}|pq_stats:initial_roll()],
+ lvl=1, exp=0, lvlexp=5},
+ %% between 1 and 5 kills required to lvl up.
+ {next_state, NS1, S1} = pq_player:killing(kill, S0),
+ M1 = read_event(),
+ {next_state, NS2, S2} = pq_player:NS1(M1, S1),
+ M2 = read_event(),
+ {next_state, NS3, S3} = pq_player:NS2(M2, S2),
+ M3 = read_event(),
+ {next_state, NS4, S4} = pq_player:NS3(M3, S3),
+ M4 = read_event(),
+ {next_state, NS5, S5} = pq_player:NS4(M4, S4),
+ M5 = read_event(),
+ {next_state, NS6, S6} = pq_player:NS5(M5, S5),
+ M6 = read_event(),
+ [?_assert(lists:any(fun(#state{lvl=L}) -> L > 1 end, [S1,S2,S3,S4,S5,S6])),
+ ?_assert(lists:any(fun(#state{lvlexp=L}) -> L >= 10 end, [S1,S2,S3,S4,S5,S6])),
+ ?_assert(lists:any(fun(#state{exp=E}) -> E >= 5 end, [S1,S2,S3,S4,S5,S6])),
+ ?_assert(lists:any(fun(FSMState) -> FSMState =:= killing end,
+ [NS1, NS2, NS3, NS4, NS5, NS6])),
+ ?_assert(lists:any(fun(Msg) -> Msg =:= kill end,
+ [M1, M2, M3, M4, M5, M6])),
+ ?_assert(lists:any(fun(Msg) -> Msg =:= lvl_up end,
+ [M1, M2, M3, M4, M5, M6]))].
+
+lvl_stats(Name) ->
+ {ok, _, S0} = pq_player:init({Name, []}),
+ _ = read_event(),
+ TotalStats = length(S0#state.stats),
+ {next_state, killing, S1} = pq_player:killing(lvl_up, S0),
+ _ = read_event(),
+ {next_state, killing, S2} = pq_player:killing(lvl_up, S0),
+ _ = read_event(),
+ SumInit = lists:sum([Pts || {_,Pts} <- S0#state.stats]),
+ SumS1 = lists:sum([Pts || {_,Pts} <- S1#state.stats]),
+ SumS2 = lists:sum([Pts || {_,Pts} <- S2#state.stats]),
+ [?_assert(SumS1 >= TotalStats+SumInit),
+ ?_assert(SumS2 >= TotalStats+SumInit),
+ ?_assert(SumS1 =< TotalStats*6 + SumInit),
+ ?_assert(SumS2 =< TotalStats*6 + SumInit),
+ ?_assert(S1#state.stats =/= S2#state.stats)].
+
+to_market(Name) ->
+ S = #state{name=Name},
+ Res = pq_player:killing(market, S),
+ M = read_event(),
+ [?_assertMatch({next_state, market, S}, Res),
+ ?_assertEqual(sell, M)].
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+read_event() ->
+ receive
+ {'$gen_event', Msg} -> Msg
+ after 0 ->
+ undefined
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_stats_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_stats_tests.erl
new file mode 100644
index 0000000..da9daa5
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_stats_tests.erl
@@ -0,0 +1,28 @@
+-module(pq_stats_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+all_stats_test_() ->
+ Stats = pq_stats:initial_roll(),
+ {"Checks whether all stats are returned",
+ [?_assertEqual([charisma, constitution, dexterity,
+ intelligence, strength, wisdom],
+ lists:sort(proplists:get_keys(Stats)))]}.
+
+initial_roll_test_() ->
+ Rolls = [pq_stats:initial_roll() || _ <- lists:seq(1,100)],
+ {"All die rolls are made out of 3 d6 dice",
+ %% 6 == number of stats
+ [?_assertEqual(6, length([S || {_,S} <- Stats, S >= 3, S =< 18]))
+ || Stats <- Rolls]}.
+
+initial_random_roll_test_() ->
+ Stats = [pq_stats:initial_roll() || _ <- lists:seq(1,100)],
+ {"All die rolls are random",
+ ?_assertEqual(lists:sort(Stats),
+ lists:sort(sets:to_list(sets:from_list(Stats))))}.
+
+single_die_roll_test_() ->
+ Rolls = [pq_stats:roll() || _ <- lists:seq(1,100)],
+ [?_assertEqual(100, length([N || N <- Rolls, N >= 1, N =< 6])),
+ ?_assert(1 =/= length(sets:to_list(sets:from_list(Rolls))))].
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/processquest_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/processquest_tests.erl
new file mode 100644
index 0000000..fa118b1
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/processquest_tests.erl
@@ -0,0 +1,49 @@
+-module(processquest_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%%% Integration tests verifying the whole app.
+-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}).
+-define(setup(T), ?setup(make_ref(), T)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+integration_test_() ->
+ [{"A player can be started from the processquest module and monitored",
+ ?setup(subscribe)}].
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start(Name) ->
+ application:start(crypto),
+ application:start(regis),
+ application:start(processquest),
+ processquest:start_player(Name, [{time,1100}]),
+ Name.
+
+stop(Name) ->
+ processquest:stop_player(Name),
+ application:stop(processquest),
+ application:stop(regis).
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+subscribe(Name) ->
+ ok = processquest:subscribe(Name, pq_events_handler, self()),
+ timer:sleep(4000),
+ Msgs = flush(),
+ [?_assertMatch([{Name, killed, _Time1, {_EnemyName1, _Props1}},
+ {Name, killed, _Time2, {_EnemyName2, _Props2}},
+ {Name, killed, _Time3, {_EnemyName3, _Props3}}],
+ Msgs)].
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+flush() ->
+ receive
+ X -> [X | flush()]
+ after 0 -> []
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/Emakefile b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/Emakefile
new file mode 100644
index 0000000..b203ea3
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/Emakefile
@@ -0,0 +1,2 @@
+{["src/*", "test/*"],
+ [{i,"include"}, {outdir, "ebin"}]}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/.this-file-intentionally-left-blank
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.app b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.app
new file mode 100644
index 0000000..d923479
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.app
@@ -0,0 +1,8 @@
+{application, processquest,
+ [{description, "Game inspired by the Progress Quest game (http://progressquest.com)"},
+ {vsn, "1.1.0"},
+ {mod, {processquest, []}},
+ {registered, [pq_supersup]},
+ {modules, [processquest, pq_stats, pq_enemy, pq_quest, pq_events,
+ pq_player]},
+ {applications, [stdlib, kernel, regis, crypto]}]}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.appup b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.appup
new file mode 100644
index 0000000..41d305c
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.appup
@@ -0,0 +1,9 @@
+{"1.1.0",
+ [{"1.0.0", [{add_module, pq_quest},
+ {load_module, pq_enemy},
+ {load_module, pq_events},
+ {update, pq_player, {advanced, []}, [pq_quest, pq_events]}]}],
+ [{"1.0.0", [{update, pq_player, {advanced, []}},
+ {delete_module, pq_quest},
+ {load_module, pq_enemy},
+ {load_module, pq_events}]}]}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/include/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/include/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/include/.this-file-intentionally-left-blank
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/priv/.this-file-intentionally-left-blank b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/priv/.this-file-intentionally-left-blank
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/priv/.this-file-intentionally-left-blank
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_enemy.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_enemy.erl
new file mode 100644
index 0000000..e1c2332
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_enemy.erl
@@ -0,0 +1,32 @@
+%% Gives random enemies
+-module(pq_enemy).
+-export([fetch/0]).
+
+fetch() ->
+ L = enemies(),
+ lists:nth(random:uniform(length(L)), L).
+
+enemies() ->
+ [{<<"Spider">>, [{drop, {<<"Spider Egg">>, 1}}, {experience, 1}]},
+ {<<"Wildcat">>, [{drop, {<<"Pelt">>, 1}}, {experience, 1}]},
+ {<<"Pig">>, [{drop, {<<"Bacon">>, 1}}, {experience, 1}]},
+ {<<"Wild Pig">>, [{drop, {<<"Tasty Ribs">>, 2}}, {experience, 1}]},
+ {<<"Goblin">>, [{drop, {<<"Goblin hair">>, 1}}, {experience, 2}]},
+ {<<"Robot">>, [{drop, {<<"Chunks of Metal">>, 3}}, {experience, 2}]},
+ {<<"Factory Worker">>, [{drop, {<<"Wrench">>,2}}, {experience,1}]},
+ {<<"Carnie">>, [{drop, {<<"Cotton Candy">>,1}}, {experience,1}]},
+ {<<"Mad Beaver">>, [{drop, {<<"Wood chips">>, 2}}, {experience, 1}]},
+ {<<"Silent magpie">>, [{drop, {<<"Shiny things">>, 3}}, {experience, 1}]},
+ {<<"Great Lizard">>, [{drop, {<<"Lizard tail">>, 1}}, {experience, 1}]},
+ {<<"Cheetah">>, [{drop, {<<"Fur">>, 3}}, {experience, 4}]},
+ {<<"Radish Horse">>, [{drop, {<<"Horseradish">>,1}}, {experience, 2}]},
+ {<<"Sand Worm">>, [{drop, {<<"Spices">>,10}}, {experience, 25}]},
+ {<<"Mule">>, [{drop, {<<"Map">>, 2}}, {experience, 12}]},
+ {<<"Man Tree">>, [{drop, {<<"branch">>,1}}, {experience, 2}]},
+ {<<"Penguin Lord">>, [{drop, {<<"Penguin Egg">>,1}}, {experience, 3}]},
+ {<<"Cursed Priest">>, [{drop, {<<"Grail">>, 3}}, {experience, 5}]},
+ {<<"Bearded cow">>, [{drop, {<<"Hairy milk">>, 1}}, {experience, 6}]},
+ {<<"Hellish crow">>, [{drop, {<<"Black feather">>, 1}}, {experience, 1}]},
+ {<<"Wolverine">>, [{drop, {<<"Puddle of blood">>, 1}}, {experience, 2}]},
+ {<<"Gangsta Bear">>, [{drop, {<<"Bear Grylls">>, 3}}, {experience, 4}]}].
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_events.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_events.erl
new file mode 100644
index 0000000..faea13e
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_events.erl
@@ -0,0 +1,53 @@
+%%% Wrapper module for the event manager of ProgressQuest players.
+%%% It adds a few functions to wrap the events and sleep on the right
+%%% scale on behalf of the pq_player process.
+-module(pq_events).
+-export([killed/3, location/3, lvl_up/5, buy/4, sell/3, quest/4]).
+-export([start_link/1, stop/1, add_handler/3, delete_handler/3, notify/2]).
+
+start_link(Name) ->
+ {ok, Pid} = gen_event:start_link(),
+ ok = regis:register({events, Name}, Pid),
+ {ok, Pid}.
+
+stop(Name) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:stop(ManagerPid).
+
+add_handler(Name, Handler, Args) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:add_handler(ManagerPid, Handler, Args).
+
+delete_handler(Name, Handler, Args) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:delete_handler(ManagerPid, Handler, Args).
+
+notify(Name, Msg) ->
+ ManagerPid = regis:whereis({events, Name}),
+ gen_event:notify(ManagerPid, Msg).
+
+killed(Name, Enemy = {_EnemyName, _Props}, Time) ->
+ notify(Name, {Name, killed, Time*2, Enemy}),
+ timer:sleep(Time*2).
+
+location(Name, Place, Time) ->
+ notify(Name, {Name, heading, Time, Place}),
+ timer:sleep(Time).
+
+lvl_up(Name, NewStats, NewLvl, NewExp, _Time) ->
+ notify(Name, {Name, lvl_up, 0, NewStats, NewLvl, NewExp}),
+ ok.
+
+buy(Name, Slot, Item, Time) ->
+ T = round(Time/2),
+ notify(Name, {Name, buy, T, Slot, Item}),
+ timer:sleep(T).
+
+sell(Name, Item, Time) ->
+ T = round(Time/5),
+ notify(Name, {Name, sell, T, Item}),
+ timer:sleep(Time).
+
+quest(Name, {Old, _}, {New, _}, _Time) ->
+ notify(Name, {Name, quest, 0, Old, New}),
+ ok.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_market.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_market.erl
new file mode 100644
index 0000000..241e9cd
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_market.erl
@@ -0,0 +1,77 @@
+%%% Can be used to obtain weapons and pieces of equipment of various types
+%%% to be equipped by the hero. The standard format is:
+%%% {Name, LevelModifier, Level, Price}.
+-module(pq_market).
+-export([helmet/2, weapon/2, shield/2, armor/2]).
+
+weapon(CombinedLvl, Money) ->
+ L = [
+ {<<"plastic knife">>, -1, 1, 2},
+ {<<"plastic knife">>, 0, 1, 3},
+ {<<"plastic knife">>, 1, 1, 5},
+ {<<"metal spoon">>, -1, 4, 3},
+ {<<"metal spoon">>, 0, 4, 4},
+ {<<"butter knife">>, -1, 6, 5},
+ {<<"butter knife">>, 0, 6, 7},
+ {<<"butter knife">>, 1, 6, 9},
+ {<<"machete">>, -1, 9, 15},
+ {<<"machete">>, 0, 9, 20},
+ {<<"machete">>, 1, 9, 25},
+ {<<"broad sword">>, -1, 12, 23},
+ {<<"broad sword">>, 0, 12, 30},
+ {<<"broad sword">>, 1, 12, 38},
+ {<<"lance">>, -1, 15, 32},
+ {<<"lance">>, 0, 15, 44},
+ {<<"lance">>, 1, 15, 57},
+ {<<"pistol">>, -1, 25, 95},
+ {<<"pistol">>, 0, 25, 105},
+ {<<"pistol">>, 1, 25, 155},
+ {<<"submachine gun">>, -1, 40, 200},
+ {<<"submachine gun">>, 0, 40, 245},
+ {<<"submachine gun">>, 1, 40, 365}
+ ],
+ first_match(fun(W = {_, Modifier, Lvl, Price}) ->
+ if Modifier+Lvl > CombinedLvl, Price =< Money -> W;
+ true -> continue
+ end
+ end, L).
+
+helmet(CombinedLvl, Money) -> pick_material(CombinedLvl, Money).
+shield(CombinedLvl, Money) -> pick_material(CombinedLvl, Money).
+armor(CombinedLvl, Money) -> pick_material(CombinedLvl, Money).
+
+pick_material(CombinedLvl, Money) ->
+ L = materials(),
+ first_match(fun(W = {_, Modifier, Lvl, Price}) ->
+ if Modifier+Lvl > CombinedLvl, Price =< Money -> W;
+ true -> continue
+ end
+ end, L).
+
+
+first_match(_, []) -> undefined;
+first_match(F, [H|T]) ->
+ case F(H) of
+ continue -> first_match(F,T);
+ Val -> Val
+ end.
+
+materials() ->
+ [{<<"wool">>, 0, 1, 25},
+ {<<"pleather">>, 0, 2, 45},
+ {<<"pleather">>, 1, 2, 50},
+ {<<"pleather">>, 2, 2, 65},
+ {<<"leather">>, -2, 7, 30},
+ {<<"leather">>, -1, 7, 35},
+ {<<"leather">>, 0, 7, 45},
+ {<<"leather">>, 2, 7, 65},
+ {<<"chain mail">>, -2, 12, 70},
+ {<<"chain mail">>, 0, 12, 85},
+ {<<"chain mail">>, 1, 12, 95},
+ {<<"chain mail">>, 2, 12, 105},
+ {<<"plate mail">>, -2, 17, 90},
+ {<<"plate mail">>, -1, 17, 95},
+ {<<"plate mail">>, 0, 17, 105},
+ {<<"plate mail">>, 1, 17, 115},
+ {<<"plate mail">>, 2, 17, 135}].
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_player.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_player.erl
new file mode 100644
index 0000000..9cca7e0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_player.erl
@@ -0,0 +1,216 @@
+%%% The core of ProcessQuest -- the player FSM,
+%%% acting for each of the players in the game.
+%%%
+%%% The FSM actually depends on no external events and only sends messages
+%%% to itself to prompt state changes. This is somewhat unusual as far as
+%%% gen_fsm usages go, but it's pretty useful when needing to work with
+%%% a standalone process.
+-module(pq_player).
+-behaviour(gen_fsm).
+-export([start_link/2]).
+-export([init/1, market/2, killing/2, handle_event/3, handle_sync_event/4,
+ handle_info/3, terminate/3, code_change/4]).
+
+-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1,
+ equip=[], money=0, loot=[], bought=[],
+ time=0, quest}).
+
+
+%%% Possible states & events
+%%
+% sell buy
+% / | | \
+% \ ^ ^ /
+% [market]<--,
+% | |
+% done buying |
+% | bag full
+% v /
+% [killing fields]
+% / V V |
+% \ / | |
+% kill lvl up
+
+start_link(Name, Opts) ->
+ gen_fsm:start_link(?MODULE, {Name, Opts}, []).
+
+init({Name, Opts}) ->
+ %% Properly seeding stuff. If not doing this, the random module will
+ %% seed it based on a slightly unique time value. However, when starting
+ %% many processes at about the same time, the seeds can be very close
+ %% and give barely random results. The crypto:rand_bytes/1 function
+ %% allows for much better seeding.
+ <<A:32, B:32, C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ %% The first event, to start the FSM
+ gen_fsm:send_event(self(), kill),
+ case regis:register(Name, self()) of
+ {error, _} ->
+ {stop, name_taken};
+ ok ->
+ %% Use proplists with default values to let the user configure
+ %% all parts of the FSM's state by using the Opts proplist.
+ S = #state{
+ name=Name,
+ stats=proplists:get_value(stats, Opts, pq_stats:initial_roll()),
+ exp=proplists:get_value(exp, Opts, 0),
+ lvlexp=proplists:get_value(lvlexp, Opts, 1000),
+ lvl=proplists:get_value(lvl, Opts, 1),
+ equip=proplists:get_value(equip, Opts, []),
+ money=proplists:get_value(money, Opts, 0),
+ loot=proplists:get_value(loot, Opts, []),
+ bought=proplists:get_value(bought, Opts, []),
+ time=proplists:get_value(time, Opts, 0),
+ quest=proplists:get_value(quest, Opts, pq_quest:fetch())
+ },
+ {ok, market, S}
+ end.
+
+%% Done selling. Switch to the event where we head to the killing fields
+market(sell, S = #state{loot=[]}) ->
+ gen_fsm:send_event(self(), buy),
+ {next_state, market, S};
+%% Selling an Item we have looted to the market, for whatever value it has
+market(sell, S = #state{loot=[{Drop,Val}|T], money=M, lvl=Lvl}) ->
+ pq_events:sell(S#state.name, {Drop, Val*Lvl}, S#state.time),
+ gen_fsm:send_event(self(), sell),
+ {next_state, market, S#state{loot=T, money=M+Val*Lvl}};
+%% When done selling, buy items with your money
+market(buy, S = #state{equip=Equip, money=Money, bought=Bought}) ->
+ %% we have slots of equipment. It's useless to buy the same
+ %% kind of item time after time, so we must select one we haven't observed yet
+ case next_slot(Equip, Bought) of
+ undefined ->
+ %% when no slot is found, go to the killing field
+ gen_fsm:send_event(self(), kill),
+ {next_state, market, S#state{bought=[]}};
+ OldItem = {Slot, {_Name, Modifier, Lvl, _Price}} ->
+ %% Replace the item by a slightly better one if possible.
+ case pq_market:Slot(Modifier+Lvl, Money) of
+ undefined ->
+ market(buy, S#state{bought=[Slot|Bought]});
+ NewItem = {_, _, _, Price} ->
+ pq_events:buy(S#state.name, Slot, NewItem, S#state.time),
+ gen_fsm:send_event(self(), buy),
+ NewEquip = [{Slot, NewItem} | Equip -- [OldItem]],
+ {next_state, market, S#state{equip=NewEquip,
+ money=Money-Price,
+ bought=[Slot|Bought]}}
+ end
+ end;
+%% Heading to the killing field. State only useful as a state transition.
+market(kill, S) ->
+ pq_events:location(S#state.name, killing, S#state.time),
+ gen_fsm:send_event(self(), kill),
+ {next_state, killing, S}.
+
+%% Killing an enemy on the killing field. Taking its drop and keeping it
+%% in our loot.
+killing(kill, S = #state{loot=Loot, stats=Stats, exp=Exp, lvlexp=LvlExp,
+ quest=Quest}) ->
+ MaxSize = proplists:get_value(strength, Stats)*2,
+ {EnemyName, Props} = pq_enemy:fetch(),
+ pq_events:killed(S#state.name, {EnemyName, Props}, S#state.time),
+ Drop = {_N, _V} = proplists:get_value(drop, Props),
+ KillExp = proplists:get_value(experience, Props),
+ NewLoot = [Drop|Loot],
+ {QuestExp, NewQuest} = case check_quest(Quest) of
+ UpdatedQuest = {0, _} -> UpdatedQuest;
+ QuestBeaten = {_, NewQuest0} ->
+ pq_events:quest(S#state.name, Quest, NewQuest0, S#state.time),
+ QuestBeaten
+ end,
+ if length(NewLoot) =:= MaxSize ->
+ gen_fsm:send_event(self(), market);
+ Exp+KillExp+QuestExp >= LvlExp ->
+ gen_fsm:send_event(self(), lvl_up);
+ true ->
+ gen_fsm:send_event(self(), kill)
+ end,
+ {next_state, killing, S#state{loot=NewLoot,
+ exp=Exp+KillExp+QuestExp,
+ quest=NewQuest}};
+%% If we just leveled up, the stats get updated before we keep killing.
+killing(lvl_up, S = #state{stats=Stats, lvl=Lvl, lvlexp=LvlExp}) ->
+ NewStats = [{charisma, proplists:get_value(charisma, Stats)+pq_stats:roll()},
+ {constitution, proplists:get_value(constitution, Stats)+pq_stats:roll()},
+ {dexterity, proplists:get_value(dexterity, Stats)+pq_stats:roll()},
+ {intelligence, proplists:get_value(intelligence, Stats)+pq_stats:roll()},
+ {strength, proplists:get_value(strength, Stats)+pq_stats:roll()},
+ {wisdom, proplists:get_value(wisdom, Stats)+pq_stats:roll()}],
+ gen_fsm:send_event(self(), kill),
+ pq_events:lvl_up(S#state.name, NewStats, Lvl+1, LvlExp*2, S#state.time),
+ {next_state, killing, S#state{stats=NewStats, lvl=Lvl+1, lvlexp=LvlExp*2}};
+%% Heading to the market state transition
+killing(market, S) ->
+ pq_events:location(S#state.name, market, S#state.time),
+ gen_fsm:send_event(self(), sell),
+ {next_state, market, S}.
+
+handle_event(_Event, StateName, State) ->
+ {next_state, StateName, State}.
+
+handle_sync_event(_Event, _From, StateName, State) ->
+ {next_state, StateName, State}.
+
+handle_info(_Event, StateName, State) ->
+ {next_state, StateName, State}.
+
+terminate(_Reason, _StateName, _State) ->
+ ok.
+
+code_change({down, _},
+ StateName,
+ #state{name=N, stats=S, exp=E, lvlexp=LE, lvl=L, equip=Eq,
+ money=M, loot=Lo, bought=B, time=T},
+ _Extra) ->
+ Old = {state, N, S, E, LE, L, Eq, M, Lo, B, T},
+ {ok, StateName, Old};
+code_change(_OldVsn,
+ StateName,
+ {state, Name, Stats, Exp, LvlExp, Lvl, Equip, Money, Loot,
+ Bought, Time},
+ _Extra) ->
+ State = #state{
+ name=Name, stats=Stats, exp=Exp, lvlexp=LvlExp, lvl=Lvl, equip=Equip,
+ money=Money, loot=Loot, bought=Bought, time=Time, quest=pq_quest:fetch()
+ },
+ {ok, StateName, State}.
+
+%%%%%%%%%%%%%%%
+%%% PRIVATE %%%
+%%%%%%%%%%%%%%%
+
+%% Picks a slot based on what has been seen so far, combined with the
+%% current weakest item.
+next_slot(Equip, Bought) ->
+ L = expand(Equip),
+ case lists:sort([{Mod+Lvl, Entry} || Entry = {Slot, {_, Mod, Lvl, _}} <- L,
+ not lists:member(Slot, Bought)]) of
+ [] -> undefined;
+ [{_, Entry}|_] -> Entry
+ end.
+
+expand(L) ->
+ [expand_field(armor, L),
+ expand_field(helmet, L),
+ expand_field(shield, L),
+ expand_field(weapon, L)].
+
+expand_field(F, L) ->
+ {F, proplists:get_value(F, L, {undefined,0,0,0})}.
+
+%% Checks quests, if they are ready for the next level or not
+check_quest({Name, Props}) ->
+ case proplists:get_value(kills, Props) of
+ 1 ->
+ case pq_quest:fetch() of
+ %% Same name, we want new stuff!
+ {Name, _} -> check_quest({Name, Props});
+ NewQuest ->
+ Exp = proplists:get_value(experience, Props),
+ {Exp, NewQuest}
+ end;
+ Q ->
+ {0, {Name, [{kills,Q-1} | Props--[{kills,Q}]]}}
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_quest.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_quest.erl
new file mode 100644
index 0000000..b8294fb
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_quest.erl
@@ -0,0 +1,15 @@
+-module(pq_quest).
+-export([fetch/0]).
+
+fetch() ->
+ L = quests(),
+ lists:nth(random:uniform(length(L)), L).
+
+quests() ->
+ [{<<"Fetch me a nut">>, [{experience, 150}, {kills, 20}]},
+ {<<"Cancel the festival">>, [{experience, 65}, {kills, 8}]},
+ {<<"Summon the dragon">>, [{experience, 1000}, {kills, 100}]},
+ {<<"Meet the invisible man">>, [{experience, 200}, {kills, 25}]},
+ {<<"Find quest ideas">>, [{experience, 340}, {kills, 32}]},
+ {<<"Invent maple syrup">>, [{experience, 1500}, {kills, 175}]},
+ {<<"Slay the Bieber">>, [{experience, 500}, {kills, 45}]}].
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_stats.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_stats.erl
new file mode 100644
index 0000000..379f5e9
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_stats.erl
@@ -0,0 +1,19 @@
+%%% Rolls dice to generate statistics or level increases for a character
+-module(pq_stats).
+-export([initial_roll/0, roll/0]).
+
+%% First roll, when setting the stats up for the first time
+initial_roll() ->
+ [{charisma, roll(3)},
+ {constitution, roll(3)},
+ {dexterity, roll(3)},
+ {intelligence, roll(3)},
+ {strength, roll(3)},
+ {wisdom, roll(3)}].
+
+%% Rolls a single die. Used when leveling up
+roll() -> roll(1).
+
+%% Rolls Num 6-faced dice
+roll(Num) ->
+ lists:sum([random:uniform(6) || _ <- lists:seq(1,Num)]).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_sup.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_sup.erl
new file mode 100644
index 0000000..de89a9a
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_sup.erl
@@ -0,0 +1,31 @@
+%%% Supervisor for each player. Goes over a pair of a
+%%% gen_fsm (pq_player) and gen_event (pq_events).
+-module(pq_sup).
+-behaviour(supervisor).
+-export([start_link/2]).
+-export([init/1]).
+
+
+start_link(Name, Info) ->
+ supervisor:start_link(?MODULE, {Name,Info}).
+
+%% The name is passed to the events so that
+%% it can register itself as {events, Name} into the
+%% 'regis' regsitry app.
+%% Same for pq_player, which also gets the info.
+%%
+%% It is important that pq_events is started before
+%% pq_player, otherwise we might create race conditions
+%% when starting a player and then quickly generating events to
+%% an event manager that doesn't exist.
+init({Name, Info}) ->
+ {ok,
+ {{one_for_all, 2, 3600},
+ [{events,
+ {pq_events, start_link, [Name]},
+ permanent, 5000, worker, [dynamic]},
+ {player,
+ {pq_player, start_link, [Name, Info]},
+ permanent, 2000, worker, [pq_player]}]}}.
+
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_supersup.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_supersup.erl
new file mode 100644
index 0000000..577bbed
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_supersup.erl
@@ -0,0 +1,28 @@
+%%% pq_supersup is the ProcessQuest top-level supervisor.
+%%% It sits over many pq_sup instances, allowing to have
+%%% a truckload of different players running at once.
+-module(pq_supersup).
+-behaviour(supervisor).
+-export([start_link/0, start_player/2, stop_player/1]).
+-export([init/1]).
+
+%% We register it so that it's guaranteed to be unique
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% Using a SOFO strategy because we get to have many
+%% supervisees of the same type.
+init([]) ->
+ {ok,
+ {{simple_one_for_one, 1, 60000},
+ [{sup,
+ {pq_sup, start_link, []},
+ permanent, infinity, supervisor, [pq_sup]}]}}.
+
+%% Starts an individual player
+start_player(Name, Info) ->
+ supervisor:start_child(?MODULE, [Name, Info]).
+
+%% Stops a player.
+stop_player(Name) ->
+ supervisor:terminate_child(?MODULE, regis:whereis(Name)).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/processquest.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/processquest.erl
new file mode 100644
index 0000000..469dcb0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/processquest.erl
@@ -0,0 +1,39 @@
+%%% ProcessQuest's main wrapping module.
+%%% Start ProcessQuest by calling application:start(processquest).
+%%% Create a player by calling processquest:start_player(Name, Info).
+%%% - Name is any term to identify the player
+%%% - Info is additional information to configure the player. Consult
+%%% the pq_player module for more info.
+%%%
+%%% You can subscribe to the player events by calling
+%%% processquest:subscribe(Name, Handler, Args).
+%%% The handler is a regular event handler. See test/pq_events_handler.erl.
+-module(processquest).
+-behaviour(application).
+-export([start/2, stop/1]).
+-export([start_player/2, stop_player/1, subscribe/3]).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% APPLICATION CALLBACKS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+start(normal, []) ->
+ pq_supersup:start_link().
+
+stop(_) -> ok.
+
+%%%%%%%%%%%%%%%%%%%%%%
+%%% USER INTERFACE %%%
+%%%%%%%%%%%%%%%%%%%%%%
+
+%% Starts a player
+start_player(Name, Info) ->
+ pq_supersup:start_player(Name, Info).
+
+%% Stops a player
+stop_player(Name) ->
+ pq_supersup:stop_player(Name).
+
+%% Subscribe to user events
+subscribe(Name, Handler, Args) ->
+ pq_events:add_handler(Name, Handler, Args).
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_enemy_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_enemy_tests.erl
new file mode 100644
index 0000000..14742fa
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_enemy_tests.erl
@@ -0,0 +1,33 @@
+-module(pq_enemy_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+is_random_test_() ->
+ F = fun(Parent, Ref) -> fun() ->
+ <<A:32,B:32,C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ Entries = [pq_enemy:fetch() || _ <- lists:seq(1,100)],
+ Parent ! {Ref, Entries}
+ end end,
+ Refs = [begin
+ Ref = make_ref(),
+ spawn_link(F(self(), Ref)),
+ Ref
+ end || _ <- lists:seq(1,3)],
+ [A,B,C] = [receive
+ {Ref, X} -> X
+ end || Ref <- Refs],
+ [?_assert(A =/= B),
+ ?_assert(A =/= C),
+ ?_assert(B =/= C)].
+
+format_test_() ->
+ [[?_assertMatch({_Name, [{drop, {_DropName, _DropVal}},
+ {experience, _Exp}]}, pq_enemy:fetch())
+ || _ <- lists:seq(1,10)],
+ begin
+ {Name, [{drop, {Drop, Val}}, {experience, Exp}]} = pq_enemy:fetch(),
+ [?_assert(is_binary(Name)),
+ ?_assert(is_binary(Drop)),
+ ?_assert(is_integer(Val)),
+ ?_assert(is_integer(Exp))]
+ end].
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_handler.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_handler.erl
new file mode 100644
index 0000000..a02df4c
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_handler.erl
@@ -0,0 +1,24 @@
+%% A fake event handler used for tests
+-module(pq_events_handler).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+init(Parent) -> {ok, Parent}.
+
+handle_event(E, Pid) ->
+ Pid ! E,
+ {ok, Pid}.
+
+handle_call(Req, Pid) ->
+ Pid ! Req,
+ {ok, ok, Pid}.
+
+handle_info(E, Pid) ->
+ Pid ! E,
+ {ok, Pid}.
+
+terminate(_, _) -> ok.
+
+code_change(_OldVsn, Pid, _Extra) ->
+ {ok, Pid}.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_tests.erl
new file mode 100644
index 0000000..3a327b7
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_tests.erl
@@ -0,0 +1,55 @@
+-module(pq_events_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}).
+-define(setup(T), ?setup(make_ref(), T)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+events_start_stop_reg_test_() ->
+ {"The event handler can be reached, started and stopped by using the "
+ "player's name",
+ ?setup(can_contact)}.
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start(Name) ->
+ application:start(regis),
+ {ok, Pid} = pq_events:start_link(Name),
+ unlink(Pid),
+ Name.
+
+stop(Name) ->
+ pq_events:stop(Name).
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+can_contact(Name) ->
+ ok = pq_events:add_handler(Name, pq_events_handler, self()),
+ pq_events:notify(Name, hello),
+ L1 = flush(),
+ pq_events:delete_handler(Name, pq_events_handler, []),
+ pq_events:notify(Name, hello),
+ L2 = flush(),
+ [?_assertEqual([hello], L1),
+ ?_assertEqual([], L2)].
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+flush() ->
+ receive
+ X -> [X | flush1()]
+ after 300 ->
+ []
+ end.
+
+flush1() ->
+ receive
+ X -> [X | flush1()]
+ after 0 ->
+ []
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_market_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_market_tests.erl
new file mode 100644
index 0000000..d689f67
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_market_tests.erl
@@ -0,0 +1,15 @@
+-module(pq_market_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+best_smallest_weapon_test_() ->
+ [?_assertMatch({<<"plastic knife">>, 0, 1, 3}, pq_market:weapon(0, 5)),
+ ?_assertMatch({<<"plastic knife">>, 1, 1, 5}, pq_market:weapon(1, 100)),
+ ?_assertMatch(undefined, pq_market:weapon(0,0)),
+ ?_assertMatch(undefined, pq_market:weapon(50000,100000000000000000000))].
+
+best_smallest_gear_test_() ->
+ [[?_assertMatch({<<"wool">>, 0, 1, 25}, pq_market:F(0, 35)),
+ ?_assertMatch({<<"pleather">>, 0, 2, 45}, pq_market:F(1, 100)),
+ ?_assertMatch(undefined, pq_market:F(0,0)),
+ ?_assertMatch(undefined, pq_market:F(50000,100000000000000000000))]
+ || F <- [helmet, shield, armor]].
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_player_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_player_tests.erl
new file mode 100644
index 0000000..d0f9c62
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_player_tests.erl
@@ -0,0 +1,395 @@
+-module(pq_player_tests).
+-include_lib("eunit/include/eunit.hrl").
+-record(state, {name, stats, exp=0, lvlexp=1000, lvl=1, % copied from pq_player.erl
+ equip=[], money=0, loot=[], bought=[],
+ time=0, quest}).
+
+-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}).
+-define(setup(T), ?setup(make_ref(), T)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+new_player_test_() ->
+ [{"A player holds its own name in its state",
+ ?setup(initial_name)},
+ {"A new player has stats randomly rolled",
+ ?setup(initial_roll)},
+ {"A new player becomes a registered process",
+ ?setup(initial_register)},
+ {"A player has a counter for experience and a counter for "
+ "the next level (which is higher than the current exp)",
+ ?setup(initial_lvl)},
+ {"A player has a basic equipment (empty) when first going",
+ ?setup(initial_equipment)},
+ {"A new player has no money",
+ ?setup(initial_money)},
+ {"A new player has no loot",
+ ?setup(initial_loot)},
+ {"A new player has a quest given to him",
+ ?setup(initial_quest)},
+ {"The state of a player can be overriden using the info "
+ "arguments to the init function",
+ ?setup(override_init)}].
+
+market_test_() ->
+ [{"A player with N items will sell all of them to the market "
+ "and end with equivalent money, before switching to the "
+ "buying state",
+ ?setup(sell_all)},
+ {"A player with nearly infinite money will buy items available "
+ "for his money, higher than his level",
+ ?setup(buy_items)},
+ {"A player with no money or no items available for the price "
+ "range leaves for the killing fields.",
+ ?setup(buy_none)},
+ {"Receiving the kill message just forwards to the killing state",
+ ?setup(to_killing)}].
+
+killing_fields_test_() ->
+ [{"Kill enemies until the loot limit is hit. Loot is 2x Strength",
+ ?setup(loot_limit)},
+ {"Killing enemies raises XP until someone levels up",
+ ?setup(kill_xp)},
+ {"Leveling up boosts stats. The sum is higher by at least as "
+ "many points as there are fields, but not more than 6 times. "
+ "Moreover, the rolling is random.",
+ ?setup(lvl_stats)},
+ {"receiving the market message just forwards to the market state",
+ ?setup(to_market)}].
+
+quest_test_() ->
+ [{"A player who kills enough enemies to complete a quest obtains "
+ "a new, different one",
+ ?setup(quest_change)},
+ {"A player who kills an enemy sees the quest kill count change",
+ ?setup(quest_kill_count)},
+ {"A player who completes a quest obtains XP in exchange",
+ ?setup(quest_exp)}].
+
+code_change_test_() ->
+ [{"Updating", update()},
+ {"Downgrading", downgrade()}].
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start(Name) ->
+ application:start(crypto),
+ application:start(regis),
+ Pid = spawn(fun() -> timer:sleep(infinity) end),
+ regis:register({events, Name}, Pid),
+ Name.
+
+stop(Name) ->
+ exit(regis:whereis({events, Name}), kill),
+ application:stop(regis),
+ application:stop(crypto).
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+
+%% Initial State
+initial_name(Name) ->
+ {ok, market, S} = pq_player:init({Name, []}),
+ M = read_event(),
+ [?_assertEqual(Name, S#state.name),
+ ?_assertEqual(M, kill)].
+
+initial_roll(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertMatch([{charisma,_}, {constitution, _}, {dexterity, _},
+ {intelligence, _}, {strength, _}, {wisdom, _}],
+ lists:sort(S#state.stats)).
+
+initial_register(Ref) ->
+ {ok, _, _} = pq_player:init({Ref, []}),
+ _ = read_event(),
+ Pid = regis:whereis(Ref),
+ Ret = pq_player:init({Ref, []}),
+ _ = read_event(),
+ [?_assert(undefined =/= Pid),
+ ?_assert(is_pid(Pid)),
+ ?_assertEqual({stop, name_taken}, Ret)].
+
+initial_lvl(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ [?_assert(is_integer(S#state.lvlexp)),
+ ?_assert(S#state.lvlexp > 0),
+ ?_assert(S#state.exp =:= 0)]. % start at 0 exp
+
+initial_equipment(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ [?_assert(is_list(S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(armor, S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(helmet, S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(weapon, S#state.equip)),
+ ?_assertEqual(undefined, proplists:get_value(shield, S#state.equip))].
+
+initial_money(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertEqual(0, S#state.money).
+
+initial_loot(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertEqual([], S#state.loot).
+
+initial_quest(Name) ->
+ {ok, _, S} = pq_player:init({Name, []}),
+ _ = read_event(),
+ ?_assertMatch({_QuestName, _Props}, S#state.quest).
+
+override_init(Name) ->
+ {ok, _, Partial} = pq_player:init({Name, [
+ {stats, [{charisma,1}, {constitution,1}, {dexterity,1},
+ {intelligence,1}, {strength,1}, {wisdom,1}]},
+ {lvlexp,1}]}),
+ regis:unregister(Name),
+ _ = read_event(),
+ {ok, _, Complete} = pq_player:init({Name, [
+ {stats, [{charisma,1}, {constitution,1}, {dexterity,1},
+ {intelligence,1}, {strength,1}, {wisdom,1}]},
+ {exp, 1}, {lvlexp,1}, {lvl,9},
+ {equip, [{weapon,{<<"plastic knife">>, -1, 1, 2}}]},
+ {money,1}, {loot, [{<<"Bacon">>, 1}]}, {bought, [helmet]},
+ {time, 1},
+ {quest, {<<"A">>, [{experience, 1},{kills,1}]}}
+ ]}),
+ _ = read_event(),
+ [?_assertMatch(#state{stats=[{_,1},{_,1},{_,1},{_,1},{_,1},{_,1}],
+ lvlexp=1, lvl=1, exp=0},
+ Partial),
+ ?_assertMatch(#state{stats=[{_,1},{_,1},{_,1},{_,1},{_,1},{_,1}],
+ exp=1, lvlexp=1, lvl=9, equip=[{weapon,_}],
+ money=1, loot=[{_,_}], bought=[_], time=1,
+ quest={<<"A">>, [_|_]}},
+ Complete)].
+
+%% Market
+sell_all(Name) ->
+ Loot = [proplists:get_value(drop, element(2, pq_enemy:fetch()))
+ || _ <- lists:seq(1,5)],
+ [{_, LV1}, {_, LV2}, {_, LV3}, {_, LV4}, {_, LV5}] = Loot,
+ LootVals = [LV1, LV2*2, LV3*3, LV4*4, LV5*5],
+ {[Sum1, Sum2, Sum3, Sum4, Sum5], _} = lists:mapfoldl(
+ fun(X, Sum) -> {X+Sum, X+Sum} end,
+ 0,
+ LootVals
+ ),
+ undefined = read_event(),
+ S0 = #state{name=Name, loot=Loot, money=0},
+ {next_state, market, S1} = pq_player:market(sell, S0),
+ M1 = read_event(),
+ {next_state, market, S2} = pq_player:market(sell, S1#state{lvl=2}),
+ M2 = read_event(),
+ {next_state, market, S3} = pq_player:market(sell, S2#state{lvl=3}),
+ M3 = read_event(),
+ {next_state, market, S4} = pq_player:market(sell, S3#state{lvl=4}),
+ M4 = read_event(),
+ {next_state, market, S5} = pq_player:market(sell, S4#state{lvl=5}),
+ M5 = read_event(),
+ {next_state, market, S6} = pq_player:market(sell, S5#state{lvl=6}),
+ M6 = read_event(),
+ [?_assertMatch(#state{money=Sum1, loot=[_,_,_,_]}, S1),
+ ?_assertMatch(#state{money=Sum2, loot=[_,_,_]}, S2),
+ ?_assertMatch(#state{money=Sum3, loot=[_,_]}, S3),
+ ?_assertMatch(#state{money=Sum4, loot=[_]}, S4),
+ ?_assertMatch(#state{money=Sum5, loot=[]}, S5),
+ ?_assertMatch(#state{money=Sum5, loot=[]}, S6),
+ ?_assertEqual([sell, sell, sell, sell, sell, buy],
+ [M1,M2,M3,M4,M5,M6])].
+
+buy_items(Name) ->
+ %% 4 different pieces of equipment to buy
+ S0 = #state{name=Name, equip=[], money=999999999999},
+ {next_state, market, S1} = pq_player:market(buy, S0),
+ M1 = read_event(),
+ {next_state, market, S2} = pq_player:market(buy, S1),
+ M2 = read_event(),
+ {next_state, market, S3} = pq_player:market(buy, S2),
+ M3 = read_event(),
+ {next_state, market, S4} = pq_player:market(buy, S3),
+ M4 = read_event(),
+ %% All slots bought. Implicit requirement: not buying for the
+ %% same slot twice.
+ {next_state, market, S5} = pq_player:market(buy, S4),
+ M5 = read_event(),
+ [?_assertEqual([S5#state.money, S4#state.money, S3#state.money,
+ S2#state.money, S1#state.money, S0#state.money],
+ lists:sort([S5#state.money, S4#state.money, S3#state.money,
+ S2#state.money, S1#state.money, S0#state.money])),
+ ?_assertEqual([1,2,3,4,4],
+ [length(L) || L <- [S1#state.equip, S2#state.equip,
+ S3#state.equip, S4#state.equip,
+ S5#state.equip]]),
+ ?_assertEqual([buy, buy, buy, buy, kill],
+ [M1, M2, M3, M4, M5])].
+
+buy_none(Name) ->
+ S0 = #state{name=Name, equip=[], money=0},
+ %% one try per part of the equipment
+ {next_state, market, S1} = pq_player:market(buy, S0),
+ _ = read_event(),
+ {next_state, market, S2} = pq_player:market(buy, S1),
+ _ = read_event(),
+ {next_state, market, S3} = pq_player:market(buy, S2),
+ _ = read_event(),
+ {next_state, market, S4} = pq_player:market(buy, S3),
+ M = read_event(),
+ [?_assertEqual(S0, S4),
+ ?_assertEqual(kill, M)].
+
+to_killing(Name) ->
+ S = #state{name=Name},
+ Res = pq_player:market(kill, S),
+ M = read_event(),
+ [?_assertMatch({next_state, killing, S}, Res),
+ ?_assertEqual(kill, M)].
+
+%% Killing fields tests
+loot_limit(Name) ->
+ S0 = #state{name=Name, stats=[{strength, 2}], loot=[],
+ quest={<<1>>,[{experience,0},{kills,10000}]}},
+ {next_state, killing, S1 = #state{loot=L1}} = pq_player:killing(kill, S0),
+ M1 = read_event(),
+ {next_state, killing, S2 = #state{loot=L2}} = pq_player:killing(kill, S1),
+ M2 = read_event(),
+ {next_state, killing, S3 = #state{loot=L3}} = pq_player:killing(kill, S2),
+ M3 = read_event(),
+ {next_state, killing, #state{loot=L4}} = pq_player:killing(kill, S3),
+ M4 = read_event(),
+ %% Group identical drops with a counter?
+ [?_assertEqual([1,2,3,4], [length(L) || L <- [L1, L2, L3, L4]]),
+ ?_assertEqual([kill, kill, kill, market], [M1, M2, M3, M4])].
+
+kill_xp(Name) ->
+ S0 = #state{name=Name, stats=[{strength, 999}|pq_stats:initial_roll()],
+ lvl=1, exp=0, lvlexp=5,
+ quest={<<1>>,[{experience,0},{kills,10000}]}},
+ %% between 1 and 5 kills required to lvl up.
+ {next_state, NS1, S1} = pq_player:killing(kill, S0),
+ M1 = read_event(),
+ {next_state, NS2, S2} = pq_player:NS1(M1, S1),
+ M2 = read_event(),
+ {next_state, NS3, S3} = pq_player:NS2(M2, S2),
+ M3 = read_event(),
+ {next_state, NS4, S4} = pq_player:NS3(M3, S3),
+ M4 = read_event(),
+ {next_state, NS5, S5} = pq_player:NS4(M4, S4),
+ M5 = read_event(),
+ {next_state, NS6, S6} = pq_player:NS5(M5, S5),
+ M6 = read_event(),
+ [?_assert(lists:any(fun(#state{lvl=L}) -> L > 1 end, [S1,S2,S3,S4,S5,S6])),
+ ?_assert(lists:any(fun(#state{lvlexp=L}) -> L >= 10 end, [S1,S2,S3,S4,S5,S6])),
+ ?_assert(lists:any(fun(#state{exp=E}) -> E >= 5 end, [S1,S2,S3,S4,S5,S6])),
+ ?_assert(lists:any(fun(FSMState) -> FSMState =:= killing end,
+ [NS1, NS2, NS3, NS4, NS5, NS6])),
+ ?_assert(lists:any(fun(Msg) -> Msg =:= kill end,
+ [M1, M2, M3, M4, M5, M6])),
+ ?_assert(lists:any(fun(Msg) -> Msg =:= lvl_up end,
+ [M1, M2, M3, M4, M5, M6]))].
+
+lvl_stats(Name) ->
+ {ok, _, S0} = pq_player:init({Name, []}),
+ _ = read_event(),
+ TotalStats = length(S0#state.stats),
+ {next_state, killing, S1} = pq_player:killing(lvl_up, S0),
+ _ = read_event(),
+ {next_state, killing, S2} = pq_player:killing(lvl_up, S0),
+ _ = read_event(),
+ SumInit = lists:sum([Pts || {_,Pts} <- S0#state.stats]),
+ SumS1 = lists:sum([Pts || {_,Pts} <- S1#state.stats]),
+ SumS2 = lists:sum([Pts || {_,Pts} <- S2#state.stats]),
+ [?_assert(SumS1 >= TotalStats+SumInit),
+ ?_assert(SumS2 >= TotalStats+SumInit),
+ ?_assert(SumS1 =< TotalStats*6 + SumInit),
+ ?_assert(SumS2 =< TotalStats*6 + SumInit),
+ ?_assert(S1#state.stats =/= S2#state.stats)].
+
+to_market(Name) ->
+ S = #state{name=Name},
+ Res = pq_player:killing(market, S),
+ M = read_event(),
+ [?_assertMatch({next_state, market, S}, Res),
+ ?_assertEqual(sell, M)].
+
+%% Quests
+quest_change(Name) ->
+ {ok, _, S0=#state{quest={N, Props}}} = pq_player:init({Name, []}),
+ _ = read_event(),
+ S = S0#state{quest={N, [{kills,1}|Props]}},
+ States = [element(3, pq_player:killing(kill, S)) || _ <- lists:seq(1,10)],
+ [read_event() || _ <- lists:seq(1,10)],
+ [?_assert(lists:all(fun(#state{quest={QName, _}}) -> QName =/= N end,
+ States))].
+
+quest_kill_count(Name) ->
+ {ok, _, S0=#state{quest={N, Props}}} = pq_player:init({Name, []}),
+ _ = read_event(),
+ S1 = S0#state{quest={N, [{kills,3}|Props]}},
+ {next_state, _, S2} = pq_player:killing(kill, S1),
+ _ = read_event(),
+ [?_assertEqual(2, proplists:get_value(kills,element(2,S2#state.quest)))].
+
+quest_exp(Name) ->
+ {ok, _, S0=#state{quest={N, _Props}}} = pq_player:init({Name, [{lvl,0},{exp,0}]}),
+ _ = read_event(),
+ S1 = S0#state{quest={N, [{kills,2},{experience, 100000}]}},
+ {next_state, _, S2 = #state{exp=E1}} = pq_player:killing(kill, S1),
+ _ = read_event(),
+ {next_state, _, #state{exp=E2}} = pq_player:killing(kill, S2),
+ _ = read_event(),
+ [?_assert(E1 < 100000),
+ ?_assert(E2 > 100000)].
+
+%% Update/Downgrade
+
+update() ->
+ Stats = [{charisma,1}, {constitution,1}, {dexterity,1},
+ {intelligence,1}, {strength,1}, {wisdom,1}],
+ OldState = {state, joe,
+ Stats,
+ 1, % exp
+ 2, % lvlexp
+ 3, % lvl
+ [{weapon,{<<"a">>, -1, 1, 2}}], % equip
+ 4, % money
+ [{<<"b">>, 1}], % loot
+ [helmet], % bought
+ 5 % time
+ },
+ ?_assertMatch({ok,
+ market,
+ #state{name=joe, stats=Stats, exp=1, lvlexp=2,
+ lvl=3, equip=[{weapon,{<<"a">>,-1,1,2}}],
+ money=4, loot=[{<<"b">>,1}], bought=[helmet],
+ quest={_, [_|_]}}},
+ pq_player:code_change("some version hash", market, OldState, none)).
+
+downgrade() ->
+ Stats = [{charisma,1}, {constitution,1}, {dexterity,1},
+ {intelligence,1}, {strength,1}, {wisdom,1}],
+ State = #state{name=joe, stats=Stats, exp=1, lvlexp=2,
+ lvl=3, equip=[{weapon,{<<"a">>,-1,1,2}}],
+ money=4, loot=[{<<"b">>,1}], bought=[helmet],
+ time=5, quest={<<"this goes">>, []}},
+ NewState = {state, joe, Stats, 1, 2, 3, [{weapon,{<<"a">>, -1, 1, 2}}],
+ 4, [{<<"b">>, 1}], [helmet], 5},
+ ?_assertMatch({ok, market, NewState},
+ pq_player:code_change({down, "old vsn hash"}, market, State, none)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+read_event() ->
+ receive
+ {'$gen_event', Msg} -> Msg
+ after 0 ->
+ undefined
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_quest_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_quest_tests.erl
new file mode 100644
index 0000000..4e4f98c
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_quest_tests.erl
@@ -0,0 +1,30 @@
+-module(pq_quest_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+format_test_() ->
+ Quest = pq_quest:fetch(),
+ [?_assertMatch({_Name, _Props}, Quest),
+ ?_assert(is_binary(element(1,Quest))),
+ ?_assert(is_integer(proplists:get_value(kills, element(2,Quest)))),
+ ?_assert(0 < proplists:get_value(kills, element(2,Quest))),
+ ?_assert(is_integer(proplists:get_value(experience, element(2,Quest)))),
+ ?_assert(0 < proplists:get_value(experience, element(2,Quest)))].
+
+is_random_test_() ->
+ F = fun(Parent, Ref) -> fun() ->
+ <<A:32,B:32,C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ Entries = [pq_quest:fetch() || _ <- lists:seq(1,100)],
+ Parent ! {Ref, Entries}
+ end end,
+ Refs = [begin
+ Ref = make_ref(),
+ spawn_link(F(self(), Ref)),
+ Ref
+ end || _ <- lists:seq(1,3)],
+ [A,B,C] = [receive
+ {Ref, X} -> X
+ end || Ref <- Refs],
+ [?_assert(A =/= B),
+ ?_assert(A =/= C),
+ ?_assert(B =/= C)].
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_stats_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_stats_tests.erl
new file mode 100644
index 0000000..da9daa5
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_stats_tests.erl
@@ -0,0 +1,28 @@
+-module(pq_stats_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+all_stats_test_() ->
+ Stats = pq_stats:initial_roll(),
+ {"Checks whether all stats are returned",
+ [?_assertEqual([charisma, constitution, dexterity,
+ intelligence, strength, wisdom],
+ lists:sort(proplists:get_keys(Stats)))]}.
+
+initial_roll_test_() ->
+ Rolls = [pq_stats:initial_roll() || _ <- lists:seq(1,100)],
+ {"All die rolls are made out of 3 d6 dice",
+ %% 6 == number of stats
+ [?_assertEqual(6, length([S || {_,S} <- Stats, S >= 3, S =< 18]))
+ || Stats <- Rolls]}.
+
+initial_random_roll_test_() ->
+ Stats = [pq_stats:initial_roll() || _ <- lists:seq(1,100)],
+ {"All die rolls are random",
+ ?_assertEqual(lists:sort(Stats),
+ lists:sort(sets:to_list(sets:from_list(Stats))))}.
+
+single_die_roll_test_() ->
+ Rolls = [pq_stats:roll() || _ <- lists:seq(1,100)],
+ [?_assertEqual(100, length([N || N <- Rolls, N >= 1, N =< 6])),
+ ?_assert(1 =/= length(sets:to_list(sets:from_list(Rolls))))].
+
diff --git a/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/processquest_tests.erl b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/processquest_tests.erl
new file mode 100644
index 0000000..fa118b1
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/processquest_tests.erl
@@ -0,0 +1,49 @@
+-module(processquest_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+%%% Integration tests verifying the whole app.
+-define(setup(Name, T), {setup, fun() -> start(Name) end, fun stop/1, fun T/1}).
+-define(setup(T), ?setup(make_ref(), T)).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+integration_test_() ->
+ [{"A player can be started from the processquest module and monitored",
+ ?setup(subscribe)}].
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start(Name) ->
+ application:start(crypto),
+ application:start(regis),
+ application:start(processquest),
+ processquest:start_player(Name, [{time,1100}]),
+ Name.
+
+stop(Name) ->
+ processquest:stop_player(Name),
+ application:stop(processquest),
+ application:stop(regis).
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+subscribe(Name) ->
+ ok = processquest:subscribe(Name, pq_events_handler, self()),
+ timer:sleep(4000),
+ Msgs = flush(),
+ [?_assertMatch([{Name, killed, _Time1, {_EnemyName1, _Props1}},
+ {Name, killed, _Time2, {_EnemyName2, _Props2}},
+ {Name, killed, _Time3, {_EnemyName3, _Props3}}],
+ Msgs)].
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+flush() ->
+ receive
+ X -> [X | flush()]
+ after 0 -> []
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/Emakefile b/learn-you-some-erlang/processquest/apps/regis-1.0.0/Emakefile
new file mode 100644
index 0000000..b8be313
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/Emakefile
@@ -0,0 +1,2 @@
+{["src/*", "test/*"], [{outdir, "ebin"}]}.
+
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/ebin/regis.app b/learn-you-some-erlang/processquest/apps/regis-1.0.0/ebin/regis.app
new file mode 100644
index 0000000..169f6d2
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/ebin/regis.app
@@ -0,0 +1,7 @@
+{application, regis,
+ [{description, "A non-distributed process registry"},
+ {vsn, "1.0.0"},
+ {mod, {regis, []}},
+ {registered, [regis_server]},
+ {modules, [regis, regis_sup, regis_server]},
+ {applications, [stdlib, kernel]}]}.
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis.erl
new file mode 100644
index 0000000..0b37c7c
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis.erl
@@ -0,0 +1,29 @@
+%%% Application wrapper module for regis,
+%%% a process registration application.
+%%%
+%%% This was added because the standard process registry has a precise
+%%% meaning of representing VM-global, non-dynamic processes.
+%%% However, for this, we needed dynamic names and so we had to write
+%%% one ourselves. Of course we could have used 'global' (but we
+%%% didn't see distributed Erlang yet) or 'gproc' (I don't want to
+%%% depend on external libs for this guide), so checkthem out
+%%% if you're writing your own app.
+-module(regis).
+-behaviour(application).
+-export([start/2, stop/1]).
+-export([register/2, unregister/1, whereis/1, get_names/0]).
+
+
+start(normal, []) ->
+ regis_sup:start_link().
+
+stop(_) ->
+ ok.
+
+register(Name, Pid) -> regis_server:register(Name, Pid).
+
+unregister(Name) -> regis_server:unregister(Name).
+
+whereis(Name) -> regis_server:whereis(Name).
+
+get_names() -> regis_server:get_names().
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_server.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_server.erl
new file mode 100644
index 0000000..a668b02
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_server.erl
@@ -0,0 +1,98 @@
+%%% The core of the app: the server in charge of tracking processes.
+-module(regis_server).
+-behaviour(gen_server).
+
+-export([start_link/0, stop/0, register/2, unregister/1, whereis/1,
+ get_names/0]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ code_change/3, terminate/2]).
+
+%% We have two indexes: one by name and one by pid, for
+%% MAXIMUM SPEED (not actually measured).
+-record(state, {pid, name}).
+
+%%%%%%%%%%%%%%%%%
+%%% INTERFACE %%%
+%%%%%%%%%%%%%%%%%
+start_link() ->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+stop() ->
+ gen_server:call(?MODULE, stop).
+
+%% Give a name to a process
+register(Name, Pid) when is_pid(Pid) ->
+ gen_server:call(?MODULE, {register, Name, Pid}).
+
+%% Remove the name from a process
+unregister(Name) ->
+ gen_server:call(?MODULE, {unregister, Name}).
+
+%% Find the pid associated with a process
+whereis(Name) ->
+ gen_server:call(?MODULE, {whereis, Name}).
+
+%% Find all the names currently registered.
+get_names() ->
+ gen_server:call(?MODULE, get_names).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% GEN_SERVER CALLBACKS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+init([]) ->
+ %% Using gb_trees to store items. gb_trees generally have
+ %% good overall performance.
+ {ok, #state{pid = gb_trees:empty(),
+ name = gb_trees:empty()}}.
+
+handle_call({register, Name, Pid}, _From, S = #state{pid=P, name=N}) ->
+ case {gb_trees:is_defined(Pid, P), gb_trees:is_defined(Name, N)} of
+ {true, _} ->
+ {reply, {error, already_named}, S};
+ {_, true} ->
+ {reply, {error, name_taken}, S};
+ {false, false} ->
+ Ref = erlang:monitor(process, Pid),
+ {reply, ok, S#state{pid=gb_trees:insert(Pid, {Name,Ref}, P),
+ name=gb_trees:insert(Name, {Pid,Ref}, N)}}
+ end;
+handle_call({unregister, Name}, _From, S = #state{pid=P, name=N}) ->
+ case gb_trees:lookup(Name, N) of
+ {value, {Pid,Ref}} ->
+ erlang:demonitor(Ref, [flush]),
+ {reply, ok, S#state{pid=gb_trees:delete(Pid, P),
+ name=gb_trees:delete(Name, N)}};
+ none ->
+ {reply, ok, S}
+ end;
+handle_call({whereis, Name}, _From, S = #state{name=N}) ->
+ case gb_trees:lookup(Name, N) of
+ {value, {Pid,_}} ->
+ {reply, Pid, S};
+ none ->
+ {reply, undefined, S}
+ end;
+handle_call(get_names, _From, S = #state{name=N}) ->
+ {reply, gb_trees:keys(N), S};
+handle_call(stop, _From, State) ->
+ {stop, normal, ok, State};
+handle_call(_Event, _From, State) ->
+ {noreply, State}.
+
+handle_cast(_Event, State) ->
+ {noreply, State}.
+
+handle_info({'DOWN', Ref, process, Pid, _Reason}, S = #state{pid=P,name=N}) ->
+ {value, {Name, Ref}} = gb_trees:lookup(Pid, P),
+ {noreply, S#state{pid = gb_trees:delete(Pid, P),
+ name = gb_trees:delete(Name, N)}};
+handle_info(_Event, State) ->
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_sup.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_sup.erl
new file mode 100644
index 0000000..be333e6
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_sup.erl
@@ -0,0 +1,18 @@
+%%% The top-level supervisor of the registration
+%%% server.
+-module(regis_sup).
+-behaviour(supervisor).
+-export([start_link/0]).
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link(?MODULE, []).
+
+init([]) ->
+ {ok, {{one_for_one, 1, 3600},
+ [{server,
+ {regis_server, start_link, []},
+ permanent,
+ 500,
+ worker,
+ [regis_server]}]}}.
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_server_tests.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_server_tests.erl
new file mode 100644
index 0000000..40e1af0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_server_tests.erl
@@ -0,0 +1,120 @@
+-module(regis_server_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+-define(setup(F), {setup, fun start/0, fun stop/1, F}).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%% TESTS DESCRIPTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+start_stop_test_() ->
+ {"The server can be started, stopped and has a registered name",
+ ?setup(fun is_registered/1)}.
+
+register_test_() ->
+ [{"A process can be registered and contacted",
+ ?setup(fun register_contact/1)},
+ {"A list of registered processes can be obtained",
+ ?setup(fun registered_list/1)},
+ {"An undefined name should return 'undefined' to crash calls",
+ ?setup(fun noregister/1)},
+ {"A process can not have two names",
+ ?setup(fun two_names_one_pid/1)},
+ {"Two processes cannot share the same name",
+ ?setup(fun two_pids_one_name/1)}].
+
+unregister_test_() ->
+ [{"A process that was registered can be registered again iff it was "
+ "unregistered between both calls",
+ ?setup(fun re_un_register/1)},
+ {"Unregistering never crashes",
+ ?setup(fun unregister_nocrash/1)},
+ {"A crash unregisters a process",
+ ?setup(fun crash_unregisters/1)}].
+
+%%%%%%%%%%%%%%%%%%%%%%%
+%%% SETUP FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%
+start() ->
+ {ok, Pid} = regis_server:start_link(),
+ Pid.
+
+stop(_) ->
+ regis_server:stop().
+
+%%%%%%%%%%%%%%%%%%%%
+%%% ACTUAL TESTS %%%
+%%%%%%%%%%%%%%%%%%%%
+is_registered(Pid) ->
+ [?_assert(erlang:is_process_alive(Pid)),
+ ?_assertEqual(Pid, whereis(regis_server))].
+
+register_contact(_) ->
+ Pid = proc_lib:spawn_link(fun() -> callback(regcontact) end),
+ timer:sleep(15),
+ Ref = make_ref(),
+ WherePid = regis_server:whereis(regcontact),
+ regis_server:whereis(regcontact) ! {self(), Ref, hi},
+ Rec = receive
+ {Ref, hi} -> true
+ after 2000 -> false
+ end,
+ [?_assertEqual(Pid, WherePid),
+ ?_assert(Rec)].
+
+noregister(_) ->
+ [?_assertError(badarg, regis_server:whereis(make_ref()) ! hi),
+ ?_assertEqual(undefined, regis_server:whereis(make_ref()))].
+
+two_names_one_pid(_) ->
+ ok = regis_server:register(make_ref(), self()),
+ Res = regis_server:register(make_ref(), self()),
+ [?_assertEqual({error, already_named}, Res)].
+
+two_pids_one_name(_) ->
+ Pid = proc_lib:spawn(fun() -> callback(myname) end),
+ timer:sleep(15),
+ Res = regis_server:register(myname, self()),
+ exit(Pid, kill),
+ [?_assertEqual({error, name_taken}, Res)].
+
+registered_list(_) ->
+ L1 = regis_server:get_names(),
+ Pids = [spawn(fun() -> callback(N) end) || N <- lists:seq(1,15)],
+ timer:sleep(200),
+ L2 = regis_server:get_names(),
+ [exit(Pid, kill) || Pid <- Pids],
+ [?_assertEqual([], L1),
+ ?_assertEqual(lists:sort(lists:seq(1,15)), lists:sort(L2))].
+
+re_un_register(_) ->
+ Ref = make_ref(),
+ L = [regis_server:register(Ref, self()),
+ regis_server:register(make_ref(), self()),
+ regis_server:unregister(Ref),
+ regis_server:register(make_ref(), self())],
+ [?_assertEqual([ok, {error, already_named}, ok, ok], L)].
+
+unregister_nocrash(_) ->
+ ?_assertEqual(ok, regis_server:unregister(make_ref())).
+
+crash_unregisters(_) ->
+ Ref = make_ref(),
+ Pid = spawn(fun() -> callback(Ref) end),
+ timer:sleep(150),
+ Pid = regis_server:whereis(Ref),
+ exit(Pid, kill),
+ timer:sleep(95),
+ regis_server:register(Ref, self()),
+ S = regis_server:whereis(Ref),
+ Self = self(),
+ ?_assertEqual(Self, S).
+
+%%%%%%%%%%%%%%%%%%%%%%%%
+%%% HELPER FUNCTIONS %%%
+%%%%%%%%%%%%%%%%%%%%%%%%
+callback(Name) ->
+ ok = regis_server:register(Name, self()),
+ receive
+ {From, Ref, Msg} -> From ! {Ref, Msg}
+ end.
diff --git a/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_tests.erl b/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_tests.erl
new file mode 100644
index 0000000..61f5f16
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_tests.erl
@@ -0,0 +1,18 @@
+-module(regis_tests).
+-include_lib("eunit/include/eunit.hrl").
+
+app_test_() ->
+ {inorder,
+ [?_assert(try application:start(regis) of
+ ok -> true;
+ {error, {already_started, regis}} -> true;
+ _ -> false
+ catch
+ _:_ -> false
+ end),
+ ?_assert(try application:stop(regis) of
+ ok -> true;
+ _ -> false
+ catch
+ _:_ -> false
+ end)]}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile
new file mode 100644
index 0000000..b203ea3
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile
@@ -0,0 +1,2 @@
+{["src/*", "test/*"],
+ [{i,"include"}, {outdir, "ebin"}]}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app
new file mode 100644
index 0000000..dd6d8c7
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app
@@ -0,0 +1,11 @@
+{application, sockserv,
+ [{description, "Socket server to forward ProcessQuest messages to a client"},
+ {vsn, "1.0.0"},
+ {mod, {sockserv, []}},
+ {registered, [sockserv_sup]},
+ {modules, [sockserv, sockserv_sup, sockserv_serv, sockserv_trans,
+ sockserv_pq_events]},
+ {applications, [stdlib, kernel, processquest]},
+ {env,
+ [{port, 8082}]}
+ ]}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl
new file mode 100644
index 0000000..bd82b6d
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl
@@ -0,0 +1,13 @@
+%%% Starting the sockserv application.
+%%% The sockserv application is a lightweight
+%%% Raw socket server that can be used with telnet
+%%% to follow updates on the process quest game.
+%%% The port is defined in the app's env
+-module(sockserv).
+-behaviour(application).
+-export([start/2, stop/1]).
+
+start(normal, []) ->
+ sockserv_sup:start_link().
+
+stop(_) -> ok.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl
new file mode 100644
index 0000000..c3c6705
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl
@@ -0,0 +1,25 @@
+%%% Converts events from a player's event manager into a
+%%% cast sent to the sockserv socket gen_server.
+-module(sockserv_pq_events).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+init(Parent) -> {ok, Parent}.
+
+handle_event(E, Pid) ->
+ gen_server:cast(Pid, E),
+ {ok, Pid}.
+
+handle_call(Req, Pid) ->
+ Pid ! Req,
+ {ok, ok, Pid}.
+
+handle_info(E, Pid) ->
+ Pid ! E,
+ {ok, Pid}.
+
+terminate(_, _) -> ok.
+
+code_change(_OldVsn, Pid, _Extra) ->
+ {ok, Pid}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl
new file mode 100644
index 0000000..cabc608
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl
@@ -0,0 +1,153 @@
+%%% Handles socket connections, and bridges a remote server
+%%% With a progressquest game.
+-module(sockserv_serv).
+-behaviour(gen_server).
+
+-record(state, {name, % player's name
+ next, % next step, used when initializing
+ socket}). % the current socket
+
+-export([start_link/1]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ code_change/3, terminate/2]).
+
+-define(SOCK(Msg), {tcp, _Port, Msg}).
+-define(TIME, 800).
+-define(EXP, 50).
+
+%% The socket is passed in from sockserv_sup.
+%% It's a listen socket, as started by gen_tcp:listen/2.
+%%
+%% In Erlang, a TCP socket must be started as a listening socket first.
+%% The listening socket can then be used to listen for a connection,
+%% meant to be accepted. To do so, use gen_tcp:accept/1-2, as it is done
+%% later in this module.
+%%
+%% A single listen socket can be used by many processes, each accepting
+%% a communication. When a communication is accepted with accept/1-2,
+%% a new socket, called accept socket, is returned. This accept socket
+%% is the one that may be used to communicate with a client.
+start_link(Socket) ->
+ gen_server:start_link(?MODULE, Socket, []).
+
+init(Socket) ->
+ %% properly seeding the process
+ <<A:32, B:32, C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ %% Because accepting a connection is a blocking function call,
+ %% we can not do it in here. Forward to the server loop!
+ gen_server:cast(self(), accept),
+ {ok, #state{socket=Socket}}.
+
+handle_call(_E, _From, State) ->
+ {noreply, State}.
+
+%% Accepting a connection
+handle_cast(accept, S = #state{socket=ListenSocket}) ->
+ %% this is the socket acceptance mentioned earlier
+ {ok, AcceptSocket} = gen_tcp:accept(ListenSocket),
+ %% Remember that thou art dust, and to dust thou shalt return.
+ %% We want to always keep a given number of children in this app.
+ sockserv_sup:start_socket(), % a new acceptor is born, praise the lord
+ send(AcceptSocket, "What's your character's name?", []),
+ {noreply, S#state{socket=AcceptSocket, next=name}};
+%% The player has given us his name (in handle_info)
+%% so we now roll stats that might or might not satisfy
+%% said player.
+handle_cast(roll_stats, S = #state{socket=Socket}) ->
+ Roll = pq_stats:initial_roll(),
+ send(Socket,
+ "Stats for your character:~n"
+ " Charisma: ~B~n"
+ " Constitution: ~B~n"
+ " Dexterity: ~B~n"
+ " Intelligence: ~B~n"
+ " Strength: ~B~n"
+ " Wisdom: ~B~n~n"
+ "Do you agree to these? y/n~n",
+ [Points || {_Name, Points} <- lists:sort(Roll)]),
+ {noreply, S#state{next={stats, Roll}}};
+%% The player has accepted the stats! Start the game!
+handle_cast(stats_accepted, S = #state{name=Name, next={stats, Stats}}) ->
+ processquest:start_player(Name, [{stats,Stats},{time,?TIME},
+ {lvlexp, ?EXP}]),
+ processquest:subscribe(Name, sockserv_pq_events, self()),
+ {noreply, S#state{next=playing}};
+%% Events coming in from process quest
+%% We know this because all these events' tuples start with the
+%% name of the player.
+handle_cast(Event, S = #state{name=N, socket=Sock}) when element(1, Event) =:= N ->
+ [case E of
+ {wait, Time} -> timer:sleep(Time);
+ IoList -> send(Sock, IoList, [])
+ end || E <- sockserv_trans:to_str(Event)], % translate to a string
+ {noreply, S}.
+
+%% The TCP client sends the string "quit". We close the connection.
+handle_info(?SOCK("quit"++_), S) ->
+ processquest:stop_player(S#state.name),
+ {stop, normal, S};
+%% We receive a string while looking for a name -- we assume that hte
+%% string is the name.
+handle_info(?SOCK(Str), S = #state{next=name}) ->
+ Name = line(Str),
+ gen_server:cast(self(), roll_stats),
+ {noreply, S#state{name=Name, next=stats}};
+%% The user might or might not accept the stats we rolled in handle_cast
+handle_info(?SOCK(Str), S = #state{socket=Socket, next={stats, _}}) ->
+ case line(Str) of
+ "y" ->
+ gen_server:cast(self(), stats_accepted);
+ "n" ->
+ gen_server:cast(self(), roll_stats);
+ _ -> % ask again because we didn't get what we wanted
+ send(Socket, "Answer with y (yes) or n (no)", [])
+ end,
+ {noreply, S};
+handle_info(?SOCK(E), S = #state{socket=Socket}) ->
+ send(Socket, "Unexpected input: ~p~n", [E]),
+ {noreply, S};
+handle_info({tcp_closed, _Socket}, S) ->
+ {stop, normal, S};
+handle_info({tcp_error, _Socket, _}, S) ->
+ {stop, normal, S};
+handle_info(E, S) ->
+ io:format("unexpected: ~p~n", [E]),
+ {noreply, S}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(normal, #state{socket=S}) ->
+ gen_tcp:close(S);
+terminate(_Reason, _State) ->
+ io:format("terminate reason: ~p~n", [_Reason]).
+
+%% Send a message through a socket, then make it active again.
+%% The difference between an active and a passive socket is that
+%% an active socket will send incoming data as Erlang messages, while
+%% passive sockets will require to be polled with gen_tcp:recv/2-3.
+%%
+%% Depending on the context, you might want one or the other. I chose
+%% to have active sockets because they feel somewhat easier to work
+%% with. However, one problem with active sockets is that all input
+%% is blindly changed into messages and makes it so the Erlang VM
+%% is somewhat more subject to overload. Passive sockets push this
+%% responsibility to the underlying implementation and the OS and are
+%% somewhat safer.
+%%
+%% A middle ground exists, with sockets that are 'active once'.
+%% The {active, once} option (can be set with inet:setopts or
+%% when creating the listen socket) makes it so only *one* message
+%% will be sent in active mode, and then the socket is automatically
+%% turned back to passive mode. On each message reception, we turn
+%% the socket back to {active once} as to achieve rate limiting.
+send(Socket, Str, Args) ->
+ ok = gen_tcp:send(Socket, io_lib:format(Str++"~n", Args)),
+ ok = inet:setopts(Socket, [{active, once}]),
+ ok.
+
+%% Let's get rid of the whitespace and ignore whatever's after.
+%% makes it simpler to deal with telnet.
+line(Str) ->
+ hd(string:tokens(Str, "\r\n ")).
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl
new file mode 100644
index 0000000..0e8cbdd
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl
@@ -0,0 +1,32 @@
+%%% The supervisor in charge of all the socket acceptors.
+-module(sockserv_sup).
+-behaviour(supervisor).
+
+-export([start_link/0, start_socket/0]).
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+ {ok, Port} = application:get_env(port),
+ %% Set the socket into {active_once} mode.
+ %% See sockserv_serv comments for more details
+ {ok, ListenSocket} = gen_tcp:listen(Port, [{active,once}, {packet,line}]),
+ spawn_link(fun empty_listeners/0),
+ {ok, {{simple_one_for_one, 60, 3600},
+ [{socket,
+ {sockserv_serv, start_link, [ListenSocket]}, % pass the socket!
+ temporary, 1000, worker, [sockserv_serv]}
+ ]}}.
+
+start_socket() ->
+ supervisor:start_child(?MODULE, []).
+
+%% Start with 20 listeners so that many multiple connections can
+%% be started at once, without serialization. In best circumstances,
+%% a process would keep the count active at all times to insure nothing
+%% bad happens over time when processes get killed too much.
+empty_listeners() ->
+ [start_socket() || _ <- lists:seq(1,20)],
+ ok.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl
new file mode 100644
index 0000000..23a70c0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl
@@ -0,0 +1,58 @@
+%%% Translates the process quest events to iolists
+%%% that can be sent over a socket.
+%%%
+%%% IO lists are lists of bytes (0..255, ASCII),
+%%% binaries and other iolists. They allow to append,
+%%% prepend and insert data in strings without re-writing
+%%% the fragments that compose them. Erlang's drivers and
+%%% IO modules accept them without an issue and are a quick,
+%%% somewhat elegant solution to immutable data structures
+%%% requiring many changes.
+-module(sockserv_trans).
+-export([to_str/1]).
+
+%% The player killed something
+to_str({_User, killed, Time, {EnemyName, Props}}) ->
+ {Drop, _} = proplists:get_value(drop, Props),
+ [["Executing a ",EnemyName, "..."],
+ {wait, Time}, % take a pause between the output values
+ ["Obtained ", Drop, "."]];
+%% Changing locations
+to_str({_Name, heading, _Time, Loc}) ->
+ [["Heading to ",
+ case Loc of
+ market -> "the marketplace to sell loot...";
+ killing -> "the killing fields..."
+ end]];
+%% Leveling up
+to_str({_Name, lvl_up, _, NewStats, NewLvl, _NewExp}) ->
+ [["Leveled up to level ", integer_to_list(NewLvl),
+ " Here are your new stats:", $\n,
+ io_lib:format(
+ " Charisma: ~B~n"
+ " Constitution: ~B~n"
+ " Dexterity: ~B~n"
+ " Intelligence: ~B~n"
+ " Strength: ~B~n"
+ " Wisdom: ~B~n~n",
+ [Points || {_, Points} <- lists:sort(NewStats)])]];
+%% Bought an item
+to_str({_Name, buy, Time, Slot, {Item, _, _, _}}) ->
+ SlotTxt = case Slot of
+ armor -> " armor";
+ weapon -> "";
+ helmet -> " helmet";
+ shield -> " shield"
+ end,
+ [["Negotiating purchase of better equipment..."],
+ {wait, Time},
+ ["Bought a ", Item, SlotTxt]];
+%% Sold an item
+to_str({_Name, sell, Time, {Item, Val}}) ->
+ [["Selling ", Item],
+ {wait, Time},
+ ["Got ", integer_to_list(Val), " bucks."]];
+%% Completed a quest
+to_str({_Name, quest, 0, Completed, New}) ->
+ [["Completed quest: ", Completed, "..."],
+ ["Obtained new quest: ", New, "."]].
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/Emakefile b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/Emakefile
new file mode 100644
index 0000000..b203ea3
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/Emakefile
@@ -0,0 +1,2 @@
+{["src/*", "test/*"],
+ [{i,"include"}, {outdir, "ebin"}]}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.app b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.app
new file mode 100644
index 0000000..63618d0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.app
@@ -0,0 +1,11 @@
+{application, sockserv,
+ [{description, "Socket server to forward ProcessQuest messages to a client"},
+ {vsn, "1.0.1"},
+ {mod, {sockserv, []}},
+ {registered, [sockserv_sup]},
+ {modules, [sockserv, sockserv_sup, sockserv_serv, sockserv_trans,
+ sockserv_pq_events]},
+ {applications, [stdlib, kernel, processquest]},
+ {env,
+ [{port, 8082}]}
+ ]}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.appup b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.appup
new file mode 100644
index 0000000..5462e65
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.appup
@@ -0,0 +1,4 @@
+{"1.0.1",
+ [{"1.0.0", [{load_module, sockserv_serv}]}],
+ [{"1.0.0", [{load_module, sockserv_serv}]}]}.
+
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv.erl
new file mode 100644
index 0000000..bd82b6d
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv.erl
@@ -0,0 +1,13 @@
+%%% Starting the sockserv application.
+%%% The sockserv application is a lightweight
+%%% Raw socket server that can be used with telnet
+%%% to follow updates on the process quest game.
+%%% The port is defined in the app's env
+-module(sockserv).
+-behaviour(application).
+-export([start/2, stop/1]).
+
+start(normal, []) ->
+ sockserv_sup:start_link().
+
+stop(_) -> ok.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_pq_events.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_pq_events.erl
new file mode 100644
index 0000000..c3c6705
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_pq_events.erl
@@ -0,0 +1,25 @@
+%%% Converts events from a player's event manager into a
+%%% cast sent to the sockserv socket gen_server.
+-module(sockserv_pq_events).
+-behaviour(gen_event).
+-export([init/1, handle_event/2, handle_call/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+init(Parent) -> {ok, Parent}.
+
+handle_event(E, Pid) ->
+ gen_server:cast(Pid, E),
+ {ok, Pid}.
+
+handle_call(Req, Pid) ->
+ Pid ! Req,
+ {ok, ok, Pid}.
+
+handle_info(E, Pid) ->
+ Pid ! E,
+ {ok, Pid}.
+
+terminate(_, _) -> ok.
+
+code_change(_OldVsn, Pid, _Extra) ->
+ {ok, Pid}.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl
new file mode 100644
index 0000000..24aed16
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl
@@ -0,0 +1,154 @@
+%%% Handles socket connections, and bridges a remote server
+%%% With a progressquest game.
+-module(sockserv_serv).
+-behaviour(gen_server).
+
+-record(state, {name, % player's name
+ next, % next step, used when initializing
+ socket}). % the current socket
+
+-export([start_link/1]).
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ code_change/3, terminate/2]).
+
+-define(SOCK(Msg), {tcp, _Port, Msg}).
+-define(TIME, 800).
+-define(EXP, 50).
+
+%% The socket is passed in from sockserv_sup.
+%% It's a listen socket, as started by gen_tcp:listen/2.
+%%
+%% In Erlang, a TCP socket must be started as a listening socket first.
+%% The listening socket can then be used to listen for a connection,
+%% meant to be accepted. To do so, use gen_tcp:accept/1-2, as it is done
+%% later in this module.
+%%
+%% A single listen socket can be used by many processes, each accepting
+%% a communication. When a communication is accepted with accept/1-2,
+%% a new socket, called accept socket, is returned. This accept socket
+%% is the one that may be used to communicate with a client.
+start_link(Socket) ->
+ gen_server:start_link(?MODULE, Socket, []).
+
+init(Socket) ->
+ %% properly seeding the process
+ <<A:32, B:32, C:32>> = crypto:rand_bytes(12),
+ random:seed({A,B,C}),
+ %% Because accepting a connection is a blocking function call,
+ %% we can not do it in here. Forward to the server loop!
+ gen_server:cast(self(), accept),
+ {ok, #state{socket=Socket}}.
+
+handle_call(_E, _From, State) ->
+ {noreply, State}.
+
+%% Accepting a connection
+handle_cast(accept, S = #state{socket=ListenSocket}) ->
+ %% this is the socket acceptance mentioned earlier
+ {ok, AcceptSocket} = gen_tcp:accept(ListenSocket),
+ %% Remember that thou art dust, and to dust thou shalt return.
+ %% We want to always keep a given number of children in this app.
+ sockserv_sup:start_socket(), % a new acceptor is born, praise the lord
+ send(AcceptSocket, "What's your character's name?", []),
+ {noreply, S#state{socket=AcceptSocket, next=name}};
+%% The player has given us his name (in handle_info)
+%% so we now roll stats that might or might not satisfy
+%% said player.
+handle_cast(roll_stats, S = #state{socket=Socket}) ->
+ Roll = pq_stats:initial_roll(),
+ send(Socket,
+ "Stats for your character:~n"
+ " Charisma: ~B~n"
+ " Constitution: ~B~n"
+ " Dexterity: ~B~n"
+ " Intelligence: ~B~n"
+ " Strength: ~B~n"
+ " Wisdom: ~B~n~n"
+ "Do you agree to these? y/n~n",
+ [Points || {_Name, Points} <- lists:sort(Roll)]),
+ {noreply, S#state{next={stats, Roll}}};
+%% The player has accepted the stats! Start the game!
+handle_cast(stats_accepted, S = #state{name=Name, next={stats, Stats}}) ->
+ processquest:start_player(Name, [{stats,Stats},{time,?TIME},
+ {lvlexp, ?EXP}]),
+ processquest:subscribe(Name, sockserv_pq_events, self()),
+ {noreply, S#state{next=playing}};
+%% Events coming in from process quest
+%% We know this because all these events' tuples start with the
+%% name of the player.
+handle_cast(Event, S = #state{name=N, socket=Sock}) when element(1, Event) =:= N ->
+ [case E of
+ {wait, Time} -> timer:sleep(Time);
+ IoList -> send(Sock, IoList, [])
+ end || E <- sockserv_trans:to_str(Event)], % translate to a string
+ {noreply, S}.
+
+%% The TCP client sends the string "quit". We close the connection.
+handle_info(?SOCK("quit"++_), S) ->
+ processquest:stop_player(S#state.name),
+ gen_tcp:close(S#state.socket),
+ {stop, normal, S};
+%% We receive a string while looking for a name -- we assume that hte
+%% string is the name.
+handle_info(?SOCK(Str), S = #state{next=name}) ->
+ Name = line(Str),
+ gen_server:cast(self(), roll_stats),
+ {noreply, S#state{name=Name, next=stats}};
+%% The user might or might not accept the stats we rolled in handle_cast
+handle_info(?SOCK(Str), S = #state{socket=Socket, next={stats, _}}) ->
+ case line(Str) of
+ "y" ->
+ gen_server:cast(self(), stats_accepted);
+ "n" ->
+ gen_server:cast(self(), roll_stats);
+ _ -> % ask again because we didn't get what we wanted
+ send(Socket, "Answer with y (yes) or n (no)", [])
+ end,
+ {noreply, S};
+handle_info(?SOCK(E), S = #state{socket=Socket}) ->
+ send(Socket, "Unexpected input: ~p~n", [E]),
+ {noreply, S};
+handle_info({tcp_closed, _Socket}, S) ->
+ {stop, normal, S};
+handle_info({tcp_error, _Socket, _}, S) ->
+ {stop, normal, S};
+handle_info(E, S) ->
+ io:format("unexpected: ~p~n", [E]),
+ {noreply, S}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+terminate(normal, _State) ->
+ ok;
+terminate(_Reason, _State) ->
+ io:format("terminate reason: ~p~n", [_Reason]).
+
+%% Send a message through a socket, then make it active again.
+%% The difference between an active and a passive socket is that
+%% an active socket will send incoming data as Erlang messages, while
+%% passive sockets will require to be polled with gen_tcp:recv/2-3.
+%%
+%% Depending on the context, you might want one or the other. I chose
+%% to have active sockets because they feel somewhat easier to work
+%% with. However, one problem with active sockets is that all input
+%% is blindly changed into messages and makes it so the Erlang VM
+%% is somewhat more subject to overload. Passive sockets push this
+%% responsibility to the underlying implementation and the OS and are
+%% somewhat safer.
+%%
+%% A middle ground exists, with sockets that are 'active once'.
+%% The {active, once} option (can be set with inet:setopts or
+%% when creating the listen socket) makes it so only *one* message
+%% will be sent in active mode, and then the socket is automatically
+%% turned back to passive mode. On each message reception, we turn
+%% the socket back to {active once} as to achieve rate limiting.
+send(Socket, Str, Args) ->
+ ok = gen_tcp:send(Socket, io_lib:format(Str++"~n", Args)),
+ ok = inet:setopts(Socket, [{active, once}]),
+ ok.
+
+%% Let's get rid of the whitespace and ignore whatever's after.
+%% makes it simpler to deal with telnet.
+line(Str) ->
+ hd(string:tokens(Str, "\r\n ")).
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_sup.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_sup.erl
new file mode 100644
index 0000000..0e8cbdd
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_sup.erl
@@ -0,0 +1,32 @@
+%%% The supervisor in charge of all the socket acceptors.
+-module(sockserv_sup).
+-behaviour(supervisor).
+
+-export([start_link/0, start_socket/0]).
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init([]) ->
+ {ok, Port} = application:get_env(port),
+ %% Set the socket into {active_once} mode.
+ %% See sockserv_serv comments for more details
+ {ok, ListenSocket} = gen_tcp:listen(Port, [{active,once}, {packet,line}]),
+ spawn_link(fun empty_listeners/0),
+ {ok, {{simple_one_for_one, 60, 3600},
+ [{socket,
+ {sockserv_serv, start_link, [ListenSocket]}, % pass the socket!
+ temporary, 1000, worker, [sockserv_serv]}
+ ]}}.
+
+start_socket() ->
+ supervisor:start_child(?MODULE, []).
+
+%% Start with 20 listeners so that many multiple connections can
+%% be started at once, without serialization. In best circumstances,
+%% a process would keep the count active at all times to insure nothing
+%% bad happens over time when processes get killed too much.
+empty_listeners() ->
+ [start_socket() || _ <- lists:seq(1,20)],
+ ok.
diff --git a/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_trans.erl b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_trans.erl
new file mode 100644
index 0000000..23a70c0
--- /dev/null
+++ b/learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_trans.erl
@@ -0,0 +1,58 @@
+%%% Translates the process quest events to iolists
+%%% that can be sent over a socket.
+%%%
+%%% IO lists are lists of bytes (0..255, ASCII),
+%%% binaries and other iolists. They allow to append,
+%%% prepend and insert data in strings without re-writing
+%%% the fragments that compose them. Erlang's drivers and
+%%% IO modules accept them without an issue and are a quick,
+%%% somewhat elegant solution to immutable data structures
+%%% requiring many changes.
+-module(sockserv_trans).
+-export([to_str/1]).
+
+%% The player killed something
+to_str({_User, killed, Time, {EnemyName, Props}}) ->
+ {Drop, _} = proplists:get_value(drop, Props),
+ [["Executing a ",EnemyName, "..."],
+ {wait, Time}, % take a pause between the output values
+ ["Obtained ", Drop, "."]];
+%% Changing locations
+to_str({_Name, heading, _Time, Loc}) ->
+ [["Heading to ",
+ case Loc of
+ market -> "the marketplace to sell loot...";
+ killing -> "the killing fields..."
+ end]];
+%% Leveling up
+to_str({_Name, lvl_up, _, NewStats, NewLvl, _NewExp}) ->
+ [["Leveled up to level ", integer_to_list(NewLvl),
+ " Here are your new stats:", $\n,
+ io_lib:format(
+ " Charisma: ~B~n"
+ " Constitution: ~B~n"
+ " Dexterity: ~B~n"
+ " Intelligence: ~B~n"
+ " Strength: ~B~n"
+ " Wisdom: ~B~n~n",
+ [Points || {_, Points} <- lists:sort(NewStats)])]];
+%% Bought an item
+to_str({_Name, buy, Time, Slot, {Item, _, _, _}}) ->
+ SlotTxt = case Slot of
+ armor -> " armor";
+ weapon -> "";
+ helmet -> " helmet";
+ shield -> " shield"
+ end,
+ [["Negotiating purchase of better equipment..."],
+ {wait, Time},
+ ["Bought a ", Item, SlotTxt]];
+%% Sold an item
+to_str({_Name, sell, Time, {Item, Val}}) ->
+ [["Selling ", Item],
+ {wait, Time},
+ ["Got ", integer_to_list(Val), " bucks."]];
+%% Completed a quest
+to_str({_Name, quest, 0, Completed, New}) ->
+ [["Completed quest: ", Completed, "..."],
+ ["Obtained new quest: ", New, "."]].
diff --git a/learn-you-some-erlang/processquest/processquest-1.0.0.config b/learn-you-some-erlang/processquest/processquest-1.0.0.config
new file mode 100644
index 0000000..2e62f5d
--- /dev/null
+++ b/learn-you-some-erlang/processquest/processquest-1.0.0.config
@@ -0,0 +1,21 @@
+{sys, [
+ {lib_dirs, ["/Users/ferd/code/learn-you-some-erlang/processquest/apps"]},
+ {erts, [{mod_cond, derived},
+ {app_file, strip}]},
+ {rel, "processquest", "1.0.0",
+ [kernel, stdlib, sasl, crypto, regis, sockserv, processquest]},
+ {boot_rel, "processquest"},
+ {relocatable, true},
+ {profile, embedded},
+ {app_file, strip},
+ {incl_cond, exclude},
+ {excl_app_filters, ["_tests.beam"]},
+ {excl_archive_filters, [".*"]},
+ {app, stdlib, [{incl_cond, include}]},
+ {app, kernel, [{incl_cond, include}]},
+ {app, sasl, [{incl_cond, include}]},
+ {app, crypto, [{incl_cond, include}]},
+ {app, regis, [{vsn, "1.0.0"}, {incl_cond, include}]},
+ {app, sockserv, [{vsn, "1.0.0"}, {incl_cond, include}]},
+ {app, processquest, [{vsn, "1.0.0"}, {incl_cond, include}]}
+]}.
diff --git a/learn-you-some-erlang/processquest/processquest-1.1.0.config b/learn-you-some-erlang/processquest/processquest-1.1.0.config
new file mode 100644
index 0000000..38a55f4
--- /dev/null
+++ b/learn-you-some-erlang/processquest/processquest-1.1.0.config
@@ -0,0 +1,21 @@
+{sys, [
+ {lib_dirs, ["/Users/ferd/code/learn-you-some-erlang/processquest/apps"]},
+ {erts, [{mod_cond, derived},
+ {app_file, strip}]},
+ {rel, "processquest", "1.1.0",
+ [kernel, stdlib, sasl, crypto, regis, processquest, sockserv]},
+ {boot_rel, "processquest"},
+ {relocatable, true},
+ {profile, embedded},
+ {app_file, strip},
+ {incl_cond, exclude},
+ {excl_app_filters, ["_tests.beam"]},
+ {excl_archive_filters, [".*"]},
+ {app, stdlib, [{incl_cond, include}]},
+ {app, kernel, [{incl_cond, include}]},
+ {app, sasl, [{incl_cond, include}]},
+ {app, crypto, [{incl_cond, include}]},
+ {app, regis, [{vsn, "1.0.0"}, {incl_cond, include}]},
+ {app, sockserv, [{vsn, "1.0.1"}, {incl_cond, include}]},
+ {app, processquest, [{vsn, "1.1.0"}, {incl_cond, include}]}
+]}.
diff --git a/learn-you-some-erlang/processquest/rel/.this-directory-must-be-tracked b/learn-you-some-erlang/processquest/rel/.this-directory-must-be-tracked
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/learn-you-some-erlang/processquest/rel/.this-directory-must-be-tracked