PDA

View Full Version : Commoner vs. Cat - A Mathematical Analysis



TripleD
2013-12-07, 10:14 AM
It's one of the fundamental criticisms of 3.5, a sign of the inherent imbalance of the system: in RAW, a tiny housecat can easily kill off your average, medium sized commoner.

But what is a commoner to do in a world where felines can lay them low at a whim? What would be the mathematically logical way for a peasant to deal with Mr.Whiskers refusing to leave their hayloft? I was curious, so I wrote a python program to simulate combat between the two.

Combatants
Commoner with 10 in all stats and no combat relevant feats.
Cat as printed in the Monster Manual.

Weapon Choices
As per RAW, a peasant can be proficient with a single common weapon. There are four different ways to wield the weapon:
{table=head]Weapon Style|Fighting Style
Single Weapon| Close
Double (TWF)| Ranged
[/table]
The commoner will try "Double" with any weapon that is not two handed. However, although there are rules for throwing any weapon, they will only try to throw/fire a weapon with a printed range.

A.I.
If the commoner's weapon has a range, and the commoner is using the "ranged" fighting style, they and the cat will be placed at the maximum distance at which the weapon can be fired with no penalty. Otherwise, both will begin 30ft. apart.
Each character rolls for initiative. The loser is flatfooted until their first action.
If the commoner is using the "ranged" fighting style, they will try to get as many ranged attack before the cat closes the distance. Afterwards they will switch to melee attacks if possible.
For "close" fighting style, the cat and commoner both try to close the distance using charges, running, and five-foot steps. This is followed by full attacks.

Analysis
Each "Battle" consists of one thousand matches of the cat against the commoner with each weapon.
Each "Trial" consists of one "Battle". The following test consists of 10 trials, for a total of 10 000 matches between the cat and the commoner with each weapon.

{table=head]Weapon|Odds of Victory
Light Crossbow(single, ranged)|63%
Light Crossbow(double, ranged)|62%
Spear(single, ranged)|61%
Javelin(single, ranged)|60%
Javelin(double, ranged)|60%
Shortspear(single, ranged)|59%
MorningStar(single)|59%
Heavy Mace(single)|58%
Spear(single)|58%
Longspear(single)|58%
Light Mace(single)|57%
Shortspear(single)|56%
Sickle(single)|56%
Dart(double, ranged)|56%
Dart(single, ranged)|56%
Sling(double, ranged)|56%
Sling(single, ranged)|55%
Quarterstaff(single)|55%
Punching Dagger(single)|53%
Spiked Gauntlet(single)|52%
Gauntlet(single)|49%
Unarmed(single)|49%
Club(single, ranged)|45%
Club(single)|44%
Dagger(single, ranged)|41%
Dagger(single)|41%
Heavy Crossbow(single, ranged)|37%
Light Mace(double)|33%
Heavy Crossbow(double, ranged)|33%
Sickle(double)|33%
Spiked Gauntlet(double)|30%
Punching Dagger(double)|30%
Heavy Mace(double)|30%
MorningStar(double)|29%
Shortspear(double, ranged)|28%
Gauntlet(double)|28%
Shortspear(double)|28%
Quarterstaff(double)|27%
Dagger(double, ranged)|27%
Unarmed(double)|27%
Dagger(double)|23%
Club(double, ranged)|20%
Club(double)|12%
[/table]

From the look of things:
- Our commoner's best bet is to stock up on light crossbows and learn how to throw a spear.
- Dual wielding clubs is tantamount to coating your arteries with tuna
- In fact, dual wielding just about anything is borderline suicide.

The biggest surprise was that yes, when the cat dominates, it dominates hard. But with the right weapon choice the commoner can put up a pretty decent fight.

I'll post the code in the next section if anyone is interested.

AstralFire
2013-12-07, 10:17 AM
Topic of the day right here, folks. :)

TripleD
2013-12-07, 10:22 AM
Program #1 - Single Combat
This is a simplified version of the program that simulates a single fight per weapon, and prints out each step to help you understand how it works.



import random, copy

class WeaponType:
"""The types of Simple Weapons"""
unarmed=0
light=1
oneHanded=2
twoHanded=3
ranged=4

class RearmTimes:
"""How long a weapon takes to re-arm (or in the case of ranged, reload)"""
move=0
moveAoO=1
fullAoO=2

WT = WeaponType()
RT = RearmTimes()

