AdventOfCode/2015/day22.py

140 lines
4.7 KiB
Python

with open('day22-input', 'r') as file:
data = [l.strip('\n') for l in file]
boss_hp = int(data[0].split()[-1])
boss_damage = int(data[1].split()[-1])
player_hp = 50
player_mp = 500
from collections import namedtuple
State = namedtuple('State', ['player_hp', 'player_mp', 'boss_hp', 'shield', 'poison', 'recharge', 'mp_spent'])
initial_state = State(player_hp, player_mp, boss_hp, 0, 0, 0, 0)
def magic_missile(state):
if state.player_mp < 53:
raise ValueError('Not enough mana')
return State(state.player_hp, state.player_mp-53, state.boss_hp-4,
state.shield, state.poison, state.recharge, state.mp_spent+53)
def drain(state):
if state.player_mp < 73:
raise ValueError('Not enough mana')
return State(state.player_hp+2, state.player_mp-73, state.boss_hp-2,
state.shield, state.poison, state.recharge, state.mp_spent+73)
def poison(state):
if state.player_mp < 173:
raise ValueError('Not enough mana')
if state.poison > 0:
raise ValueError('Already poisoned')
return State(state.player_hp, state.player_mp-173, state.boss_hp,
state.shield, state.poison+6, state.recharge, state.mp_spent+173)
def shield(state):
if state.player_mp < 113:
raise ValueError('Not enough mana')
if state.shield > 0:
raise ValueError('Already shielded')
return State(state.player_hp, state.player_mp-113, state.boss_hp,
state.shield+6, state.poison, state.recharge, state.mp_spent+113)
def recharge(state):
if state.player_mp < 229:
raise ValueError('Not enough mana')
if state.recharge > 0:
raise ValueError('Already recharging')
return State(state.player_hp, state.player_mp-229, state.boss_hp,
state.shield, state.poison, state.recharge+5, state.mp_spent+229)
def turn_start(state, hardmode_player_turn=False):
s = state._asdict()
if hardmode_player_turn:
s['player_hp'] -= 1
if s['player_hp'] <= 0:
return State(**s)
if s['recharge'] > 0:
s['player_mp'] += 101
s['recharge'] -= 1
if s['poison'] > 0:
s['boss_hp'] -= 3
s['poison'] -= 1
if s['shield'] > 0:
s['shield'] -= 1
return State(**s)
def boss_action(state):
damage = boss_damage
if state.shield > 0:
damage = max(damage-7, 1)
return State(state.player_hp-damage, state.player_mp, state.boss_hp,
state.shield, state.poison, state.recharge, state.mp_spent)
spells = [magic_missile, drain, shield, poison, recharge]
mana_reqs = [53, 73, 113, 173, 229]
def execute_round(state, command, hardmode=False):
state = turn_start(state, hardmode)
if state.player_hp <= 0:
return state
state = spells[command](state)
state = turn_start(state)
if state.boss_hp > 0:
state = boss_action(state)
return state
maximum_mana_usage = 100000 # Arbitrarily large
winning_commands = []
def tree_simulation(state, commands, hardmode=False):
global maximum_mana_usage, winning_commands
state = execute_round(state, commands[-1], hardmode)
if state.player_hp <= 0 or state.mp_spent > maximum_mana_usage:
return 100000
if state.boss_hp <= 0:
maximum_mana_usage = state.mp_spent
winning_commands = commands
return state.mp_spent
mp_spent = 100000
for cmd, mana in enumerate(mana_reqs):
if state.player_mp > mana:
try:
mp_spent = min(tree_simulation(state, commands+[cmd], hardmode), mp_spent)
except ValueError:
pass
return mp_spent
mana = min([tree_simulation(initial_state, [c]) for c in range(len(spells))])
print(mana) # Part 1
part1_commands = winning_commands
maximum_mana_usage = 100000
mana_hard = min([tree_simulation(initial_state, [c], hardmode=True) for c in range(len(spells))])
print(mana_hard) # Part 2
part2_commands = winning_commands
# Bonus: show the winning simulations
spell_names = ['Magic Missile', 'Drain', 'Shield', 'Poison', 'Recharge']
def perform_simulation(command_list, maximum_mana_usage=None, hardmode=False):
state = initial_state
for i, command in enumerate(command_list):
print(state)
print(f'Player casts {spell_names[command]}!')
try:
state = execute_round(state, command, hardmode)
except ValueError:
return f'Failed at round #{i+1}: Insufficient mana'
if maximum_mana_usage and state.mp_spent >= maximum_mana_usage:
return f'Failed at round #{i+1}: Exceeded mana budget'
if state.player_hp <= 0:
return f'Failed at round #{i+1}: Player died'
if state.boss_hp <= 0:
return f'Succeeded at round #{i+1}', state.mp_spent
return f'Ran out of commands but game is not over', state
print('Ideal normal mode fight:')
print(perform_simulation(part1_commands))
print('')
print('Ideal hard mode fight:')
print(perform_simulation(part2_commands, hardmode=True))