Advertisement
aher

combat.pl

Jan 21st, 2013
253
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Perl 39.84 KB | None | 0 0
  1. #!/usr/bin/perl
  2. use 5.10.1;
  3. use strict;
  4. use Getopt::Long;
  5. use Pod::Usage;
  6.  
  7. our $VERSION = 0.1;
  8. our $number = 100;
  9. our $wimpy = 0;
  10. our $man = {};
  11. our $monster = "";
  12. our $surprise = 0;
  13. our $highdex = 0;
  14. our $verbose = 0;
  15. our $help = 0;
  16. our $list = 0;
  17.  
  18. GetOptions(
  19.   "number=i"      => \$number,  # Number of simulations to run
  20.   "wimpy=f"       => \$wimpy,   # Wimpy mode
  21.   "man=s%"        => \$man,     # Man
  22.   "monster=s"     => \$monster, # Monster
  23.   "surprise+"     => \$surprise,# flag
  24.   "highdex=i"     => \$highdex, # high DEX for initiative bonus
  25.   "verbose+"      => \$verbose, # flag
  26.   "list"          => \$list,    # list the monsters implemented so far & exit
  27.   "version|V"     => sub { VERSION(); exit; },
  28.   'help|?'        => \$help)    or pod2usage(2);
  29.  
  30. pod2usage(1) if $help;
  31.  
  32. sub VERSION {
  33. print <<EOF
  34.  
  35. This is combat.pl version $VERSION.
  36.  
  37. Help with command-line options should be found by using "combat.pl --help".
  38. Complete documentation including copyright and license should be found by
  39. using "perldoc combat.pl".
  40. EOF
  41. }
  42.  
  43. package Dice;
  44. sub d20 {  int(rand(20)) + 1; }
  45. sub d12 {  int(rand(12)) + 1; }
  46. sub d10 {  int(rand(10)) + 1; }
  47. sub d8  {  int(rand(8))  + 1; }
  48. sub d6  {  int(rand(6))  + 1; }
  49. sub d4  {  int(rand(4))  + 1; }
  50.  
  51. sub roll {
  52.   my ($num,$sides) = @_;
  53.   my $result = 0;
  54.   while($num--) {
  55.     $result += $sides->();
  56.   }
  57.   return $result;
  58. }
  59.  
  60. our $d20 = \&d20;
  61. our $d6 = \&d6;
  62. our $d4 = \&d4;
  63.  
  64. package Logger;
  65. sub log {
  66.   my $msg = shift;
  67.   if($verbose){
  68.     print $msg;
  69.   }
  70. }
  71.  
  72. package Formatter;
  73. sub s { # Number-sensitive plural "s" ending
  74.   my $num = shift;
  75.   if($num == 1 || $num =~ m/one/i){
  76.     return "";
  77.   } else {
  78.     return "s";
  79.   }
  80. }
  81.  
  82. sub plus { # Return "+number" with plus sign if given number is positive
  83.   my $num = shift;
  84.   if($num > 0){
  85.     return "+$num";
  86.   } else {
  87.     return "$num";
  88.   }
  89. }
  90.  
  91. package Statistics;
  92. sub new {
  93.   my $proto = shift;
  94.   my $class = ref($proto) || $proto;
  95.   my $name = shift;
  96.   my $self  = { NAME=>$name, MIN=>undef, MAX=>undef, TOTAL=>0 };
  97.   bless ($self, $class);
  98.   return $self;
  99. }
  100.  
  101. sub record {
  102.   my $self = shift;
  103.   my $round = shift;
  104.   $self->{$round}++;
  105.   $self->{TOTAL}++;
  106.   if(!defined($self->{MAX}) || $round > $self->{MAX}) { $self->{MAX} = $round; }
  107.   if(!defined($self->{MIN}) || $round < $self->{MIN}) { $self->{MIN} = $round; }
  108. }
  109.  
  110. sub element {
  111.   my $self = shift;
  112.   my $x = shift;
  113.   my $count = 0;
  114.   if($self->{TOTAL} == 0) { return 0; }
  115.   if($x > $self->{TOTAL}) { return 0; }
  116.   for my $group ($self->{MIN} ..  $self->{MAX}) {
  117.     $count += $self->{$group};
  118.     if($count >= $x) { return $group; }
  119.   }
  120.   return 0;
  121. }
  122.  
  123. sub moment {
  124.   my $self = shift;
  125.   my $power = shift;
  126.   my $sum = 0;
  127.   if($self->{TOTAL} == 0) { return 0; }
  128.   if(!defined($power)) { $power = 1; }
  129.   for my $group ($self->{MIN} ..  $self->{MAX}) {
  130.     $sum += ($group**$power) * ($self->{$group});
  131.   }
  132.   return $sum/$self->{TOTAL};
  133. }
  134.  
  135. sub sd {
  136.   my $self = shift;
  137.   my $mean = $self->moment();
  138.   my $mu2 = $self->moment(2);
  139.   my $n =  $self->{TOTAL};
  140.   if($n <= 1) { return "NaN"; }
  141.   return sqrt(($n/($n - 1)) * ($mu2 - $mean**2));
  142. }
  143.  
  144. sub min {
  145.   my $self = shift;
  146.   !defined($self->{MIN}) ? "NaN" : $self->{MIN};
  147. }
  148.  
  149. sub max {
  150.   my $self = shift;
  151.   !defined($self->{MAX}) ? "NaN" : $self->{MAX};
  152. }
  153.  
  154. sub median {
  155.   my $self = shift;
  156.   my $x;
  157.   if($self->{TOTAL} == 0) { return "NaN"; }
  158.   if($self->{TOTAL} % 2 == 0) {
  159.     $x = $self->{TOTAL} / 2;
  160.     return ($self->element($x) + $self->element($x + 1))/2;
  161.    } else {
  162.     $x = ($self->{TOTAL} + 1) / 2;
  163.     return $self->element($x);
  164.    }
  165. }
  166.  
  167. package Items;
  168. our @Armor = ("AC 0", "AC 1", "Plate Armor & Shield", "Plate Armor", "Chain Mail & Shield", "Chain Mail", "Leather & Shield", "Leather Armor", "Shield Only", "No Armor or Shield");
  169.  
  170. package Thing;
  171.  
  172. our @Abilities = ("STR","INT","WIS","CON","DEX","CHA");
  173.  
  174. sub new {
  175.   my $proto = shift;
  176.   my $class = ref($proto) || $proto;
  177.   my (%params) = @_;
  178.   my $self  = {
  179.     NAME => $params{"NAME"},
  180.     STR => $params{"STR"},
  181.     INT => $params{"INT"},
  182.     WIS => $params{"WIS"},
  183.     CON => $params{"CON"},
  184.     DEX => $params{"DEX"},
  185.     CHA => $params{"CHA"},
  186.     AC => $params{"AC"},
  187.     XP => $params{"XP"},
  188.     LEVEL => $params{"LEVEL"},
  189.     HD => $params{"HD"},
  190.     MAXHP => $params{"MAXHP"},
  191.     CURHP => $params{"CURHP"},
  192.     RACE => $params{"RACE"},
  193.     ALIGN => $params{"ALIGN"},
  194.     MONEY => $params{"MONEY"},
  195.     ENCUMBRANCE => $params{"ENCUMBRANCE"},
  196.     ITEMS => $params{"ITEMS"},  # This should be an array reference
  197.     SPELLS => $params{"SPELLS"} # Ditto
  198.   };
  199.   bless ($self, $class);
  200.   return $self;
  201. }
  202.  
  203. sub as_string {
  204.   my $self = shift;
  205.   Logger::log(sprintf("%s [MV %d\", AC %d, HD %s, hp %d/%d]\n",
  206.     $self->{NAME},
  207.     $self->{MV},
  208.     $self->AC(),
  209.     $self->HD_as_string(),
  210.     $self->{CURHP},
  211.     $self->{MAXHP}));
  212. }
  213.  
  214. # HD could be its own object with these methods:
  215. # HD.as_string(), HD.dice(), HD.bonus()
  216. sub HD_as_string {
  217.   my $self = shift;
  218.   my ($ndice,$plusses) = ($self->{HD}->[0], $self->{HD}->[1]);
  219.   if($ndice == 1/2) { $ndice = "1/2"; }
  220.   return $plusses ? $ndice . Formatter::plus($plusses) : $ndice;
  221. }
  222.  
  223. sub HD_dice {
  224.   my $self = shift;
  225.   return $self->{HD}->[0];
  226. }
  227. sub HD_bonus {
  228.   my $self = shift;
  229.   return $self->{HD}->[1];
  230. }
  231.  
  232. sub AC {
  233.   my $self = shift;
  234.   return $self->{AC};
  235. }
  236.  
  237. sub surprised {
  238.   my $self = shift;
  239.   if (@_) { $self->{SURPRISED} = shift }
  240.   Logger::log($self->{NAME} . " is surprised for " . $self->{SURPRISED} .
  241.     " round" . Formatter::s($self->{SURPRISED}) . ".\n");
  242.   return $self->{SURPRISED};
  243. }
  244.  
  245. sub disarmed {
  246.   my $self = shift;
  247.   if (@_) { $self->{DISARMED} = shift }
  248.   Logger::log($self->{NAME} . " drops his weapon!\n");
  249.   return $self->{DISARMED};
  250. }
  251.  
  252. sub check_initiative {
  253.   my $self = shift;
  254.   # The commented out code gives a +1 bonus for high DEX
  255.   if( $self->{HIGHDEX} && $self->{DEX} >= $self->{HIGHDEX} ){
  256.     Logger::log($self->{NAME} . "'s high DEX (" . $self->{DEX} .
  257.       ") gives him a +1 to initiative.\n");
  258.     $self->{INITIATIVE} = Dice::d6() + 1;
  259.   } else {
  260.     $self->{INITIATIVE} = Dice::d6();
  261.   }
  262.   Logger::log($self->{NAME} . " throws " . $self->{INITIATIVE} . " initiative.\n");
  263.   return $self->{INITIATIVE};
  264. }
  265.  
  266. sub check_surprise {
  267.   my $self = shift;
  268.   my $roll = Dice::d6();
  269.   if($roll == 1 || $roll == 2) {
  270.     if($main::surprise) {
  271.       $self->surprised($roll); # 1 or 2 rounds of surprise
  272.     } else {
  273.       $self->surprised(1); # only ever 1 surprise round
  274.     }
  275.     $self->check_drop();  
  276.   }
  277. }
  278.  
  279. sub check_drop {
  280.   my $self = shift;
  281.   if( Dice::d4() == 1 ){
  282.     return $self->disarmed(1);
  283.   }
  284. }
  285.  
  286. sub is_dead {
  287.   my $self = shift;
  288.   if( $self->{CURHP} <= 0 ){
  289.     return 1;
  290.   } else {
  291.     return 0;
  292.   }
  293. }
  294.  
  295. sub flees { # Check if Thing will flee. Perform calculation here.
  296.   my $self = shift;
  297.   return 0; # fight-to-the-death by default.
  298.   # Right now, this is only used to implement Diku-/LP- MUD-style "wimpy"
  299.   # mode in the "Man" class. But could be overridden in the monster class or
  300.   # future CM classes to implement morale checks and routs...although I will
  301.   # probably implement check_morale later and override that to implement a
  302.   # "wimpy" mode option.
  303. }
  304. sub fled { # Thing has fled (past tense): No calculation performed here.
  305.   my $self = shift;
  306.   return 0; # fight-to-the-death by default.
  307. }
  308.  
  309. # Calculate chance to hit target
  310. sub to_hit {
  311.   my $self = shift;
  312.   my $ac = shift;
  313.   # N.B. Right now, this method's argument is AC because this is LBB ACS.
  314.   # In the future, we might want to change this to the target's object.
  315.   # For example, if we implement CM's CT, we need to know type of attacker
  316.   # and defender, e.g., Heavy Foot versus Medium Horse. AC doesn't make sense
  317.   # here. But if we implement CM's Man-to-Man table, we need to know attacker's
  318.   # weapon type and defender's armor class, which we can always get from the
  319.   # objects, e.g., $self->{WT}, $target->AC().
  320.   die "Called pure virtual method: Thing::to_hit.";
  321.   # This is a pure virtual method. Must be overriden by subclasses.
  322. }
  323.  
  324. # Roll for HP
  325. sub roll_hp {      
  326.   my $self = shift;
  327.   my $HP;
  328.   my ($ndice,$plusses) = ($self->{HD}->[0],$self->{HD}->[1]);
  329.   # Take account of half n(umber of)dice
  330.   if($ndice == 1/2) {
  331.     $HP = int((Dice::d6() + 1)/2) + $plusses;
  332.   } else {
  333.     $HP = Dice::roll($ndice,$Dice::d6) + $plusses;
  334.   }
  335.   # Take account of negative "plusses" bringing the result to zero (or less)
  336.   if($HP <= 0) { $HP = 1; }
  337.   return $HP;
  338. }
  339.  
  340. # Roll for damage
  341. sub calc_damage {      
  342.   my $self = shift;
  343.   my $damage = Dice::d6();
  344.   Logger::log($self->{NAME} . " throws " . $damage . " hp of damage.\n");
  345.   return $damage;
  346. }
  347.  
  348. sub on_hit {
  349.   # Event Handler
  350.   # called after successfully hitting victim
  351.   my $self = shift;
  352.   my $victim = shift;
  353.   # subclasses should override this method if they have special attacks, e.g.,
  354.   # $self->paralyze($victim); # for ghouls
  355. }
  356.  
  357. sub on_damage {      
  358.   # Event Handler
  359.   # called when damage is to be applied
  360.   my $self = shift;
  361.   my $damage = shift;
  362.   $self->{CURHP} -= $damage;
  363.   Logger::log($self->{NAME} . " sustains " . $damage . " hp of damage" .
  364.     " and has " . $self->{CURHP} . " hp remaining.\n");
  365. }
  366.  
  367. sub on_dead {
  368.   my $self = shift;
  369.   Logger::log($self->{NAME} . " is slain.\n");
  370. }
  371.  
  372. package Man;
  373. @Man::ISA = ("Thing");
  374.  
  375. our %MenAttacking = (
  376. #       Level
  377. # AC    1-3  4-6  7-9  10-12  13-15   16&+
  378.   2 =>  [17,  15,  12,   10,     8,     5],
  379.   3 =>  [16,  14,  11,    9,     7,     4],
  380.   4 =>  [15,  13,  10,    8,     6,     3],
  381.   5 =>  [14,  12,   9,    7,     5,     2],
  382.   6 =>  [13,  11,   8,    6,     4,     1],
  383.   7 =>  [12,  10,   7,    5,     3,     1],
  384.   8 =>  [11,   9,   6,    4,     2,     1],
  385.   9 =>  [10,   8,   5,    3,     1,     1]
  386. );
  387.  
  388. sub to_hit {
  389.   my $self = shift;
  390.   my $ac = shift;
  391.   my $group = int(($self->{LEVEL} - 1)/$self->{STEP});
  392.   return $MenAttacking{$ac}->[$group];
  393. }
  394.  
  395. # wimpy mode
  396. # TODO: You can only flee on your move.  You must have initiative.
  397. # You can't be dead, paralyzed or surprised, but you can be disarmed.
  398.  
  399. sub flees {
  400.   my $self = shift;
  401.   my $wimpy = $self->{WIMPY};
  402.   if($wimpy == 0) { return 0; } # wimpy mode is off
  403.   # Can't flee if you can't move (dur to death, paralysis or surprise
  404.   if($self->{PARALYZED} || $self->{SURPRISED} || $self->is_dead()) {
  405.     return 0;
  406.   }
  407.   if(int($wimpy) == $wimpy) {   # wimpy is an integer
  408.     if($self->{CURHP} <= $wimpy){
  409.       $self->{FLED} = 1;
  410.       return 1;
  411.     } else { return 0; }
  412.   }
  413.   # wimpy is a fraction of max hp
  414.   elsif($self->{CURHP} <= int($wimpy * $self->{MAXHP})) {
  415.     $self->{FLED} = 1;
  416.     return 1;
  417.   } else { return 0; }
  418. }
  419.  
  420. sub fled {
  421.   my $self = shift;
  422.   return $self->{FLED};
  423. }
  424.  
  425. sub wimpy {
  426.   my $self = shift;
  427.   my $wimpy = shift;
  428.   $self->{WIMPY} = $wimpy;
  429.   if($wimpy) {
  430.     Logger::log($self->{NAME} . "'s wimpy mode turned on and set to " .
  431.       $wimpy ."\n");
  432.   } else {
  433.     #Logger::log($self->{NAME} . "'s wimpy mode turned off\n");
  434.   }
  435.   return $wimpy;
  436. }
  437.  
  438. sub highdex {
  439.   my $self = shift;
  440.   my $highdex = shift;
  441.   $self->{HIGHDEX} = $highdex;
  442.   if($highdex) {
  443.     Logger::log($self->{NAME} . "'s highdex mode turned on and set to " .
  444.       $highdex ."\n");
  445.   } else {
  446.     #Logger::log($self->{NAME} . "'s highdex mode turned off\n");
  447.   }
  448.   return $highdex;
  449. }
  450.  
  451.  
  452. package FightingMan;
  453. @FightingMan::ISA = ("Man");
  454.  
  455. our @XP = (0, 0, 2000, 4000, 8000, 16_000, 32_000, 64_000, 120_000, 240_000, 360_000);
  456. our @Title = ("Normal Man", "Veteran", "Warrior", "Swordsman", "Hero", "Swashbuckler", "Myrmidon", "Champion", "Superhero", "Lord", "Lord, 10th Level");
  457. our @HD = ( [1,0], [1,1], [2,0], [3,0], [4,0], [5,1], [6,0], [7,1], [8,2], [9,3], [10,1]);
  458.  
  459. sub new {
  460.   my $proto = shift;
  461.   my $class = ref($proto) || $proto;
  462.   my($self) = $class->SUPER::new(@_);
  463.   $self->{"STEP"} = 3;
  464.   bless ($self,$class);
  465.   return $self;
  466. }
  467.  
  468. sub rand {
  469.   my $self = shift;
  470.  
  471. # Use these defaults if they're not defined.
  472. # If they're defined, don't modify them...
  473.   if(!defined($self->{LEVEL})){ $self->{LEVEL} = 1; }
  474.   if(!defined($self->{AC})){ $self->{AC} = 6; }
  475.   if(!defined($self->{MV})){ $self->{MV} = 12; }
  476.  
  477. # Set these appropriately by level...  
  478.   $self->{HD} = $HD[$self->{LEVEL}];
  479.   $self->{XP} = $XP[$self->{LEVEL}];
  480.   $self->{NAME} = $Title[$self->{LEVEL}];
  481.  
  482. # Randomize these...
  483.   $self->{MAXHP} =
  484.     $self->{CURHP} =
  485.       $self->roll_hp();
  486. # Roll 3d6 in order for ability scores
  487.   foreach my $ability (@Abilities) {
  488.     $self->{$ability} = Dice::roll(3,$Dice::d6);
  489.   }
  490.  
  491. # Reset these...      
  492.   $self->{SURPRISED} = 0;
  493.   $self->{DISARMED} = 0;
  494.   $self->{PARALYZED} = 0;
  495.   $self->{INITIATIVE} = 0;
  496.   $self->{FLED} = 0;
  497. }
  498.  
  499. # Saving throws...These are class/level/type specific, more so than Men
  500. # Attacking table where you can use the same table and just regroup it
  501. # 1-3,1-4,1-5 based on class. Therefore, I'm going to implement SVs at this
  502. # level of the code.
  503.  
  504. # Saving throw for all wands, including polymorph and paralization
  505. # See MM/10.
  506. sub save_versus_paralization {
  507.   my $self = shift;
  508.   my $condition = shift;
  509.   my $row = int(($self->{LEVEL} - 1)/3);
  510.   my @saving_throw = (13,11,9,7,5);
  511.   my $saving_throw = $row > 4 ? 5 : $saving_throw[$row];
  512.   my $throw = Dice::d20();
  513.   Logger::log($self->{NAME} . " requires a roll of at least " . $saving_throw .
  514.     " to save versus " . $condition . ", and throws " . $throw . "\n");
  515.   return ($throw >= $saving_throw);
  516. }
  517.  
  518. package MagicUser;
  519. @MagicUser::ISA = ("Man");
  520. sub new {
  521.   my $proto = shift;
  522.   my $class = ref($proto) || $proto;
  523.   my($self) = $class->SUPER::new(@_);
  524.   $self->{"STEP"} = 5;
  525.   bless ($self,$class);
  526.   return $self;
  527. }
  528.  
  529. package Cleric;
  530. @Cleric::ISA = ("Man");
  531. sub new {
  532.   my $proto = shift;
  533.   my $class = ref($proto) || $proto;
  534.   my($self) = $class->SUPER::new(@_);
  535.   $self->{"STEP"} = 4;
  536.   bless ($self,$class);
  537.   return $self;
  538. }
  539.  
  540. package Monster;
  541. @Monster::ISA = ("Thing");
  542.  
  543. our %MonstersAttacking = (
  544. #        HD Up to
  545. # AC       1  1+1  2-3  3-4  4-6  6-8  9-10  11&+
  546.   2 =>   [17,  16,  15,  13,  12,  11,    9,   7],
  547.   3 =>   [16,  15,  14,  12,  11,  10,    8,   6],
  548.   4 =>   [15,  14,  13,  11,  10,   9,    7,   5],
  549.   5 =>   [14,  13,  12,  10,   9,   8,    6,   4],
  550.   6 =>   [13,  12,  11,   9,   8,   7,    5,   3],
  551.   7 =>   [12,  11,  10,   8,   7,   6,    4,   2],
  552.   8 =>   [11,  10,   9,   7,   6,   5,    3,   1],
  553.   9 =>   [10,   9,   8,   6,   5,   4,    2,   0]
  554. );
  555.  
  556. sub to_hit {
  557.   my $self = shift;
  558.   my $ac = shift || 9;
  559.   my ($dice, $plusses) = ($self->{HD}->[0], $self->{HD}->[1]);
  560.   my $group;
  561.   if($dice == 1/2)                   { $group = 0; }
  562.   elsif($dice == 1 && $plusses <= 0) { $group = 0; }
  563.   elsif($dice == 1 && $plusses  > 0) { $group = 1; }
  564.   elsif($dice == 2)                  { $group = 2; }
  565.   elsif($dice == 3 && $plusses <= 0) { $group = 2; }
  566.   elsif($dice == 3 && $plusses  > 0) { $group = 3; }
  567.   elsif($dice == 4 && $plusses <= 0) { $group = 3; }
  568.   elsif($dice == 4 && $plusses  > 0) { $group = 4; }
  569.   elsif($dice == 5)                  { $group = 4; }
  570.   elsif($dice == 6 && $plusses <= 0) { $group = 4; }
  571.   elsif($dice == 6 && $plusses  > 0) { $group = 5; }
  572.   elsif($dice == 7)                  { $group = 5; }
  573.   elsif($dice == 8)                  { $group = 5; }
  574.   elsif($dice == 9)                  { $group = 6; }
  575.   elsif($dice == 10)                 { $group = 6; }
  576.   else                               { $group = 7; } # $dice >= 11
  577.   return $MonstersAttacking{$ac}->[$group];
  578. }
  579.  
  580. sub rand {
  581.   my $self = shift;
  582.   $self->{MAXHP} =
  583.     $self->{CURHP} =
  584.       $self->roll_hp();
  585.  
  586.   if(!defined($self->{AC})){ $self->{AC} = 6; }
  587.   if(!defined($self->{MV})){ $self->{MV} = 9; }
  588.  
  589. # Reset these...      
  590.   $self->{SURPRISED} = 0;
  591.   $self->{DISARMED} = 0;
  592.   $self->{PARALYZED} = 0;
  593.   $self->{INITIATIVE} = 0;
  594.   $self->{FLED} = 0;
  595. }
  596.  
  597. package Ghoul;
  598. @Ghoul::ISA = ("Monster");
  599.  
  600. sub new {
  601.   my $proto = shift;
  602.   my $class = ref($proto) || $proto;
  603.   my($self) = $class->SUPER::new(
  604.     HD=>[2,0],MV=>9,AC=>6,NAME=>"Ghoul",
  605.     @_);
  606.   bless ($self,$class);
  607.   return $self;
  608. }
  609.  
  610. sub on_hit {
  611.   # Event Handler
  612.   # called after successfully hitting victim
  613.   my $self = shift;
  614.   my $victim = shift;
  615.   $self->paralyze($victim);
  616. }
  617.  
  618. sub paralyze {
  619.   # When a ghoul successfully hits a victim,
  620.   # the victim must save versus paralization
  621.   # or he's paralyzed for 2d4 turns
  622.   my $self = shift;
  623.   my $victim = shift;
  624.   # Elves are immune from paralization
  625.   if($victim->{RACE} eq "ELF") { return; }
  626.   if(!$victim->save_versus_paralization("paralysis")) {
  627.     my $duration = Dice::roll(2,$Dice::d4);
  628.     Logger::log("Fails! " . $victim->{NAME} . " is paralysed for " .
  629.       $duration . " rounds.\n");
  630.     if($victim->{PARALYZED} <= $duration) { # if the victim is already paralyzed
  631.       $victim->{PARALYZED} = $duration;     # for longer, do nothing
  632.     }
  633.   }
  634. }
  635.  
  636. package Ogre;
  637. @Ogre::ISA = ("Monster");
  638.  
  639. sub new {
  640.   my $proto = shift;
  641.   my $class = ref($proto) || $proto;
  642.   my($self) = $class->SUPER::new(
  643.     NAME=>"Ogre",AC=>5,MV=>9,HD=>[4,1],
  644.     @_);
  645.   bless ($self,$class);
  646.   return $self;
  647. }
  648.  
  649. # Ogres cause 3-8 (1d6+2) damage per hit
  650. sub calc_damage {      
  651.   my $self = shift;
  652.   my $damage = Dice::d6() + 2;
  653.   Logger::log($self->{NAME} . " throws " . $damage . " hp of damage.\n");
  654.   return $damage;
  655. }
  656.  
  657. package MonsterFactory;
  658.  
  659. our @Monsters = qw(Skeleton Zombie Goblin Orc Hobgoblin Ghoul Gnoll Ogre);
  660.  
  661. sub CreateMonster {
  662.   my $self = shift;
  663.   my $type = shift;
  664.   if($type eq "") {
  665.     $type = $Monsters[Dice::d8() - 1];
  666.     printf "Monster not specified. Randomizing... $type.\n";
  667.   }
  668.   for ($type) {
  669.     when (/Skeleton/i) { return Monster->new(NAME=>"Skeleton",
  670.       AC=>7,MV=>6,HD=>[1/2,0]); } # TODO: never ck morale
  671.     when (/Zombie/i) { return Monster->new(NAME=>"Zombie",
  672.       AC=>8,MV=>6,HD=>[1,0]); } # TODO: never ck morale
  673.     when (/Hobgoblin/i) { return Monster->new(NAME=>"Hobgoblin",
  674.       AC=>5,MV=>9,HD=>[1,1]); } # TODO: +1 morale
  675.     # TODO: Orcs and Goblins suffer -1 penalties to attack/morale in daylight
  676.     when (/Goblin/i) { return Monster->new(NAME=>"Goblin",
  677.       AC=>6,MV=>6,HD=>[1,-1]); }
  678.     when (/Orc/i) { return Monster->new(NAME=>"Orc",
  679.       AC=>6,MV=>9,HD=>[1,0]); }
  680.     when (/Ghoul/i) { return Ghoul->new(); }
  681.     when (/Gnoll/i) { return Monster->new(NAME=>"Gnoll",
  682.       AC=>5,MV=>9,HD=>[2,0]); } # TODO: +2 morale
  683.     when (/Ogre/i) { return Ogre->new(); }
  684.     default { die "Couldn't create monster '$type'\n"; }
  685.     }
  686. }
  687.  
  688. sub ListMonsters {
  689.   my $self = shift;
  690.   print "The available monsters are ", join(", ", @Monsters), "\n";
  691. }
  692.  
  693. package ManFactory;
  694.  
  695. sub CreateMan {
  696.   my $self = shift;
  697.   my $params = shift;
  698.   my ($LEVEL, $AC);
  699.   $LEVEL = $$params{LEVEL};
  700.   $AC = $$params{AC};
  701.   if(!defined($LEVEL)) {
  702.     # This gives 15% chance to levels 1-2; 10% chance to levels 3-9
  703.     # $LEVEL = ((Dice::d20() - 1) % 9) + 1;
  704.     # This gives level 1 prob 1/6 & levels 2-6 with prob (7-level)/18
  705.     $LEVEL = abs(Dice::roll(2,$Dice::d6) - 7) + 1;
  706.     printf "No level for Fighting Man specified. Randomizing... $LEVEL.\n";
  707.   }
  708.   if(!defined($AC)) {
  709.     $AC = Dice::d8() + 1;
  710.     printf "No AC for Fighting Man specified. Randomizing... $AC.\n";
  711.   }
  712.   return FightingMan->new(LEVEL=>$LEVEL,AC=>$AC);
  713. }
  714.  
  715. package TurnSequence;
  716. sub new {
  717.   my $proto = shift;
  718.   my $class = ref($proto) || $proto;
  719.   my ($sidea,$sideb) = @_;
  720.   my $self = {QUEUE=>[[$sidea,$sideb],[$sideb,$sidea]]};
  721.   # TODO: SimultaneousMovementSystem should override this constructor,
  722.   # make a deep copy of $sideb called $sidebcopy, and do this...
  723.   #   my $self = {QUEUE=>[[$sidea,$sideb],[$sidebcopy,$sidea]]};
  724.   # That way, any changes to $sideb on $sidea's turn will not affect
  725.   # how $sideb performs on it's turn.
  726.   bless($self,$class);
  727.   return $self;
  728. }
  729.  
  730. sub resolve {
  731.   my $self = shift;
  732.   until($self->is_done()) {
  733.       $self->begin_turn();
  734.     # A full turn sequence might go through the following phases:
  735.     # $self->fire_missiles();
  736.     # $self->cast_spells();
  737.     # Turning on wimpy mode allows the man to flee on his move() if he's low on hp
  738.       $self->move();
  739.     # For now we're primarily interested in melee()
  740.       $self->melee()
  741.         unless $self->attacker()->fled();
  742.       $self->end_turn();
  743.   }
  744. }
  745.  
  746. sub attacker {
  747.   my $self = shift;
  748.   return $self->{SIDEA};
  749. }
  750.  
  751. sub defender {
  752.   my $self = shift;
  753.   return $self->{SIDEB};
  754. }
  755.  
  756. sub begin_turn {
  757.   my $self = shift;
  758.   my $next = shift @{$self->{QUEUE}};
  759.   if(defined($next)) {
  760.     ($self->{SIDEA}, $self->{SIDEB}) = ($next->[0],$next->[1]);
  761.   } else { die "Bad turn!\n"; }
  762. }
  763.  
  764. sub is_done {
  765.   my $self = shift;
  766.   return scalar @{$self->{QUEUE}} == 0;
  767. }
  768.  
  769. sub move {
  770.   my $self = shift;
  771.   if($self->attacker()->flees()) {
  772.     Logger::log($self->attacker()->{NAME} . " flees.\n");
  773.   }
  774. }
  775.  
  776. sub melee {
  777.   my $self = shift;
  778.  
  779.   if($self->attacker()->{INITIATIVE} != 0) {
  780.     if($self->defender()->{PARALYZED}) {
  781.       # Paralysed victims are automatically hit each round.
  782.       Logger::log($self->attacker()->{NAME} . " attacks paralyzed " . $self->defender()->{NAME} .
  783.         " on initiative ". $self->attacker()->{INITIATIVE} ." and hits automatically.\n");
  784.       $self->apply_damage();
  785.     } else {
  786.       my ($to_hit, $throw) = ($self->attacker()->to_hit($self->defender()->AC()), Dice::d20());
  787.       Logger::log($self->attacker()->{NAME} . " attacks " . $self->defender()->{NAME} .
  788.         " on initiative " . $self->attacker()->{INITIATIVE} . ", requiring at least " .
  789.         $to_hit . " to hit and throws " . $throw . "\n");
  790.       if($self->defender()->{SURPRISED}){
  791.         $throw += 4;
  792.         Logger::log($self->defender()->{NAME} . " is surprised, adjusting attack throw by +4 to " . $throw. "\n");
  793.       }
  794.       # TODO: Monster makes up to HD attacks versus men up to 1 HD
  795.       # and Monster applies HD bonus to one attack
  796.       #if($self->attacker()->isa("Monster") && $self->attacker()->HD_bonus()){
  797.       #  $throw += $self->attacker()->HD_bonus();
  798.       #  Logger::log($self->attacker()->{NAME} . " adds it's HD bonus, adjusting attack throw by " . Formatter::plus($self->attacker()->HD_bonus()) . " to " . $throw. "\n");
  799.       #}
  800.       if($throw >= $to_hit){
  801.         Logger::log($self->defender()->{NAME} . " is hit.\n");
  802.         $self->apply_damage();
  803.       }
  804.     }
  805.   }
  806. }
  807.  
  808. sub apply_damage {
  809.   my $self = shift;
  810.   my $damage =
  811.     $self->attacker()->calc_damage();
  812.   $self->defender()->on_damage($damage);
  813.   $self->attacker()->on_hit($self->defender());
  814.   if($self->defender()->is_dead()) {
  815.     $self->defender()->on_dead();
  816.   }
  817. }
  818.  
  819. sub end_turn {
  820.   my $self = shift;
  821.   die "Pure virtual method 'TurnSequence::end_turn()' called\n";
  822. }
  823.  
  824. package MoveCountermoveSystem;
  825. @MoveCountermoveSystem::ISA = ("TurnSequence");
  826.  
  827. sub end_turn {
  828.   my $self = shift;
  829.  
  830.   # Check that defender still has a turn
  831.   return if($self->is_done());
  832.  
  833.   # In the following cases, the defender doesn't get his turn
  834.   # because of some new condition: death, paralysis, fled, etc.
  835.   if($self->defender()->is_dead() ||
  836.     $self->defender()->{PARALYZED} ||
  837.     $self->attacker()->fled()) {
  838.     shift @{$self->{QUEUE}};
  839.   }
  840. }
  841.  
  842. package SimultaneousMovementSystem;
  843. @SimultaneousMovementSystem::ISA = ("TurnSequence");
  844.  
  845. sub end_turn {
  846.   my $self = shift;
  847.  
  848.   # Check that defender still has a turn
  849.   return if($self->is_done());
  850.  
  851.   # In the following cases, the defender only gets his turn because he attacks simultaneously
  852.   if($self->defender()->is_dead()) {
  853.     Logger::log($self->defender()->{NAME} .
  854.       " has been slain but attacks with simultaneous initiative.\n");
  855.   } elsif($self->defender()->{PARALYZED} && $self->defender()->{INITIATIVE}) {
  856.     # $self->defender() combatant was paralyzed this round but attacks simultaneously
  857.     # TODO: Do we need to $self->defender()->{PARALYZED}++ in this case due to simultaneity???
  858.     Logger::log($self->defender()->{NAME} .
  859.       " is paralysed for " . $self->defender()->{PARALYZED} .
  860.       " rounds but attacks simultaneously this round.\n");
  861.   }
  862. }
  863.  
  864. sub move {
  865.   my $self = shift;
  866.   # TODO: To simplify things for now, we don't allow a man to flee
  867.   # on simultaneous initiative.
  868.   # Alternatively: You could allow the attacker to disengage by
  869.   # up to 1/2 move, but give defender attack of opportunity.
  870.   # Or: You could allow defender to retreat--full move--but
  871.   # defender gets attack of opportunity at +2 to hit.
  872. }
  873.  
  874. package Combat;
  875.  
  876. sub new {
  877.   my $proto = shift;
  878.   my $class = ref($proto) || $proto;
  879.   my ($man,$monster,$number,$highdex,$wimpy) = @_;
  880.   my $self = {};
  881.  
  882.   # Choose opponents
  883.   $self->{MONSTER} = MonsterFactory->CreateMonster($monster);
  884.  
  885.   $self->{MAN} = ManFactory->CreateMan($man);
  886.   $self->{MAN}->rand(); # need to do this to set his name as a fxn of cls/lvl
  887.   $self->{MAN}->highdex($highdex);
  888.   $self->{MAN}->wimpy($wimpy);
  889.  
  890.   # These objects will tally simulation results
  891.   $self->{WIN} = Statistics->new("wins");
  892.   $self->{LOSE} = Statistics->new("loses");
  893.   $self->{DRAW} = Statistics->new("draws");
  894.   $self->{FLEES} = Statistics->new("flees");
  895.  
  896.   $self->{NUMBER} = $number;
  897.  
  898.   bless($self,$class);
  899.   return $self;
  900. }
  901.  
  902. sub check_surprise {
  903.   my $self = shift;
  904.   Logger::log("Surprise is checked...\n");
  905.   Logger::log($self->{MONSTER}->{NAME} . " attempts to surprise " . $self->{MAN}->{NAME} . "\n");
  906.   $self->{MAN}->check_surprise();
  907.   Logger::log($self->{MAN}->{NAME} . " attempts to surprise " . $self->{MONSTER}->{NAME} . "\n");
  908.   $self->{MONSTER}->check_surprise();
  909. }
  910.  
  911. sub check_distance {
  912.   my $self = shift;
  913.   # TODO: In the future, combatants must close distance to melee
  914.   # But may keep distance to fire missiles or cast spells.
  915.   # There is a difference between underworld distance and wilderness distance. See U&WA.
  916.   # E.g., underworld distance = 4" + d6" = 5"-10"
  917. }
  918.  
  919. sub check_initiative {
  920.   my $self = shift;
  921.  
  922.   # Check initiative
  923.   # Combatants who are surprised, disarmed or paralyzed don't have initiative
  924.   # Otherwise, have each combatant roll for initiative
  925.   foreach my $i ($self->{MONSTER}, $self->{MAN}) {
  926.     if($i->{SURPRISED}){
  927.       Logger::log($i->{NAME} . " is surprised and has no initiative.\n");
  928.       $i->{INITIATIVE} = 0;
  929.     } elsif($i->{SURPRISED} == 0 && $i->{DISARMED} == 1) {
  930.       Logger::log($i->{NAME} . " is disarmed and cannot attack, instead spending the round re-arming.\n");
  931.       $i->{INITIATIVE} = 0;
  932.     } elsif($i->{PARALYZED}) {
  933.       Logger::log($i->{NAME} . " is paralyzed and cannot attack.\n");
  934.       $i->{INITIATIVE} = 0;
  935.     } else {
  936.       $i->check_initiative();
  937.     }
  938.   }
  939.  
  940.   # Put combatants in order of initiative, highest to lowest
  941.   if($self->{MAN}->{INITIATIVE} > $self->{MONSTER}->{INITIATIVE}){
  942.     return MoveCountermoveSystem->new($self->{MAN}, $self->{MONSTER});
  943.   } elsif ($self->{MAN}->{INITIATIVE} < $self->{MONSTER}->{INITIATIVE}){
  944.     return MoveCountermoveSystem->new($self->{MONSTER}, $self->{MAN});
  945.   } else { # initiative is equal
  946.     return SimultaneousMovementSystem->new($self->{MONSTER}, $self->{MAN});
  947.   }
  948. }
  949.  
  950. sub round {
  951.   my $self = shift;
  952.   my $round = shift;
  953.   Logger::log("\n--- ROUND NUMBER $round ---\n");
  954.  
  955.   # Check initiative
  956.   Logger::log("Initiative is checked...\n");
  957.   my $turn_sequence =
  958.     $self->check_initiative();
  959.  
  960.   # Resolve attacks
  961.   Logger::log("Attacks are resolved...\n");
  962.   $turn_sequence->resolve();
  963.  
  964.   # End of round cleanup
  965.   $self->post_exchange_of_blows();
  966. }
  967.  
  968. sub post_exchange_of_blows {
  969.   my $self = shift;
  970.   # Basically, these are state transitions for surprised and disarmed combatants
  971.   foreach my $i ($self->{MONSTER}, $self->{MAN}) {
  972.     if($i->{SURPRISED} == 0 && $i->{DISARMED} == 1) {
  973.       $i->{DISARMED} = 0;
  974.     }
  975.     if($i->{SURPRISED}){
  976.       $i->{SURPRISED}--;
  977.     }
  978.     if($i->{PARALYZED}){
  979.       $i->{PARALYZED}--;
  980.     }
  981.   }
  982. }
  983.  
  984. sub initialize {
  985.   my $self = shift;
  986.  
  987.   # Randomize opponents OR reset comatants to max hit points
  988.   $self->{MONSTER}->rand();
  989.   $self->{MAN}->rand();
  990.   Logger::log("\n" . $self->{MONSTER}->{NAME} . " encounters " . $self->{MAN}->{NAME} . "\n");
  991.   $self->{MONSTER}->as_string();
  992.   $self->{MAN}->as_string();
  993. }
  994.  
  995. sub record {
  996.   my $self = shift;
  997.   my $round = shift;
  998.  
  999.   Logger::log("\n=== RESULT ===\n");
  1000.   if($self->{MONSTER}->is_dead() && $self->{MAN}->is_dead()){
  1001.     Logger::log($self->{MAN}->{NAME} . " and " . $self->{MONSTER}->{NAME} .
  1002.       " have slain each other in " . $round . " round" . Formatter::s($round) .
  1003.       ".\n");
  1004.     $self->{DRAW}->record($round);
  1005.   } elsif ($self->{MONSTER}->is_dead()) {
  1006.     Logger::log($self->{MAN}->{NAME} . " has slain " . $self->{MONSTER}->{NAME} .
  1007.       " in " . $round . " round" . Formatter::s($round) . ".\n");
  1008.     $self->{WIN}->record($round);
  1009.   } elsif($self->{MAN}->is_dead()) {
  1010.     Logger::log($self->{MONSTER}->{NAME} . " has slain " . $self->{MAN}->{NAME} .
  1011.       " in " . $round . " round" . Formatter::s($round) . ".\n");
  1012.     $self->{LOSE}->record($round);
  1013.   } elsif($self->{MAN}->fled()) {
  1014.     Logger::log($self->{MAN}->{NAME} . " fled after " .
  1015.       $round . " round" . Formatter::s($round) . ".\n");
  1016.     $self->{FLEES}->record($round);
  1017.   }
  1018. }
  1019.  
  1020. sub encounter {
  1021.   my $self = shift;
  1022.   my $encounter = shift;
  1023.   my $round = 0; # reset clock
  1024.  
  1025.   Logger::log("\n~~~ ENCOUNTER $encounter ~~~\n");
  1026.   $self->initialize();
  1027.  
  1028.   $self->check_surprise();
  1029.   $self->check_distance();
  1030.  
  1031.   # Loop until someone dies or routs
  1032.   until($self->{MONSTER}->is_dead() ||
  1033.     $self->{MAN}->is_dead() || $self->{MAN}->fled()){
  1034.     $self->round(++$round);
  1035.   }
  1036.  
  1037.   $self->record($round);
  1038. }
  1039.  
  1040. sub simulate {
  1041.   my $self = shift;
  1042.   print "\n" . $self->{MONSTER}->{NAME} . " encounters " . $self->{MAN}->{NAME} . "\n";
  1043.   for my $n (1..$self->{NUMBER}){
  1044.     $self->encounter($n);
  1045.   }
  1046. }
  1047.  
  1048. # Display the final results in plain text
  1049. # You could have as_html(), as_csv(), or save() as well
  1050. sub as_string_old {
  1051.   my $self = shift;
  1052.   print "\n*** FINAL TALLY ***\n";
  1053.   my @results = ($self->{WIN},$self->{LOSE},$self->{DRAW});
  1054.   if($self->{MAN}->{WIMPY}) { push(@results, $self->{FLEES}); }
  1055.   foreach my $result (@results) {
  1056.     print $self->{MAN}->{NAME} .  " " . $result->{NAME} . " " . $result->{TOTAL} .
  1057.       " times: " .
  1058.       " min " . $result->min() .
  1059.       ", med " . $result->median() .
  1060.       ", max " . $result->max() .
  1061.       ", mean " . sprintf("%5.2f",$result->moment()) .
  1062.       #", mu2 " . sprintf("%5.2f",$result->moment(2)) .
  1063.       ", sd " . sprintf("%5.2f",$result->sd()) .
  1064.       " rounds.\n" ;
  1065.   }
  1066. }
  1067.  
  1068. sub as_string {
  1069.   my $self = shift;
  1070.   print "\n*** FINAL TALLY ***\n";
  1071.   my @results = ($self->{WIN},$self->{LOSE},$self->{DRAW});
  1072.   if($self->{MAN}->{WIMPY}) { push(@results, $self->{FLEES}); }
  1073.  
  1074.   my $max = 0;
  1075.  
  1076.   foreach my $result (@results) {
  1077.     print $self->{MAN}->{NAME} .  " " . $result->{NAME} . " " . $result->{TOTAL} .
  1078.       " times. Round:" .
  1079.       " min " . $result->min() .
  1080.       ", med " . $result->median() .
  1081.       ", max " . $result->max() .
  1082.       ", mean " . sprintf("%5.2f",$result->moment()) .
  1083.       #", mu2 " . sprintf("%5.2f",$result->moment(2)) .
  1084.       ", sd " . sprintf("%5.2f",$result->sd()) .
  1085.       " \n" ;
  1086.  
  1087.     $max = ($result->max() > $max) ? $result->max() : $max;
  1088.   }
  1089.  
  1090. }
  1091.  
  1092. ##########     M A I N    R O U T I N E     ##########
  1093.  
  1094. if($list) { MonsterFactory->ListMonsters(); exit }
  1095.  
  1096. my $combat = Combat->new($man,$monster,$number,$highdex,$wimpy);
  1097. $combat->simulate();
  1098. $combat->as_string();
  1099.  
  1100. __END__
  1101.  
  1102. =head1 NAME
  1103.  
  1104. combat.pl - ODE<amp>D combat simulator
  1105.  
  1106. =head1 SYNOPSIS
  1107.  
  1108.   combat.pl [options]
  1109.  
  1110. Examples:
  1111.  
  1112.   combat.pl --man LEVEL=2 --man AC=4 --monster=Ogre
  1113.  
  1114.   combat.pl --list
  1115.  
  1116.   combat.pl --verbose
  1117.  
  1118.   combat.pl --number 10000
  1119.  
  1120. =head1 OPTIONS
  1121.  
  1122. =over 8
  1123.  
  1124. =item B<--man>
  1125.  
  1126. Specify options for the Fighting Man. Currently supported options are:
  1127.  
  1128. =over 8
  1129.  
  1130. =item B<LEVEL=integer>
  1131.  
  1132. Level of the Fighting Man. Note that LEVEL must be all uppercase. If no level is specified, one is generated at random, with lower levels weighted more heavily.
  1133.  
  1134. =item B<AC=integer>
  1135.  
  1136. Armor Class of the Fighting Man. Note that AC must be all uppercase. If no AC is specified, one is generated at random with equal probability from 2 to 9.
  1137.  
  1138. =back
  1139.  
  1140. =item B<--monster>
  1141.  
  1142. Specify the monster to use. If none is specified, choose one at random with equiprobability.
  1143.  
  1144. =item B<--list>
  1145.  
  1146. Print the list of available monsters and exit.
  1147.  
  1148. =item B<--number>
  1149.  
  1150. Specify the number of simulations to run. The default is 100.
  1151.  
  1152. =item B<--verbose>
  1153.  
  1154. Print a blow-by-blow description of each simulated combat. Otherwise, only the final tallies are printed.
  1155.  
  1156. =item B<--highdex>
  1157.  
  1158. The Fighting Man's dexterity is generated at random using 3d6. If it's equal to or greater than this integer value, he get's a +1 bonus to initiative as per the FAQ in the Strategic Review No 2. By default, no bonus for high dexterity is granted.
  1159.  
  1160. =item B<--surprise>
  1161.  
  1162. By default, we check surprise by rolling 1d6 for each combatant, and on a 1 or 2, the combatant is surprised for one round. By turning on this option, the combatant will be surprised for one round on a 1 and two rounds on a 2, as per Gary Gygax's 3 LBB house rules.
  1163.  
  1164. =item B<--wimpy>
  1165.  
  1166. Emulate the wimpy mode from DikuMUD or LPMud. You must specify either a positive integer or a fraction. If you specify an integer value, the Fighting Man will attempt to flee combat when his hit points fall to or below this value. He may not be able to flee if he's helpless (e.g., paralyzed, stunned, surprised) or if he dies before getting his turn to move. If you specify a fraction, the man attempts to flee as soon as his current HPs fall to or below the fraction of his maximum HPs.
  1167.  
  1168. By "flee" we mean "disengage" (as opposed to "retreat") as per Philotomy's "Rules for Melee & Movement." See E<lt>http://web.archive.org/web/20101126040841/http://philotomy.com/combat_sequence.htmlE<gt>.
  1169.  
  1170. =item B<--version>
  1171.  
  1172. Print the version of the script and exit.
  1173.  
  1174. =item B<--help>
  1175.  
  1176. Print a brief help message and exit.
  1177.  
  1178. =back
  1179.  
  1180. =head1 DESCRIPTION
  1181.  
  1182. B<combat.pl> will simulate combat between the specificied combatants using the ODE<amp>D combat rules.
  1183.  
  1184. =head1 TO DO
  1185.  
  1186. Plenty. There are a few TODOs scattered throughout the source code. Here are some more:
  1187.  
  1188. =over 8
  1189.  
  1190. =item * B<--system> command-line-switch. Specify rule system, e.g., Whitebox, "Supplement I", EPT, etc.
  1191.  
  1192. =item * B<--sample integer>. Print out a random sample of the encounters. E.g., run a million encounters but print out 10 of them, selected randomly.
  1193.  
  1194. =item * For each design decision made in this program, cite rule book and page number and include quote.
  1195.  
  1196. =item * I took the 25% chance to drop one's weapon upon surprise quite literally: Unlike Ways, I don't require the suprised combatant to be hit by his attacker before I roll for the chance to drop (at least that's what the output of Ways program seems to do--I could be wrong). This could be implemented as an option. It doesn't seem to effect the outcomes much.
  1197.  
  1198. =item * Attacker with Hit Dice + Bonus versus Normal Men gets Dice attacks with Bonus going to one attack (MM/5). Problem deciding who is a "normal man".
  1199.  
  1200. =item * Fighting Conditions. E.g., goblin gets -1 to attacks and morale when fighting in Full Daylight MM/7, as do Orcs MM/8.
  1201.  
  1202. =item * Support for unarmed opponents (like Ghouls).
  1203.  
  1204. =item * Check distance at beginning of encounter. Distance depends on Wilderness or Underground. Combatants must spend initial rounds closing in to melee range. In this case, MV becomes an important stat. And so do ENCUMBRANCE and AC insofar as they may effect MV. Conditions (PARALYZED, STUNNED, ...) will temporarily effect MV as well. May also need to know how big of an area/arena the combat takes place in.
  1205.  
  1206. =item * Missile fire/ranged attacks.
  1207.  
  1208. =item * Spells.
  1209.  
  1210. =item * Many-to-many combat rather than just one-to-one.
  1211.  
  1212. =item * Morale checks for NPCs once we get many-to-many combats.
  1213.  
  1214. =item * Optional Low DEX penalty. N.B. Already implemented: high DEX gives +1 bonus to initiative (Srv #2, FAQ/?).
  1215.  
  1216. =item * Regeneration. Trolls and vampires regenerate 3 hp/round starting 3rd round after being hit.
  1217.  
  1218. =item * Immunities. Vampires and Spectres immune to attacks except silver/magic.
  1219.  
  1220. =item * Weaknesses. Vampires only killed by sunlight/immersed in running water/wooden stake through heart.
  1221.  
  1222. =item * Not dead at 0 hp exception: Vampires assume gaseous form at 0 hp.
  1223.  
  1224. =item * Type or amount of treasure... After winning a combat we should calculate the payoff, MONEY and XP. We should calculate the expected payoff of each combat.
  1225.  
  1226. =item * Option to roll combatants HP etc each encounter or use static values.
  1227.  
  1228. =item * Optional EPT critical hit system, EPT/32 Sec. 731.  Nat 20 does "double damage," rolls gain where 19-20 deal "instant death." No "fumble" rule until AD&D(?)
  1229.  
  1230. =item * Implement different output devices:
  1231.  
  1232.   # print plain text to standard output
  1233.   $combat->as_string() or $combat->as_string(STDOUT)
  1234.  
  1235.   # save results to database using given DBI handle
  1236.   $combat->as_string($mysql)
  1237.  
  1238.   # output HTML to TCP/HTTP socket
  1239.   $combat->as_string($cgi)
  1240.  
  1241. Probably need multiple dispatch for this. Easier than using "Visitor" design pattern.
  1242.  
  1243. =item * Only Fighting Man is implemented. No Magic User or Cleric yet.
  1244.  
  1245. =item * Only Save Versus Paralysis is implemented (due to Ghoul). Implement others.
  1246.  
  1247. =item * Human race assumed. Implement others. E.g., elves not effected by Ghoul paralysis.
  1248.  
  1249. =back
  1250.  
  1251. =head1 BUGS
  1252.  
  1253. Lots. Maybe. This is an early release.
  1254.  
  1255. =head1 NOTES
  1256.  
  1257. =over 8
  1258.  
  1259. =item * This program could just as well have been written more naturally in a "Discrete Event Simulation" language, like Simula (GNU Cim), which supports a clock, random-number-generation, an events list (in a priority queue), an ending-condition, and tracking statistics, as well as object-orientation (classes, objects, single-inheritance, virtual-methods). It also could have been written in Perl6, which has better support for object-orientation as well as multiple-dispatch and other neat features. Common Lisp or Clojure were also considered. I am considering NetLogo for many-to-many combats, since it excels at multi-agent, complex systems modeling in a 2D grid. But ultimately, I used Perl5 because I could bash out the code the fastest.
  1260.  
  1261. =item * There is a UML class diagram to show the relations between the classes here. The source is in Visio. A JPEG version is available at E<lt>http://www.flickr.com/photos/79364035@N04/8402807365/E<gt>
  1262.  
  1263. =item * "Template" design pattern for main Combat class. Subclasses could then implement optional rules or variant rules or house rules.
  1264.  
  1265. =item * "Strategy" design pattern to model turn sequence. Similar to "Template" but finer granularity. Did this because ODE<amp>D allows for both move-countermove and simultaneous turn sequences.
  1266.  
  1267. =item * "Factory" design pattern to build men and monsters intended, but not quite there yet. Need to refactor each factory class into one abstract factory class.
  1268.  
  1269. =back
  1270.  
  1271. =head1 AUTHOR
  1272.  
  1273. Elisha "Aher" Abuyah
  1274.  
  1275. =head1 HISTORY
  1276.  
  1277. This program began as an attempt to replicate the results presented by the author "waysoftheearth" in the discussion thread "A Veteran's Odds" on the Original DE<amp>D Discussion board at E<lt>http://odd74.proboards.com/index.cgi?board=study&action=display&thread=7562E<gt>.
  1278.  
  1279. =head1 COPYRIGHT
  1280.  
  1281. Copyright 2012 Elisha "Aher" Abuyah
  1282.  
  1283. =head1 LICENSE
  1284.  
  1285. This program is free software; you may redistribute it and/or modify it under the same terms as Perl itself.
  1286.  
  1287. =head1 DISCLAIMERS
  1288.  
  1289. This program is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose.
  1290.  
  1291. =head1 AVAILABILITY
  1292.  
  1293. The current version is available at Aher's pastebin at E<lt>http://pastebin.com/u/aherE<gt>. A UML class diagram is available at E<lt>http://www.flickr.com/photos/79364035@N04/8402807365/E<gt>.
  1294.  
  1295. =head1 DATE
  1296.  
  1297. 02-Sep-2012
  1298.  
  1299. =cut
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement