183 lines
5.0 KiB
Python
183 lines
5.0 KiB
Python
from __future__ import annotations
|
|
|
|
import heapq
|
|
from typing import Any, Iterator, Literal, TypeAlias, cast
|
|
|
|
from ..base import BaseSolver
|
|
|
|
PlayerType: TypeAlias = Literal["player", "boss"]
|
|
SpellType: TypeAlias = Literal["magic missile", "drain", "shield", "poison", "recharge"]
|
|
BuffType: TypeAlias = Literal["shield", "poison", "recharge"]
|
|
Node: TypeAlias = tuple[
|
|
PlayerType,
|
|
int,
|
|
int,
|
|
int,
|
|
int,
|
|
int,
|
|
tuple[tuple[BuffType, int], ...],
|
|
tuple[tuple[SpellType, int], ...],
|
|
]
|
|
|
|
ATTACK_SPELLS: list[tuple[SpellType, int, int, int]] = [
|
|
("magic missile", 53, 4, 0),
|
|
("drain", 73, 2, 2),
|
|
]
|
|
BUFF_SPELLS: list[tuple[BuffType, int, int]] = [
|
|
("shield", 113, 6),
|
|
("poison", 173, 6),
|
|
("recharge", 229, 5),
|
|
]
|
|
|
|
|
|
def play(
|
|
player_hp: int,
|
|
player_mana: int,
|
|
player_armor: int,
|
|
boss_hp: int,
|
|
boss_attack: int,
|
|
hard_mode: bool,
|
|
) -> tuple[tuple[SpellType, int], ...]:
|
|
winning_node: tuple[tuple[SpellType, int], ...] | None = None
|
|
|
|
visited: set[
|
|
tuple[PlayerType, int, int, int, int, tuple[tuple[BuffType, int], ...]]
|
|
] = set()
|
|
nodes: list[Node] = [
|
|
("player", 0, player_hp, player_mana, player_armor, boss_hp, (), ())
|
|
]
|
|
|
|
while winning_node is None:
|
|
(
|
|
player,
|
|
mana,
|
|
player_hp,
|
|
player_mana,
|
|
player_armor,
|
|
boss_hp,
|
|
buffs,
|
|
spells,
|
|
) = heapq.heappop(nodes)
|
|
|
|
if (player, player_hp, player_mana, player_armor, boss_hp, buffs) in visited:
|
|
continue
|
|
|
|
visited.add((player, player_hp, player_mana, player_armor, boss_hp, buffs))
|
|
new_buffs: list[tuple[BuffType, int]] = []
|
|
for buff, length in buffs:
|
|
length = length - 1
|
|
match buff:
|
|
case "poison":
|
|
boss_hp = max(boss_hp - 3, 0)
|
|
case "shield":
|
|
if length == 0:
|
|
player_armor -= 7
|
|
case "recharge":
|
|
player_mana += 101
|
|
|
|
if length > 0:
|
|
new_buffs.append((buff, length))
|
|
|
|
if hard_mode and player == "player":
|
|
player_hp = player_hp - 1
|
|
|
|
if player_hp <= 0:
|
|
continue
|
|
|
|
if boss_hp <= 0:
|
|
winning_node = spells
|
|
continue
|
|
|
|
buffs = tuple(new_buffs)
|
|
|
|
if player == "boss":
|
|
heapq.heappush(
|
|
nodes,
|
|
(
|
|
"player",
|
|
mana,
|
|
max(0, player_hp - max(boss_attack - player_armor, 1)),
|
|
player_mana,
|
|
player_armor,
|
|
boss_hp,
|
|
buffs,
|
|
spells,
|
|
),
|
|
)
|
|
else:
|
|
buff_types = {b for b, _ in buffs}
|
|
|
|
for spell, cost, damage, regeneration in ATTACK_SPELLS:
|
|
if player_mana < cost:
|
|
continue
|
|
|
|
heapq.heappush(
|
|
nodes,
|
|
(
|
|
"boss",
|
|
mana + cost,
|
|
player_hp + regeneration,
|
|
player_mana - cost,
|
|
player_armor,
|
|
max(0, boss_hp - damage),
|
|
buffs,
|
|
spells + cast("tuple[tuple[SpellType, int]]", ((spell, cost),)),
|
|
),
|
|
)
|
|
|
|
for buff_type, buff_cost, buff_length in BUFF_SPELLS:
|
|
if buff_type in buff_types:
|
|
continue
|
|
|
|
if player_mana < buff_cost:
|
|
continue
|
|
|
|
heapq.heappush(
|
|
nodes,
|
|
(
|
|
"boss",
|
|
mana + buff_cost,
|
|
player_hp,
|
|
player_mana - buff_cost,
|
|
player_armor + 7 * (buff_type == "shield"),
|
|
boss_hp,
|
|
buffs
|
|
+ cast(
|
|
"tuple[tuple[BuffType, int]]", ((buff_type, buff_length),)
|
|
),
|
|
spells
|
|
+ cast(
|
|
"tuple[tuple[SpellType, int]]", ((buff_type, buff_cost),)
|
|
),
|
|
),
|
|
)
|
|
|
|
return winning_node
|
|
|
|
|
|
class Solver(BaseSolver):
|
|
def solve(self, input: str) -> Iterator[Any]:
|
|
lines = input.splitlines()
|
|
|
|
player_hp = 50
|
|
player_mana = 500
|
|
player_armor = 0
|
|
|
|
boss_hp = int(lines[0].split(":")[1].strip())
|
|
boss_attack = int(lines[1].split(":")[1].strip())
|
|
|
|
yield sum(
|
|
c
|
|
for _, c in play(
|
|
player_hp, player_mana, player_armor, boss_hp, boss_attack, False
|
|
)
|
|
)
|
|
|
|
# 1242 (not working)
|
|
yield sum(
|
|
c
|
|
for _, c in play(
|
|
player_hp, player_mana, player_armor, boss_hp, boss_attack, True
|
|
)
|
|
)
|