From 5a9cdd3cc89507d4d74f8bded56ce5e037b3b56e Mon Sep 17 00:00:00 2001 From: Trygve Laugstøl Date: Fri, 23 Feb 2024 07:08:18 +0100 Subject: wip --- .../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 +++++ 8 files changed, 435 insertions(+) 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 (limited to 'learn-you-some-erlang/processquest/apps/processquest-1.0.0/src') 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). -- cgit v1.2.3