class Weapon:
"""Simple Method of storing D&D Weapons"""
damage = 1
critical = 2
critRange = 20
type = WT.unarmed
range = 0
rearm = RT.move
double = False
reach = False

#All weapons are assumed to be ready to fire/swing/stab
usable = True

offhandPenalty = WT.oneHanded

def __init__(self, damage, critical, critRange=20, type=WT.unarmed,
range=0, rearm=RT.move, double=False, reach=False, offhandPenalty = WT.oneHanded):
self.damage = damage
self.critical = critical
self.critRange = critRange
self.type = type
self.range = range
self.rearm = rearm
self.double = double
self.reach = reach
self.offhandPenalty = offhandPenalty

class Commoner:
"""An average, everyday peasant"""
strengthBonus = 0
dexterityBonus = 0
attack = 0
health = 4
AC = 10

initiative = 0

charged = False

primary = Weapon(3,2)
offhand = None

def __init__(self, primary, offhand = None):
self.primary = primary
self.offhand = offhand

class Cat:
"""Just your average housecat"""
strengthBonus = -4
dexterityBonus = 2
attack = 4
health = 2
AC = 14

initiative = 0

charged = False

claw = Weapon(1, 2, 20, WT.light)
bite = Weapon(1, 2, 20, WT.light)

weapons = { "Unarmed": Weapon(3, 2, 20, WT.unarmed),
"Gauntlet": Weapon(3, 2, 20, WT.unarmed),
"Dagger": Weapon(4, 3, 19, WT.light, 10),
"Punching Dagger": Weapon(4, 3, 20, WT.light),
"Spiked Gauntlet": Weapon(4, 2, 20, WT.light),
"Light Mace": Weapon(6, 2, 20, WT.light),
"Sickle": Weapon(6, 2, 20, WT.light),
"Club": Weapon(6, 2, 20, WT.oneHanded, 10),
"Heavy Mace": Weapon(8, 2, 20, WT.oneHanded),
"MorningStar": Weapon(8, 2, 20, WT.oneHanded),
"Shortspear": Weapon(6, 2, 20, WT.oneHanded, 20),
"Longspear": Weapon(8, 3, 20, WT.twoHanded, reach=True),
"Quarterstaff": Weapon(6, 2, 20, WT.twoHanded, double=True),
"Spear": Weapon(8, 3, 20, WT.twoHanded, 20),
"Heavy Crossbow": Weapon(10, 2, 19, WT.ranged, 120, RT.fullAoO),
"Light Crossbow": Weapon(8, 2, 19, WT.ranged, 80, RT.moveAoO, offhandPenalty = WT.light),
"Dart": Weapon(4, 2, 20, WT.ranged, 20, RT.move, offhandPenalty = WT.light),
"Javelin": Weapon(6, 2, 20, WT.ranged, 30),
"Sling": Weapon(4, 2, 20, WT.ranged, 50, RT.moveAoO)
}

class Distance:
""" How far the cat and human are from each other """
none = 0
five = 1
charge = 2
long = 3

def rollDamage(attacker, defender, weapon, bonus=0):
"""Rolls a generic attack roll"""
roll = random.randint(1,20)
damage = random.randint(1,weapon.damage)

print("Rolls: " + str(roll))

if roll == 1:
print("Miss!")
return()

if roll == 20 or roll >= weapon.critRange:
roll = random.randint(1,20)

if (attacker.attack + roll + bonus) >= defender.AC:
defender.health = defender.health - (damage * weapon.critical)
print("Critical! " + str(damage * weapon.critical) + " damage")
return()
defender.health = defender.health - damage
print("Hit! " + str(damage) + " damage")
return()

if(attacker.attack + roll + bonus) >= defender.AC:
defender.health = defender.health - damage
print("Hit! " + str(damage) + " damage")
else:
print("Miss!")

def charge(attacker, weapon, defender):
"""One character charges another"""
attacker.AC = attacker.AC - 2
attacker.charged = True

rollDamage(attacker, defender, weapon, 2)

def catFullAttack(cat, commoner):
"""Cat does full attack against commoner"""
rollDamage(cat, commoner, cat.claw)
rollDamage(cat, commoner, cat.claw)
rollDamage(cat, commoner, cat.bite, -5)

def fullAttack(commoner, weaponStyle, cat):
"""Commoner makes a full attack against the cat"""

if not commoner.primary.usable:
print("Commoner draws primary weapon")
commoner.primary.usable = True
rollDamage(commoner, cat, commoner.primary)
return()

if weaponStyle == "single":
print("Commoner Full Attacks!")
rollDamage(commoner, cat, commoner.primary)
return()

if not commoner.offhand.usable:
print("Commoner draws offhand weapon")
commoner.offhand.usable = True

print("Commoner Full Attacks!")

if commoner.offhand.type == WT.light or commoner.offhand.type == WT.unarmed:
rollDamage(commoner, cat, commoner.primary, -6)
rollDamage(commoner, cat, commoner.offhand, -8)
else:
rollDamage(commoner, cat, commoner.primary, -8)
rollDamage(commoner, cat, commoner.offhand, -10)

def rangedAttack(commoner, weaponStyle, cat):
"""Commoner makes a ranged attack against the cat"""
#print("Offhand Usable (beg Rang): " + str(commoner.offhand.usable))

if not commoner.primary.usable:
#Reloading takes Full Round Action
if commoner.primary.rearm == RT.fullAoO:
print("Commoner reloads.")
commoner.primary.usable = True
return()
elif commoner.primary.rearm == RT.moveAoO:
print("Commoner reloads.")
commoner.primary.usable = True
elif commoner.primary.rearm == RT.move:
print("Commoner draws new weapon")
commoner.primary.usable = True

print("Commoner fired weapon") if commoner.primary.type == WT.ranged else print("Commoner threw weapon")

if weaponStyle == "double":
if commoner.offhand.usable:
print("Commoner Full Attacks!")
if commoner.offhand.offhandPenalty == WT.light:
rollDamage(commoner, cat, commoner.primary, -6)
rollDamage(commoner, cat, commoner.offhand, -8)
else:
rollDamage(commoner, cat, commoner.primary, -8)
rollDamage(commoner, cat, commoner.offhand, -10)
commoner.primary.usable = False
commoner.offhand.usable = False
return()

rollDamage(commoner, cat, commoner.primary)
commoner.primary.usable = False


def catAttackCommoner(cat, commoner, range):
"""Decides which attack to use against the commoner"""

#First we check if we need to cancel a charge penalty
if cat.charged:
cat.charged = False
cat.AC = cat.AC + 2

if range == Distance.charge:
print("Cat Charges!")
charge(cat, cat.claw, commoner)
return(Distance.none)

if range == Distance.long:
print("Cat runs to commoner")
return(Distance.none)

if range == Distance.five:
print("Cat Full Attacks!")
catFullAttack(cat, commoner)
return(Distance.none)

if range == Distance.none:
print("Cat Full Attacks!")
catFullAttack(cat, commoner)
return(Distance.none)

def commonerAttackCat(commoner, cat, range, weaponStyle, fightingStyle):
"""Decides which attack to use against the cat"""

#First we check if we need to cancel a charge penalty
if commoner.charged:
commoner.charged = False
commoner.AC = commoner.AC + 2

if range == Distance.charge:
if fightingStyle == "close":

print("Commoner Charges!")

charge(commoner, commoner.primary, cat)

if commoner.primary.reach:
return(Distance.five)
else:
return(Distance.none)
else:
rangedAttack(commoner, weaponStyle, cat)
return(Distance.charge)

if range == Distance.five:
if fightingStyle == "close":
fullAttack(commoner, weaponStyle, cat)
else:
rangedAttack(commoner, weaponStyle, cat)
return(Distance.five)

if range == Distance.long:
rangedAttack(commoner, weaponStyle, cat)
return(Distance.long)

if range == Distance.none:

if commoner.primary.reach:
print("Commoner Full Attacks!")
fullAttack(commoner, weaponStyle, cat)
return(Distance.five)

if commoner.primary.type == WT.ranged:
rangedAttack(commoner, weaponStyle, cat)
return(Distance.none)

fullAttack(commoner, weaponStyle, cat)
return(Distance.none)

def printRange(range):
"""Prints the Current Range"""
if range == Distance.long:
print("Range: Long")
elif range == Distance.charge:
print("Range: Charge")
elif range == Distance.five:
print("Range: Five")
elif range == Distance.none:
print("Range: None")

def battle(weapon, weaponStyle="single", fightingStyle="close"):
"""Simulates a battle between a cat and human commoner"""
cat = Cat()
commoner = Commoner(weapon, copy.deepcopy(weapon) if weaponStyle == "double" else None)

