Skip to content

Commit 8ae0a42

Browse files
committed
Added day 2018-15
1 parent ed10edc commit 8ae0a42

File tree

1 file changed

+322
-0
lines changed

1 file changed

+322
-0
lines changed

2018/15-Beverage Bandits.py

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
# -------------------------------- Input data -------------------------------- #
2+
import os, pathfinding, complex_utils, copy
3+
4+
test_data = {}
5+
6+
test = 1
7+
test_data[test] = {
8+
"input": """#######
9+
#G..#E#
10+
#E#E.E#
11+
#G.##.#
12+
#...#E#
13+
#...E.#
14+
#######""",
15+
"expected": ["36334 (37, 982, E)", "Unknown"],
16+
}
17+
18+
test += 1
19+
test_data[test] = {
20+
"input": """#######
21+
#E..EG#
22+
#.#G.E#
23+
#E.##E#
24+
#G..#.#
25+
#..E#.#
26+
#######""",
27+
"expected": ["39514 (46 rounds, 859 HP, E)", "Unknown"],
28+
}
29+
30+
test += 1
31+
test_data[test] = {
32+
"input": """#######
33+
#E.G#.#
34+
#.#G..#
35+
#G.#.G#
36+
#G..#.#
37+
#...E.#
38+
#######""",
39+
"expected": ["27755 (35 rounds, 793 HP, G)", "Unknown"],
40+
}
41+
42+
test += 1
43+
test_data[test] = {
44+
"input": """#######
45+
#.G...#
46+
#...EG#
47+
#.#.#G#
48+
#..G#E#
49+
#.....#
50+
#######""",
51+
"expected": ["Unknown", "15 attack power, 4988 (29 rounds, 172 HP)"],
52+
}
53+
54+
test = "real"
55+
input_file = os.path.join(
56+
os.path.dirname(__file__),
57+
"Inputs",
58+
os.path.basename(__file__).replace(".py", ".txt"),
59+
)
60+
test_data[test] = {
61+
"input": open(input_file, "r+").read().strip(),
62+
"expected": ["207542", "64688"],
63+
}
64+
65+
# -------------------------------- Control program execution ------------------------- #
66+
67+
case_to_test = 4
68+
part_to_test = 2
69+
verbose_level = 2
70+
71+
# -------------------------------- Initialize some variables ------------------------- #
72+
73+
puzzle_input = test_data[case_to_test]["input"]
74+
puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1]
75+
puzzle_actual_result = "Unknown"
76+
77+
78+
# -------------------------------- Player class definition --------------------------- #
79+
80+
81+
class Player:
82+
position = 0
83+
type = ""
84+
HP = 200
85+
graph = ""
86+
alive = True
87+
attack_power = 3
88+
89+
def __init__(self, type, position, attack_power=3):
90+
self.position = position
91+
self.type = type
92+
if self.type == "E":
93+
self.attack_power = attack_power
94+
95+
def __lt__(self, other):
96+
if self.position.imag < other.position.imag:
97+
return True
98+
else:
99+
return self.position.real < other.position.real
100+
101+
def move(self, graph, creatures):
102+
"""
103+
Searches for the closest ennemy
104+
105+
:param Graph graph: The game map
106+
:param list creatures: A list of creatures
107+
:return: The target position
108+
"""
109+
110+
# Modify graph so that allies are walls, ennemies are traps
111+
self.graph = copy.deepcopy(graph)
112+
verbose = False
113+
if False:
114+
verbose = True
115+
allies = [
116+
c.position
117+
for c in creatures
118+
if c.type == self.type and c != self and c.alive
119+
]
120+
ennemies = [c.position for c in creatures if c.type != self.type and c.alive]
121+
self.graph.add_traps(ennemies)
122+
self.graph.add_walls(allies)
123+
124+
# Run BFS from my position to determine closest target
125+
self.graph.breadth_first_search(self.position)
126+
127+
# Determine all target positions (= cells next to the ennemies), then choose closest
128+
target_positions = [
129+
(self.graph.distance_from_start[e + dir], e + dir)
130+
for e in ennemies
131+
for dir in complex_utils.directions_straight
132+
if e + dir in self.graph.distance_from_start
133+
]
134+
if not target_positions:
135+
return
136+
137+
min_distance = min([pos[0] for pos in target_positions])
138+
closest_targets = [pos[1] for pos in target_positions if pos[0] == min_distance]
139+
target = complex_utils.complex_sort(closest_targets, "reading")[0]
140+
141+
if min_distance == 0:
142+
return
143+
144+
if verbose:
145+
print("before", self.position, target_positions, closest_targets, target)
146+
147+
# Then we do the opposite, to know in which direction to go
148+
# Run BFS from the target
149+
self.graph.breadth_first_search(target)
150+
# Determine which direction to go to is best
151+
next_positions = [
152+
(self.graph.distance_from_start[self.position + dir], self.position + dir,)
153+
for dir in complex_utils.directions_straight
154+
if self.position + dir in self.graph.vertices
155+
]
156+
min_distance = min([pos[0] for pos in next_positions])
157+
closest_positions = [pos[1] for pos in next_positions if pos[0] == min_distance]
158+
target = complex_utils.complex_sort(closest_positions, "reading")[0]
159+
if verbose:
160+
print(
161+
"after", self.position, next_positions, closest_positions, target, self
162+
)
163+
164+
self.position = target
165+
166+
def attack(self, creatures):
167+
"""
168+
Attacks an ennemy in range
169+
170+
:param Graph graph: The game map
171+
:param list creatures: A list of creatures
172+
:return: Nothing
173+
"""
174+
175+
# Find who to attack
176+
ennemies = [
177+
c
178+
for c in creatures
179+
for dir in complex_utils.directions_straight
180+
if self.position + dir == c.position and c.type != self.type and c.alive
181+
]
182+
if not ennemies:
183+
return
184+
185+
min_HP_ennemies = player_sort(
186+
[e for e in ennemies if e.HP == min([e.HP for e in ennemies])]
187+
)
188+
ennemy = player_sort(min_HP_ennemies)[0]
189+
190+
ennemy.lose_HP(self.attack_power)
191+
192+
def lose_HP(self, HP):
193+
"""
194+
Loses HP following an attack
195+
196+
:param int HP: How many HP to lose
197+
:return: Nothing
198+
"""
199+
self.HP -= HP
200+
self.alive = self.HP > 0
201+
202+
203+
def player_sort(players):
204+
players.sort(key=lambda a: (-a.position.imag, a.position.real))
205+
return players
206+
207+
208+
# -------------------------------- Actual code execution ----------------------------- #
209+
210+
211+
if part_to_test == 1:
212+
grid = puzzle_input
213+
214+
# Initial grid with everything
215+
graph = pathfinding.Graph()
216+
graph.grid_to_vertices(grid)
217+
218+
# Identify all creatures
219+
creatures = graph.grid_search(grid, ("E", "G"))
220+
221+
creatures = [
222+
Player(type, position) for type in creatures for position in creatures[type]
223+
]
224+
factions = set(c.type for c in creatures)
225+
226+
round = 0
227+
if verbose_level >= 2:
228+
print("Start")
229+
print(graph.vertices_to_grid({c.position: c.type for c in creatures}))
230+
print([(c.type, c.position, c.HP) for c in player_sort(creatures)])
231+
while True:
232+
player_sort(creatures)
233+
for i, creature in enumerate(creatures):
234+
if not creature.alive:
235+
continue
236+
creature.move(graph, creatures)
237+
creature.attack(creatures)
238+
239+
creatures = [c for c in creatures if c.alive]
240+
factions = set(c.type for c in creatures)
241+
if len(factions) == 1:
242+
break
243+
244+
round += 1
245+
if verbose_level >= 3:
246+
print("round", round)
247+
print(graph.vertices_to_grid({c.position: c.type for c in creatures}))
248+
print([(c.type, c.position, c.HP, c.alive) for c in player_sort(creatures)])
249+
250+
if verbose_level >= 2:
251+
print("End of combat")
252+
print(graph.vertices_to_grid({c.position: c.type for c in creatures}))
253+
print([(c.type, c.position, c.HP) for c in player_sort(creatures)])
254+
print(
255+
"Reached round:",
256+
round,
257+
"- Remaining HP:",
258+
sum(c.HP for c in creatures),
259+
"- Winner:",
260+
factions,
261+
)
262+
puzzle_actual_result = sum(c.HP for c in creatures if c.alive) * round
263+
264+
265+
else:
266+
grid = puzzle_input
267+
268+
# Initial grid with everything
269+
graph = pathfinding.Graph()
270+
graph.grid_to_vertices(grid)
271+
272+
# Identify all creatures
273+
creatures_positions = graph.grid_search(grid, ("E", "G"))
274+
275+
for attack in range(3, 100):
276+
creatures = [
277+
Player(type, position, attack)
278+
for type in creatures_positions
279+
for position in creatures_positions[type]
280+
]
281+
factions = set(c.type for c in creatures)
282+
dead_elves = 0
283+
284+
round = 0
285+
while dead_elves == 0:
286+
player_sort(creatures)
287+
for i, creature in enumerate(creatures):
288+
if not creature.alive:
289+
continue
290+
creature.move(graph, creatures)
291+
creature.attack(creatures)
292+
293+
dead_elves = len([c for c in creatures if c.type == "E" and not c.alive])
294+
creatures = [c for c in creatures if c.alive]
295+
factions = set(c.type for c in creatures)
296+
if len(factions) == 1:
297+
break
298+
299+
round += 1
300+
301+
if verbose_level >= 2:
302+
print("End of combat with attack", attack)
303+
if verbose_level >= 3:
304+
print(graph.vertices_to_grid({c.position: c.type for c in creatures}))
305+
print(
306+
"Reached round:",
307+
round,
308+
"- Remaining HP:",
309+
sum(c.HP for c in creatures),
310+
"- Winner:",
311+
factions,
312+
)
313+
print("Dead elves:", dead_elves)
314+
315+
if factions == set("E",):
316+
puzzle_actual_result = sum(c.HP for c in creatures if c.alive) * round
317+
break
318+
319+
# -------------------------------- Outputs / results -------------------------------- #
320+
321+
print("Expected result : " + str(puzzle_expected_result))
322+
print("Actual result : " + str(puzzle_actual_result))

0 commit comments

Comments
 (0)