In [1]:
name = '2018-02-05-oop-vs-procedural'
title = 'Advent of Code Battle'
tags = 'oop'
author = 'Mark Prosser'

In [2]:
from nb_tools import connect_notebook_to_post
from IPython.core.display import HTML

html = connect_notebook_to_post(name, title, tags, author)

Instructions for the example in the code can be found here: https://adventofcode.com/2015/day/21 And other approaches to this problem (including other languages) can be found on Reddit: https://www.reddit.com/r/adventofcode/comments/3xspyl/day_21_solutions/

First, we import Packages


In [3]:
import numpy as np
import itertools
import warnings
warnings.simplefilter(action='ignore')

Procedural code version


In [4]:
startbosshp = 104
bossdamage = 8
bossarmor = 1

startplayerhp = 100
playerdamage = 0
playerarmor = 0

Named tuples tuples but BETTER!


In [5]:
from collections import namedtuple
Item = namedtuple('item', ['name', 'cost', 'damage', 'armor'])

weaponsnt = [
    Item('Dagger', 8, 4, 0),
    Item('Shortsword', 10, 5, 0),
    Item('Warhammer', 25, 6, 0),
    Item('Longsword', 40, 7, 0),
    Item('Greataxe', 74, 8, 0),
]

armornt = [
    Item('Leather', 13, 0, 1),
    Item('Chainmail', 31, 0, 2),
    Item('Splintmail', 53, 0, 3),
    Item('Bandedmail', 75, 0, 4),
    Item('Platemail', 102, 0, 5),
    Item('Naked', 0, 0, 0),
]

ringsnt = [
    Item('Damage +1', 25, 1, 0),
    Item('Damage +2', 50, 2, 0),
    Item('Damage +3', 100, 3, 0),
    Item('Defense +1', 20, 0, 1),
    Item('Defense +2', 40, 0, 2),
    Item('Defense +3', 80, 0, 3),
]

Set up arrays


In [6]:
wn = 5 #weapons
an = 6 #armor
rn = 22 #rings

comb = wn * an * rn #total number of possibilities

#setup arrays
player_spent = np.full((wn, an, rn), np.nan)
player_damage = np.full((wn, an, rn), np.nan) #damage
player_armor = np.full((wn, an, rn), np.nan) #cost

#setup arrays
#weapons = np.full((wn, 3), 0)
armor = np.full((an, 3), 0)
rings0 = np.full((6, 3), 0)
rings = np.full((rn, 3), 0)

weapons = np.array([[8, 4, 0], [10, 5, 0], [25, 6, 0], [40, 7, 0], [74, 8, 0]])
armor[:,:] = [[13, 0, 1], [31, 0, 2], [53, 0, 3], [75, 0, 4], [102, 0, 5], [0, 0, 0]]
rings0[:,:] = [[25, 1, 0], [50, 2, 0], [100, 3, 0], [20, 0, 1], [40, 0, 2], [80, 0, 3]]

Use itertool package to get 15 combinations of 2 rings


In [7]:
ring_combs = (list(itertools.combinations(range(6), 2)))
print(ring_combs)


[(0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)]

The [Cost, Damage, Armor] of the 22 ring combinations


In [8]:
for i in range(0, len(ring_combs)):
    rings[i, 0] = int(rings0[ring_combs[i][0]][0] + rings0[ring_combs[i][1]][0]) #spent
    rings[i, 1] = int(rings0[ring_combs[i][0]][1] + rings0[ring_combs[i][1]][1]) #damage
    rings[i, 2] = int(rings0[ring_combs[i][0]][2] + rings0[ring_combs[i][1]][2]) #armor
rings[15:-1, :] = rings0[:,:]

print(rings)


[[ 75   3   0]
 [125   4   0]
 [ 45   1   1]
 [ 65   1   2]
 [105   1   3]
 [150   5   0]
 [ 70   2   1]
 [ 90   2   2]
 [130   2   3]
 [120   3   1]
 [140   3   2]
 [180   3   3]
 [ 60   0   3]
 [100   0   4]
 [120   0   5]
 [ 25   1   0]
 [ 50   2   0]
 [100   3   0]
 [ 20   0   1]
 [ 40   0   2]
 [ 80   0   3]
 [  0   0   0]]

