A Monster for Every Season: Summer 2
You can get A Monster for Every Season: Summer 2 now at Gumroad
Results 1 to 12 of 12
  1. - Top - End - #1
    Barbarian in the Playground
     
    WhiteWizardGirl

    Join Date
    Feb 2013

    Default Commoner vs. Cat - A Mathematical Analysis

    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.

  2. - Top - End - #2
    Firbolg in the Playground
     
    AstralFire's Avatar

    Join Date
    Oct 2007
    Gender
    Female

    Default Re: Commoner vs. Cat - A Mathematical Analysis

    Topic of the day right here, folks. :)


    a steampunk fantasy ♦ the novelthe album

  3. - Top - End - #3
    Barbarian in the Playground
     
    WhiteWizardGirl

    Join Date
    Feb 2013

    Default Re: Commoner vs. Cat - A Mathematical Analysis

    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.

    Code:
    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.

    Code:
    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|[color=%s]%d%%[/color]" % (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.

  4. - Top - End - #4
    Ogre in the Playground
     
    Kobold

    Join Date
    Jan 2012

    Default Re: Commoner vs. Cat - A Mathematical Analysis


  5. - Top - End - #5
    Barbarian in the Playground
     
    Chimera

    Join Date
    Mar 2012
    Location
    NJ
    Gender
    Male

    Default Re: Commoner vs. Cat - A Mathematical Analysis

    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?

  6. - Top - End - #6
    Titan in the Playground
     
    Greenish's Avatar

    Join Date
    Feb 2010
    Location
    Finland

    Default Re: Commoner vs. Cat - A Mathematical Analysis

    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.
    Last edited by Greenish; 2013-12-07 at 10:46 AM.
    Quotes:
    Spoiler
    Show
    Quote Originally Posted by Claudius Maximus View Post
    Also fixed the money issue by sacrificing a goat.
    Quote Originally Posted by subject42 View Post
    This board needs a "you're technically right but I still want to crawl into the fetal position and cry" emoticon.
    Quote Originally Posted by Yukitsu View Post
    I define [optimization] as "the process by which one attains a build meeting all mechanical and characterization goals set out by the creator prior to its creation."
    Praise for avatar may be directed to Derjuin.

  7. - Top - End - #7
    Titan in the Playground
    Join Date
    May 2007
    Location
    The Land of Cleves
    Gender
    Male

    Default Re: Commoner vs. Cat - A Mathematical Analysis

    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?
    Time travels in divers paces with divers persons.
    As You Like It, III:ii:328

    Chronos's Unalliterative Skillmonkey Guide
    Current Homebrew: 5th edition psionics

  8. - Top - End - #8
    Barbarian in the Playground
     
    WhiteWizardGirl

    Join Date
    Feb 2013

    Default Re: Commoner vs. Cat - A Mathematical Analysis

    Quote Originally Posted by GutterFace View Post
    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.

    Quote Originally Posted by Greenish
    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.

    Quote Originally Posted by Chronos View Post
    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.
    Last edited by TripleD; 2013-12-07 at 11:01 AM.

  9. - Top - End - #9
    Eldritch Horror in the Playground Moderator
    Join Date
    Feb 2005
    Gender
    Male

    Default Re: Commoner vs. Cat - A Mathematical Analysis

    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.
    Last edited by The Glyphstone; 2013-12-07 at 10:58 AM.

  10. - Top - End - #10
    Titan in the Playground
     
    Greenish's Avatar

    Join Date
    Feb 2010
    Location
    Finland

    Default Re: Commoner vs. Cat - A Mathematical Analysis

    Quote Originally Posted by TripleD View Post
    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.
    Quote Originally Posted by TripleD View Post
    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.
    Last edited by Greenish; 2013-12-07 at 11:05 AM.
    Quotes:
    Spoiler
    Show
    Quote Originally Posted by Claudius Maximus View Post
    Also fixed the money issue by sacrificing a goat.
    Quote Originally Posted by subject42 View Post
    This board needs a "you're technically right but I still want to crawl into the fetal position and cry" emoticon.
    Quote Originally Posted by Yukitsu View Post
    I define [optimization] as "the process by which one attains a build meeting all mechanical and characterization goals set out by the creator prior to its creation."
    Praise for avatar may be directed to Derjuin.

  11. - Top - End - #11
    Barbarian in the Playground
     
    WhiteWizardGirl

    Join Date
    Feb 2013

    Default Re: Commoner vs. Cat - A Mathematical Analysis

    Quote Originally Posted by The Glyphstone View Post
    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.
    Yep. Although thanks to +2 dex, the cat still catches the commoner flatfooted more often than not.

    Quote Originally Posted by Greenish View Post
    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.
    Last edited by TripleD; 2013-12-07 at 11:09 AM. Reason: Spelling Correction

  12. - Top - End - #12
    Eldritch Horror in the Playground Moderator
    Join Date
    Feb 2005
    Gender
    Male

    Default Re: Commoner vs. Cat - A Mathematical Analysis

    Quote Originally Posted by Greenish View Post
    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.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •