The following examples and translations are intended to showcase dyce’s flexibility.
If you have exposure to another tool, they may also help with transition.
We need to map semantic outcomes to numbers (and back again).
How can we represent those in dyce?
One way is IntEnums.
IntEnums have a property that allows them to substitute directly for ints, which, with a little nudging, is very convenient.
>>>fromenumimportIntEnum>>>classComplication(IntEnum):...NONE=0# this will come in handy later...COMMON=1...UNCOMMON=2...RARE=3...VERY_RARE=4>>>OUTCOME_TO_RARITY_MAP={...2:Complication.VERY_RARE,...3:Complication.VERY_RARE,...4:Complication.VERY_RARE,...5:Complication.RARE,...6:Complication.RARE,...7:Complication.UNCOMMON,...8:Complication.UNCOMMON,...9:Complication.COMMON,...10:Complication.COMMON,...11:Complication.COMMON,...12:Complication.COMMON,...13:Complication.COMMON,...14:Complication.UNCOMMON,...15:Complication.UNCOMMON,...16:Complication.RARE,...17:Complication.RARE,...18:Complication.VERY_RARE,...19:Complication.VERY_RARE,...20:Complication.VERY_RARE,...}
Now let’s use our map to validate the probabilities of a particular outcome using that d8 and d12.
Well butter my butt, and call me a biscuit!
That Angry guy sure knows his math!
Modeling Ironsworn’s core mechanic
Shawn Tomlin’s Ironsworn melds a number of different influences in a fresh way.
Its core mechanic involves rolling an action die (a d6), adding a modifier, and comparing the resulting value to two challenge dice (d10s).
If the modified value from the action die is strictly greater than both challenge dice, the result is a strong success.
If it is strictly greater than only one challenge die, the result is a weak success.
If it is equal to or less than both challenge dice, it’s a failure.
A verbose way to model this is to enumerate the product of the three dice and then perform logical comparisons.
However, if we recognize that our problem involves a dependent probability, we can craft a solution in terms of foreach.
We can also deploy a counting trick with the two d10s.
Let’s break it down.
H(10).lt(value) will tell us how often a single d10 is less than value.
12
>>>H(10).lt(5)# how often a d10 is strictly less than 5H({False:6,True:4})
By summing those results (and taking advantage of the fact that, in Python, bools act like ints when it comes to arithmetic operators), we can count how often that happens with more than one interchangeable d10.
How do we interpret those results?
36 times out of a hundred, neither d10 will be strictly less than five.
48 times out of a hundred, exactly one of the d10s will be strictly less than five.
16 times out of a hundred, both d10s will be strictly less than five.
>>># The parentheses are technically redundant, but clarify the intention>>>2@(H(10).lt(5))==H(10).lt(5)+H(10).lt(5)True
Why doesn’t 2@(H(6).gt(H(10)) work?
H(6).gt(H(10)) will compute how often a six-sided die is strictly greater than a ten-sided die.
2@(H(6).gt(H(10))) will show the frequencies that a first six-sided die is strictly greater than a first ten-sided die and a second six-sided die is strictly greater than a second ten-sided die.
This isn’t quite what we want, since the mechanic calls for rolling a single six-sided die and comparing that result to each of two ten-sided dice.
Now for a twist.
In cooperative or solo play, a failure or success is particularly spectacular when the d10s come up doubles.
The key to mapping that to dyce internals is recognizing that we have a dependent probability that involves three independent variables: the (modded) d6, a first d10, and a second d10.
foreach is especially useful where there are multiple independent terms.
By defining our dependent term function to include mod as a parameter with a default argument, we can use partial to manipulate it, which is helpful for visualization.
# ======================================================================================# Copyright and other protections apply. Please see the accompanying LICENSE file for# rights and restrictions governing use of this software. All rights not expressly# waived or licensed are reserved. If that file is missing or appears to be modified# from its original, then please contact the author before viewing or using this# software in any capacity.# ======================================================================================fromenumimportIntEnum,autofromfunctoolsimportpartialfromdyceimportH,Pfromdyce.evaluationimportHResult,PResult,foreachclassIronSoloResult(IntEnum):SPECTACULAR_FAILURE=-1FAILURE=auto()WEAK_SUCCESS=auto()STRONG_SUCCESS=auto()SPECTACULAR_SUCCESS=auto()defdo_it(style:str)->None:importmatplotlib.pyplotimportmatplotlib.tickerimportpandasd6=H(6)d10=H(10)defiron_solo_dependent_term(action:HResult,challenges:PResult,mod=0,):modded_action=action.outcome+modfirst_challenge_outcome,second_challenge_outcome=challenges.rollbeats_first_challenge=modded_action>first_challenge_outcomebeats_second_challenge=modded_action>second_challenge_outcomedoubles=first_challenge_outcome==second_challenge_outcomeifbeats_first_challengeandbeats_second_challenge:return(IronSoloResult.SPECTACULAR_SUCCESSifdoubleselseIronSoloResult.STRONG_SUCCESS)elifbeats_first_challengeorbeats_second_challenge:returnIronSoloResult.WEAK_SUCCESSelse:return(IronSoloResult.SPECTACULAR_FAILUREifdoubleselseIronSoloResult.FAILURE)mods=list(range(0,5))# TODO(posita): See <https://github.com/pandas-dev/pandas/issues/54386>df=pandas.DataFrame(columns=[v.nameforvinIronSoloResult])formodinmods:h_for_mod=foreach(partial(iron_solo_dependent_term,mod=mod),action=d6,challenges=2@P(d10),)# TODO(posita): See <https://github.com/pandas-dev/pandas/issues/54386>results_for_mod={outcome.name:count# type: ignoreforoutcome,countinh_for_mod.zero_fill(IronSoloResult).distribution(rational_t=lambdan,d:n/d)}row=pandas.DataFrame(# TODO(posita): See <https://github.com/pandas-dev/pandas/issues/54386>results_for_mod,columns=[v.nameforvinIronSoloResult],index=[mod],)df=pandas.concat((df,row))df.index.name="Modifier"# TODO(posita): See <https://github.com/pandas-dev/pandas/issues/54386># # DataFrames use enum's values for displaying column names, so we convert them to# # names# df = df.rename(columns={v: v.name for v in IronSoloResult})print(df.style.format("{:.2%}").to_html())ax=df.plot(kind="barh",stacked=True)text_color="white"ifstyle=="dark"else"black"ax.tick_params(axis="x",colors=text_color)ax.tick_params(axis="y",colors=text_color)ylabel=ax.get_ylabel()ax.set_ylabel(ylabel,color=text_color)ax.xaxis.set_major_formatter(matplotlib.ticker.PercentFormatter(xmax=1))ax.legend()ax.set_title("Ironsworn distributions",color=text_color)
Advanced topic – modeling Risis
Risus and its many community-developed alternative rules not only make for entertaining reading, but are fertile ground for stressing ergonomics and capabilities of any discrete outcome modeling tool.
We can easily model the first round of its opposed combat system for various starting configurations.
1 2 3 4 5 6 7 8 9101112131415161718
>>>fortheminrange(3,6):...print("---")...forusinrange(them,them+3):...first_round=(us@H(6)).vs(them@H(6))# -1 is a loss, 0 is a tie, 1 is a win...risus_results=first_round.format(width=0)...print(f"{us}d6 vs. {them}d6: {risus_results}")---3d6vs.3d6:{...,-1:45.36%,0:9.28%,1:45.36%}4d6vs.3d6:{...,-1:19.17%,0:6.55%,1:74.28%}5d6vs.3d6:{...,-1:6.07%,0:2.99%,1:90.93%}---4d6vs.4d6:{...,-1:45.95%,0:8.09%,1:45.95%}5d6vs.4d6:{...,-1:22.04%,0:6.15%,1:71.81%}6d6vs.4d6:{...,-1:8.34%,0:3.26%,1:88.40%}---5d6vs.5d6:{...,-1:46.37%,0:7.27%,1:46.37%}6d6vs.5d6:{...,-1:24.24%,0:5.79%,1:69.96%}7d6vs.5d6:{...,-1:10.36%,0:3.40%,1:86.24%}
This highlights the mechanic’s notorious “death spiral”, which we can visualize as a heat map.
# ======================================================================================# Copyright and other protections apply. Please see the accompanying LICENSE file for# rights and restrictions governing use of this software. All rights not expressly# waived or licensed are reserved. If that file is missing or appears to be modified# from its original, then please contact the author before viewing or using this# software in any capacity.# ======================================================================================fromdyceimportHdefdo_it(style:str)->None:importmatplotlib.pyplotcol_names=["Loss","Tie","Win"]col_ticks=list(range(len(col_names)))num_scenarios=3text_color="white"ifstyle=="dark"else"black"fori,theminenumerate(range(3,3+num_scenarios)):ax=matplotlib.pyplot.subplot(1,num_scenarios,i+1)row_names:list[str]=[]rows:list[tuple[float,...]]=[]num_rows=3forusinrange(them,them+num_rows):row_names.append(f"{us}d6 …")results=(us@H(6)).vs(them@H(6))rows.append(results.distribution_xy()[-1])ax.imshow(rows)ax.set_title(f"… vs. {them}d6",color=text_color)ax.set_xticks(col_ticks)ax.set_xticklabels(col_names,color=text_color,rotation=90)ax.set_yticks(list(range(len(rows))))ax.set_yticklabels(row_names,color=text_color)foryinrange(len(row_names)):forxinrange(len(col_names)):ax.text(x,y,f"{rows[y][x]:.0%}",ha="center",va="center",color="w",)
Modeling entire multi-round combats
With a little elbowfinger grease, we can roll up our … erm … fingerless gloves and even model various starting configurations through to completion to get a better sense of the impact of any initial disparity (in this case, applying dynamic programming to avoid redundant computations).
>>>fromdyceimportH,P>>>fromdyce.evaluationimportHResult,foreach>>>importsys>>>fromenumimportIntEnum,auto>>>fromtypingimportCallable>>>fromfunctoolsimportcache>>>classRisus(IntEnum):...LOSS=-1...DRAW=auto()...WIN=auto()>>>defrisus_combat_driver(...us:int,# number of dice we still have...them:int,# number of dice they still have...us_vs_them_func:Callable[[int,int],H],...)->H:...ifus<0orthem<0:...raiseValueError(f"cannot have negative numbers (us: {us}, them: {them})")...ifus==0andthem==0:...returnH({0:1})# should not happen unless combat(0, 0) is called from the start......@cache...def_resolve(us:int,them:int)->H:...ifus==0:returnH({-1:1})# we are out of dice, they win...ifthem==0:returnH({1:1})# they are out of dice, we win...this_round=us_vs_them_func(us,them)......def_next_round(this_round:HResult)->H:...ifthis_round.outcome==Risus.LOSS:return_resolve(us-1,them)# we lost this round, and one die...elifthis_round.outcome==Risus.WIN:return_resolve(us,them-1)# they lost this round, and one die...elifthis_round.outcome==Risus.DRAW:returnH({})# ignore (immediately re-roll) all ties...else:assertFalse,f"unrecognized this_round.outcome {this_round.outcome}"......returnforeach(_next_round,this_round=this_round,limit=-1)...return_resolve(us,them)>>>fortinrange(3,6):...print("---")...foruinrange(t,t+3):...risus_results=risus_combat_driver(...u,t,...lambdau,t:(u@H(6)).vs(t@H(6))...).format(width=0)...print(f"{u}d6 vs. {t}d6: {risus_results}")---3d6vs.3d6:{...,-1:50.00%,1:50.00%}4d6vs.3d6:{...,-1:10.50%,1:89.50%}5d6vs.3d6:{...,-1:0.66%,1:99.34%}---4d6vs.4d6:{...,-1:50.00%,1:50.00%}5d6vs.4d6:{...,-1:12.25%,1:87.75%}6d6vs.4d6:{...,-1:1.07%,1:98.93%}---5d6vs.5d6:{...,-1:50.00%,1:50.00%}6d6vs.5d6:{...,-1:13.66%,1:86.34%}7d6vs.5d6:{...,-1:1.49%,1:98.51%}
There’s lot going on there.
Let’s dissect it.
17181920212223
@cachedefrisus_combat_driver(us:int,# number of dice we still havethem:int,# number of dice they still haveus_vs_them_func:Callable[[int,int],H],)->H:...
Our “driver” takes three arguments:
How many dice we have left (us);
How many dice the opposition has left (them); and
A resolution function (us_vs_them_func) that takes counts of each party’s remaining dice and returns a histogram encoding the probability of winning or losing a single round akin to the H.vs method:
An outcome of -1 signals the opposition’s victory
An outcome of 1 signals our victory.
An outcome of 0 signals a tie.
The @cache decorator does simple memoization for us because there are redundancies.
For example, we might compute a case where we lose a die, then our opposition loses a die.
We arrive at a similar case where our opposition loses a die, then we lose a die.
Both cases would be identical from that point on.
In this context, @cache helps us avoid recomputing redundant sub-trees.
23242526
ifus<0orthem<0:raiseValueError(f"cannot have negative numbers (us: {us}, them: {them})")ifus==0andthem==0:returnH({0:1})# should not happen unless combat(0, 0) is called from the start
We make some preliminary checks that guard access to our recursive implementation so that it can be a little cleaner.
2829
def_resolve(us:int,them:int)->H:...
40
return_resolve(us,them)
Skipping over its implementation for now, we define a our memoized recursive implementation (_resolve) and then call it with our initial arguments.
282930
def_resolve(us:int,them:int)->H:ifus==0:returnH({-1:1})# we are out of dice, they winifthem==0:returnH({1:1})# they are out of dice, we win
Getting back to that implementation, these are our base cases.
We check whether either party has run out of dice, in which case the combat is over.
If we have none of those cases, we get to work.
Note
In this function, we do not check for the case where both parties are at zero.
Because only one party can lose a die during each round, the only way both parties can be at zero simultaneously is if they both started at zero.
Since we guard against that case in the enclosing function, we don’t have to worry about it here.
Either us is zero, them is zero, or neither is zero.
31
this_round=us_vs_them_func(us,them)
Then, we compute the outcomes for this round using the provided resolution function.
3334
def_next_round(this_round:HResult)->H:...
38
return_next_round(this_round=this_round)
Keeping in mind that we’re inside our recursive implementation, we define a dependent term.
This allows us to take our computation for this round, and “fold in” subsequent rounds.
33343536
def_next_round(this_round)->H:ifthis_round.outcome<0:return_resolve(us-1,them)# we lost this round, and one dieelifthis_round.outcome>0:return_resolve(us,them-1)# they lost this round, and one dieelse:returnH({})# ignore (immediately re-roll) all ties
Our dependent term is pretty straightforward.
Where we are asked to resolve a round we lost, we lose a die and recurse.
Where we are asked to resolve a round we won, our opposition loses a die and we recurse.
We ignore ties (simulating that we re-roll them in place until they are no longer ties).
At this point, we can define a simple lambda that wraps H.vs and submit it to our driver to enumerate resolution outcomes from various starting positions.
Note
This is a complicated example that involves some fairly sophisticated programming techniques (recursion, memoization, nested functions, etc.).
The point is not to suggest that such techniques are required to be productive.
However, it is useful to show that dyce is flexible enough to model these types of outcomes in a couple dozen lines of code.
It is high-level enough to lean on for nuanced number crunching without a lot of detailed knowledge, while still being low-level enough that authors knowledgeable of advanced programming techniques are not precluded from using them.
Modeling different combat resolution methods
Using our risus_combat_driver from above, we can craft a alternative resolution function to model the less death-spirally “Best of Set” alternative mechanic from The Risus Companion with the optional “Goliath Rule” for resolving ties.
>>>defdeadly_combat_vs(us:int,them:int)->H:...best_us=(us@P(6)).h(-1)...best_them=(them@P(6)).h(-1)...h=best_us.vs(best_them)...# Goliath Rule: tie goes to the party with fewer dice in this round...h=foreach(...lambdah_result:(us<them)-(us>them)ifh_result.outcome==0elseh_result.outcome,...h_result=h,...)...returnh>>>fortinrange(3,6):...print("---")...foruinrange(t,t+3):...risus_results=risus_combat_driver(u,t,deadly_combat_vs).format(width=0)...print(f"{u}d6 vs. {t}d6: {risus_results}")---3d6vs.3d6:{...,-1:50.00%,1:50.00%}4d6vs.3d6:{...,-1:36.00%,1:64.00%}5d6vs.3d6:{...,-1:23.23%,1:76.77%}---4d6vs.4d6:{...,-1:50.00%,1:50.00%}5d6vs.4d6:{...,-1:40.67%,1:59.33%}6d6vs.4d6:{...,-1:30.59%,1:69.41%}---5d6vs.5d6:{...,-1:50.00%,1:50.00%}6d6vs.5d6:{...,-1:44.13%,1:55.87%}7d6vs.5d6:{...,-1:36.89%,1:63.11%}
The “Evens Up” alternative dice mechanic presents some challenges.
First, dyce’s substitution mechanism only resolves outcomes through a fixed number of iterations, so it can only approximate probabilities for infinite series.
Most of the time, the implications are largely theoretical with a sufficient number of iterations.
This is no exception.
Second, with one narrow exception, dyce only provides a mechanism to directly foreach outcomes, not counts.
This means we can’t arbitrarily increase the likelihood of achieving a particular outcome through replacement.
With some creativity, we can work around that, too.
In the case of “Evens Up”, we need to keep track of whether an even number was rolled, but we also need to keep rolling (and accumulating) as long as sixes are rolled.
This behaves a lot like an exploding die with three values (miss, hit, and hit-and-explode).
Further, we can observe that every “run” will be zero or more exploding hits terminated by either a miss or a non-exploding hit.
If we choose our values carefully, we can encode how many times we’ve encountered relevant events as we explode.
For every value that is even, we ended in a miss.
For every value that is odd, we ended in a hit that will need to be tallied.
Dividing by two and ignoring any remainder will tell us how many exploding hits we had along the way.
1 2 3 4 5 6 7 8 910111213
>>>defdecode_hits(h_result:HResult):...return(h_result.outcome+1)//2# equivalent to h_result.outcome // 2 + h_result.outcome % 2>>>d_evens_up=foreach(decode_hits,h_result=d_evens_up_exploded)>>>print(d_evens_up.format())avg|0.60std|0.69var|0.480|50.00%|#########################1|41.67%|####################2|6.94%|###3|1.16%|4|0.23%|
We can now approximate a complete “Evens Up” combat using our risus_combat_driver from above.
We can also deploy a trick using partial to parameterize use of the Goliath Rule.
1 2 3 4 5 6 7 8 910111213141516171819202122232425
>>>fromfunctoolsimportpartial>>>defevens_up_vs(us:int,them:int,goliath:bool=False)->H:...h=(us@d_evens_up).vs(them@d_evens_up)...ifgoliath:...h=foreach(...lambdah_result:(us<them)-(us>them)ifh_result.outcome==0elseh_result.outcome,...h_result=h,...)...returnh>>>fortinrange(3,5):...print("----------- ---- With Goliath Rule ----- --- Without Goliath Rule ---")...foruinrange(t,t+3):...goliath_results=risus_combat_driver(u,t,partial(evens_up_vs,goliath=True)).format(width=0)...no_goliath_results=risus_combat_driver(u,t,partial(evens_up_vs,goliath=False)).format(width=0)...print(f"{u}d6 vs. {t}d6: {goliath_results}{no_goliath_results}")---------------WithGoliathRule--------WithoutGoliathRule---3d6vs.3d6:{...,-1:50.00%,1:50.00%}{...,-1:50.00%,1:50.00%}4d6vs.3d6:{...,-1:29.51%,1:70.49%}{...,-1:19.08%,1:80.92%}5d6vs.3d6:{...,-1:12.32%,1:87.68%}{...,-1:4.57%,1:95.43%}---------------WithGoliathRule--------WithoutGoliathRule---4d6vs.4d6:{...,-1:50.00%,1:50.00%}{...,-1:50.00%,1:50.00%}5d6vs.4d6:{...,-1:30.52%,1:69.48%}{...,-1:21.04%,1:78.96%}6d6vs.4d6:{...,-1:13.68%,1:86.32%}{...,-1:5.88%,1:94.12%}
# ======================================================================================# Copyright and other protections apply. Please see the accompanying LICENSE file for# rights and restrictions governing use of this software. All rights not expressly# waived or licensed are reserved. If that file is missing or appears to be modified# from its original, then please contact the author before viewing or using this# software in any capacity.# ======================================================================================fromanydyce.vizimportplot_linefromdyceimportH,Pfromdyce.evaluationimportexplodedefdo_it(style:str)->None:importmatplotlib.pyplotp_4d6=4@P(6)d6_reroll_first_one=explode(H(6),lambdaresult:result.outcome==1)p_4d6_reroll_first_one=4@P(d6_reroll_first_one)p_4d6_reroll_all_ones=4@P(H(5)+1)markers="Ds^*xo"attr_results:dict[str,H]={"3d6":3@H(6),# marker="D""4d6 - discard lowest":p_4d6.h(slice(1,None)),# marker="s""4d6 - re-roll first 1, discard lowest":p_4d6_reroll_first_one.h(slice(1,None)),# marker="^""4d6 - re-roll all 1s (i.e., 4d(1d5 + 1)), discard lowest":p_4d6_reroll_all_ones.h(slice(1,None)),# marker="*""2d6 + 6":2@H(6)+6,# marker="x""4d4 + 2":4@H(4)+2,# marker="o"}ax=matplotlib.pyplot.axes()text_color="white"ifstyle=="dark"else"black"ax.tick_params(axis="x",colors=text_color)ax.tick_params(axis="y",colors=text_color)plot_line(ax,[(label,res)forlabel,resinattr_results.items()],)forline,markerinzip(ax.lines,markers):line.set_marker(marker)ax.legend()ax.set_title("Comparing various take-three-of-4d6 methods",color=text_color)
# ======================================================================================# Copyright and other protections apply. Please see the accompanying LICENSE file for# rights and restrictions governing use of this software. All rights not expressly# waived or licensed are reserved. If that file is missing or appears to be modified# from its original, then please contact the author before viewing or using this# software in any capacity.# ======================================================================================fromanydyce.vizimportplot_linefromdyceimportHdefdo_it(style:str)->None:importmatplotlib.pyplotsave_roll=H(20)burning_arch_damage=10@H(6)+10pass_save=save_roll.ge(10)damage_half_on_save=burning_arch_damage//(pass_save+1)ax=matplotlib.pyplot.axes()text_color="white"ifstyle=="dark"else"black"ax.tick_params(axis="x",colors=text_color)ax.tick_params(axis="y",colors=text_color)plot_line(ax,[("",damage_half_on_save)])ax.set_title("Attack with saving throw for half damage",color=text_color)
>>>fromdyceimportH>>>fromdyce.evaluationimportHResult,foreach>>>single_attack=2@H(6)+5>>>defgwf(dmg):...returndmg.hifdmg.outcomein(1,2)elsedmg.outcome>>>great_weapon_fighting=2@(foreach(gwf,dmg=H(6)))+5# reroll either die if it is a one or two>>>print(single_attack.format(width=0)){...,7:2.78%,8:5.56%,9:8.33%,10:11.11%,11:13.89%,12:16.67%,13:13.89%,...}>>>print(great_weapon_fighting.format(width=0)){...,7:0.31%,8:0.62%,9:2.78%,10:4.94%,11:9.88%,12:14.81%,13:17.28%,...}
# ======================================================================================# Copyright and other protections apply. Please see the accompanying LICENSE file for# rights and restrictions governing use of this software. All rights not expressly# waived or licensed are reserved. If that file is missing or appears to be modified# from its original, then please contact the author before viewing or using this# software in any capacity.# ======================================================================================fromanydyce.vizimportplot_burst,plot_linefromdyceimportHfromdyce.evaluationimportHResult,foreachdefdo_it(style:str)->None:importmatplotlib.pyplotsingle_attack=2@H(6)+5defgwf(result:HResult):returnresult.hifresult.outcomein(1,2)elseresult.outcomegreat_weapon_fighting=2@foreach(gwf,result=H(6))+5text_color="white"ifstyle=="dark"else"black"label_sa="Normal attack"label_gwf="“Great Weapon Fighting”"ax_plot=matplotlib.pyplot.subplot2grid((1,2),(0,0))ax_plot.tick_params(axis="x",colors=text_color)ax_plot.tick_params(axis="y",colors=text_color)plot_line(ax_plot,[(label_sa,single_attack),(label_gwf,great_weapon_fighting)])ax_plot.lines[0].set_color("tab:green")ax_plot.lines[1].set_color("tab:blue")ax_plot.legend()ax_burst=matplotlib.pyplot.subplot2grid((1,2),(0,1))plot_burst(ax_burst,h_inner=great_weapon_fighting,h_outer=single_attack,title=f"{label_sa}\nvs.\n{label_gwf}",inner_cmap="RdYlBu_r",outer_cmap="RdYlGn_r",text_color=text_color,)
# ======================================================================================# Copyright and other protections apply. Please see the accompanying LICENSE file for# rights and restrictions governing use of this software. All rights not expressly# waived or licensed are reserved. If that file is missing or appears to be modified# from its original, then please contact the author before viewing or using this# software in any capacity.# ======================================================================================fromanydyce.vizimportplot_linefromdyceimportH,Pfromdyce.evaluationimportHResult,foreachdefdo_it(style:str)->None:importmatplotlib.pyplotnormal_hit=H(12)+5critical_hit=3@H(12)+5advantage=(2@P(20)).h(-1)defcrit(result:HResult):ifresult.outcome==20:returncritical_hitelifresult.outcome+5>=14:returnnormal_hitelse:return0advantage_weighted=foreach(crit,result=advantage)ax=matplotlib.pyplot.axes()text_color="white"ifstyle=="dark"else"black"ax.tick_params(axis="x",colors=text_color)ax.tick_params(axis="y",colors=text_color)plot_line(ax,[("Normal hit",normal_hit),("Critical hit",critical_hit),("Advantage-weighted",advantage_weighted),],)ax.legend()ax.set_title("Advantage-weighted attack with critical hits",color=text_color)
# ======================================================================================# Copyright and other protections apply. Please see the accompanying LICENSE file for# rights and restrictions governing use of this software. All rights not expressly# waived or licensed are reserved. If that file is missing or appears to be modified# from its original, then please contact the author before viewing or using this# software in any capacity.# ======================================================================================fromanydyce.vizimportplot_linefromdyceimportH,Pfromdyce.evaluationimportexplodedefdo_it(style:str)->None:importmatplotlib.pyplotax=matplotlib.pyplot.axes()text_color="white"ifstyle=="dark"else"black"ax.tick_params(axis="x",colors=text_color)ax.tick_params(axis="y",colors=text_color)plot_line(ax,[(f"{depth} rerolls",(10@P(explode(H(10),limit=depth))).h(slice(-3,None)),)fordepthinrange(5,-1,-1)],)forlineinax.lines:line.set_marker("")ax.legend()ax.set_title("Taking the three highest of ten exploding d10s",color=text_color)
# ======================================================================================# Copyright and other protections apply. Please see the accompanying LICENSE file for# rights and restrictions governing use of this software. All rights not expressly# waived or licensed are reserved. If that file is missing or appears to be modified# from its original, then please contact the author before viewing or using this# software in any capacity.# ======================================================================================fromanydyce.vizimportplot_scatterfromdyceimportPfromdyce.evaluationimportPResult,foreachdefdo_it(style:str)->None:importmatplotlib.pyplotdefdupes(result:PResult):dupes=0foriinrange(1,len(result.roll)):ifresult.roll[i]==result.roll[i-1]:dupes+=1returndupesres_15d6=foreach(dupes,result=15@P(6))res_8d10=foreach(dupes,result=8@P(10))matplotlib.pyplot.rcParams["lines.markersize"]*=2ax=matplotlib.pyplot.axes()text_color="white"ifstyle=="dark"else"black"ax.tick_params(axis="x",colors=text_color)ax.tick_params(axis="y",colors=text_color)plot_scatter(ax,[("15d6",res_15d6),("8d10",res_8d10)],alpha=1.0)ax.legend()ax.set_title("Chances of rolling $n$ duplicates",color=text_color)
# ======================================================================================# Copyright and other protections apply. Please see the accompanying LICENSE file for# rights and restrictions governing use of this software. All rights not expressly# waived or licensed are reserved. If that file is missing or appears to be modified# from its original, then please contact the author before viewing or using this# software in any capacity.# ======================================================================================fromtypingimportIteratorfromanydyce.vizimportplot_linefromdyceimportH,Pdefdo_it(style:str)->None:importmatplotlib.pyplotdefroll_and_keep(p:P,k:int):assertp.is_homogeneous()max_d=max(p[-1])ifpelse0forroll,countinp.rolls_with_counts():total=sum(roll[-k:])+sum(1foroutcomeinroll[:-k]ifoutcome==max_d)yieldtotal,countd,k=6,3ax=matplotlib.pyplot.axes()text_color="white"ifstyle=="dark"else"black"ax.tick_params(axis="x",colors=text_color)ax.tick_params(axis="y",colors=text_color)marker_start=0def_roll_and_keep_hs()->Iterator[tuple[str,H]]:forninrange(k+1,k+9):p=n@P(d)yieldf"{n}d{d} keep {k} add +1",H(roll_and_keep(p,k))plot_line(ax,tuple(_roll_and_keep_hs()),alpha=0.75)foriinrange(marker_start,len(ax.lines)):ax.lines[i].set_marker(".")marker_start=len(ax.lines)def_normal()->Iterator[tuple[str,H]]:forninrange(k+1,k+9):p=n@P(d)yieldf"{n}d{d} keep {k}",p.h(slice(-k,None))plot_line(ax,tuple(_normal()),alpha=0.25)foriinrange(marker_start,len(ax.lines)):ax.lines[i].set_marker("o")ax.legend(loc="upper left")ax.set_title("Roll-and-keep mechanic comparison",color=text_color)
function: brawl A:s vs B:s {
SA: A >= 1@B
SB: B >= 1@A
if SA-SB=0 {
result:(A > B) - (A < B)
}
result:SA-SB
}
output [brawl 3d6 vs 3d6] named "A vs B Damage"
function: set element I:n in SEQ:s to N:n {
NEW: {}
loop J over {1 .. #SEQ} {
if I = J { NEW: {NEW, N} }
else { NEW: {NEW, J@SEQ} }
}
result: NEW
}
function: brawl A:s vs B:s with optional swap {
if #A@A >= 1@B {
result: [brawl A vs B]
}
AX: [sort [set element #A in A to 1@B]]
BX: [sort [set element 1 in B to #A@A]]
result: [brawl AX vs BX]
}
output [brawl 3d6 vs 3d6 with optional swap] named "A vs B Damage"
Translation:
1 2 3 4 5 6 7 8 91011121314
>>>defbrawl_w_optional_swap(p_result_a:PResult,p_result_b:PResult):...roll_a,roll_b=p_result_a.roll,p_result_b.roll...ifroll_a[0]<roll_b[-1]:...roll_a,roll_b=roll_a[1:]+roll_b[-1:],roll_a[:1]+roll_b[:-1]...# Sort greatest-to-least after the swap...roll_a=tuple(sorted(roll_a,reverse=True))...roll_b=tuple(sorted(roll_b,reverse=True))...else:...# Reverse to be greatest-to-least...roll_a=roll_a[::-1]...roll_b=roll_b[::-1]...a_successes=sum(1forvinroll_aifv>=roll_b[0])...b_successes=sum(1forvinroll_bifv>=roll_a[0])...returna_successes-b_successesor(roll_a>roll_b)-(roll_a<roll_b)