Using math (not simulations), this calculates the exact win probabilities
and average length of battle in number of attacks (which is the same as number of half turns
in the absence of multiple attacks, frenzy, or squads).
The average survivors is the average number of life or figures left assuming that the
figure or squad wins.
[Back to intro page][About this program][Detailed instructions][To do list][What's new][Access stats]
Version 6.20: Added more choices to "Squad size" to include squads of more than 1 common heroes, which is essentially what you get when you use Wyrmling Bonding or Master of the Elements. This only makes a difference for powers that distinguish squad figures from common heros such as Chomp, Poison Acid Breath, Stare of Stone.
Version 6.11: Modified Searing Intensity.
Version 6.10: Added implementable abilities from D12: the fledgling abilities and Psionic Blast. Also fixed a typo that would have affected results of ChillingTouch and Trample and made them worse than they should be. All Chilling Touch, Throw, and Trample numbers before Aug 28, 2010 should be redone.
Version 6.00: New interface improvements, including mouseover activating balloons that describe the abilities.
Remember, it is up to the user to use this calculator wisely.
This calculator does not take into account of range or movement.
So all battles are either melee vs melee or range vs range.
A better way of thinking of it is you are assuming both sides get to use
the attacks and abilities that you give them.
You must interpret the results appropriately.
Furthermore, it doesn't make sense to matchup certain abilities.
For example, the Ullar Rifle special is only good for ranged duels, so it doesn't make sense to
match that against say melee figures.
And it is up to the user to remember that certain abilities (example: smoke powder, counterstrike, disappearing ninja, tough, etc.) work only against normal attacks.
Remember also, that in addition to using official figures, feel free to customize your own figures (within reason!).
\n^;
$maxLockFile = 150; # 2.5 minutes max for running a Mathematica calculation.
$color1="#33ff33"; $color2="#ff5550";
$gray="#d0d0d0";
$dataDir = "";
$lockfile=$dataDir."mathlock";
$mafile=$dataDir."mathinput";
$outputfile=$dataDir."mathoutput.ma";
$donefile=$dataDir."mathdone";
$datafile=$dataDir."datafile.txt";
$star=qq^ * ^;
sub mystar{ ##This will be later when we figure out how to do anchor display when returned as a CGI page
my ($anchor)=@_;
return qq^ * ^;
}
### This is an array of the web displayed names
### and names used in the Mathematica program, in pairs.
### The displayed name may be changed without affecting the Mathematica program.
$nyi=' [not done]';
sub trim{
my ($x)=@_;
$x=~s/^\s+//;
$x=~s/\s+$//;
return $x;
}
@Abilities=();
@AbilitiesMa=();
@AbilitiesDesc=();
@AbilitiesDescJS=();
%AbilityToN=();
if(open(ABIL, ";
close(ABIL);
chomp @all;chomp @all;
$all=join(" ",@all);
$all=~s/\n/ /g;
@all=split(/\#\#/, $all);
$ctr=3;
for($n=0;$n<@all;$n++){
$x=$all[$n];
if($x !~ /\-\-\-\-/){
($i, $ma, $a, $desc) = split(/\@/, $x);
$ctr++;
if($ctr!=$i){
&errorPage("ERROR in reading abilities.txt file (unmatched ctr=$ctr, i=$i).");
}
push @Abilities, trim($a);
push @AbilitiesMa, trim($ma);
$desc=trim($desc);
push @AbilitiesDesc, $desc;
$desc=~s/\'/\\\'/g;
$desc=~s/\"/\\\'/g;
push @AbilitiesDescJS, $desc;
$AbilityToN{$ma}=$ctr;
}else{
$n=$#all+10;
}
}
}
&setupAbilitiesParrallelArray;
###check consistency with old method###
for($i=0;$i<@AbilitiesOLD;$i++){
if($AbilitiesMa[$i] ne $AbilitiesMaOLD[$i]){
$ii=$i+4;
errorPage("abma:$ii, new:$AbilitiesMa[$i], old:$AbilitiesMaOLD[$i].");
#.qq^
# ^.join("/",@AbilitiesMa)." ".join("/",@AbilitiesMaOLD).
#" ");
}
}
########### The following @AbilitiesParrallelArray is no longer used ##########
sub setupAbilitiesParrallelArray{
@AbilitiesParrallelArray=(
"Deadly Strike", 'DeadlyStrike', #4
"Double Shield", 'DoubleShield', #5
"Double Attack", 'DoubleAttack', #6
"Counterstrike / Evil Eye / Arcane Riposte", 'CounterStrike', #7
"Wounded Smash",'WoundedAttack', #8
"Frenzy 16", 'Frenzy', #9
"Stealth Dodge", 'StealthDodge', #10
"Mind Shackle 20", 'MindShackle', #11
"Chomp 0/16 ", 'Chomp', #12
"Unleashed Fury", 'UnleashedFury', #13
"Paralyzing Stare 16",'ParalyzingGaze',#14
"Crippling Gaze 15", 'CripplingGaze', #15
"Rapid Fire", 'RapidFire', #16
"Wait Then Fire (see implemntation$star)", 'WaitAndFire', #17
"Hive Swarm", 'HiveSwarm', #18
"Zettian Targetting", 'ZettianTargetting', #19
"Shocking Grasp", 'BonusSkull', #20
"Tough", 'BonusShield', #21
"Blood Hungry", 'BloodHungry' , #22
"Stealth Armor 15", 'StealthArmor', #23
"Shield Wall (modified$star)", 'ShieldWall' #24
,"Rod of Negation 16", 'RodOfNegation' #25
,"Marro Plague 16 (simplified$star)", 'MarroPlague' #26
,"Shaolin Assault (simplified$star) / Whirlwind Attack", 'ShaolinAttack' #27
,"Whip 12", 'Whip' #28
,"Stare of Stone 7/17", 'StareOfStone' #29
,"Sharpshooter 19", 'Sharpshooter' #30
,"Ullar Rifle", 'UllarRifle' #31
,"Blood Lust", 'BloodLust' #32
,"Poison Acid Breath 8/17", 'PoisonAcidBreath' #33
,"Ice Shard Breath / Shield Throw", 'IceShardBreath' #34
,"Throw 14", 'Throw14' #35
,"Gueglix Gun 333", 'Gueglix333' #36
,"Gueglix Gun 22221", 'Gueglix22221' #37
,"Gueglix Gun 1x9", 'Gueglix1x9' #38
,"Gueglix Gun 2223", 'Gueglix2223' #39
,"Rod of Negation & no attack until negated", 'RodOfNegationVSWS' #40
,"Arrow Volley / Zombie Onslaught / Encircle", 'ArrowVolley' #41
,"Blind Rage", 'BlindRage' #42
,"Stab in the Back", 'StabInTheBack' #43
,"Maul / Venomous Sting", 'Maul' #44
,"Disappearing Ninja 12", 'DisappearingNinja' #45
,"Net Trip 14", 'NetTrip14' #46
,"One Shield Defense", 'OneShieldDefense' #47
,"Tough x2", 'Toughx2' #48
,"Smoke Powder 13", 'SmokePowder' #49
,"Valkyrie Dice Attack Bonus", 'AttackAura46' #50
,"Valkyrie Dice Defense Bonus", 'DefenseAura36' #51
,"Leaf of Home Tree Bonus", 'HomeTree' #52
,"AutoLoad", 'AutoLoad' #53
,"Gift of Empress Bonus", 'DefenseAura59' #54
,"Soul Devour / Mind Shackle 19", 'MindShackle19' #55
,"Toxic Skin 17", 'ToxicSkin' #56
,"Toxic Skin 17 vs melee squad", 'ToxicSkinVsMeleeSquad' #57
,"Helm of Mitonsul", 'HelmOfMitonsul' #58
,"Helm of Mitonsul vs melee squad", 'HelmOfMitonsulVsMeleeSquad' #59
,"Zombies Rise Again / Life Drain", 'ZombiesRiseAgain' #60
,"Web Attack", 'WebAttack' #61
,"Spidey Sense 11", 'SpideySense' #62
,"Spider Sense 14", 'SpiderSense' #63
,"Dust of Death", 'DustOfDeath' #64
,"Dust of Death vs melee squad", 'DustOfDeathVsMeleeSquad' #65
,"Vanish 9", 'Vanish9' #66
,"Rage Smash 5", 'RageSmash5' #67
,"Stomp 3 / Earth Slam (vs melee squad$star)", 'Stomp' #68
,"Eternal Hatred", 'EternalHatred' #69
,"Chilling Touch", 'ChillingTouch' #70
,"Chilling Touch vs ranged squad", 'ChillingTouchVsRangedSquad' #71
,"Trample (simplified$star)", 'Trample' #72
,"Trample vs ranged squad", 'TrampleVsRangedSquad' #73
,"Stinger Drain", 'StingerDrain' #74
,"Concentrated Will", 'ConcentratedWill' #75
,"Machine Pistol 2222", 'Gueglix2222' #76
,"Quick Release / Wrist Rocket 44", 'Gueglix44' #77
,"Dragon Heal", 'DragonHeal' #78
,"Dragon Swoop", 'DragonSwoop' #79
,"Cosmic Blast", 'CosmicBlast' #80
,"Lethal Sting", 'LethalSting' #81
,"Chalice of Fortitude", 'ChaliceOfFortitude' #82
,"Coil Crush", 'CoilCrush' #83
,"Poison Sting/Venom Ray", 'PoisonSting' #84
,"Entangling Web", 'EntanglingWeb' #85
,"Saber Storm 1x6", 'SaberStorm1x6' #86
,"Saber Storm 222", 'SaberStorm222' #87
,"Saber Storm 33", 'SaberStorm33' #88
,"Majestic Fires", 'MajesticFires' #89
,"Defense Agility (see note$star)", 'DefenseAgility' #90
,"Pounce Vs Range (see note$star)", 'PounceVsRange' #91
,"Pounce Vs Melee (see note$star)", 'PounceVsMelee' #92
,"Concealment 19 at range 2 (see note$star)", 'StealthArmor17' #93
,"Concealment 10 at range 2 (see note$star)", 'StealthArmor8' #94
,"Cell Divide 17", 'CellDivide17' #95
,"Circuitry Overload", 'CircuitryOverload' #96
,"EMP Response", 'EMPResponse' #97
,"Combined Arbalest", 'CombinedArbalest' #98
,"Ullars Bolt 16", 'UllarsBolt' #99
,"Ninjutsu Barrage", 'NinjutsuBarrage' #100
,"Feral Rage (simplified$star)", 'ShaolinAttack3' #101
,"Blood Frenzy 3", 'BloodFrenzy3' #102
,"Regenerate / Cold Healing", 'Regenerate' #103
,"Poison Weapon 12", 'PoisonWeapon' #104
,"Hide In Darkness/Shadows 16", 'StealthArmor16' #105
,"Healing From Ana Karithon (see note$star)", 'HealingFromAna' #106
,"Cleave", 'Cleave' #107
,"Fire Blast 2", 'FireBlast2' #108
,"Fire Blast 3", 'FireBlast3' #109
,"Fire Blast 4", 'FireBlast4' #110
,"Fire Blast 2 then 4 strategy (see note$star)", 'FireBlast24' #111
,"Wraith Attack using 1 drow", 'WraithAttack1drow' #112
,"Wraith Attack using 2 drow", 'WraithAttack2drow' #113
,"Wraith Attack using 3 drow", 'WraithAttack3drow' #114
,"Lurking Ambush / First Assault 3 (see implemntation$star)", 'FirstAssault3' #115
,"Air Mastery (vs flyers only)", 'AirMastery' #116
,"Searing Intensity 14 (see implemntation$star)", 'SearingIntensity' #117
,"Hydra Heads", 'HydraHeads' #118
,"Cloud Of Darkness", 'CloudOfDarkness' #119
,"Triple Attack", 'TripleAttack' #120
,"Engagement Strike 15 vs melee using ranged attack ", 'EngagementStrike15usingRange' #121
,"Engagement Strike 15 vs melee using melee attack", 'EngagementStrike15usingMelee' #122
### To be implemented: EngagementStrike15usingRange EngagementStrike15usingRange
);
@AbilitiesOLD=();
@AbilitiesMaOLD=();
%AbilityToNOLD=();
$ctr=4;
for($i=0;$i<@AbilitiesParrallelArray;$i+=2){
push @AbilitiesOLD, $AbilitiesParrallelArray[$i];
push @AbilitiesMaOLD, $AbilitiesParrallelArray[$i+1];
$AbilityToNOLD{$AbilitiesParrallelArray[$i+1]}=$ctr;$ctr++;
}
}
$maxD20Bonus=13;
%D20hash=(
0,0,
1,1,
2,2,
3,3,
4,4,
5,5,
6,6,
7,7,
8,8,
9,-1,
10,-2,
11,-3,
12,-4,
13,-5);
$maxSquadSizeIndex=7;
%squadSizeDisplay=(
0,"---",
1, "1 (common hero)",
2, "2 (squad)",
3, "3 (squad)",
4, "4 (squad)",
5, "2 (common heroes)",
6, "3 (common heroes)",
7, "4 (common heroes)"
);
%squadSize=(
0, 0,
1, 1,
2, 2,
3, 3,
4, 4,
5, 2,
6, 3,
7, 4
);
%IsSquadForPowers=(
0, "False",
1, "False",
2, "True",
3, "True",
4, "True",
5, "False",
6, "False",
7, "False"
);
@ForbiddenPairs=( #### Use forbidden.txt instead!!
(6,19), (10,21), (17,13), (6,27), (42,44), (42,53), (44,53), (11,55),(51,54), (91,92)
, (91,91),(97,97),(19,98), (107,108), (107,109),(108,109),
(13,110),(13,111),(13,112), (17,110),(17,111),(17,112)
);
if(open(FORBIDDEN, ";
close(FORBIDDEN);
chomp @all;
@ForbiddenPairs=();
foreach $x (@all){
($y, $z) = split(/:/, $x);
@y = $z =~/\d+/g;
for($i=1; $i<@y; $i++){
for($j=0; $j<$i; $j++){
push @ForbiddenPairs, ($y[$i], $y[$j]);
}
}
}
### print "(" . join(",", @ForbiddenPairs) .")\n"; <>;
}
if(open(FORBIDDEN, ";
close(FORBIDDEN);
chomp @all;
@ForbiddenPairsOpps=();
foreach $x (@all){
($y, $z) = split(/:/, $x);
@y = $z =~/\d+/g;
for($i=1; $i<@y; $i++){
for($j=0; $j<$i; $j++){
push @ForbiddenPairsOpps, ($y[$i], $y[$j]);
push @ForbiddenPairsOpps, ($y[$j], $y[$i]);
}
}
}
### print "(" . join(",", @ForbiddenPairsOpps) .")\n"; <>;
}
$hitfile="hitcounter.txt";
$HITCOUNTER = "[hit count not available]";
if(open(HIT, "<$hitfile")){
$HITCOUNTER=;
$HITCOUNTER++;
close(HIT);
if(($HITCOUNTER > 1) &&(open(HITW, ">$hitfile"))){ #because sometimes a zero is read from a busy file!
print HITW $HITCOUNTER, "\n";
close(HTW);
}
#### print "(" . $HITCOUNTER .")\n"; <>;
}
$emailLink=qq^mathguy^;
$ender=qq^\n[To mathguy's homepage]
[Email $emailLink]^;
$instructions=qq^
^;
@zName=(); @zUC=(); @zHS=(); @zLife=();
@zMove=(); @zRange=(); @zAtt=(); @zDef=(); @zAbilities=();
@zPoints=();
@zSize=();
@zAttEnh=();
@zDefEnh=();
@zInitEnh=();
@zD20Enh=();
%zNameToIndex=();
if(open(F, ";
while( $line && ($line !~ /---begin data---/) ){$line=;}
while($line=){
if($line=~/\w/){
chomp $line;
@a = split(/\s*,\s*/, $line);
push @zName, $a[0];
push @zUC, $a[1];
push @zHS, $a[2];
push @zLife, $a[3];
push @zMove, $a[4];
push @zRange, $a[5];
push @zAtt, $a[6];
push @zDef, $a[7];
push @zAbilities, $a[8];
push @zSize, $a[9];
push @zPoints, $a[10];
if($a[11] eq ""){$a[11]=0;}push @zAttEnh, $a[11];
if($a[12] eq ""){$a[12]=0;}push @zDefEnh, $a[12];
if($a[13] eq ""){$a[13]=0;}push @zInitEnh, $a[13];
if($a[14] eq ""){$a[14]=0;}push @zD20Enh, $a[14];
$zNameToIndex{$a[0]}=$#zName;
}
}
}
&initializeJS;
print qq^ Heroscape matchup calculator$js
\n
^;
### app logic #############################################
if($formHash{"request"} eq "findprob") {
&findprob("");
}
elsif($formHash{"request"} eq "instructions") {
&instructions("");
}
elsif($formHash{"request"} eq "todo") {
&todo("");
}
elsif($formHash{"request"} eq "aboutthis") {
&aboutthis("");
}
elsif($formHash{"request"} eq "stats") {
&statspage("");
}
else {
&default_page("");
}
### end app logic #########################################
#################################################################
sub instructions {
$message = $_[0]; # an optional message
my $version4=qq^
Select type of battle.
Simple battle: This uses the stats and abilities as entered and disregards range.
(In other words, this is the original matchup program.)
Ranged vs melee battle:
This takes into account that the ranged units will often get free attacks on the melee units
as the melee units rush the ranged units.
The RANGE stat of the ranged unit and the MOVE stat of the melee unit are used to calculate
how many turns are required for the melee unit to reach the ranged unit.
Examples:
RANGE 6 vs MOVE 5. Then the melee unit requires no extra turns to reach the ranged unit.
In effect, the only advantage the ranged unit has in this example is that it always gets to attack
first.
RANGE 6 vs MOVE 4. The melee unit will require 2 moves to get to the ranged unit.
That is, a melee unit will not be able to attack on its 1st turn, but will be able to attack on its
2nd turn (unless it has the Berserk ability).
RANGE 9 vs MOVE 4. The melee unit will require 2 moves to reach the ranged unit.
RANGE 10 vs MOVE 4. The melee unit will require 3 moves to reach the ranged unit.
^;
}
print<Instructions
$message
For each side of the match up, select each of the following:
$version4
You can use the optional drop-down menu to select an army card. This will automatically select
all the stat numbers and abilities for that army card.
You do not have to use this;
you can customize your own stats and abilities.
But if chosen, the point box will also display point value.
Choose Hero or Squad(s)
If single Hero, select the number of LIFE points.
If Squad or common hero, select the number of figures; each figure
automatically has 1 LIFE point.
If the number of figures exceed the squad size, then select the Squad SIZE also.
Note that a squad of size 1 is interpreted as a common hero;
this allows you to model multiple common heros.
If you are doing a single figure with just 1 LIFE, then in general it does not matter
whether you select Hero with 1 LIFE or Squad with 1 UNIT; the
only time this matters is if the opponent has the Chomp, Stare of Stone, or any ability
that distinquishes between hero and squad units.
ATTACK: select the number of attack dice. Note that at least one
of the two figures must have at least 1 ATTACK. (Currently, there are no
official figures with 0 attack. But you might want to compare say, how
Ne-Gok-Sa would do against Krug if Ne-Gok-Sa only used Mind Shackle
and no regular attack. Also you can imagine in the future where
a figure with 0 attack might be used to slow down attackers. In that case,
a relevant piece of data might be: what is the expected number of turns
that this figure can survive?)
NOTE: If a figure has a modified attack of 0, then no attack roll is made.
DEFENSE: select the number of defense dice.
Then select any combination of abilities.
Note that for programming reasons,
certain ability combinations are not permitted;
you will be warned upon execution if such combinations are selected.
The light blue boxes summarizes the stats and abilities.
The example above shows Krug versus Tagawa samurai with 2 experience markers.
In the table labeled "Battle begins at the beginning of a round, with an initiative roll",
the assumption is the the initiative rolled at the beginning of a round
to determine who goes first.
In the above example, Krug wins 48.210% of the time and the Samurai win
51.790%.
The average attacks column gives the average length of a battle
in terms of the number of attacks.
So for single figures with no multiple attacks or frenzies,
this would be the same as the number of half-turns.
In the above example, the battle would last 7.07 attacks. This is not the same
as number of turns because Krug has double attack and also because the Samurai form
a squad.
The average survivors column gives the average number of LIFE or UNITs left
if that player wins.
If the Krug wins, on average he has 2.5 life left.
If the Samurai win, on average they have 2.0 figures left.
In the bottom table labeled
"If we specify who goes first and battle begins which round",
the scenarios are broken down according to when
the battle begins and who has initiative.
Note that the initiative is still rerolled at the beginning of subsequent rounds.
There are 6 rows of results that correspond to each scenario
whether the melee begins at turn #1 or #2 or #3 of a round,
and whether player #1 or #2 has the initiative on the very first round.
In the above example, note that Krug does best if the battle begins in the first turn
AND he has initiative (win at 51.20%)
or if the battle begins in the 3rd turn of the round and the enemy has initiative
(win at 61.47%).
Krug has a losing percentage in the other 4 scenarios.
I will leave it up to you to reason out why Krug does best if the battle begins the
3rd turn with the enemy having the initiative!
The numbers such as transition matrix size and matchcode
are used for diagnostics.
Include this info in any bug report emailed to mathguy.
The bottom gives time spent on a calculation.
It might also say the the data was read from a database
because the calculation was already done previously.
WHOLEPAGE
################################# deleted Marro Plague note ######################
#(
#A future feature might try to do attempt the following model:
#Assume each attacker is adjacent to only one defender.
#The number of such adjacency is modeled as [previous adjacent] minus
#[killed in previous turn] plus [squad size]. This models the optimum strategy of
#moving unadjacent attackers to maximize the number of enemies adjacent.
#But this model would require keeping track of more information and is likely to take too long
#in a calculation with large squads.
#)
}
#################################################################
sub todo {
$message = $_[0]; # an optional message
print<$message
To do list
Future abilities as they come out.
Adding the option of giving one of the players some free attacks at the beginning to simulate
melee characters getting to the ranged characters.
This might never be implemented because it would increase the size of the calculation too much.
Future features?
The web interface can be improved by adding Javascript features, such as:
Hovering over an ability will activate a bubble that describes the ability
A selection box of current figures/squads will allow a select that will
automatically input all the data for that figure/squad.
[DONE!]
Hitting calculate will bring up a dialog box, estimating the length of time
required for the calculation. (I'll need more data to be able to guess
the length of time based on size of transition matrix.)
Send suggestions to $emailLink
Version history
Version 1.0: the first working version.
But this had a typo that affected results of squads (those results were a little bit off).
Version 2.0: Corrected typo/bug in Version 1.0 that affected squads.
This version now allows the numbers of units to be higher than the squad size, so that you can
simulate using multiple copies of a squad.
Also implements Rage.
Version 2.1: corrects the typo that had labeled "Tough" in the wrong spot.
(I accidentally relabeled Bonus Skull to Tough.)
All Version 2.0: calculations with Tough had wrong answers - sorry!
Version 2.2: Major improvements on efficiency when dealing with multiple common squads.
(This reduced the size of the transition matrix in these cases.)
Version 2.21 made minor improvements.
Version 2.3: improvements: Interface renovated to be clearer. More abilities added: Blood Hungry,.
Also, a minor typo in "Frenzy" ability fixed.
(Calculations done with previous versions involving "Frenzy" might be off by less than 0.1%.)
Version 2.31: Implemented Stealth Armor, Shield Wall. Fixed typo in Crippling Gaze. (Previous Crippling Gaze calculations would just fail because
the transition matrix would be detected to be nonstochastic - a double check that is always done.)
Version 2.32: Fixed typo in Unleashed Fury. (Previous calculations would just fail because
the transition matrix would be detected to be nonstochastic.) Hopefully, these are the last typos
introduced when Version 2.2 efficiency improvements were made.
Version 2.40: Interface now has a quick selection box for existing Heroscape characters; selecting one
will fill in all stats & abilities for that army card.
Warnings will sound if certain combinations are selected such as Grimnak versus Charos, in this example, you
should select "Grimnak (vs Large or Huge)" instead of "Grimnak".
And really fixed CripplingGaze this time (hopefully).
Version 2.41: Paralyzing Stare warning added (requires opponent to be small or medium).
Version 2.42: A textbox that shows point value if official units are chosen (prorated if fractional squads).
Version 3.1: New interface that is more space efficient.
Created Attack Enhancement and Defense Ehancement selection boxes, for abilities
such as Sword of Reckoning and Melee Defense.
This is now needed because of the Rod of Negation! (It used to be that you could
just enter a higher Attack of Defense number.)
Abilities from Zanafor's Discovery, Raknar's Vision, and Thaelunk Tundra added.
Version 3.12: HiveSwarm fixed.
Version 3.2: Radio buttons added for limiting number of attackers when enemy is down to 1 figure.
This is mainly used in conjunction with HiveSwarm. Set this number to 6 if enemy is a one-hex-base figure and
to 8 if enemy is a two-hex-base figure.
Version 3.21: More documentation.
Version 3.30: Some abilities and figures from Thora's Vengeance added.
Version 4.00: Abilities from Thora's Vengeance, Sir Hawthorne, Flagbearers, Dawn of Darkness added.
The Valkyrie dice abilities required revamping the way probabilities were computed using generating functions.
Version 4.01: Fixed some typos.
(Previous version crashed on some inputs, but did not return any false results.)
Version 4.11: Some Marvel characters added. Rest to be added later.
Version 4.20: WaitAndFire and UnleashedFury fixed (typos were introduced in Version 4.0).
Version 4.21: Fixed Throw14 when vs squads.
Version 4.40: Wave 7 and Swarm of the Marro and rest of Marvel characters implemented (except for Silver Surfer).
Version 4.50: Cosmic Blast implemented.
Version 4.60: Lethal Sting implemented.
Version 4.70: Updated the database of abilities that should not be used together.
Fixed database of previous calculations, where Throw14 versus squads, HomeTree vs CounterStrike,
Unleashed Fury with more than 1 squad were done incorrectly.
Version 4.80: Smoke Powder fixed so that it occurs before every attack, not just once per turn.
Version 4.81: Added Master Win Chiu Woo.
Version 4.82: Fixed HomeTree for heros. Any calculations involving heros using the Leaf of HomeTree aura should be recalculated.
(there were 13 of these calculations - they have been purged from the database).
Version 5.00: Added Wave 8 abilities except for Pounce, which will be added soon, with an explanation of how it is implemented.
Version 5.01: Entangling Web fixed. Fixed Eternal Hatred.
Version 5.02: The ability Pounce is added.
Version 5.03: Agent Skahen added.
Version 5.04: Fixed stats of Marcu.
Version 5.10: Fixed StingerDrain (StingerDrain calculations prior to 4-26-09 are incorrect.
Those calculations have been expunged from the database.).
Version 5.20: Fixed DoubleShields (Shields of Valor). A typo bug was introduced in
Version 4.00 (May 15, 2007). D'oh. Calculations done with DoubleShields between
May 15, 2007 and May 21, 2009 are incorrect.
Version 5.30: Added Wave 9. Also fixed a bug in Rod of Negation where the rod was not used if
it was at the very end of a round (it makes about 0.5-2.5% difference).
Also fixed Pounce, which was not killing the pouncer sometimes.
It might be worthwhile to redo any previous Rod of Negation or Pounce calculations. (Sorry.)
Any Rod of Negation or Pounce data from the data base has been expunged (88 of these).
Version 5.40: Added Wave 10 and D&D Master Set figures.
Also fixed WaitAndFire again. Arg.
Any WaitThenFire data from the data base has been expunged (690 of these).
Remember the implementation of WaitThenFire is that the figures get +1 attack
except on the very first turn if they go first; this simulates them
being the ones to move into range of the battle.
Version 5.45: Fixed BloodFrenzy (Sorry I misread the card to be a Krug-like ability.)
Any calculations done with the Feral troll using testing beta version before Feb 14, 2010 are incorrect.
Version 5.60: Implemented Squad of size 1 to mean common heroes. (For example, this makes a difference
for Sudema and Braxas.) Added negative modifiers for D20Enhancement. (Useful for custom figures.)
Version 6.00: New interface improvements, including mouseover activating balloons that describe the abilities.
Version 6.10: Added implementable abilities from D12: the fledgling abilities and Psionic Blast. Also fixed a typo that would have affected results of ChillingTouch and Trample and made them worse than they should be. All Chilling Touch and Trample and Throw numbers before Aug 28, 2010 should be redone. There were 883 such calculations from before and this data has been expunged from the database.
Version 6.11: Modified Searing Intensity.
Version 6.20: Added more choices to "Squad size" to include squads of more than 1 common heroes, which is essentially what you get when you use Wyrmling Bonding or Master of the Elements. This only makes a difference for powers that distinguish squad figures from common heros such as Chomp, Poison Acid Breath, Stare of Stone.
WHOLEPAGE
}
#################################################################
sub statspage {
$message = $_[0]; # an optional message
my $num="[data unavailable]";
my $avg="[data unavailable]";
my $maxmat="[data unavailable]";
my $maxsizetime="[data unavailable]";
my $ctr=0;
my $sum=0;
my ($in,$x,$y,@x, @z);
if(1){if(open(IN, "<$datafile")){
while($in=){
($x,$y)=split(/=/,$in);
if($y!~/custom/){
$ctr++;
@x = $y=~/(\d+\.*\d*)/g;
$sum+=$x[49];
if($x[24]>$maxmat){$maxmat=$x[24];$maxsizetime=$x[49];}
}
}
close(IN);
$num=$ctr;
$avg=sprintf("%.1f",$sum*1.0/$ctr);
}}
#### Version 2.3x, 2.4x had 2350 hits before version 3.x ####
print<$message
Version 2.3x, 2.4x, 3.xx, 4.xx, 5.xx, 6.xx Access stats
From December 9, 2005 to now
There have been $HITCOUNTER hits to this program. (Remember that successful calculations are stored
in a database, and are NOT recalculated when the same calculation is later requested. Such a calculation
is counted only once in the number of "successful calculations" below.
There have been $num successful calculations,
each taking an average of $avg seconds.
The maximum size of a transition matrix in a successful calculation on this server
was $maxmat,
which took $maxsizetime seconds.
No data is kept on unsuccessful calculations (those that exceed the imposed
time limit of $maxLockFile seconds).
Versions 2.3x, 2.4x had 2350 hits, December 10, 2005 to May 8, 2006. (The new versions are backwards compatible
with old stored data.)
Version 2.2x Access stats
(November 10, 2005 to December 9, 2005)
(Calculations done with these versions
involving "Frenzy" might be off by 0.1% to the Frenzier's disadvantage.)
There have been 634 successful calculations,
each taking an average of 4.8 seconds.
The maximum size of a transition matrix in a successful calculation
was 3252,
which took 90 seconds.
No data is kept on unsuccessful calculations (those that exceed the imposed
time limit of 150 seconds).
Version 2.0 & 2.1 Access stats
(October 24, 2005 to November 9, 2005)
(Calculations done with these versions involving "Tough" should be ignored.)
There have been 454 successful calculations,
each taking an average of 5.3 seconds.
The maximum size of a transition matrix in a successful calculation
was 4044,
which took 144 seconds.
No data is kept on unsuccessful calculations (those that exceed the imposed
time limit of 150 seconds).
Version 1.0 Access stats
(September 16, 2005 to October 23, 2005)
There have been 648 successful calculations,
each taking an average of 7.3 seconds.
The maximum size of a transition matrix in a successful calculation
was 3252,
which took 84 seconds.
No data is kept on unsuccessful calculations (those that exceed the imposed
time limit of 120 seconds).
WHOLEPAGE
}
#################################################################
sub aboutthis {
$message = $_[0]; # an optional message
print<About this program...
$message
Here are the gory details of how math is used to calculate the exact theoretical
probability. (Version 5.40+ version)
We create a probabilistic finite state machine.
Each state is of the form
life1: number hit points or units left in player #1
life2: number hit points or units left in player #2
halfTurnNumber: This goes from 1 to 6, the half-turn number
attacker: This is either 1 or 2, which player is attacking.
attackersLeft: This is the number of figures yet to attack in this turn; this
is only relevant for squads
attacksLeft: This is the number of attacks yet to be executed by the current attacking
figure; this is only relevant for Double Attack ability (or Triple Attack).
Note that you cannot lump attackersLeft and attacksLeft together because conceivably
you could have a squad of figures with multiple attacks and it is possible a figure
could be killed in the middle of its first attack.
otherCrippled: This is 0 or 1. It is 1 if the other player has been crippled or EMPResponse activated,
with order markers removed, or Cloud of Darkness activated with turns skipped until your next turn.
attackDice: This number keeps track of any attack modifier for this turn. For example,
this is used for Unleashed Fury, for Wait And Fire, Stinger Drain.
targettingSame: This is 0 or 1. It is 1 after a figure has fired and the target
has not been destroyed. This is needed for Zettian Targetting bonus
negated: This is 0, 1, or 2. This keeps track of whether abilities have been negated by the Rod
of negation. A 1 or 2 refers to which player has been negated. Note that both players cannot
be negated simultaneously.
enemiesToAttack: This is used for the simplified ShaolinAttack ability.
This field should have value 0 or x, where x is the number of extra attacks the last
attacker gets; so only needed whether attackersLeft > 0.
smoked/skipTurn: For use with Smoke Powder. 0 = normal. 1 = smoked by defender and all normal attacks are skipped.
For use with Eternal Hatred or Stinger Drain, 0 = normal, 1 = skip rest of turn (i.e. no attacks).
blasted1 is the number of player2's unrevealed order markers that have been discarded,
blasted2 is the number of player1's unrevealed order markers that have been discarded,
adjacentWolves is the number of Wolves of Badru that are adjacent; this is used for the Pounce ability.
limiteduse1 is the number of limited use weapons left for player1, such as Ullar's Bolt, WraithAttack drows.
limiteduse2 is the number of limited use weapons left for player2, such as Ullar's Bolt, WraithAttack drows.
numEngaged: Keeps track of number of engaged figures for use with Engagement Strike and Ice Spikes.
We may need to keep track of more stuff in the future, as more abilities are invented
The states are given an arbitrary indexing. For efficiency, it is likely best to use
a hash (associative array) to assign a an index to each state.
There are also initial states and final states. There are 12 possible initial states, depending
on which halfTurn and which player starts up.
The final states keep track of who won, with how many hit points or units left.
After the list of states is made up,
a big transition matrix is built.
The rows and columns correspond to the states.
So the size is the number of states (which could be well over a thousand).
What is a transition matrix?
For each state State1, we calculate all the possible states that we can end up in
after one attack (one set of dice rolls, including any Chomp or Gaze).
The probability P that we land in state State2 is stored as an entry in the
transition matrix in row State1 and Column State2.
Call this transition matrix S. The part where we land in a final state is separated
out into a matrix T.
So S has dimension n x n and T has dimension n x m
where m is the number of final states, and n is the number of non-final states.
Here is the math theory calculation...
So T gives us the matrix of probabilities of starting in any state and landing in a final state
after exactly 1 move.
And ST (matrix multiplication!) gives us the matrix of probabilities of starting in any state and landing in a final state
after exactly 2 moves.
And SST gives us the matrix of probabilities of starting in any state and landing in a final state
after exactly 3 moves.
So the matrix of probabilities of starting in any state and landing in a final state
after any number of moves is
T + ST + SST + SSST + ....
an infinite sum.
But this infinite sum simplifies into
(I + S + S^2 + S^3 +...) T
= (InverseMatrix(I - S)) T.
Compute
Q = InverseMatrix(I - S)
Then QT gives the matrix of probabilities of starting in any particular state
and eventually landing in any final state.
You use QT to compute the probability of a certain player winning, given that any particular
starting state.
You can also use QT to compute the average hit points or units left with a certain player
conditioned upon that player winning.
And finally,
Q = I + S + S^2 + S^3 +...
gives the expected number of visits to each state, so that a sum across a row
would give the average number of moves (attacks) before landing in a final state.
And there you have it, that's how we compute the exact probabilities.
The tricky part in writing this program is generating the transition matrix.
The potential time consuming part is calculating the Q matrix because it
involves a matrix inversion.
Hooray for math.
How does the web interface work?
The Perl-CGI program reads the submitted form data,
and writes a Mathematica program. The Mathematica program is then
executed on the server and the Perl program sleeps while waiting for the Mathematica
program to finish.
The Perl program will also abort the Mathematica program if it takes too long
(defined to be $maxLockFile seconds).
Only one heroscape Mathematica program is allowed to execute at atime.
WHOLEPAGE
}
#################################################################
sub default_page {
$message = $_[0]; # an optional message
print<$message
SELECT ABILITIES
WHOLEPAGE
&printForm(%formHash);
print<
$instructions
$ender
WHOLEPAGE
}
#################################################################
sub pInitDisadv{
my ($diff)=@_;
my $b=19-$diff;
return $b*($b+1)/(2*(399-$b));
}
#################################################################
sub pInitProb{
my ($diff)=@_;
if($diff>=0){
return 1-pInitDisadv($diff);
}else{
return pInitDisadv(-$diff);
}
}
#################################################################
sub findprob{
my $message = $_[0]; # an optional message
my ($i,$j,$x,$y,$x2,$y2,$c,$z,$c1,$c2,$fig1spec,$fig2spec,$p, $return, $a1,$a2,$a,$b,$aa,$bb);
my $fig1="Figure"; my $f1="Fig";
if($formHash{psquad} eq "squad"){
$fig1="Squad";$f1="Squ";
if($formHash{ps}==1){$fig1="Figures";$f1="Figs";}
}
my $fig2="Figure";my $f2="Fig";
if($formHash{qsquad} eq "squad"){
$fig2="Squad";$f2="Squ";
if($formHash{qs}==1){$fig2="Figures";$f2="Figs";}
}
$return = 0;
for($i=0;$i<@ForbiddenPairs;$i+=2){
foreach $p (("p","q")){
$j=$i+1;
$x=$p.($ForbiddenPairs[$i]);
$y=$p.($ForbiddenPairs[$j]);
if((exists $formHash{$x})&&(exists $formHash{$y})){
$a1=$Abilities[$ForbiddenPairs[$i]-4];
$a2=$Abilities[$ForbiddenPairs[$j]-4];
$message .= qq^
\n"; system("./myflush");}
}
}
if($timelimit<0){ #### need to abort mathematica program....
system("./findAndAbortMath");
unlink $lockfile;
return "Mathematica program aborted. Time limit of $maxLockFile seconds exceeded.
(Either the calculation was too big, or there was a problem with the calculation.
If you suspect there is a problem, please report this problem to $emailLink.)
(or try again. If successful after several tries, and if the calculation is
important to you, you may ask $emailLink to do the calculation for you
on a bigger faster machine [this webserver is a few years old])";
}
###system("./math < $mafile"); ## for diagnostics of subroutine logic
###system("sleep 2"); ## simulation only
#### Run mathematica in background.
#### read mathoutput file, delete mathlock, and parse
if(!open(IN, "<$outputfile")){
unlink $lockfile;
return "Error in reading output of mathematica program. Report this problem to $emailLink.".
"(matchcode = $matchcode)";
}
my @all = ;
close(IN);
##$timeSpent="Time spent on calculation: ".(int(24*3600*(-M $lockfile)))." seconds.";
##$timeSpent="Time spent on calculation: ".(int(24*3600*(-C $lockfile)))." seconds.".
###"diag: ".$lockfile." ".(-e $lockfile)."/".(-C $lockfile)."/".$timelimit."/";
$timeSpent="Time spent on calculation: ".($maxLockFile-$timelimit)." seconds.";
unlink $lockfile;
if(-e $lockfile){sleep 1; unlink $lockfile;}
chomp @all;
my $all = join(" ", @all);
$all=~s/\\ //g; ### Delete \\ in case of input spread over multiple lines
if($all =~/error/i){ ###
$all=~/=.*\"(.*)\"/;
return $1;
}else{
$tsp=($maxLockFile-$timelimit);
($x,$z,$y) = split(/=/, $all);
$y=~s/\}/ \,$tsp\}/;
$z=~/\"(.*)\"/;
$redMatchcode=$1;
if($redMatchcode eq $matchcode){
@x = $y=~/(\d+\.*\d*)/g;
@probs=@x[0..11];
@lens=@x[12..23];
$MatrixSize=$x[24];
@survivors1=@x[25..36];
@survivors2=@x[37..48];
#### append result to datafile
if(open(OUT, ">>$datafile")){
print OUT qq^res["$matchcode"]=$y;\n^;
close(OUT);
}
}else{
return "Error in processing output. Coincidental error because two users interfered with each other?".
" Please try again or report this error to $emailLink. ".
"(matchcode1 = $redMatchcode), (matchcode2 = $matchcode)";
}
return "";
}
}
#################################################################
sub encodeMatchup{
my %f=@_;
my $h1="H"; if($formHash{psquad} eq "squad"){$h1="S";}
my $h2="H"; if($formHash{qsquad} eq "squad"){$h2="S";}
my $ans = "$h1$h2$f{p1}-$f{q1}-$f{p2}-$f{q2}-$f{p3}-$f{q3}-$f{ps}-$f{qs}-";
my @y=();
my $i; my $x;
for($i=0;$i<@Abilities;$i++){
$x="p".($i+4); if(exists $f{$x}){push @y, 1;}else{push @y, 0;}
$x="q".($i+4); if(exists $f{$x}){push @y, 1;}else{push @y, 0;}
}
my $y =join("",@y);
$y=~s/0+$//; # Truncate last zeroes. This will make for backwards compatibility in the future.
$ans .= $y."-$f{p0}-$f{q0}";
my $mel = "-$f{pmoff}-$f{qmoff}-$f{pmdef}-$f{qmdef}";
## append initiative enhancements .............................................
my $initEnh="-$f{pminit}-$f{qminit}";
if($initEnh eq "-0-0"){$initEnh="";}
my $attLim="-$f{pattlim}-$f{qattlim}".$initEnh;
if($attLim eq "-0-0"){$attLim="";}
$mel.=$attLim;
if($mel eq "-0-0-0-0"){$mel="";}
return $ans.$mel;
}
#################################################################
sub convertNN{
my ($dp)=@_;
if($dp=~/n/){$dp=~/(\d+)/;$dp=-$1;}
return $dp;
}
sub makeMathInput{
my %f = @_;
my $all="";
my $v; my $i; my ($x,$y);
$v="False"; if($f{psquad} eq "squad"){$v="True";}
$all.=qq^IsSquad[1]=$v;\n^;
$v="False"; if($f{qsquad} eq "squad"){$v="True";}
$all.=qq^IsSquad[2]=$v;\n^;
$v="False"; if($f{psquad} eq "squad"){
$v=$IsSquadForPowers{$f{ps}};
}
$all.=qq^IsSquadForPowers[1]=$v;\n^;
$v="False"; if($f{qsquad} eq "squad"){
$v=$IsSquadForPowers{$f{qs}};
}
$all.=qq^IsSquadForPowers[2]=$v;\n^;
my($dp,$dq)=($f{p0},$f{q0});
$dp=$D20hash{$dp};
$dq=$D20hash{$dq};
$all.=qq^
Life[1]=$f{p1};
Life[2]=$f{q1};
Attack[1]=$f{p2};
Attack[2]=$f{q2};
Defense[1]=$f{p3};
Defense[2]=$f{q3};
SquadSize[1]=$squadSize{$f{ps}};
SquadSize[2]=$squadSize{$f{qs}};
D20Enhancement[1]=$dp;
D20Enhancement[2]=$dq;
AttackEnhancement[1]=$f{pmoff};
AttackEnhancement[2]=$f{qmoff};
DefenseEnhancement[1]=$f{pmdef};
DefenseEnhancement[2]=$f{qmdef};
AttackerLimit[1]=$f{pattlim};
AttackerLimit[2]=$f{qattlim};
InitiativeEnhancement[1]=$f{pminit};
InitiativeEnhancement[2]=$f{qminit};
matchcode="$matchcode";
^;
for($i=0;$i<@Abilities;$i++){
$y=$AbilitiesMa[$i];
$x="p".($i+4);$v="False"; if(exists $f{$x}){$v="True";}
$all.=qq^${y}[1]=$v;\n^;
$x="q".($i+4);$v="False"; if(exists $f{$x}){$v="True";}
$all.=qq^${y}[2]=$v;\n^;
}
$all.=qq^Get["hero-in.ma"];
DoTheCalculation;
Quit
^;
if(open(OUT, ">$mafile")){
print OUT $all;
close(OUT);
return "";
}else{
return "Error making Mathematica input file.";
}
}
#################################################################
sub initializeJS{
$js=qq^\n^;
}
sub makeClearQuickFunction{
my ($p)=@_;
my ($i, $x);
my $ans=qq^function clearQuick$p(){\n^;
foreach $x (@AbilitiesMa){
$ans.=qq^myform.$p$AbilityToN{$x}.checked=false;\n^;
}
return $ans."}\n";
}
sub makeResetFunction{
my ($p,$q)=@_;
my ($i, $x);
my $ans=qq^
function resetChomp$p(){
reset$p();
checkChomp$p();
}
function checkChomp$p(){
var n;
if(myform.$p$AbilityToN{"Chomp"}.checked){
n = myform.quick$q.selectedIndex-1;
var morestr="";
if(n>=0)if(!isSM[n]){
if((myform.quick$p.selectedIndex-1)==$zNameToIndex{Grimnak})
{morestr=" You might change \\"Grimnak\\" to \\"Grimnak (vs Large or Huge)\\".";}
alert("Warning: Chomp has been selected but "+
name[n]+" is not small or medium."+morestr);
}
}else if((myform.quick$p.selectedIndex-1)==$zNameToIndex{"Grimnak (vs Large or Huge)"}){
n = myform.quick$q.selectedIndex-1;
if(n>=0)if(isSM[n]){
alert("Warning: \\"Grimnak (vs Large or Huge)\\" has been selected but "+
name[n]+" is not Large or Huge, so you should select plain \\"Grimnak\\".");
}
}
}
function resetParalyzingGaze$p(){
reset$p();
checkParalyzingGaze$p();
}
function checkParalyzingGaze$p(){
var n;
if(myform.$p$AbilityToN{"ParalyzingGaze"}.checked){
n = myform.quick$q.selectedIndex-1;
var morestr="";
if(n>=0)if(!isSM[n]){
if((myform.quick$p.selectedIndex-1)==$zNameToIndex{"Me-Burq-Sa"})
{morestr=" You might change \\"Me-Burq-Sa\\" to \\"Me-Burq-Sa (vs Large or Huge)\\".";}
alert("Warning: Paralyzing Stare has been selected but "+
name[n]+" is not small or medium."+morestr);
}
}else if((myform.quick$p.selectedIndex-1)==$zNameToIndex{"Me-Burq-Sa (vs Large or Huge)"}){
n = myform.quick$q.selectedIndex-1;
if(n>=0)if(isSM[n]){
alert("Warning: \\"Me-Burq-Sa (vs Large or Huge)\\" has been selected but "+
name[n]+" is not Large or Huge, so you should select plain \\"Me-Burq-Sa\\".");
}
}
}
function resetMindShackle$p(){
reset$p();
checkMindShackle$p();
}
function checkMindShackle$p(){
var n;
if(myform.$p$AbilityToN{"MindShackle"}.checked){
n = myform.quick$q.selectedIndex-1;
var morestr="";
if(n>=0)if(!isU[n]){
if((myform.quick$p.selectedIndex-1)==$zNameToIndex{"Ne-Gok-Sa"})
{morestr=" You might change \\"Ne-Gok-Sa\\" to \\"Ne-Gok-Sa (vs common)\\"";}
alert("Warning: Mind Shackle has been selected but "+
name[n]+" is not unique.");
}
}else if((myform.quick$p.selectedIndex-1)==$zNameToIndex{"Ne-Gok-Sa (vs common)"}){
n = myform.quick$q.selectedIndex-1;
if(n>=0)if(isU[n]){
alert("Warning: \\"Ne-Gok-Sa (vs common)\\" has been selected but "+
name[n]+" is unique, so you should select plain \\"Ne-Gok-Sa\\".");
}
}
}
function checkDenrick$p(){
var n;
if((myform.quick$p.selectedIndex-1)==$zNameToIndex{"Sir Denrick"}){
n = myform.quick$q.selectedIndex-1;
if(n>=0)if(isH[n]){
alert("Warning: \\"Sir Denrick\\" has been selected but "+
name[n]+" is Huge, so you should select \\"Sir Denrick (vs Huge)\\".");
}
}else if((myform.quick$p.selectedIndex-1)==$zNameToIndex{"Sir Denrick (vs Huge)"}){
n = myform.quick$q.selectedIndex-1;
if(n>=0)if(!isH[n]){
alert("Warning: \\"Sir Denrick (vs Huge)\\" has been selected but "+
name[n]+" is not Huge, so you should select plain \\"Sir Denrick\\".");
}
}
}
function resetFigures$p(){
var n;
n = myform.quick$p.selectedIndex-1;
if(n>=0)if(myform.${p}squad[1].checked){
var p = Math.round(pts[n]*parseFloat(myform.squad${p}1.value)/parseFloat(myform.${p}s.value));
myform.pt$p.value=p;
}
if(myform.${p}squad[0].checked){
myform.${p}squad[1].checked=true;
reset$p();
}
}
^;
return $ans;
}
sub makeChangeQuickFunction{
my ($p,$q)=@_;
my ($i, $x);
my $ans=qq^\nfunction changeQuick$p(){\n^;
$ans.=qq^var n = myform.quick$p.selectedIndex-1;if(n>=0){clearQuick$p();}\n^;
for($i=0;$i<@zName;$i++){
$ans.=qq^if(n==$i){^;
foreach $x (split(/:/, $zAbilities[$i])){
if(exists $AbilityToN{$x}){
$ans.=qq^myform.$p$AbilityToN{$x}.checked=true;^;
}
}
if($zHS[$i] eq "hero"){
$ans.=qq^myform.${p}squad[0].checked=true;^;
$ans.=qq^myform.hero${p}1.value="$zLife[$i]";^;
}
else{
$ans.=qq^myform.${p}squad[1].checked=true;^;
$ans.=qq^myform.squad${p}1.value="$zLife[$i]";^;
$ans.=qq^myform.${p}s.value="$zLife[$i]";^;
}
$ans.=qq^myform.${p}2.value="$zAtt[$i]";^;
$ans.=qq^myform.${p}3.value="$zDef[$i]";^;
$ans.=qq^myform.pt${p}.value="$zPoints[$i]";^;
$ans.=qq^myform.${p}moff.value="0";^;
$ans.=qq^myform.${p}mdef.value="0";^;
$ans.=qq^myform.${p}minit.value="0";^;
$ans.=qq^myform.${p}0.value="0";^;
$ans.=qq^myform.${p}moff.value="$zAttEnh[$i]";^;
$ans.=qq^myform.${p}mdef.value="$zDefEnh[$i]";^;
$ans.=qq^myform.${p}minit.value="$zInitEnh[$i]";^;
$ans.=qq^myform.${p}0.value="$zD20Enh[$i]";^;
$ans.=qq^resetcolors();resetAbilitiesColors();^;
$ans.=qq^}\n^;
}
return $ans."if(n>=0){checkSpecial();}\n}\n";
}
#################################################################
#################################################################
# end app logic functions
# begin toolkit functions
#################################################################
#################################################################
#################################################################
sub errorPage {
my $message = $_[0]; # optional message parameter
print<Server Error
Server Error Encountered
$message
If the problem persists, please notify $emailLink
ALL
exit;
}
##################################################################
# OLD SUBROUTINES no longer used, will eventually delete
#################################################################