-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathaoc201522.py
182 lines (150 loc) · 4.86 KB
/
aoc201522.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""AoC 22, 2015: Wizard Simulator 20XX."""
# Standard library imports
import functools
import heapq
import pathlib
import sys
from dataclasses import dataclass, field
@dataclass(order=True)
class Player:
hit_points: int
mana: int
armor: int = 0
effects: dict[str, int] = field(default_factory=dict, compare=False)
@dataclass(order=True)
class Boss:
hit_points: int
damage: int
@dataclass(order=True)
class Attack:
cost: int
effect: int
damage: int
heal: int
armor: int
poison: int
mana: int
ATTACKS = {
"missile": Attack(53, effect=0, damage=4, heal=0, armor=0, poison=0, mana=0),
"drain": Attack(73, effect=0, damage=2, heal=2, armor=0, poison=0, mana=0),
"shield": Attack(113, effect=6, damage=0, heal=0, armor=7, poison=0, mana=0),
"poison": Attack(173, effect=6, damage=0, heal=0, armor=0, poison=3, mana=0),
"recharge": Attack(229, effect=5, damage=0, heal=0, armor=0, poison=0, mana=101),
}
def parse_data(puzzle_input):
"""Parse input."""
return Boss(
**{
key.replace(" ", "_").lower(): int(value)
for key, value in [line.split(": ") for line in puzzle_input.split("\n")]
}
)
def part1(data, hit_points=50, mana=500):
"""Solve part 1."""
return fight(player=Player(hit_points=hit_points, mana=mana), boss=data)
def part2(data, hit_points=50, mana=500):
"""Solve part 2."""
return fight(
player=Player(hit_points=hit_points, mana=mana), boss=data, level_hard=True
)
def fight(player, boss, level_hard=False):
"""Find the cheapest way for the player to defeat the boss."""
attacks = [(0, player, boss, [])]
while True:
cost, player, boss, names = heapq.heappop(attacks)
if boss.hit_points <= 0:
return cost, names
if player.hit_points <= 0:
continue
for attack_name, attack in ATTACKS.items():
if attack.cost <= player.mana and player.effects.get(attack_name, 0) <= 1:
heapq.heappush(
attacks,
(
cost + attack.cost,
*do_attacks(player, boss, attack_name, level_hard),
names + [attack_name],
),
)
def do_attacks(player, boss, attack_name, level_hard):
"""Perform one player and one boss attack, including hard level and other
effects."""
attacks = ([hard_attack] if level_hard else []) + [
apply_effects,
functools.partial(player_attack, attack_name=attack_name),
apply_effects,
boss_attack,
]
for attack in attacks:
player, boss = attack(player, boss)
if player.hit_points <= 0 or boss.hit_points <= 0:
break
return player, boss
def hard_attack(player, boss):
"""Perform a hard attack."""
return (
Player(
hit_points=player.hit_points - 1,
mana=player.mana,
armor=player.armor,
effects=player.effects,
),
boss,
)
def player_attack(player, boss, attack_name):
"""Perform player attack."""
attack = ATTACKS[attack_name]
return Player(
hit_points=player.hit_points + attack.heal,
mana=player.mana - attack.cost,
armor=0,
effects=(
player.effects | ({attack_name: attack.effect} if attack.effect else {})
),
), Boss(hit_points=boss.hit_points - attack.damage, damage=boss.damage)
def boss_attack(player, boss):
"""Perform boss attack."""
return (
Player(
hit_points=player.hit_points - (boss.damage - player.armor),
mana=player.mana,
armor=0,
effects=player.effects,
),
boss,
)
def apply_effects(player, boss):
"""Apply effects to player and boss."""
for effect_name in player.effects:
effect = ATTACKS[effect_name]
player = Player(
hit_points=player.hit_points,
armor=max(player.armor, effect.armor),
mana=player.mana + effect.mana,
effects=player.effects,
)
boss = Boss(
hit_points=boss.hit_points - effect.poison,
damage=boss.damage,
)
return (
Player(
hit_points=player.hit_points,
armor=player.armor,
mana=player.mana,
effects={
name: timer - 1 for name, timer in player.effects.items() if timer > 1
},
),
boss,
)
def solve(puzzle_input):
"""Solve the puzzle for the given input."""
data = parse_data(puzzle_input)
yield part1(data)
yield part2(data)
if __name__ == "__main__":
for path in sys.argv[1:]:
print(f"\n{path}:")
solutions = solve(puzzle_input=pathlib.Path(path).read_text().strip())
print("\n".join(str(solution) for solution in solutions))