PDA

View Full Version : Stun likelihood script



x3n0n
2021-02-07, 12:25 AM
How likely is a Monk to be able to focus fire and stun a given target in one round?

I ran some experiments in another thread, just simulating it over and over. This new script directly calculates the likelihood of each branch, and then evaluates the associated outcome.

Assumptions:
* the Monk has Extra Attack, Stunning Strike, and Flurry of Blows available
* the Monk will Flurry if and only if the first two attacks don't stun (i.e. prioritize stun over ki, and ki over damage)

Key inputs:
* Monk ki DC minus target Con save bonus (i.e. required d20 roll to avoid a stun, given a hit)
* target AC minus Monk to-hit bonus (i.e. required d20 roll to hit, ignoring 1 and 20)

Output:
* likelihood to stun (and conversely to fail)
* likelihood to have a "hanging" attack (2nd attack or 2nd flurry attack)
* likelihood to have an unused bonus action
* expected number of ki spent
* expected number of hits (and critical hits)
* and all of the same if the Monk has advantage on all attacks (say, if the target is already stunned)

For example, consider a Monk with +11 to hit and ki DC 19, vs a creature with AC 19 and Con save bonus +3.
d20 to hit the target: 19 - 11 = 8
d20 for the target to save: 19 - 3 = 16




; perl stun.pl 8 16
Given d20tohit 8, d20tosave 16, advantage? (0):
P(stun) = 0.931011694335938
E(ki|stun) = 1.44737119311598
E(hits|stun) = 1.23935238356271
E(hanging-attack|stun) = 0.661157024793389
E(unused-BA|stun) = 0.791981190446727
P(nostun) = 0.0689883056640625
E(ki|nostun) = 2.26829268292683
E(hits|nostun) = 1.26829268292683
E(ki) = 1.50400517578125
E(hit) = 1.24134892578125
E(crits) = 0.09548837890625
E(hanging-attack) = 0.615544921875
E(unused-BA) = 0.73734375
hits-per-adv-attack = 0.8775
crits-per-adv-attack = 0.0975

Given d20tohit 8, d20tosave 16, advantage? (1):
P(stun) = 0.986339412585297
E(ki|stun) = 1.40243218563631
E(hits|stun) = 1.29778471145726
E(hanging-attack|stun) = 0.745225896599907
E(unused-BA|stun) = 0.895352525820953
P(nostun) = 0.0136605874147034
E(ki|nostun) = 3.56672760511883
E(hits|nostun) = 2.56672760511883
E(ki) = 1.4319977324054
E(hit) = 1.3151192167804
E(crits) = 0.146124357420044
E(hanging-attack) = 0.735045673095703
E(unused-BA) = 0.883121484375
hits-per-adv-attack = 0.8775
crits-per-adv-attack = 0.0975



So the Monk is 93.10% likely to stun initially, having spent on average 1.5 ki and gotten 1.24 hits, having 61% likelihood of a pending attack and 73% likelihood of an unused bonus action (and the hanging attack and bonus action are easily converted into extra damage: each post-stun attack is 87.75% likely to hit).
If the target is already stunned, the likelihood of keeping the stun is 98.6%, using 1.43 ki, with even higher likelihood of having unused attacks and bonus actions.

Consider instead the same Monk (+11, DC 19), but targeting a creature with AC 22 and +10 Con save: d20 to hit is 11, d20 to save is 9.




; perl stun.pl 11 9
Given d20tohit 11, d20tosave 9, advantage? (0):
P(stun) = 0.5904
E(ki|stun) = 1.84959349593496
E(hits|stun) = 1.45934959349593
E(hanging-attack|stun) = 0.555555555555555
E(unused-BA|stun) = 0.609756097560975
P(nostun) = 0.4096
E(ki|nostun) = 2.5
E(hits|nostun) = 1.5
E(ki) = 2.116
E(hit) = 1.476
E(crits) = 0.1476
E(hanging-attack) = 0.328
E(unused-BA) = 0.36
hits-per-adv-attack = 0.75
crits-per-adv-attack = 0.0975

Given d20tohit 11, d20tosave 9, advantage? (1):
P(stun) = 0.7599
E(ki|stun) = 2.01638373470194
E(hits|stun) = 1.68752467429925
E(hanging-attack|stun) = 0.588235294117647
E(unused-BA|stun) = 0.671140939597315
P(nostun) = 0.2401
E(ki|nostun) = 3.57142857142857
E(hits|nostun) = 2.57142857142857
E(ki) = 2.38975
E(hit) = 1.89975
E(crits) = 0.2469675
E(hanging-attack) = 0.447
E(unused-BA) = 0.51
hits-per-adv-attack = 0.75
crits-per-adv-attack = 0.0975