# Establish how far apart they begin
range = Distance.charge

if weapon.range > 60:
range = Distance.long
elif weapon.range > 30:
range = Distance.charge
elif range == 10:
range = Distance.five

#Role for initiative
cat.initiative = cat.dexterityBonus + random.randint(1,20)
commoner.initiative = commoner.dexterityBonus + random.randint(1,20)

#First round
printRange(range)
if cat.initiative >= commoner.initiative:
print("Cat Wins Initiative")
commoner.AC = commoner.AC - commoner.dexterityBonus

range = catAttackCommoner(cat, commoner, range)

commoner.AC = commoner.AC + commoner.dexterityBonus
else:
print("Commoner Wins Initiative")
cat.AC = cat.AC - cat.dexterityBonus

range = commonerAttackCat(commoner, cat, range, weaponStyle, fightingStyle)

cat.AC = cat.AC + cat.dexterityBonus

while cat.health > 0 and commoner.health > 0:
printRange(range)

if cat.initiative < commoner.initiative:
range = catAttackCommoner(cat, commoner, range)
if commoner.health > 0:
range = commonerAttackCat(commoner, cat, range, weaponStyle, fightingStyle)
else:
range = commonerAttackCat(commoner, cat, range, weaponStyle, fightingStyle)
if cat.health > 0:
range = catAttackCommoner(cat, commoner, range)

return(True if commoner.health > 0 else False)

for weapon in sorted(weapons.keys()):
#First we check if it is possible to fight with a weapon in this way

if not weapons[weapon].type == WT.ranged:
print(weapon + "(single) vs. Cat")
if battle(weapons[weapon]):
print("Commoner Wins\n")
else:
print("Cat Wins\n")
if not weapons[weapon].type == WT.twoHanded or weapons[weapon].double:
print(weapon + "(double) vs. Cat")
if battle(weapons[weapon], "double"):
print("Commoner Wins\n")
else:
print("Cat Wins\n")

if weapons[weapon].range > 0:
print(weapon + "(single, ranged) vs. Cat")
if battle(weapons[weapon], "single", "ranged"):
print("Commoner Wins\n")
else:
print("Cat Wins\n")
if not weapons[weapon].type == WT.twoHanded:
print(weapon + "(double, ranged) vs. Cat")
if battle(weapons[weapon], "double", "ranged"):
print("Commoner Wins\n")
else:
print("Cat Wins\n")

print("---------------------------------\n")


Program #2 - Full Simulation

This is the big one. At the beginning of the script you can change the "Battles" variable to change how many battles per trial, and the "Trials" variable for how many trials should be run.

You can also change the "Format". "Console" will print it for the command line, while "GITP" formats it for copy-pasting as a table to these forums.



import random, copy, threading, queue

#Battles per Trial
Battles = 1000

#How many trials to run
Trials = 10

#Threads to run. Generally, "optimum threads" = "number of processors"
Threads = 2

#Two options for output: "Console" or "GITP"
Format="Console"

class WeaponType:
"""The types of Simple Weapons"""
unarmed=0
light=1
oneHanded=2
twoHanded=3
ranged=4

class RearmTimes:
"""How long a weapon takes to re-arm (or in the case of ranged, reload)"""
move=0
moveAoO=1
fullAoO=2

WT = WeaponType()
RT = RearmTimes()

class Weapon:
"""Simple Method of storing D&D Weapons"""
damage = 1
critical = 2
critRange = 20
type = WT.unarmed
range = 0
rearm = RT.move
double = False
reach = False

#All weapons are assumed to be ready to fire/swing/stab
usable = True

offhandPenalty = WT.oneHanded

def __init__(self, damage, critical, critRange=20, type=WT.unarmed,
range=0, rearm=RT.move, double=False, reach=False, offhandPenalty = WT.oneHanded):
self.damage = damage
self.critical = critical
self.critRange = critRange
self.type = type
self.range = range
self.rearm = rearm
self.double = double
self.reach = reach
self.offhandPenalty = offhandPenalty

class Commoner:
"""An average, everyday peasant"""
strengthBonus = 0
dexterityBonus = 0
attack = 0
health = 4
AC = 10

initiative = 0

charged = False

primary = Weapon(3,2)
offhand = None

