From 5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Fri, 23 Feb 2024 07:08:18 +0100 Subject: wip --- .../processquest/apps/processquest-1.0.0/Emakefile | 2 + .../ebin/.this-file-intentionally-left-blank | 0 .../apps/processquest-1.0.0/ebin/processquest.app | 7 + .../include/.this-file-intentionally-left-blank | 0 .../priv/.this-file-intentionally-left-blank | 0 .../apps/processquest-1.0.0/src/pq_enemy.erl | 17 + .../apps/processquest-1.0.0/src/pq_events.erl | 49 +++ .../apps/processquest-1.0.0/src/pq_market.erl | 77 ++++ .../apps/processquest-1.0.0/src/pq_player.erl | 175 +++++++++ .../apps/processquest-1.0.0/src/pq_stats.erl | 19 + .../apps/processquest-1.0.0/src/pq_sup.erl | 31 ++ .../apps/processquest-1.0.0/src/pq_supersup.erl | 28 ++ .../apps/processquest-1.0.0/src/processquest.erl | 39 ++ .../processquest-1.0.0/test/pq_enemy_tests.erl | 33 ++ .../processquest-1.0.0/test/pq_events_handler.erl | 24 ++ .../processquest-1.0.0/test/pq_events_tests.erl | 55 +++ .../processquest-1.0.0/test/pq_market_tests.erl | 15 + .../processquest-1.0.0/test/pq_player_tests.erl | 303 ++++++++++++++++ .../processquest-1.0.0/test/pq_stats_tests.erl | 28 ++ .../processquest-1.0.0/test/processquest_tests.erl | 49 +++ .../processquest/apps/processquest-1.1.0/Emakefile | 2 + .../ebin/.this-file-intentionally-left-blank | 0 .../apps/processquest-1.1.0/ebin/processquest.app | 8 + .../processquest-1.1.0/ebin/processquest.appup | 9 + .../include/.this-file-intentionally-left-blank | 0 .../priv/.this-file-intentionally-left-blank | 0 .../apps/processquest-1.1.0/src/pq_enemy.erl | 32 ++ .../apps/processquest-1.1.0/src/pq_events.erl | 53 +++ .../apps/processquest-1.1.0/src/pq_market.erl | 77 ++++ .../apps/processquest-1.1.0/src/pq_player.erl | 216 +++++++++++ .../apps/processquest-1.1.0/src/pq_quest.erl | 15 + .../apps/processquest-1.1.0/src/pq_stats.erl | 19 + .../apps/processquest-1.1.0/src/pq_sup.erl | 31 ++ .../apps/processquest-1.1.0/src/pq_supersup.erl | 28 ++ .../apps/processquest-1.1.0/src/processquest.erl | 39 ++ .../processquest-1.1.0/test/pq_enemy_tests.erl | 33 ++ .../processquest-1.1.0/test/pq_events_handler.erl | 24 ++ .../processquest-1.1.0/test/pq_events_tests.erl | 55 +++ .../processquest-1.1.0/test/pq_market_tests.erl | 15 + .../processquest-1.1.0/test/pq_player_tests.erl | 395 +++++++++++++++++++++ .../processquest-1.1.0/test/pq_quest_tests.erl | 30 ++ .../processquest-1.1.0/test/pq_stats_tests.erl | 28 ++ .../processquest-1.1.0/test/processquest_tests.erl | 49 +++ .../processquest/apps/regis-1.0.0/Emakefile | 2 + .../processquest/apps/regis-1.0.0/ebin/regis.app | 7 + .../processquest/apps/regis-1.0.0/src/regis.erl | 29 ++ .../apps/regis-1.0.0/src/regis_server.erl | 98 +++++ .../apps/regis-1.0.0/src/regis_sup.erl | 18 + .../apps/regis-1.0.0/test/regis_server_tests.erl | 120 +++++++ .../apps/regis-1.0.0/test/regis_tests.erl | 18 + .../processquest/apps/sockserv-1.0.0/Emakefile | 2 + .../apps/sockserv-1.0.0/ebin/sockserv.app | 11 + .../apps/sockserv-1.0.0/src/sockserv.erl | 13 + .../apps/sockserv-1.0.0/src/sockserv_pq_events.erl | 25 ++ .../apps/sockserv-1.0.0/src/sockserv_serv.erl | 153 ++++++++ .../apps/sockserv-1.0.0/src/sockserv_sup.erl | 32 ++ .../apps/sockserv-1.0.0/src/sockserv_trans.erl | 58 +++ .../processquest/apps/sockserv-1.0.1/Emakefile | 2 + .../apps/sockserv-1.0.1/ebin/sockserv.app | 11 + .../apps/sockserv-1.0.1/ebin/sockserv.appup | 4 + .../apps/sockserv-1.0.1/src/sockserv.erl | 13 + .../apps/sockserv-1.0.1/src/sockserv_pq_events.erl | 25 ++ .../apps/sockserv-1.0.1/src/sockserv_serv.erl | 154 ++++++++ .../apps/sockserv-1.0.1/src/sockserv_sup.erl | 32 ++ .../apps/sockserv-1.0.1/src/sockserv_trans.erl | 58 +++ .../processquest/processquest-1.0.0.config | 21 ++ .../processquest/processquest-1.1.0.config | 21 ++ .../rel/.this-directory-must-be-tracked | 0 68 files changed, 3036 insertions(+) create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/Emakefile create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/.this-file-intentionally-left-blank create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/ebin/processquest.app create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/include/.this-file-intentionally-left-blank create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/priv/.this-file-intentionally-left-blank create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_enemy.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_events.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_market.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_player.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_stats.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_sup.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/pq_supersup.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/src/processquest.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_enemy_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_handler.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_events_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_market_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_player_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/pq_stats_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.0.0/test/processquest_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/Emakefile create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/.this-file-intentionally-left-blank create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.app create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/ebin/processquest.appup create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/include/.this-file-intentionally-left-blank create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/priv/.this-file-intentionally-left-blank create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_enemy.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_events.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_market.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_player.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_quest.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_stats.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_sup.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/pq_supersup.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/src/processquest.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_enemy_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_handler.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_events_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_market_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_player_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_quest_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/pq_stats_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/processquest-1.1.0/test/processquest_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/regis-1.0.0/Emakefile create mode 100644 learn-you-some-erlang/processquest/apps/regis-1.0.0/ebin/regis.app create mode 100644 learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis.erl create mode 100644 learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_server.erl create mode 100644 learn-you-some-erlang/processquest/apps/regis-1.0.0/src/regis_sup.erl create mode 100644 learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_server_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/regis-1.0.0/test/regis_tests.erl create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.0/Emakefile create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.0/ebin/sockserv.app create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv.erl create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_pq_events.erl create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_serv.erl create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_sup.erl create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.0/src/sockserv_trans.erl create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.1/Emakefile create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.app create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.1/ebin/sockserv.appup create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv.erl create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_pq_events.erl create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_serv.erl create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_sup.erl create mode 100644 learn-you-some-erlang/processquest/apps/sockserv-1.0.1/src/sockserv_trans.erl create mode 100644 learn-you-some-erlang/processquest/processquest-1.0.0.config create mode 100644 learn-you-some-erlang/processquest/processquest-1.1.0.config create mode 100644 learn-you-some-erlang/processquest/rel/.this-directory-must-be-tracked (limited to 'learn-you-some-erlang/processquest') 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 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 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 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. + <> = 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() -> + <> = 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 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 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 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. + <> = 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() -> + <> = 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() -> + <> = 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 + <> = 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 + <> = 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 -- cgit v1.2.3