-module(rps). %The "rock paper scissors" style of military simulation %lessons learned: % mutually recursive fluent queries are dangerous % consider sub-history variant instead of [{general, units}, ...] % also consider different time coordinate systems % -export([available_fighters/5, add_armies/2, add_units/2, units/2, print_day/0]). -export([start/0, start/3, stop/0, advance/3, fortify/3]). %include record and macro definitions. -include("../include/records.hrl"). -include("../include/macros.hrl"). start() -> start(100, 100, 100). %Start the army with the given number of soldiers. NSw+NSp+NAx =< 300 start(NSw, NSp, NAx) when NSw >= 0 andalso NSp >= 0 andalso NAx >= 0 andalso NSw+NSp+NAx =< 300 -> io:format("fielding army with ~p soldiers~n", [NSw+NSp+NAx]), %Herodotus and Simulation are processes that can be messaged. herodotus:start(rps_hero), thucydides:start(rps_hero, rps_sim), %Reset empties out the Mnesia databases so that each %playthrough is fresh. It also sets up the initial state %of the history. reset(), setup_init(), %fill out the i_arcs here %advance chooses the proportion of troops to field %for this advance(max 50 troops) %An advance reaches its goal 8 hours after being issued setup_advance(), %fortification chooses the proportion of troops to define %the front-line defenses(max 50 troops) %a fortification is prepared 6 hours after being issued. setup_fort(), %skirmish is a military encounter. they occur hourly until %a side is defeated or night falls setup_skirmish(), %Withdrawal occurs when the defenses are slain or the offense %are defeated. It also occurs at the end of the day: whichever %side has fewer at that point withdraws. setup_withdrawal(), %Surrender occurs when an army is wholly defeated. setup_surrender(), setup_advance_follows(), setup_skirmish_follows(), setup_withdraw_follows(), %trigger the init archetype initialize(NSw, NSp, NAx), ok . %Stop terminates the simulation and herodotus processes. stop() -> thucydides:stop(rps_sim), herodotus:stop(rps_hero) . %reset is a private function that resets the time to 0 and clears the histories reset() -> herodotus:clear(rps_hero), thucydides:clear(rps_sim) . field(General, T) -> units(General, herodotus:value( selector:new({field}, T), [], main, rps_hero )) . units(General, Armies) -> io:format("checking units of ~p in ~p~n", [General, Armies]), {value, {General, Result}} = lists:keysearch(General, 1, Armies), Result . initialize(NSw, NSp, NAx) -> PlayerBindings = dict:store( "General", player, dict:store("NSw", NSw, dict:store("NSp", NSp, dict:store("NAx", NAx, dict:new())))), % Tot = NSw + NSp + NAx, % {FSw, FSp, FAx} = {random:uniform(), random:uniform(), random:uniform()}, % Mag = FSw+FSp+FAx, % {NormFSw, NormFSp, NormFAx} = {FSw / Mag, FSp / Mag, FAx / Mag}, % {ENSw, ENSp, ENAx} = {NormFSw * Tot, NormFSp * Tot, NormFAx * Tot}, {ENSw, ENSp, ENAx} = {NSw, NSp, NAx}, EnemyBindings = dict:store( "General", enemy, dict:store("NSw", trunc(ENSw), dict:store("NSp", trunc(ENSp), dict:store("NAx", trunc(ENAx), dict:new())))), thucydides:trigger( init, current_time(), dict:store("Armies", [PlayerBindings, EnemyBindings], dict:new()), rps_sim ) . add_armies(undefined, V) when is_tuple(V) -> [V]; add_armies(N, undefined) when is_tuple(N) -> [N]; add_armies(undefined, V) when is_list(V) -> V; add_armies(N, undefined) when is_list(N) -> N; add_armies(N, V) when is_tuple(V) -> add_armies(N, [V]); add_armies(N, V) when is_tuple(N) -> add_armies([N], V); add_armies(N, V) when is_list(N) and is_list(V) -> %problem: V is a superset of N - include those parts of V Res = lists:map(fun({NG, NArmy}) -> case lists:keysearch(NG, 1, V) of {value, {NG, VArmy}} -> {NG, add_units(NArmy, VArmy)}; _ -> {NG, NArmy} end end, N) ++ lists:filter(fun({VG, _}) -> lists:keymember(VG, 1, N) == false end, V), %io:format("Added ~p to ~p and got ~p~n", [V, N, Res]), Res . add_units(NArmy, VArmy) -> lists:map(fun({Type, Amt}) -> case lists:keysearch(Type, 1, VArmy) of {value, {Type, ValAmt}} -> {Type, Amt + ValAmt}; _ -> {Type, Amt} end end, NArmy) . -define(F_DISPATCH(SRC, DEST, DT, NSW, NSP, NAX), { [ ?A_FLUENT( %modify the source's counts in the given rank of the general {SRC}, %at the given time _Time, %forever infinity, %until the units are returned returned, %Subtract this many men {dict:fetch("General", _Bindings), [{swords, -1*NSW}, {spears, -1*NSP}, {axes, -1*NAX}]}, rps:add_armies(_Net, _Val) ), ?A_FLUENT( %modify the transit counts in the given rank of the general {transit}, %at the given time _Time, %forever infinity, %until the units are returned returned, %add this many men {dict:fetch("General", _Bindings), [{swords, NSW}, {spears, NSP}, {axes, NAX}]}, rps:add_armies(_Net, _Val) ), ?A_FLUENT( %modify the transit counts {transit}, %at the given time _Time+DT, %forever infinity, %until the units are returned returned, %Subtract this many men {dict:fetch("General", _Bindings), [{swords, -1*NSW}, {spears, -1*NSP}, {axes, -1*NAX}]}, rps:add_armies(_Net, _Val) ), ?A_FLUENT( %modify the destination's counts {DEST}, %at the given time _Time+DT, %forever infinity, %until the units are returned returned, %add this many men {dict:fetch("General", _Bindings), [{swords, NSW}, {spears, NSP}, {axes, NAX}]}, rps:add_armies(_Net, _Val) ) ] , [ %army must be available requirement:new( main, ?A_SEL({SRC}, _Time, infinity), fun(_Time, _Bindings) -> [] end, fun(_Val, _Time, _Bindings) -> Army = units(dict:fetch("General", _Bindings), _Val), lists:all(fun({Type, Amt}) -> case lists:keysearch(Type, 1, Army) of {value, {Type, ValAmt}} -> ValAmt >= Amt; _ -> true end end, [{swords, NSW}, {spears, NSP}, {axes, NAX}]) end ) , %army must not already be deployed ?REQ( main, {[DEST, transit]}, _Time, [], lists:all(fun({_Unit, Num}) -> Num =:= 0 end, units(dict:fetch("General", _Bindings), _Val)) ) ] } ). -define(F_WITHDRAWAL, [ ?A_FLUENT( {withdrawing, [dict:fetch("General", _Bindings)]}, _Time, _Time+6, false, true, _Val ), ?A_FLUENT( {[fort, field, transit]}, _Time+1, infinity, returned, {dict:fetch("General", _Bindings), [{swords, 0}, {spears, 0}, {axes, 0}]}, lists:keystore(dict:fetch("General", _Bindings), 1, _Net, _Val) ), ?A_FLUENT( {reserve}, _Time+1, infinity, returned, {dict:fetch("General", _Bindings), units( dict:fetch("General", _Bindings), herodotus:value( Self, selector:new({[fort, field, transit]}, _Time), [], main, rps_hero ) )}, rps:add_armies(_Net, _Val) ) ] ). weakness(swords) -> spears; weakness(spears) -> axes; weakness(axes) -> swords. strength(swords) -> axes; strength(axes) -> spears; strength(spears) -> swords. -define(A_LOSSES, f_arc:new( s_arc:new(fun(_Time, _Bindings) -> {field} end, fun(_Time, _Bindings) -> _Time end, fun(_Time, _Bindings) -> infinity end), fun(_Time, _Bindings) -> revived end, fun(Self, _Now, _Args, Armies) -> _Time = selector:set_time(Self), _Bindings = Self#fluent.bindings, {value, {G1, VArmy}} = lists:keysearch(dict:fetch("Attacker", _Bindings), 1, Armies), {value, {G2, EArmy}} = lists:keysearch(dict:fetch("Defender", _Bindings), 1, Armies), [ {G1, lists:map(fun({Unit, Amt}) -> {Weakness, Same, Strength} = available_fighters( Unit, weakness(Unit), strength(Unit), VArmy, EArmy ), {Unit, -1 * lists:min([Amt, lists:max([0, round(Weakness * 0.5 + Same * 0.25 + Strength * 0.125)])])} % {Unit, -1 * lists:min([Amt, lists:max([0, round(Weakness * 0.25 + Same * 0.125 + Strength * 0.0625)])])} end, VArmy)}, {G2, lists:map(fun({Unit, Amt}) -> {Weakness, Same, Strength} = available_fighters( Unit, weakness(Unit), strength(Unit), EArmy, VArmy ), {Unit, -1 * lists:min([Amt, lists:max([0, round(Weakness * 0.5 + Same * 0.25 + Strength * 0.125)])])} % {Unit, -1 * lists:min([Amt, lists:max([0, round(Weakness * 0.25 + Same * 0.125 + Strength * 0.0625)])])} end, EArmy)} ] end, fun(_Self, _Now, _Args, _Prev, _Net, _Val) -> rps:add_armies(_Net, _Val) end ) ). available_fighters(Same, Weakness, Strength, MyArmy, TheirArmy) -> {value, {Weakness, EWeakness}} = lists:keysearch(Weakness, 1, TheirArmy), {value, {Same, ESame}} = lists:keysearch(Same, 1, TheirArmy), {value, {Strength, EStrength}} = lists:keysearch(Strength, 1, TheirArmy), {value, {Weakness, MyWeakness}} = lists:keysearch(Weakness, 1, MyArmy), {value, {Strength, MyStrength}} = lists:keysearch(Strength, 1, MyArmy), %spears, swords - s_axes, axes - s_spears - s_axes {EWeakness, lists:max([0, ESame - MyStrength]), lists:max([0, EStrength - MyWeakness - MyStrength])} . %what fluents to use? setup_init() -> Generals = ?A_FLUENT( generals, 0, infinity, defeated, lists:map(fun(A) -> dict:fetch("General", A) end, dict:fetch("Armies", _Bindings)), fluent:fmerge(_Net, _Val) ), %set up the military Armies = ?A_FLUENT( {reserve}, 0, infinity, surrender, lists:map(fun(A) -> {dict:fetch("General", A), [{swords, dict:fetch("NSw", A)}, {spears, dict:fetch("NSp", A)}, {axes, dict:fetch("NAx", A)}]} end, dict:fetch("Armies", _Bindings)), rps:add_armies(_Net, _Val) ), EmptyPlaces = ?A_FLUENT( {[field, fort]}, 0, infinity, surrender, lists:map(fun(A) -> {dict:fetch("General", A), [{swords, 0}, {spears, 0}, {axes, 0}]} end, dict:fetch("Armies", _Bindings)), rps:add_armies(_Net, _Val) ), NoWithdrawing = ?A_FLUENT( {withdrawing, [player, enemy]}, 0, infinity, persist, false, _Val ), NoConflict = ?A_FLUENT( {last_conflict, [player, enemy], [player, enemy]}, 0, infinity, persist, -24, _Val ), Daylight = ?A_ADD( light, 0, infinity, persist, %0 to 1 %0 means 'pitch black' %1 means 'high noon' (12 - abs(12 - (trunc(_Now) rem 24))) / 12.0 ), Fs = [Generals, Armies, EmptyPlaces, NoWithdrawing, NoConflict, Daylight], Vars = ["Armies"], Rs = [], Extras = [], Cancels = [], Arc = i_arc:new(init, main, Fs, Vars, Rs, Extras, Cancels), thucydides:add_iarc(Arc, rps_sim) . %advance differs from fortification in the post-trigger simulation behavior setup_advance() -> {Flus, Reqs} = ?F_DISPATCH( reserve, field, 8, dict:fetch("NSw", _Bindings), dict:fetch("NSp", _Bindings), dict:fetch("NAx", _Bindings) ), Vars = ["General", "NSw", "NSp", "NAx"], Extras = [], Cancels = [], Arc = i_arc:new(advance, main, Flus, Vars, Reqs, Extras, Cancels), thucydides:add_iarc(Arc, rps_sim) . setup_fort() -> nop. %ignore for now, we'll just do violent brutal smashing fights % [Flus, Reqs] = ?F_DISPATCH( % reserve, % fort, % dict:fetch("NSw", _Bindings), % dict:fetch("NSp", _Bindings), % dict:fetch("NAx", _Bindings) % ) % Vars = ["General", "NSw", "NSp", "NAx"], % Extras = [], % Cancels = [], % Arc = i_arc:new(fortification, Flus, Vars, Reqs, Extras, Cancels), % thucydides:add_iarc(Arc, rps_sim) %. setup_skirmish() -> Conflict = ?A_FLUENT( {last_conflict, dict:fetch("Attacker", _Bindings), dict:fetch("Defender", _Bindings)}, _Time, infinity, persist, _Time, _Val ), Losses = ?A_LOSSES, Flus = [Conflict, Losses], ReqDaylight = ?REQ( main, light, _Time, [], _Val > 0.5 ), ReqAtkNotWithdrawing = ?REQ( main, {withdrawing, dict:fetch("Attacker", _Bindings)}, _Time, [], _Val =:= false ), ReqDefNotWithdrawing = ?REQ( main, {withdrawing, dict:fetch("Defender", _Bindings)}, _Time, [], _Val =:= false ), ReqArmies = ?REQ( main, {field}, _Time, [], lists:any(fun({_, Amt}) -> Amt > 0 end, units(dict:fetch("Attacker", _Bindings), _Val)) andalso lists:any(fun({_, Amt}) -> Amt > 0 end, units(dict:fetch("Defender", _Bindings), _Val)) ), Reqs = [ReqDaylight, ReqAtkNotWithdrawing, ReqDefNotWithdrawing, ReqArmies], Vars = ["Attacker", "Defender"], Extras = [], Cancels = [], Arc = i_arc:new(skirmish, main, Flus, Vars, Reqs, Extras, Cancels), thucydides:add_iarc(Arc, rps_sim) . setup_withdrawal() -> %follow skirmish(G1, G2) if G1 or G2 army = 0 %follow skirmish(G1, G2) if time >= 18, trigger on weaker G Flus = ?F_WITHDRAWAL, Vars = ["General"], Reqs = [ ?REQ( main, {withdrawing, dict:fetch("General", _Bindings)}, _Time, [], _Val =:= false ) ], Extras = [], Cancels = [], Arc = i_arc:new(withdrawal, main, Flus, Vars, Reqs, Extras, Cancels), thucydides:add_iarc(Arc, rps_sim) . setup_surrender() -> nop%ignore for now %the player is forced to surrender after ten days. %if the player wipes out the enemy army before then, %then the enemy army surrenders. %follow withdrawal of G when army = 0 %follow withdrawal of G when time > 24*10 %require no previous surrenders for G %set surrender for G . setup_advance_follows() -> %follow advance of player with an enemy advance of the same forces PlayerAdvanced = follow:new( advance_follows_advance, [advance], [advance], [ requirement:new( fun(_, _T, Bindings) -> dict:fetch("OldGeneral", Bindings) =:= player end ), %require that the enemy isn't already advanced ?REQ( main, {[transit, field]}, _Time, [], lists:all(fun({_, Amt}) -> Amt =:= 0 end, units(dict:fetch("General", _Bindings), _Val)) ) ], 0, 0, [ fun(_Time, _Bindings) -> %choose the army weak to the player's army io:format("bindings are ~p~n", [_Bindings]), ENSw = dict:fetch("NSp", _Bindings), ENSp = dict:fetch("NAx", _Bindings), ENAx = dict:fetch("NSw", _Bindings), [{"General", [enemy]}, {"OldGeneral", [dict:fetch("General", _Bindings)]}, {"NSw", [ENSw]}, {"NSp", [ENSp]}, {"NAx", [ENAx]}] end ] ), thucydides:add_follow(PlayerAdvanced, rps_sim) . setup_skirmish_follows() -> %follow advance of G1 vs G2 when both armies are on the field ArmiesMeet = follow:new( skirmish_when_armies_meet, %follow anything [], %trigger a skirmish [skirmish], [ %only trigger if the last conflict was yesterday ?REQ( main, {last_conflict, dict:fetch("Attacker", _Bindings), dict:fetch("Defender", _Bindings)}, _Time, [], (_Time - _Val) > 8 ) ], %we should start checking immediately 0, %and we should check indefinitely infinity, %gather the bindings - return a list of fun(_Time, _Bindings), each of which returns %a list of {Var, [Binding]} tuples. %If the binding list has multiple values, the functions evaluated later will be called %several times with different _Bindings. This occurs until the very last function is %evaluated, and the result is a list of bindings dictionaries that hold all valid sets %of bindings. [ fun(_Time, _Bindings) -> %we want all combinations of two generals(don't care about permutations) {Atks, Defs} = hero_lists:combinations(herodotus:value( selector:new(generals, _Time), [], main, rps_hero )), [{"Attacker", Atks}, {"Defender", Defs}] end ] ), %follow skirmish of G1 vs G2 after 1hr if time < 18 HourlySkirmish = follow:new( skirmish_follows_skirmish, %follow other skirmishes [skirmish], %trigger a skirmish [skirmish], [ %trigger if we've rested for at least an hour ?REQ( main, {last_conflict, dict:fetch("Attacker", _Bindings), dict:fetch("Defender", _Bindings)}, _Time, [], (_Time - _Val) >= 1 ) ], %a skirmish should trigger 1 hr after this is met 1, %but it should only try to meet the skirmish's requirements once 0, [ fun(_Time, _Bindings) -> [{"Attacker", [dict:fetch("Attacker", _Bindings)]}, {"Defender", [dict:fetch("Defender", _Bindings)]}] end ] ), Follows = [ArmiesMeet, HourlySkirmish], thucydides:add_follow(Follows, rps_sim) . setup_withdraw_follows() -> %follow skirmish of G1 vs G2 when one army or both is defeated ArmyBeaten = follow:new( withdrawal_follows_beaten_army, %follow skirmish [skirmish], %trigger a withdrawal [withdrawal], [ %this isn't getting sprung :O ?REQ( main, {field}, _Time, [], lists:all(fun({_, Amt}) -> Amt =:= 0 end, units(dict:fetch("General", _Bindings), _Val)) ) ], %we should start checking in a bit to avoid paradox with the skirmish that may have caused the withdrawal 0.5, %and we should check once 1, [ fun(_Time, _Bindings) -> Armies = herodotus:value(selector:new({field}, _Time), [], main, rps_hero), [{"General", lists:foldl(fun(Gen, Acc) -> case lists:all(fun({_, Amt}) -> Amt =:= 0 end, units(Gen, Armies)) of true -> [Gen | Acc]; false -> Acc end end, [], [dict:fetch("Attacker", _Bindings), dict:fetch("Defender", _Bindings)])}] end ] ), DayOver = follow:new( withdrawal_follows_end_of_day, %follow skirmish [skirmish], %trigger a withdrawal [withdrawal], [ ?REQ( main, light, _Time, [], _Val =< 0.5 ) ], %we should start checking in a bit to avoid paradox with the skirmish that may have caused the withdrawal 0.5, %and we should check once 1, [ fun(_Time, _Bindings) -> DayStart = (_Time - (trunc(_Time) rem 24)), PreArmies = herodotus:value(selector:new({field}, DayStart), [], main, rps_hero), Armies = herodotus:value(selector:new({field}, _Time), [], main, rps_hero), Atk = dict:fetch("Attacker", _Bindings), Def = dict:fetch("Defender", _Bindings), PreAtkUnits = lists:foldl(fun({_, Amt}, Acc) -> Amt + Acc end, 0, units(Atk, PreArmies)), AtkUnits = lists:foldl(fun({_, Amt}, Acc) -> Amt + Acc end, 0, units(Atk, Armies)), PreDefUnits = lists:foldl(fun({_, Amt}, Acc) -> Amt + Acc end, 0, units(Atk, PreArmies)), DefUnits = lists:foldl(fun({_, Amt}, Acc) -> Amt + Acc end, 0, units(Def, Armies)), AtkLosses = PreAtkUnits - AtkUnits, DefLosses = PreDefUnits - DefUnits, case AtkLosses >= DefLosses of true -> [{"General", [Atk]}]; false -> [{"General", [Def]}] end end ] ), EnemyWithdrew = follow:new( withdrawal_follows_enemy_withdrawal, %follow withdrawal [withdrawal], %trigger a withdrawal [withdrawal], [ ], %we should start checking in a bit to avoid paradox with the skirmish that may have caused the withdrawal 0.5, %and we should check once 1, [ fun(_Time, _Bindings) -> General = dict:fetch("General", _Bindings), [{"General", lists:foldl(fun(Gen, Acc) -> case Gen of General -> Acc; _ -> Conflict = herodotus:value( selector:new({last_conflict, General, Gen}, _Time), [], main, rps_hero ), io:format("Conflict between ~p and ~p at ~p was ~p: ~p~n", [General, Gen, _Time, Conflict, _Time - Conflict]), case is_number(Conflict) andalso ((_Time - Conflict) =< 2) of true -> io:format("adding ~p~n", [Gen]), [Gen | Acc]; false -> Acc end end end, [], herodotus:value(selector:new(generals, _Time), [], main, rps_hero))}] end ] ), Follows = [ArmyBeaten, DayOver, EnemyWithdrew], thucydides:add_follow(Follows, rps_sim) . %Current_time is a private function that returns the time. current_time() -> thucydides:current_time(rps_sim) . %Tick is a private function that increments the time. tick() -> tick(1). tick(N) -> %Tick the simulation forward by N in increments of half an hour. thucydides:progress_time(current_time()+N, 0.5, rps_sim) . advance(NSw, NSp, NAx) when NSw+NSp+NAx =< 50 -> OldArmy = units(player, herodotus:value( selector:new( {reserve}, current_time() ), [], main, rps_hero )), Sum = [{swords, NSw}, {spears, NSp}, {axes, NAx}], NewArmy = add_units(OldArmy, lists:map(fun({U, A}) -> {U, -1 * A} end, Sum)), %trigger - catch not_met case thucydides:trigger(advance, current_time(), dict:store("General", player, dict:store("NSw", NSw, dict:store("NSp", NSp, dict:store("NAx", NAx, dict:new())))), rps_sim) of not_met -> io:format("Thine forces are too few!~n"), not_met; {ok, Triggered} -> case tick() of [ReplyIncident] -> io:format("Thine enemy has replied with ~p!~n", [ReplyIncident#incident.isa]), NextTriggered = Triggered ++ [ReplyIncident], case {units(player, herodotus:value( selector:new( {[transit, field, fort]}, current_time() ), [], main, rps_hero )), units(player, herodotus:value( selector:new( {reserve}, current_time() ), [], main, rps_hero ))} of {Sum, NewArmy} -> %poor man's error checking io:format("Thine armies approach the enemy!~n"), FinalTriggered = NextTriggered ++ tick(23), %go to the next day %print_day(), lists:foreach(fun(I) -> case I#incident.isa of advance -> io:format("At ~p, ~p advanced.~n", [I#incident.set_time, dict:fetch("General", I#incident.bindings)]); skirmish -> io:format("At ~p, the armies of ~p clashed with those of ~p.~n", [I#incident.set_time, dict:fetch("Attacker", I#incident.bindings), dict:fetch("Defender", I#incident.bindings)]); withdrawal -> io:format("At ~p, the armies of ~p withdrew from the battlefield.~n", [I#incident.set_time, dict:fetch("General", I#incident.bindings)]); X -> io:format("At ~p, ~p occurred~n", [I#incident.set_time, X]) end end, FinalTriggered), io:format("It is now ~p.~n", [current_time()]); Other -> io:format("Unfortunately, ~p was returned~n", [Other]) end; X -> io:format("Reply was ~p!~n", [X]) end end . fortify(NSw, NSp, NAx) when NSw+NSp+NAx =< 50 -> %validate input, then trigger, then tick nop. print_day() -> lists:foreach(fun(H) -> io:format("At the day's hour ~p, thine numbers wert as follows:~n", [H rem 24]), io:format("~p~n", [field(player, H)]), io:format("Thine enemy's forces wert as follows:~n"), io:format("~p~n", [field(enemy, H)]) end, lists:seq(current_time() - 24, current_time())) .