def __init__(self, primary, offhand = None):
self.primary = primary
self.offhand = offhand

class Cat:
"""Just your average housecat"""
strengthBonus = -4
dexterityBonus = 2
attack = 4
health = 2
AC = 14

initiative = 0

charged = False

claw = Weapon(1, 2, 20, WT.light)
bite = Weapon(1, 2, 20, WT.light)

weapons = { "Unarmed": Weapon(3, 2, 20, WT.unarmed),
"Gauntlet": Weapon(3, 2, 20, WT.unarmed),
"Dagger": Weapon(4, 3, 19, WT.light, 10),
"Punching Dagger": Weapon(4, 3, 20, WT.light),
"Spiked Gauntlet": Weapon(4, 2, 20, WT.light),
"Light Mace": Weapon(6, 2, 20, WT.light),
"Sickle": Weapon(6, 2, 20, WT.light),
"Club": Weapon(6, 2, 20, WT.oneHanded, 10),
"Heavy Mace": Weapon(8, 2, 20, WT.oneHanded),
"MorningStar": Weapon(8, 2, 20, WT.oneHanded),
"Shortspear": Weapon(6, 2, 20, WT.oneHanded, 20),
"Longspear": Weapon(8, 3, 20, WT.twoHanded, reach=True),
"Quarterstaff": Weapon(6, 2, 20, WT.twoHanded, double=True),
"Spear": Weapon(8, 3, 20, WT.twoHanded, 20),
"Heavy Crossbow": Weapon(10, 2, 19, WT.ranged, 120, RT.fullAoO),
"Light Crossbow": Weapon(8, 2, 19, WT.ranged, 80, RT.moveAoO, offhandPenalty = WT.light),
"Dart": Weapon(4, 2, 20, WT.ranged, 20, RT.move, offhandPenalty = WT.light),
"Javelin": Weapon(6, 2, 20, WT.ranged, 30),
"Sling": Weapon(4, 2, 20, WT.ranged, 50, RT.moveAoO)
}

class Distance:
""" How far the cat and human are from each other """
none = 0
five = 1
charge = 2
long = 3

def rollDamage(attacker, defender, weapon, bonus=0):
"""Rolls a generic attack roll"""
roll = random.randint(1,20)
damage = random.randint(1,weapon.damage)

if roll == 1:
return()

if roll == 20 or roll >= weapon.critRange:
roll = random.randint(1,20)

if (attacker.attack + roll + bonus) >= defender.AC:
defender.health = defender.health - (damage * weapon.critical)
return()
defender.health = defender.health - damage
return()

if(attacker.attack + roll + bonus) >= defender.AC:
defender.health = defender.health - damage

def charge(attacker, weapon, defender):
"""One character charges another"""
attacker.AC = attacker.AC - 2
attacker.charged = True

rollDamage(attacker, defender, weapon, 2)

def catFullAttack(cat, commoner):
"""Cat does full attack against commoner"""
rollDamage(cat, commoner, cat.claw)
rollDamage(cat, commoner, cat.claw)
rollDamage(cat, commoner, cat.bite, -5)

def fullAttack(commoner, weaponStyle, cat):
"""Commoner makes a full attack against the cat"""

if not commoner.primary.usable:
commoner.primary.usable = True
rollDamage(commoner, cat, commoner.primary)
return()

if weaponStyle == "single":
rollDamage(commoner, cat, commoner.primary)
return()

if not commoner.offhand.usable:
commoner.offhand.usable = True

if commoner.offhand.type == WT.light or commoner.offhand.type == WT.unarmed:
rollDamage(commoner, cat, commoner.primary, -6)
rollDamage(commoner, cat, commoner.offhand, -8)
else:
rollDamage(commoner, cat, commoner.primary, -8)
rollDamage(commoner, cat, commoner.offhand, -10)

def rangedAttack(commoner, weaponStyle, cat):
"""Commoner makes a ranged attack against the cat"""

if not commoner.primary.usable:
#Reloading takes Full Round Action
if commoner.primary.rearm == RT.fullAoO:
commoner.primary.usable = True
return()
elif commoner.primary.rearm == RT.moveAoO:
commoner.primary.usable = True
elif commoner.primary.rearm == RT.move:
commoner.primary.usable = True

if weaponStyle == "double":
if commoner.offhand.usable:
if commoner.offhand.offhandPenalty == WT.light:
rollDamage(commoner, cat, commoner.primary, -6)
rollDamage(commoner, cat, commoner.offhand, -8)
else:
rollDamage(commoner, cat, commoner.primary, -8)
rollDamage(commoner, cat, commoner.offhand, -10)
commoner.primary.usable = False
commoner.offhand.usable = False
return()

rollDamage(commoner, cat, commoner.primary)
commoner.primary.usable = False


def catAttackCommoner(cat, commoner, range):
"""Decides which attack to use against the commoner"""

#First we check if we need to cancel a charge penalty
if cat.charged:
cat.charged = False
cat.AC = cat.AC + 2

if range == Distance.charge:
charge(cat, cat.claw, commoner)
return(Distance.none)

if range == Distance.long:
return(Distance.none)

if range == Distance.five:
catFullAttack(cat, commoner)
return(Distance.none)

if range == Distance.none:
catFullAttack(cat, commoner)
return(Distance.none)

def commonerAttackCat(commoner, cat, range, weaponStyle, fightingStyle):
"""Decides which attack to use against the cat"""

#First we check if we need to cancel a charge penalty
if commoner.charged:
commoner.charged = False
commoner.AC = commoner.AC + 2

if range == Distance.charge:
if fightingStyle == "close":

charge(commoner, commoner.primary, cat)

if commoner.primary.reach:
return(Distance.five)
else:
return(Distance.none)
else:
rangedAttack(commoner, weaponStyle, cat)
return(Distance.charge)

if range == Distance.five:
if fightingStyle == "close":
fullAttack(commoner, weaponStyle, cat)
else:
rangedAttack(commoner, weaponStyle, cat)

return(Distance.five)

if range == Distance.long:
rangedAttack(commoner, weaponStyle, cat)
return(Distance.long)

if range == Distance.none:

if commoner.primary.reach:
fullAttack(commoner, weaponStyle, cat)
return(Distance.five)

if commoner.primary.type == WT.ranged:
rangedAttack(commoner, weaponStyle, cat)
return(Distance.none)

fullAttack(commoner, weaponStyle, cat)
return(Distance.none)

def battle(weapon, weaponStyle="single", fightingStyle="close"):
"""Simulates a battle between a cat and human commoner"""
cat = Cat()
commoner = Commoner(weapon, copy.deepcopy(weapon) if weaponStyle == "double" else None)

# Establish how far apart they begin
range = Distance.charge

if weapon.range > 60:
range = Distance.long
elif weapon.range > 30:
range = Distance.charge
elif weapon.range == 10:
range = Distance.five

#Role for initiative
cat.initiative = cat.dexterityBonus + random.randint(1,20)
commoner.initiative = commoner.dexterityBonus + random.randint(1,20)

#First round
if cat.initiative >= commoner.initiative:
commoner.AC = commoner.AC - commoner.dexterityBonus

range = catAttackCommoner(cat, commoner, range)

commoner.AC = commoner.AC + commoner.dexterityBonus
else:
cat.AC = cat.AC - cat.dexterityBonus

range = commonerAttackCat(commoner, cat, range, weaponStyle, fightingStyle)

cat.AC = cat.AC + cat.dexterityBonus

while cat.health > 0 and commoner.health > 0:

if cat.initiative < commoner.initiative:
range = catAttackCommoner(cat, commoner, range)
if commoner.health > 0:
range = commonerAttackCat(commoner, cat, range, weaponStyle, fightingStyle)
else:
range = commonerAttackCat(commoner, cat, range, weaponStyle, fightingStyle)
if cat.health > 0:
range = catAttackCommoner(cat, commoner, range)

return(True if commoner.health > 0 else False)

def multiBattle(weapon, weaponStyle="single", fightingStyle="close"):
commonerWins = 0
catWins = 0

for match in range(0, Battles):
if battle(weapon, weaponStyle, fightingStyle):
commonerWins += 1
else:
catWins += 1

#print("Commoner Wins: " + str(commonerWins) + " | Cat Wins " + str(catWins))
return(commonerWins)

weaponAndVictory = {}
victoryOverTime = {}

TrialsCompleted = 0

def trialFinished():
global TrialsCompleted
global Trials
#print("Values: " + str(TrialsCompleted) + "," + "Trials" + str(Trials))
TrialsCompleted += 1
print("%d%%" % int(TrialsCompleted/Trials * 100))

