# Skeleton vs. Zombie¶

A fight breaks out between a zombie and a skeleton. Can we predict who is going to win?

Lets start with a simple scenario: the skeleton and zombie are standing next to one another when the hostilities begin.

First, lets figure out who goes first.

Each creature rolls initiative (a d20 plus dexterity modifier).

### Roll Initiative¶

In [1]:
import numpy as np

zombie_initiative = np.random.choice(20) + 1 + 2
skeleton_initiative = np.random.choice(20) + 1 - 2

# re-roll if there is a tie
while zombie_initiative == skeleton_initiative:
zombie_initiative = np.random.choice(20) + 1 + 2
skeleton_initiative = np.random.choice(20) + 1 - 2

# determine who goes first
if skeleton_initiative > zombie_initiative:
print("Skeleton goes first")
elif skeleton_initiative < zombie_initiative:
print("Zombie goes first")
else:
raise ValueError("we shouldn't be here!")

Zombie goes first

In [2]:
'''

At 0 ft: the zombie wins about 77% of the time
At infinite feet: the skeleton wins 100% of the time if it keeps moving away

'''

# simulates a fight between a skeleton and a zombie
import numpy as np
import time

class Monster:

def __init__(self, position_x=0, position_y=0):
self.position_x = position_x
self.position_y = position_y

def initiative(self):
# roll to hit
d20 = np.random.choice(20) + self.dexterity_mod + 1
return d20

def melee(self, target):
# roll to hit
d20 = np.random.choice(20) + self.melee_attack_bonus + 1
if d20 < target.ac:
# print('%s missed %s' % (self.name, target.name))
return

# roll damage
damage = np.random.choice(self.melee_damage_dice) + self.melee_damage_bonus + 1

if self.melee_damage_type == target.damage_vulnerability:
damage *= 2
# print('target is vulnerable to %s' % self.melee_damage_type)

target.hp -= damage
# print('%s dealt %d damage to %s' % (self.name, damage, target.name))
# print('%s has %d hit points left' % (target.name, target.hp))
return damage

def ranged(self, target):
# roll to hit
d20 = np.random.choice(20) + self.ranged_attack_bonus + 1
if d20 < target.ac:
# print('%s missed %s' % (self.name, target.name))
return

# roll damage
damage = np.random.choice(self.ranged_damage_dice) + self.ranged_damage_bonus + 1

if self.ranged_damage_type == target.damage_vulnerability:
damage *= 2
# print('target is vulnerable to %s' % self.melee_damage_type)

target.hp -= damage
# print('%s dealt %d damage to %s' % (self.name, damage, target.name))
# print('%s has %d hit points left' % (target.name, target.hp))
return damage

class Skeleton(Monster):

def __init__(self, position_x=0, position_y=0):
super(Skeleton, self).__init__(position_x=0, position_y=0)
self.name = 'skeleton'
self.dexterity = 14
self.dexterity_mod = 2
self.constitution = 15
self.constitution_mod = 2
self.ac = 13
self.hp = 13
self.speed = 30
self.melee_attack_bonus = 4
self.melee_damage_bonus = 2
self.melee_damage_dice = 6
self.melee_damage_type = 'piercing'
self.ranged_attack_bonus = 4
self.ranged_damage_bonus = 2
self.ranged_damage_dice = 6
self.ranged_damage_type = 'piercing'
self.ranged_distance = 80
self.ranged_long_distance = 320
self.damage_vulnerability = 'bludgeoning'

class Zombie(Monster):

def __init__(self, position_x=0, position_y=0):
super(Zombie, self).__init__(position_x=0, position_y=0)
self.name = 'zombie'
self.dexterity = 6
self.dexterity_mod = -2
self.constitution = 16
self.constitution_mod = 3
self.ac = 8
self.hp = 22
self.speed = 20
self.melee_attack_bonus = 3
self.melee_damage_bonus = 1
self.melee_damage_dice = 6
self.melee_damage_type = 'bludgeoning'
self.ranged_attack_bonus = None
self.ranged_damage_bonus = None
self.ranged_damage_dice = None
self.ranged_damage_type = None
self.ranged_distance = None
self.ranged_long_distance = None
self.damage_vulnerability = None

if self.hp != 0:
raise ValueError('only use undead fortitute when you have zero hit points')
# roll a constitution save
d20 = np.random.choice(20) + self.constitution_mod + 1
if d20 >= damage + 5:
self.hp = 1
return True
else:
return False

t0 = time.time()
starting_distance = 320
skeleton_wins = 0
zombie_wins = 0

n_perm = int(1e5)

for p in range(n_perm):
# initialize monsters
s = Skeleton()
z = Zombie(position_x=starting_distance)

# roll initiative
si = s.initiative()
zi = z.initiative()
# print('skeleton initiative: ', si)
# print('zombie initiative: ', zi)

# re-reroll in case of tie
while (si == zi):
# print('a tie!')
si = s.initiative()
zi = z.initiative()
# print(si, zi)

# check to see who goes first
if si > zi:
first = 'skeleton'
# print('skeleton goes first')
elif zi > si:
first = 'zombie'
# print('zombie goes first')
else:
raise ValueError('how did we get a tie!?')

# loop through 10000 rounds of combat
for n in range(1, 10000):

# compute distance
r = np.sqrt((s.position_x - z.position_x)**2 + (s.position_y - z.position_y)**2)

if first == 'skeleton':

# skeleton moves away

# skeleton attacks zombie
d = s.melee(z)
if z.hp < 0:
# print('skeleton wins!!!')
skeleton_wins += 1
break
elif z.hp == 0:
if save:
# print('undead fortitude save the zombie!')
pass
else:
# print('skeleton wins!!!')
skeleton_wins += 1
break

# zombie attacks skeleton
d = z.melee(s)
if s.hp <= 0:
# print('zombie wins!!!')
zombie_wins += 1
break

elif first == 'zombie':

# zombie moves toward skeleton

# zombie attacks skeleton
d = z.melee(s)
if s.hp <= 0:
# print('zombie wins!!!')
zombie_wins += 1
break

# skeleton attacks zombie
d = s.melee(z)
if z.hp < 0:
# print('skeleton wins!!!')
skeleton_wins += 1
break
elif z.hp == 0:
if save:
# print('undead fortitude save the zombie!')
pass
else:
# print('skeleton wins!!!')
skeleton_wins += 1
break

else:
raise ValueError('how did we get here!?')

print('%d permutations in %.2f seconds' % (n_perm, time.time() - t0))
print('%d skeleton wins' % skeleton_wins)
print('%d zombie wins' % zombie_wins)

100000 permutations in 6.37 seconds
22694 skeleton wins
77306 zombie wins