prkill-1.11, now stops drain going over max hp,

...calculates probability function for the case where a unit was killed
This commit is contained in:
András Salamon 2005-11-01 19:12:02 +00:00
parent 03c1a36908
commit 0fe75098e2

View file

@ -9,8 +9,8 @@ prkill - calculate probability-to-kill in a Wesnoth skirmish
attacker: 24hp, 5-4; defender: 30hp, 6-3
prkill -p 0.4 -P 0.5 -n 4 -N 3 -d 5 -D 6 -h 24 -H 30
attacker: 24hp, 5-4; defender: 30hp, 6-3 (3 drain); with berserk
prkill -p 0.4 -P 0.5 -n 4 -N 3 -d 5 -D 6 -h 24 -H 30 -x -R
attacker: 24hp, 5-4; defender: 30hp (32 max), 6-3 (3 drain); with berserk
prkill -p 0.4 -P 0.5 -n 4 -N 3 -d 5 -D 6 -h 24 -H 30 -M 32 -x -R
=head1 DESCRIPTION
@ -25,7 +25,7 @@ and optionally prints the joint conditional probability function
Pr[hp_A_final = x, hp_B_final = y | hp_A_init = h, hp_B_init = H]
for supplied initial values h and H. In verbose mode, it also shows the
probability function after each swing, not just after the last one.
Usually the values Pr[hp_A_final < 0] and Pr[hp_B_final < 0] are of most
Usually the values Pr[hp_A_final <= 0] and Pr[hp_B_final <= 0] are of most
interest: these correspond to the probability of A or B being killed,
respectively.
@ -42,15 +42,20 @@ attack and unit A does not, in which case unit B gets the first swing.
interchanged.)
The output of prkill is the probability that A kills B, that B kills A,
and that both survive the skirmish. In addition, if there is a
reasonable probability that both survive, then the joint conditional
probability function is printed. The function is displayed using a two
dimensional grid, where the horizontal axis represents the hitpoints of
unit A, the vertical axis the hitpoints of unit B, and the values of the
probability function are displayed for each pair of hitpoints. Blanks
are used for zero values, '*' represents a 100% value, and integer
values are scaled by 10,000 (so 1864 represents 18.64%). In verbose
mode, the probability function is printed at the end of each swing.
and that both survive the skirmish. The expected hitpoints of A
(conditional on B being killed) and of B (conditional on A being killed)
are also printed.
In addition, the joint conditional probability function is printed. The
function is displayed using a two dimensional grid, where the horizontal
axis represents the hitpoints of unit A, the vertical axis the hitpoints
of unit B, and the values of the probability function are displayed for
each pair of hitpoints. Blanks are used for zero values, '*' represents
a 100% value, and integer values are scaled by 10,000 (so 1864
represents 18.64%). The values where either coordinate is 0 represent
the cumulative probabilities that one unit dies and the other ends up on
that number of hitpoints. In verbose mode, the probability function is
printed at the end of each swing.
The output of prkill is somewhat more general than is provided by the
game. In-game calculations were coded based on a prior version of
@ -98,6 +103,16 @@ Hitpoints of attacker.
Hitpoints of defender.
=item B<-m>
Maximum hitpoints of attacker -- only relevant if attacker drains.
Default: equal to the value of the -h option.
=item B<-M>
Maximum hitpoints of defender -- only relevant if defender drains.
Default: equal to the value of the -H option.
=item B<-r>
Specify this if the attacker has drain. Default: no.
@ -130,6 +145,28 @@ each swing are shown. Default: no.
prkill currently does not deal with skirmishes where one of the units
has the slow ability on its attack.
= head1 CAVEATS
Note that in the game, drain will drain half the hitpoints of the
attack, rounded down. This is the case even if the unit being hit has
fewer hitpoints: an 8 damage swing that drains will add 4 hitpoints to
the health of the unit draining, even if the other unit only has 1
hitpoint left at that stage.
Using -x -r -R together will result in a skirmish that cannot occur in
the game, since the game currently has no way to specify both berserk
and drain on a single attack.
The method of enumerating the state space scales badly. Time to run
this script grows linearly with the number of swings in the skirmish:
this is only an issue when berserk is involved, since this typically
multiplies the number of potential swings by 30. However, runtime also
grows linearly with the product of the initial hitpoints of A and B.
Further, drain will slow convergence even further. Ideally, a
reasonably accurate estimate for the berserk case should be constructed
from the result of one round -- this is left as an exercise for the
reader.
=head1 AUTHOR
ott <ott@gaon.net>
@ -155,7 +192,7 @@ L<wesnoth>, F<http://www.wesnoth.org/>
=cut
use strict;
($main::VERSION) = ('$Revision: 1.10 $' =~ /[^\d\.]*([\d\.]*)/);
($main::VERSION) = ('$Revision: 1.11 $' =~ /[^\d\.]*([\d\.]*)/);
use Getopt::Std;
$Getopt::Std::STANDARD_HELP_VERSION = 1;
@ -173,6 +210,8 @@ calculate probability of either unit killing the other in a Wesnoth skirmish
-D damage per successful swing by defender
-h hitpoints of attacker
-H hitpoints of defender
-m maximum hitpoints of attacker, if attacker drains [value of -h]
-M maximum hitpoints of defender, if defender drains [value of -H]
-r attacker has drain? [no]
-R defender has drain? [no]
-x berserk? (berserker attack, up to 30 rounds) [no]
@ -183,11 +222,11 @@ EOI
}
use vars qw(
$opt_n $opt_d $opt_p $opt_h $opt_r
$opt_N $opt_D $opt_P $opt_H $opt_R
$opt_n $opt_d $opt_p $opt_h $opt_m $opt_r
$opt_N $opt_D $opt_P $opt_H $opt_M $opt_R
$opt_x $opt_y $opt_v
);
exit 1 unless (getopts('d:D:h:H:n:N:p:P:rRxyv'));
exit 1 unless (getopts('d:D:h:H:m:M:n:N:p:P:rRxyv'));
my $VERBOSE = $opt_v;
my $maxrounds = ($opt_x ? 30 : 1);
@ -195,6 +234,8 @@ my $firststrike_active = $opt_y;
my ($hpa,$dmga,$pa,$swa) = ($opt_h,$opt_d,$opt_p,$opt_n);
my ($hpb,$dmgb,$pb,$swb) = ($opt_H,$opt_D,$opt_P,$opt_N);
my $hpma = $opt_m ? $opt_m : $opt_h;
my $hpmb = $opt_M ? $opt_M : $opt_H;
my $dra = $opt_r ? int ($dmga / 2) : 0;
my $drb = $opt_R ? int ($dmgb / 2) : 0;
@ -218,57 +259,95 @@ my $th = 0.9999995;
my @P;
my $adf; # intersect y-axis first: A dies first
my $bdf; # intersect x-axis first: B dies first
my $hpt = $hpa + $hpb;
my $kf = $hpb + $hpt;
my $lf = $hpa + $hpt;
my $hpt = $hpa + $hpb; # total initial hitpoints
my $kf = $hpb + $hpt; # 2*hpb + hpa
my $lf = $hpa + $hpt; # 2*hpa + hpb
my $k = $hpb;
$k += int($hpa/2) if $drb; # B drains so provide space
$k += int($hpa/2) + $drb if $drb; # B drains so provide space
$k = $hpmb if $k > $hpmb; # but limit it to max hp
my $l = $hpa;
$l += int($hpb/2) if $dra; # A drains so provide space
$l += int($hpb/2) + $dra if $dra; # A drains so provide space
$l = $hpma if $l > $hpma; # but limit it to max hp
# size of matrix needed is max($k,$l)^2
# k +
# drb{|
# +
# |\
# | \
# hpb +--+
# | |\
# | | \
# +--+--+-----+
# 0 hpa dra l
# note that k=int(kf/2)+drb and l=int(lf/2)+dra when drain is involved
# with the limitation that k can't exceed B's max hp (and l those of A)
# kf and lf are used to calculate all the relevant lattice points, as
# lines through (0,k)-(hpa,hpb) and (l,0)-(hpa,hpb) can omit some
sub print_state {
my $s = shift;
print "swing $s:\n";
my $pbs = 0;
if ($adf + $bdf < $th) {
foreach my $jj ( 0..($k-1) ) {
my $j = $k - $jj;
my $t = $j;
printf "%2d | ", $j;
foreach my $i ( 1..$l ) {
++$t; # invariant: $t == $i + $j
last if ($t > $hpt) or ($j + $t > $kf) or ($i + $t > $lf);
#printf "%.4f ", $P[$i][$j];
my $pij = $P[$i][$j];
if ($pij == 1) {
print ' 'x3 . '* ';
} elsif ($pij > 0.0) {
printf "%4d ", int(10000*$P[$i][$j]);
} else {
print ' 'x5;
}
$pbs += $P[$i][$j];
if ($s) {
print "swing $s (last swing by ". (a_swings($s) ? 'A' : 'B') ."):\n";
} else {
print "initial state\n";
}
my $pbs = 0; # probability both survive
my $aev = 0; # expected value of hp(A), given A survives
my $bev = 0; # expected value of hp(B), given B survives
foreach my $jj ( 0..$k ) {
my $j = $k - $jj;
my $t = $j - 1;
printf "%2d | ", $j;
foreach my $i ( 0..$l ) {
++$t; # invariant: $t == $i + $j
last if ($t > $hpt)
or ($i > 0 and $j + $t > $kf)
or ($j > 0 and $i + $t > $lf);
my $pij = $P[$i][$j];
if ($pij == 1) {
print ' 'x3 . '* ';
} elsif ($pij > 0.0) {
printf "%4d ", int(10000*$pij);
} else {
print ' 'x5;
}
print "\n";
$pbs += $pij if ($i > 0 and $j > 0);
}
print " |";
foreach my $i ( 1..$l ) { printf " %4d", $i }
print "\n";
}
print "A: ${hpa}hp, ${dmga}-${swa}";
print " ($dra drain)" if $dra;
print ", B: ${hpb}hp, ${dmgb}-${swb}";
print " ($drb drain)" if $drb;
print " firststrike" if $firststrike_active;
print " berserk" if $opt_x;
print " |";
foreach my $i ( 0..$l ) { printf " %4d", $i }
print "\n";
printf "Pr[A kills B] = %8.4f%%\n", $bdf*100;
printf "Pr[B kills A] = %8.4f%%\n", $adf*100;
if ($bdf) {
foreach my $i ((1+$dra)..$l) { $aev += $i * $P[$i][0] }
$aev /= $bdf;
}
if ($adf) {
foreach my $j ((1+$drb)..$k) { $bev += $j * $P[0][$j] }
$bev /= $adf;
}
print "A: ${hpa}hp";
print " (${hpma} max)" if $dra;
print ", ${dmga}-${swa}, ". 100*$pa . '% to hit';
print " ($dra drain)" if $dra;
print "; B: ${hpb}hp";
print " (${hpmb} max)" if $drb;
print ", ${dmgb}-${swb}, ". 100*$pb . '% to hit';
print " ($drb drain)" if $drb;
print "; firststrike" if $firststrike_active;
print "; berserk" if $opt_x;
print "\n";
printf "Pr[A kills B] = %8.4f%%\n", $bdf*100;
printf "Pr[B kills A] = %8.4f%%\n", $adf*100;
printf "Pr[both survive] = %8.4f%%\n", $pbs*100;
$DEBUG and print "total: ", $bdf + $adf + $pbs, "\n";
print "total: ", $bdf + $adf + $pbs, "\n" if $bdf + $adf + $pbs < $th;
printf "E[hpA | B dies] = %5.1f\n", $aev if $bdf;
printf "E[hpB | A dies] = %5.1f\n", $bev if $adf;
print "\n";
}
@ -283,43 +362,82 @@ my $swt = $maxrounds * ($swa+$swb);
foreach my $s ( 1..$swt ) {
my $a = a_swings($s); # if true, it is A's turn, otherwise it is B's turn
my $akz = 0; # kill zone of A, where B can be killed with one swing
my @akz = ();
my $bkz = 0; # kill zone of B, where A can be killed with one swing
my @bkz = ();
# values that are outside the valid zone
my @a_large = ();
my @b_large = ();
my $ht = $hpt;
my $kft = $kf;
my $lft = $lf;
# traverse all lines x+y=t, where t=1,2,...,hpt
foreach my $t ( 1..$hpt ) {
--$ht; # invariant: $ht == $hpt - $t
--$kft; # invariant: $kft == $kf - $t;
--$lft; # invariant: $kft == $lf - $t;
my $ca = ($dmga - $dra <= $ht);
my $cb = ($dmgb - $drb <= $ht);
--$lft; # invariant: $lft == $lf - $t;
my $j = $t;
# traverse the specific line where x+y=t, with x=i and y=j
foreach my $i ( 1..($t-1) ) {
--$j;
# invariant: $t == $i + $j
# skip computing if this point is unreachable
next if $j > $kft;
next if $i > $lft;
my $pij = $P[$i][$j];
$akz += $pij if $j <= $dmga;
$bkz += $pij if $i <= $dmgb;
if ($a) {
my $pij_mod = (1 - $pa) * $pij;
if ($ca && $i > $dra) {
$P[$i][$j] = $pij_mod + $pa * $P[$i - $dra][$j + $dmga];
} else {
$P[$i][$j] = $pij_mod;
# check if this point is in 1-swing kill zone of A
if ($j <= $dmga) {
$akz += $pij;
my $hpa_after = $i + $dra;
$hpa_after = $hpma if $hpa_after > $hpma;
$akz[$hpa_after] += $pij;
}
} else {
my $pij_mod = (1 - $pb) * $pij;
if ($cb && $j > $drb) {
$P[$i][$j] = $pij_mod + $pb * $P[$i + $dmgb][$j - $drb];
my $pij_prev_mod = 0;
# P[u][v]=0 if u+v>hpt, while u<=0 or v>hpmb are invalid
if (($dmga - $dra <= $ht) and ($j + $dmga <= $hpmb)
and ($i > $dra)) {
$pij_prev_mod = $pa * $P[$i - $dra][$j + $dmga];
}
if ($i > $hpma) {
$a_large[$j] += $pij_prev_mod;
} else {
$P[$i][$j] = $pij_mod;
my $pij_mod = (1 - $pa) * $P[$i][$j];
$P[$i][$j] = $pij_mod + $pij_prev_mod;
}
} else { # it's B's swing
# check if this point is in 1-swing kill zone of B
if ($i <= $dmgb) {
$bkz += $pij;
my $hpb_after = $j + $drb;
$hpb_after = $hpmb if $hpb_after > $hpmb;
$bkz[$hpb_after] += $pij;
# invariant: $bkz == sum $bkz[.]
}
my $pij_prev_mod = 0;
# P[u][v]=0 if u+v>hpt, while v<=0 or u>hpma are invalid
if (($dmgb - $drb <= $ht) and ($i + $dmgb <= $hpma)
and ($j > $drb)) {
$pij_prev_mod = $pb * $P[$i + $dmgb][$j - $drb];
}
if ($j > $hpmb) {
$b_large[$i] += $pij_prev_mod;
} else {
my $pij_mod = (1 - $pb) * $P[$i][$j];
$P[$i][$j] = $pij_mod + $pij_prev_mod;
}
}
}
} # end foreach $i
} # end foreach $t
if ($a) {
$bdf += $akz * $pa;
foreach my $i ((1+$dra)..$l) { $P[$i][0] += $akz[$i] * $pa }
foreach my $j ((1+$drb)..$k) { $P[$hpma][$j] += $a_large[$j] }
} else {
$adf += $bkz * $pb;
foreach my $j ((1+$drb)..$k) { $P[0][$j] += $bkz[$j] * $pb }
foreach my $i ((1+$dra)..$l) { $P[$i][$hpmb] += $b_large[$i] }
}
if ($a) { $bdf += $akz * $pa } else { $adf += $bkz * $pb }
print_state($s) if $VERBOSE;
if ($adf + $bdf >= $th) { $swt = $s; last }
}