In [2]:
# Boss
# Hit Points: 55
# Damage: 8

class Player(object):
    def __init__(self, hp, mana, damage, armor):
        self.hp = hp
        self.mana = mana
        self.damage = damage
        self.armor = armor
        
me = Player(50, 500, 0, 0)
boss = Player(55, 0, 8, 0)

In [37]:
class Spell(object):
    @classmethod
    def cost(cls):
        return 0
    
    @classmethod
    def duration(cls):
        return 0

    def __init__(self, me, boss):
        self.me = me
        self.boss = boss
        self.cost = type(self).cost()
        self.started = False
        
    def effect(self):
        assert False

    def cast(self):
        if self.started:
            return False
        # print 'Spell: ', type(self).__name__
        self.started = True
        self.me.mana -= self.cost
        self.effect()
        return True
    
class MagicMissile(Spell):
    @classmethod
    def cost(cls):
        return 53

    def __init__(self, me, boss):
        super(MagicMissile, self).__init__(me, boss)
        
    def effect(self):
        self.boss.hp -= max(1, 4 - boss.armor)
        
class Die(Spell):
    @classmethod
    def cost(cls):
        return 0

    def __init__(self, me, boss):
        super(Die, self).__init__(me, boss)
        
    def effect(self):
        self.me.hp = 0

test = MagicMissile(me, boss)
test.cast()
type(test).duration()

class Drain(Spell):
    @classmethod
    def cost(cls):
        return 73

    def __init__(self, me, boss):
        super(Drain, self).__init__(me, boss)
    
    def effect(self):
        self.boss.hp -= max(1, 2 - boss.armor)
        self.me.hp += 2
        
test = Drain(me, boss)
test.cast()

class Effect(Spell):
    @classmethod
    def cost(cls):
        return 0
    
    @classmethod
    def duration(cls):
        assert False

    def __init__(self, me, boss):
        super(Effect, self).__init__(me, boss)
        self.duration = type(self).duration()
        
    def done(self):
        pass
    
    def cast(self):
        if not self.started:
            self.started = True
            self.me.mana -= type(self).cost()
            return True
        if 0 < self.duration:
            # print 'Effect: ', type(self).__name__
            self.effect()
            self.duration -= 1
            return True
        if 0 == self.duration:
            self.done()
            self.duration -= 1
            return False
        return False
    
# test = Effect(me, boss)
# test.cast()
    
class Shield(Effect):
    @classmethod
    def cost(cls):
        return 113
    
    @classmethod
    def duration(cls):
        return 6

    def __init__(self, me, boss):
        super(Shield, self).__init__(me, boss)
        self.active = False
    
    def effect(self):
        if not self.active:
            self.me.armor += 7
            self.active = True
            
    def done(self):
        if self.active:
            self.me.armor -= 7
            self.active = False
            
test = Shield(me, boss)
test.cast()
            
class Poison(Effect):
    @classmethod
    def cost(cls):
        return 173
    
    @classmethod
    def duration(cls):
        return 6

    def __init__(self, me, boss):
        super(Poison, self).__init__(me, boss)
    
    def effect(self):
        self.boss.hp -= 3
        
test = Poison(me, boss)
test.cast()
            
class Recharge(Effect):
    @classmethod
    def cost(cls):
        return 229
    
    @classmethod
    def duration(cls):
        return 5

    def __init__(self, me, boss):
        super(Recharge, self).__init__(me, boss)
    
    def effect(self):
        self.me.mana += 101
        
test = Recharge(me, boss)
test.cast()


Out[37]:
True

In [35]:
from copy import deepcopy

def duel(me, boss, spells, dbg=False):
    me = deepcopy(me)
    boss = deepcopy(boss)
    spells = [cls(me, boss) for cls in spells]
    def effects(count):
        [spell.cast() for spell in spells[:count]]
    def cast(count):
        if len(spells) < count:
            return False
        spells[count - 1].cast()
        return True
    def check():
        if dbg:
            print 'Boss hp: ', boss.hp
            print 'Me hp: ', me.hp
            print 'Me mana: ', me.mana
        if me.hp <= 0:
            return False, (False, 'hp')
        if me.mana < 0:
            return False, (False, 'mana')
        if boss.hp <= 0:
            return False, (True, 'win!')
        return True, None
    round = 0
    while True:
        round += 1
        if dbg:
            print '== My turn =='
        me.hp -= 1
        res, ret = check()
        if not res:
            return ret
        effects(round - 1)
        res, ret = check()
        if not res:
            return ret
        if not cast(round):
            return False, 'no spells'
        res, ret = check()
        if not res:
            return ret
        if dbg:
            print '== Boss\'s turn =='
        effects(round)
        res, ret = check()
        if not res:
            return ret
        me.hp -= max(1, boss.damage - me.armor)
        res, ret = check()
        if not res:
            return ret

