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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy