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.
Powered by vBulletin® Copyright © 2024 vBulletin Solutions, Inc. All rights reserved.