diff options
Diffstat (limited to '22')
-rw-r--r-- | 22/solution.py | 134 |
1 files changed, 98 insertions, 36 deletions
diff --git a/22/solution.py b/22/solution.py index 6df8379..c0ff521 100644 --- a/22/solution.py +++ b/22/solution.py @@ -1,42 +1,104 @@ -from functools import cache -from collections import namedtuple - -Opponent = namedtuple('Opponent', ['hp', 'damage']) - -def part1(opponent: Opponent) -> int: - @cache - def min_cost(php: int, pmana: int, ohp: int, sld_timer: int, psn_timer: int, rch_timer: int) -> int | float: - psld = 0 - def effects(): - nonlocal ohp, pmana, psld, psn_timer, rch_timer, sld_timer - if sld_timer > 0: - sld_timer -= 1 - psld = 7 - else: - psld = 0 - if psn_timer > 0: - psn_timer -= 1 - ohp -= 3 - if rch_timer > 0: - rch_timer -= 1 - pmana += 101 - - def opponent_turn(cost: int) -> int | float: - effects() - nonlocal php - if ohp <= 0: - return cost - php -= min(1, opponent.attack - psld) - if php <= 0: +from dataclasses import dataclass, astuple, replace + +@dataclass +class State: + boss_hp: int + boss_damage: int + player_hp: int = 50 + player_mana: int = 500 + player_shield: int = 0 + shield_timer: int = 0 + poison_timer: int = 0 + recharge_timer: int = 0 + +@dataclass +class Effect: + timer: str + start: int + +@dataclass +class Instant: + effects: dict[str, int] + +@dataclass +class Spell: + cost: int + action: Effect | Instant + +def solve(boss_hp: int, boss_damage: int, hard_mode: bool) -> int | float: + spells: list[Spell] = [ + Spell(53, Instant({'boss_hp': -4})), + Spell(73, Instant({'boss_hp': -2, 'player_hp': 2})), + Spell(113, Effect('shield_timer', 6)), + Spell(173, Effect('poison_timer', 6)), + Spell(229, Effect('recharge_timer', 6)), + ] + + seen: set[tuple] = set() + + def simulate_effects(state: State): + if state.shield_timer > 0: + state.shield_timer -= 1 + state.player_shield = 7 + else: + state.player_shield = 0 + if state.poison_timer > 0: + state.poison_timer -= 1 + state.boss_hp -= 3 + if state.recharge_timer > 0: + state.recharge_timer -= 1 + state.player_mana += 101 + + def player_turn(state: State) -> int | float: + t: tuple = astuple(state) + if t in seen: + return float('inf') + seen.add(t) + if hard_mode: + state.player_hp -= 1 + if state.player_hp <= 0: return float('inf') - return cost + min_cost(php, pmana, ohp, sld_timer, psn_timer, rch_timer) + simulate_effects(state) + if state.boss_hp <= 0: + return 0 + min_mana: int | float = float('inf') + cost: int + action: Effect | Instant + for spell in spells: + if state.player_mana < spell.cost: + continue + nstate = replace(state) + nstate.player_mana -= spell.cost + if isinstance(spell.action, Effect): + if getattr(nstate, spell.action.timer) != 0: + continue + setattr(nstate, spell.action.timer, spell.action.start) + else: + for attr, delta in spell.action.effects.items(): + setattr(nstate, attr, getattr(nstate, attr) + delta) + min_mana = min(min_mana, spell.cost + boss_turn(nstate)) + return min_mana - effects() - if ohp <= 0: + def boss_turn(state: State) -> int | float: + simulate_effects(state) + if state.boss_hp <= 0: return 0 + state.player_hp -= max(1, state.boss_damage - state.player_shield) + if state.player_hp <= 0: + return float('inf') + return player_turn(state) + + return player_turn(State(boss_hp=boss_hp, boss_damage=boss_damage)) + +def part1(boss_hp: int, boss_damage: int) -> int | float: + return solve(boss_hp, boss_damage, False) + +def part2(boss_hp: int, boss_damage: int) -> int | float: + return solve(boss_hp, boss_damage, True) if __name__ == '__main__': - opponent = Opponent(51, 9) + boss_hp: int = 51 + boss_damage: int = 9 - print(part1(opponent)) - #print(part2(opponent)) + print(part1(boss_hp, boss_damage)) + print(part2(boss_hp, boss_damage)) |