Initial stun is 59% likely, costing 2.116 ki, with much lower likelihood of "leftover" attacks compared to above (32.8% to attack, 36% to have a full BA).
Once stunned, each succeeding round is 76% likely to preserve stun.

In principle, that information is enough to estimate how likely you are to stunlock a target for a given number of rounds, as well as to estimate how much damage you can do (or how likely you are to be able to spend your bonus action on something else).

Here is the script itself:



use strict;
use warnings;

sub d20pct {
my ($target, $adv) = @_;

if ($target > 20) { return 0; }
elsif ($target < 2) { return 1; }
elsif (!$adv) { return 0.05 * (21 - $target); }

my $p1 = d20pct($target); # straight roll
return $adv < 0 ? $p1 * $p1
: 1 - (1 - $p1) * (1 - $p1);
}

sub tryone {
my ($hitroll, $saveroll, $adv) = @_;
# outcomes:
# miss: 0 ki
# hit, fail to stun: 1 ki
# hit, stun: 1 ki

my $phit = d20pct($hitroll, $adv);
my $psave = d20pct($saveroll);

my $pmiss = 1 - $phit;
my $pfail = $phit * $psave;
return $pmiss, $pfail, 1 - $pmiss - $pfail;
}

sub round {
my ($hitd20, $saved20, $adv) = @_;

$adv = 0 if !defined($adv) || $adv eq q{};

my ($pm1, $pf1, $ps1) = tryone($hitd20, $saved20, $adv);

my @stuns;
my @nostuns = ([1, q{}]);

for my $n (1 .. 4) {
my @new_ns;
for my $ms (@nostuns) {
my @withmiss = ($ms->[0] * $pm1, "$ms->[1]M");
my @withfail = ($ms->[0] * $pf1, "$ms->[1]F");
my @withstun = ($ms->[0] * $ps1, "$ms->[1]S");

push @new_ns => \@withmiss, \@withfail;
push @stuns => \@withstun;
}
@nostuns = @new_ns;
}

my $pstun = 0;

my $tski = 0;
my $t**** = 0;
my $tsfa = 0; # forced attack
my $tsba = 0; # bonus action

for (@stuns) {
my ($p, $trc) = @$_;
$pstun += $p;

my $nf = grep { /F/ } split '' => $trc;
my $flurried = length($trc) > 2;
my $fa = length($trc) % 2;

$tski += $p * ($nf + 1 + $flurried);
$t**** += $p * ($nf + 1);
$tsfa += $p * $fa;
$tsba += $p * !$flurried;
}

my $eski = $tski / $pstun;
my $e**** = $t**** / $pstun;
my $esfa = $tsfa / $pstun;
my $esba = $tsba / $pstun;

my $pnostun = 0;

my $tnki = 0;
my $tnhit = 0;

for (@nostuns) {
my ($p, $trc) = @$_;
$pnostun += $p;

my $nf = grep { /F/ } split '' => $trc;
$tnki += $p * ($nf + 1);
$tnhit += $p * $nf;
}

my $enki = $tnki / $pnostun;
my $enhit = $tnhit / $pnostun;

my $eki = $tski + $tnki;
my $ehit = $t**** + $tnhit;

my $crits_per_adv = d20pct(20, 1);
my $hits_per_adv = d20pct($hitd20, 1);
my $crits_per_hit = d20pct(20, $adv) / d20pct($hitd20, $adv);

my $ecrit = $ehit * $crits_per_hit;

print <<EOM;
Given d20tohit $hitd20, d20tosave $saved20, advantage? ($adv):
P(stun) = $pstun
E(ki|stun) = $eski
E(hits|stun) = $e****
E(hanging-attack|stun) = $esfa
E(unused-BA|stun) = $esba
P(nostun) = $pnostun
E(ki|nostun) = $enki
E(hits|nostun) = $enhit
E(ki) = $eki
E(hit) = $ehit
E(crits) = $ecrit
E(hanging-attack) = $tsfa
E(unused-BA) = $tsba
hits-per-adv-attack = $hits_per_adv
crits-per-adv-attack = $crits_per_adv

EOM
}

if (@ARGV) {
my $hitd20 = shift || 8;
my $saved20 = shift || 11;

round($hitd20, $saved20);
round($hitd20, $saved20, 1);
}
else {
for my $hd20 (2 .. 20) {
for my $sd20 (2 .. 20) {
print "--------------------------------------------------\n";
round($hd20, $sd20);
round($hd20, $sd20, 1);
}
}
}



Does anybody have advice on the most useful way to present this?
With the existing output, it's not particularly easy to eyeball "how many hits am I likely to get if I focus all additional damage on the target? What if I Flurry with a leftover BA vs just use Martial Arts?"

Would people find 2 tables useful? One axis d20 to hit, the other d20 to save, probability to stun in the cell? (Then duplicate for the already-stunned case.)
Is that even useful without displaying the other info?