In [34]:
me = Player(10, 250, 0, 0)
boss = Player(13, 0, 8, 0)

duel(me, boss, [Poison, MagicMissile])


Out[34]:
(False, 'hp')

In [27]:
me = Player(10, 250, 0, 0)
boss = Player(14, 0, 8, 0)

duel(me, boss, [Recharge, Shield, Drain, Poison, MagicMissile])


Out[27]:
(False, 'hp')

In [50]:
me = Player(50, 500, 0, 0)
boss = Player(55, 0, 8, 0)

all_spells = [Recharge, Shield, Drain, Poison, MagicMissile]

ret = None
best_spells = None

reasons = {}

def f(spells):
    global ret
    # print 'Depth: ', len(spells)
    if 13 < len(spells):
        return None
    result, reason = duel(me, boss, spells)
    if reason not in reasons:
        reasons[reason] = 0
    cost = sum([spell.cost() for spell in spells])
    reasons[reason] += 1
    if result:
        if ret is None or cost < ret:
            ret = cost
            best_spells = spells
            # print 'Duel!'
            # print [spell.__name__ for spell in spells]
            # duel(me, boss, spells, True)
            print ret, len(spells)
            print [spell.__name__ for spell in best_spells]
            # print
    if reason == 'no spells' and (ret is None or cost < ret):
        for spell in all_spells:
            # print spell
            # print spells[-spell.duration():]
            d = spell.duration()
            if d < 3 or spell not in spells[-((d - 1) / 2):]:
                f(spells + [spell])
        
f([])

print ret
print reasons

# between 953 and 1295


1501 13
['Recharge', 'Shield', 'Drain', 'Recharge', 'Shield', 'Drain', 'Poison', 'Shield', 'MagicMissile', 'Poison', 'MagicMissile', 'MagicMissile', 'MagicMissile']
1481 13
['Recharge', 'Shield', 'Drain', 'Recharge', 'Shield', 'Poison', 'MagicMissile', 'Shield', 'Poison', 'MagicMissile', 'MagicMissile', 'MagicMissile', 'MagicMissile']
1475 11
['Recharge', 'Shield', 'Drain', 'Poison', 'Recharge', 'Shield', 'Poison', 'Drain', 'Drain', 'Poison', 'MagicMissile']
1455 11
['Recharge', 'Shield', 'Drain', 'Poison', 'Recharge', 'Shield', 'Poison', 'Drain', 'MagicMissile', 'Poison', 'MagicMissile']
1435 11
['Recharge', 'Shield', 'Drain', 'Poison', 'Recharge', 'Shield', 'Poison', 'MagicMissile', 'MagicMissile', 'Poison', 'MagicMissile']
1402 10
['Recharge', 'Shield', 'Poison', 'Recharge', 'Shield', 'Poison', 'Drain', 'Drain', 'Poison', 'MagicMissile']
1382 10
['Recharge', 'Shield', 'Poison', 'Recharge', 'Shield', 'Poison', 'Drain', 'MagicMissile', 'Poison', 'MagicMissile']
1362 10
['Recharge', 'Shield', 'Poison', 'Recharge', 'Shield', 'Poison', 'MagicMissile', 'MagicMissile', 'Poison', 'MagicMissile']
1295 11
['Recharge', 'Shield', 'Poison', 'Recharge', 'Shield', 'Poison', 'MagicMissile', 'MagicMissile', 'MagicMissile', 'MagicMissile', 'MagicMissile']
1289 9
['Poison', 'Recharge', 'Shield', 'Poison', 'Recharge', 'Drain', 'Poison', 'Drain', 'MagicMissile']
1289
{'mana': 221688, 'hp': 292028, 'no spells': 669717, 'win!': 881}