Fill 3x 660-cell 3D arrays with the cost, damage and armor for each kit-combination


In [9]:
for w in range(0, wn):
    for a in range(0, an):
        for r in range(0, rn):
            player_spent[w, a, r] = weapons[w, 0] + armor[a, 0] + rings[r, 0]
            player_damage[w, a, r] = weapons[w, 1] + armor[a, 1] + rings[r, 1]
            player_armor[w, a, r] = weapons[w, 2] + armor[a, 2] + rings[r, 2]

E.g. [0,0,0] = rings: dam+1, dam+2, leather, dagger


In [10]:
playerspent = player_spent[0,0,0]
print('playerspent=',playerspent)
playerdamage = player_damage[0,0,0]
print('playerdamage=',playerdamage)
playerarmor = player_armor[0,0,0]
print('playerarmor=',playerarmor)


playerspent= 96.0
playerdamage= 7.0
playerarmor= 1.0

660 boss vs player battles


In [11]:
bestspend = 999
wi = 0
ai = 0
ri = 0
worstspend = 0
wi2 = 0
ai2 = 0
ri2 = 0
win_no = 0
lose_no = 0

for w in range(0, wn): #length=5
    for a in range(0, an): #length=6
        for r in range(0, rn):  #length=22
            #get 1 of 660
            playerspent = player_spent[w, a, r]
            playerdamage = player_damage[w, a, r]
            playerarmor = player_armor[w, a, r]
            bosshp = startbosshp
            playerhp = startplayerhp

            playeractdam = playerdamage - bossarmor
            if (playeractdam < 1):
                playeractdam = 1
#             playactdam = max(playeractdam, 1)
            bossactdam = bossdamage - playerarmor
            if (bossactdam < 1):
                bossactdam = 1

            while (bosshp > 0) and (playerhp > 0): 
                #bosshp = bosshp - playeractdam
                bosshp -= playeractdam
                #playerhp = playerhp - bossactdam
                playerhp -= bossactdam
                
            if playerhp > bosshp: #if I win
                #win_no = win_no += 1
                win_no += 1
                if playerspent < bestspend:
                    bestspend = playerspent
                    wi = w
                    ai = a
                    ri = r
                    
            if playerhp < bosshp: #if I lose
                #lose_no = lose_no + 1
                lose_no += 1
                if playerspent > worstspend:
                    worstspend = playerspent
                    wi2 = w
                    ai2 = a
                    ri2 = r




print('lowest cost while still winning =',bestspend)
print(weaponsnt[wi])
print(armornt[ai])
print('ringscombi',rings[ri])
print(wi)
print(ai)
print(ri)
print('-')
print('highest cost while still losing =',worstspend)
print(weaponsnt[wi2])
print(armornt[ai2])
print('ringscombi',rings[ri2])
print(wi2)
print(ai2)
print(ri2)
print('-')
print('win_no=',win_no)
print('lose_no=',lose_no)


lowest cost while still winning = 91.0
item(name='Longsword', cost=40, damage=7, armor=0)
item(name='Chainmail', cost=31, damage=0, armor=2)
ringscombi [20  0  1]
3
1
18
-
highest cost while still losing = 158.0
item(name='Dagger', cost=8, damage=4, armor=0)
item(name='Naked', cost=0, damage=0, armor=0)
ringscombi [150   5   0]
0
5
5
-
win_no= 525
lose_no= 135

Test a specific combination of weapons, armor and rings


In [12]:
ww = 0
aa = 5
rr = 5

playerspend = player_spent[ww, aa, rr]
playerdamage = player_damage[ww, aa, rr]
playerarmor = player_armor[ww, aa, rr]
bosshp = startbosshp
playerhp = startplayerhp

playeractdam = playerdamage - bossarmor
if (playeractdam < 1):
    playeractdam = 1
bossactdam = bossdamage - playerarmor
if (bossactdam < 1):
    bossactdam = 1

while (bosshp > 0) & (playerhp > 0): 
    bosshp = bosshp - playeractdam
    playerhp = playerhp - bossactdam
    print('bosshp=',bosshp)
    print('playerhp=',playerhp)
    print('-')


bosshp= 96.0
playerhp= 92.0
-
bosshp= 88.0
playerhp= 84.0
-
bosshp= 80.0
playerhp= 76.0
-
bosshp= 72.0
playerhp= 68.0
-
bosshp= 64.0
playerhp= 60.0
-
bosshp= 56.0
playerhp= 52.0
-
bosshp= 48.0
playerhp= 44.0
-
bosshp= 40.0
playerhp= 36.0
-
bosshp= 32.0
playerhp= 28.0
-
bosshp= 24.0
playerhp= 20.0
-
bosshp= 16.0
playerhp= 12.0
-
bosshp= 8.0
playerhp= 4.0
-
bosshp= 0.0
playerhp= -4.0
-

OOP code version

Make the boss and player class individually or..


In [13]:
# class Boss:
#     def __init__(self, hp, damage, armor):
#         self.hp = hp
#         self.damage = damage
#         self.armor = armor
        
#     def calc_actdamage(self, playerarmor):
#         self.actdamage = self.damage - playerarmor
#         if (self.actdamage < 1):
#             self.actdamage = 1
        
# class Player:
#     def __init__(self, spent, hp, damage, armor):
#         self.spent = spent
#         self.hp = hp
#         self.damage = damage
#         self.armor = armor
    
#     def calc_actdamage(self, bossarmor):
#         self.actdamage = self.damage - bossarmor
#         if (self.actdamage < 1):
#             self.actdamage = 1

...Use inheritance


In [14]:
class RpgChara:
    def __init__(self, hp, damage, armor):
        self.hp = hp
        self.damage = damage
        self.armor = armor
        
    def calc_actdamage(self, enemyarmor):
        self.actdamage = self.damage - enemyarmor
        if (self.actdamage < 1):
            self.actdamage = 1
            
class Boss(RpgChara):
    pass
    
class Player(RpgChara):
    def __init__(self, spent, hp, damage, armor):
        super().__init__(hp, damage, armor)
        self.spent = spent

another 660 fights, OOP style


In [15]:
bestspend = 999 #too high in order to come down
wi = 0
ai = 0
ri = 0
worstspend = 0 #too low in order to go up
wi2 = 0
ai2 = 0
ri2 = 0
win_no = 0
lose_no = 0

for w in range(0, wn): #length5
    for a in range(0, an): #length6
        for r in range(0, rn): #length22
            #get 1 of 660 instances per loop         
            boss_i = Boss(startbosshp, bossdamage, bossarmor)
            player_i = Player(player_spent[w, a, r], startplayerhp, player_damage[w, a, r], \
                              player_armor[w, a, r])
            
            #but what is their actual damage
            boss_i.calc_actdamage(player_i.armor)
            player_i.calc_actdamage(boss_i.armor)
            
            while (boss_i.hp > 0) & (player_i.hp > 0): 
                boss_i.hp -= player_i.actdamage
                player_i.hp -= boss_i.actdamage
            
            if player_i.hp > boss_i.hp: #if I win
                win_no += 1
                if player_i.spent < bestspend:
                    bestspend = player_i.spent
                    wi = w
                    ai = a
                    ri = r
                
            if player_i.hp < boss_i.hp: #if I lose
                lose_no += 1   
                if player_i.spent > worstspend:
                    worstspend = player_i.spent
                    wi2 = w
                    ai2 = a
                    ri2 = r

print('lowest spend while still winning =',bestspend)
print(weaponsnt[wi])
print(armornt[ai])
print('ringscombi',rings[ri])
print(ri)
print('-')
print('highest spend while still losing =',worstspend)
print(weaponsnt[wi2])
print(armornt[ai2])
print('ringscombi',rings[ri2])
print(ri2)
print('-')
print('win_no=',win_no)
print('lose_no=',lose_no)


lowest spend while still winning = 91.0
item(name='Longsword', cost=40, damage=7, armor=0)
item(name='Chainmail', cost=31, damage=0, armor=2)
ringscombi [20  0  1]
18
-
highest spend while still losing = 158.0
item(name='Dagger', cost=8, damage=4, armor=0)
item(name='Naked', cost=0, damage=0, armor=0)
ringscombi [150   5   0]
5
-
win_no= 525
lose_no= 135

In [16]:
HTML(html)


Out[16]:

This post was written as an IPython (Jupyter) notebook. You can view or download it using nbviewer.