def war():
#print(threading.currentThread().name)
for weapon in weapons.keys():
#First we check if it is possible to fight with a weapon in this way

commonerWins = 0
currentWeapon = ""

if not weapons[weapon].type == WT.ranged:
#print(weapon + "(single) vs. Cat")

commonerWins = multiBattle(copy.deepcopy(weapons[weapon]))

weaponAndVictory[weapon + "(single)"] = commonerWins

if not weapons[weapon].type == WT.twoHanded or weapons[weapon].double:
#print(weapon + "(double) vs. Cat")
commonerWins = multiBattle(copy.deepcopy(weapons[weapon]), "double")
weaponAndVictory[weapon + "(double)"] = commonerWins

if weapons[weapon].range > 0:
#print(weapon + "(single, ranged) vs. Cat")
commonerWins = multiBattle(copy.deepcopy(weapons[weapon]), "single", "ranged")
weaponAndVictory[weapon + "(single, ranged)"] = commonerWins

if not weapons[weapon].type == WT.twoHanded:
#print(weapon + "(double, ranged) vs. Cat")

commonerWins = multiBattle(copy.deepcopy(weapons[weapon]), "double", "ranged")
weaponAndVictory[weapon + "(double, ranged)"] = commonerWins

#print("---------------------------------\n")

for scenario in weaponAndVictory.keys():
victoryOverTime[scenario] += weaponAndVictory[scenario]
#print(scenario + " - " + str(weaponAndVictory[scenario]))
trialFinished()


def threadTrials():
while True:
war()

def printToCommandLine():
"""Formats the string for the command line"""
for scenario in sorted(victoryOverTime, key=victoryOverTime.get, reverse=True):
print("%-32s%d%%" % (scenario, int((victoryOverTime[scenario]/(Trials * Battles)) * 100)))

def printToGiantInThePlayground():
"""Formats the output for printing on the GITP forums"""
#Print table header
print("{table=head]Weapon|Odds of Victory")

colour = "red"

#Print Columns
for scenario in sorted(victoryOverTime, key=victoryOverTime.get, reverse=True):
if int((victoryOverTime[scenario]/(Trials * Battles)) * 100) >= 50:
colour = "green"
else:
colour = "red"

print("%s|%d%%" % (scenario, colour, int((victoryOverTime[scenario]/(Trials * Battles)) * 100)))

#Close Table
print("[/table]")

#initialize the dictionary
for weapon in weapons.keys():
if not weapons[weapon].type == WT.ranged:
victoryOverTime[weapon + "(single)"] = 0
if not weapons[weapon].type == WT.twoHanded or weapons[weapon].double:
victoryOverTime[weapon + "(double)"] = 0
if weapons[weapon].range > 0:
victoryOverTime[weapon + "(single, ranged)"] = 0
if not weapons[weapon].type == WT.twoHanded:
victoryOverTime[weapon + "(double, ranged)"] = 0

lock = threading.Lock()

print("Prepare For Battle!")

for thread in range(Threads):
t = threading.Thread(target=threadTrials)
t.daemon = True
t.start()

while TrialsCompleted < Trials:
True

if Format == "Console":
printToCommandLine()
elif Format == "GITP":
printToGiantInThePlayground()


You can also change the "Threads" variable if your machine has more than two cores.

ahenobarbi
2013-12-07, 10:31 AM
:smallbiggrin:

GutterFace
2013-12-07, 10:42 AM
All this code blows my mind. since i can't decipher it....was there a way to make sure since the cat is a size category smaller it ended up with the +1 to hit a larger commoner?

and vice versa?

Greenish
2013-12-07, 10:42 AM
No reach weapons?

Also, cat has hide/move silently scores the commoner will be hard pressed to match (assuming proper concealment, such as from dim light – the cat has low-light vision). The entry does mention that "[c]ats prefer to sneak up on their prey".


[Edit]: Ah, longspear is there, but it apparently grants no benefit over spear.

Chronos
2013-12-07, 10:54 AM
What happens if the commoner tries to grapple the cat? And are you counting attacks of opportunity when the cat attempts to enter the commoner's space?

TripleD
2013-12-07, 10:56 AM
All this code blows my mind. since i can't decipher it....was there a way to make sure since the cat is a size category smaller it ended up with the +1 to hit a larger commoner?

and vice versa?

That's actually not too difficult. In the class "Commoner" and "Cat" there is a variable called "Attack" that represents the attack bonus they receive. Right now the Cat's bonus is set to +4 (+2 dex, +2 size), while the commoners is 0.



No reach weapons?

Also, cat has hide/move silently scores the commoner will be hard pressed to match (assuming proper concealment, such as from dim light – the cat has low-light vision). The entry does mention that "[c]ats prefer to sneak up on their prey".


[Edit]: Ah, longspear is there, but it apparently grants no benefit over spear.


I thought about factoring in AoO, but realized that in the simplified scenario I described, they will never arise. The cat and commoner will always try to close the distance between them. Once they are within 5 feet, they start the "five-foot-step" tango.

As for hide/move silently, that is far beyond the scope of the hour or two I had to work on this. Would be neat to try and factor in though.


What happens if the commoner tries to grapple the cat? And are you counting attacks of opportunity when the cat attempts to enter the commoner's space?

I barely understand the grapple rules as written, let alone how to encapsulate them in a program. Although this is probably a great chance to learn.

Good catch about the tiny creature/lack of reach. I'll have to update it to allow the commoner an AoO.

The Glyphstone
2013-12-07, 10:57 AM
I thought the core of the Cat vs. Commoner meme was that the Cat would almost always get a surprise round vs. the commoner because of its stealth scores, letting it open with a free attack, then roll into a full attack next round. Your script doesn't seem to allow for the possibility of the battle starting at a distance of less than 30ft.


EDIT: Ah, so it was just a matter of time to code rather than an intentional/accidental omission. Still very neat, and shows that even in the best circumstances. Mr. Whiskers still has almost a 30% chance to kill a man.:smallbiggrin:

Greenish
2013-12-07, 11:02 AM
I thought about factoring in AoO, but realized that in the simplified scenario I described, they will never arise. The cat and commoner will always try to close the distance between them. Once they are within 5 feet, they start the "five-foot-step" tango.The commoner, should she win the initiative, could ready an attack against the cat, thus getting two attacks in before the cat can close to melee. After all, she has more than 2 Int, unlike the cat.

For that matter, the cat is a Tiny creature, with reach of 0 ft. It would therefore have to enter the commoner's square to attack, provoking an AoO. So, reach weapon or no reach weapon, if the commoner wins the initiative, she gets two attacks.

As for hide/move silently, that is far beyond the scope of the hour or two I had to work on this. Would be neat to try and factor in though.I can imagine it'd be tricky alright.


[Edit]: I can't actually remember (or parse) right now whether entering the opponent's square provokes on it's own. Regardless, since the cat needs to enter the commoner's square, it can't just 5' step from where the commoner hit it with reach weapon, but would actually have to provoke an AoO.

TripleD
2013-12-07, 11:02 AM
I thought the core of the Cat vs. Commoner meme was that the Cat would almost always get a surprise round vs. the commoner because of its stealth scores, letting it open with a free attack, then roll into a full attack next round. Your script doesn't seem to allow for the possibility of the battle starting at a distance of less than 30ft.


EDIT: Ah, so it was just a matter of time to code rather than an intentional/accidental omission. Still very neat, and shows that even in the best circumstances. Mr. Whiskers still has almost a 30% chance to kill a man.:smallbiggrin:

Yep. Although thanks to +2 dex, the cat still catches the commoner flatfooted more often than not.


The commoner, should she win the initiative, could ready an attack against the cat, thus getting two attacks in before the cat can close to melee. After all, she has more than 2 Int, unlike the cat.

I thought about that possibility, but I decided against it because the goal is to maximize the wins by the commoner. The cat only has 2hp, so the issue is just being able to beat the cat's 14 AC. In that case I figured the +2 bonus from charging was worth more than an extra attack given by AoO.

The Glyphstone
2013-12-07, 11:03 AM
The commoner, should she win the initiative, could ready an attack against the cat, thus getting two attacks in before the cat can close to melee. After all, she has more than 2 Int, unlike the cat.

For that matter, the cat is a Tiny creature, with reach of 0 ft. It would therefore have to enter the commoner's square to attack, provoking an AoO. So, reach weapon or no reach weapon, if the commoner wins the initiative, she gets two attacks.
I can imagine it'd be tricky alright.

Unless they're unarmed. Then they don't threaten without IUS, and so can't take AoO.

This is complicated.:smallbiggrin: