-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathq10.py
345 lines (296 loc) · 9.15 KB
/
q10.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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
'Day 10 - Monitoring Station'
import math
import unittest
from collections import defaultdict
from decimal import Decimal
from util import slurp
def clean(string):
"strip trailing and leading spaces from all lines and remove empty lines"
return "\n".join(line.strip() for line in string.strip().split('\n'))
def findAsteroids(mapString):
"""
return a set of tuples representing the co-ordinates of all asteroids
in the map.
"""
grid = mapString.split('\n')
height = len(grid)
width = len(grid[0])
asteroids = set()
for y in range(height):
for x in range(width):
if grid[y][x] == '#':
asteroids.add((x, y))
return asteroids
def findBestLocation(mapString):
"""
find the location of the asteroid that can detect the most other asteroids
in its line of sight. also returns the number detected.
"""
asteroids = findAsteroids(mapString)
best = None
# Compute the angle and distance of each asteroid relative to the origin
for src in asteroids:
seen = set()
for dest in asteroids:
if dest is src:
continue
vector = (dest[0] - src[0], dest[1] - src[1])
angle = clockwiseAngle(vector)
seen.add(angle)
detected = len(seen)
if best is None or best['detected'] < detected:
best = {'detected': detected, 'position': src}
return best
def destroyAsteroids(mapString):
"""
destroy asteroids in clockwise order repeatedly. return co-ordinates of asteroids
in the order they were destroyed
"""
# Find the positions of all asteroids
asteroids = findAsteroids(mapString)
# Get the position of our monitoring station
origin = findBestLocation(mapString)['position']
asteroids.remove(origin)
# Compute the angle and distance of each asteroid relative to the origin
byAngle = defaultdict(list)
for asteroid in asteroids:
vector = (asteroid[0] - origin[0], asteroid[1] - origin[1])
distance = vectorLength(vector)
angle = clockwiseAngle(vector)
byAngle[angle].append((asteroid, distance))
# Start the death ray
destroyed = []
while asteroids:
for angle in sorted(byAngle):
# Destroy the asteroid with the smallest distance for each given
# angle
if byAngle[angle]:
closest = min(byAngle[angle], key=lambda x: x[1])
byAngle[angle].remove(closest)
asteroids.remove(closest[0])
destroyed.append(closest[0])
return destroyed
def vectorLength(u):
return math.sqrt(u[0] * u[0] + u[1] * u[1])
def vectorDotProduct(u, v):
return u[0] * v[0] + u[1] * v[1]
def clockwiseAngle(v, precision=10):
"""
return clockwise angle for vector: the angle from vector (0,-1)
pointing straight up {well, down in Cartesian} to the vector
E.g. for a vector (0,-1) the angle is 0
for a vector (1,-1) the angle is 45 (going up-right)
for a vector (0, 1) the angle is 180 (going down)
for a vector (-1,-1) the angle is 315 (going up-left)
The angle is truncated to precision decimal places (default 10)
to allow equality comparison.
"""
u = (0, -1)
cosAngle = vectorDotProduct(u, v) / (vectorLength(u) * vectorLength(v))
angle = math.acos(cosAngle)
# We know u is pointing up. If v is negative (left side of the clock)
# convert the angle from counter-clockwise to clockwise
if v[0] < 0:
angle = 2 * math.pi - angle
# Turn into degrees for ease of debugging
angle = math.degrees(angle)
# We need to normalise the angle as floating point errors can prevent
# equality of angles that are actually the same. For example:
# angle=14.036243467926457 contents=[((14, 1), 12.36931687685298)]
# angle=14.036243467926484 contents=[((13, 5), 8.246211251235321)]
# Use Decimal to allow the angles to be stored and compared without further
# floating point errors (although float passes the tests)
angle = Decimal(f'{angle:.{precision}f}')
return angle
class TestQ10(unittest.TestCase):
def test_vector(self):
self.assertEqual(vectorLength((math.sqrt(2), math.sqrt(2))), 2.0)
self.assertEqual(vectorLength((0, 3)), 3.0)
self.assertEqual(vectorLength((0, -3)), 3.0)
self.assertEqual(vectorDotProduct((2, 2), (0, 3)), 6)
def test_clean(self):
self.assertEqual(clean("""
aa
bb
"""), "aa\nbb")
def test_basic(self):
"basic test to make sure hiding works in all four directions"
test = clean("""
...#...
.......
#..#..#
...#...
""")
self.assertEqual(
findBestLocation(test), {
'detected': 4, 'position': (
3, 2)})
def test_example1(self):
"first example in question spec"
test = clean("""
.#..#
.....
#####
....#
...##
""")
self.assertEqual(
findBestLocation(test), {
'detected': 8, 'position': (
3, 4)})
def test_large_example1(self):
test = clean("""
......#.#.
#..#.#....
..#######.
.#.#.###..
.#..#.....
..#....#.#
#..#....#.
.##.#..###
##...#..#.
.#....####
""")
self.assertEqual(
findBestLocation(test), {
'detected': 33, 'position': (
5, 8)})
def test_large_example2(self):
test = clean("""
#.#...#.#.
.###....#.
.#....#...
##.#.#.#.#
....#.#.#.
.##..###.#
..#...##..
..##....##
......#...
.####.###.
""")
self.assertEqual(
findBestLocation(test), {
'detected': 35, 'position': (
1, 2)})
def test_large_example3(self):
test = clean("""
.#..#..###
####.###.#
....###.#.
..###.##.#
##.##.#.#.
....###..#
..#.#..#.#
#..#.#.###
.##...##.#
.....#.#..
""")
self.assertEqual(
findBestLocation(test), {
'detected': 41, 'position': (
6, 3)})
def test_large_example4(self):
test = clean("""
.#..##.###...#######
##.############..##.
.#.######.########.#
.###.#######.####.#.
#####.##.#.##.###.##
..#####..#.#########
####################
#.####....###.#.#.##
##.#################
#####.##.###..####..
..######..##.#######
####.##.####...##..#
.#####..#.######.###
##...#.##########...
#.##########.#######
.####.#.###.###.#.##
....##.##.###..#####
.#.#.###########.###
#.#.#.#####.####.###
###.##.####.##.#..##
""")
self.assertEqual(
findBestLocation(test), {
'detected': 210, 'position': (
11, 13)})
def test_part1(self):
test = slurp('inputs/q10')
self.assertEqual(
findBestLocation(test), {
'detected': 269, 'position': (
13, 17)})
def test_destroy_basic(self):
test = clean("""
.#..
####
.#..
""")
self.assertEqual(
findBestLocation(test), {
'detected': 4, 'position': (
1, 2)})
self.assertEqual(destroyAsteroids(test), [
(1, 1), (2, 1), (3, 1), (0, 1), (1, 0)
])
def test_destroy_example1(self):
test = clean("""
.#....#####...#..
##...##.#####..##
##...#...#.#####.
..#.....#...###..
..#.#.....#....##
""")
destroyed = destroyAsteroids(test)
self.assertEqual(destroyed[0:9], [
(8, 1), (9, 0), (9, 1), (10, 0), (9, 2), (11, 1), (12, 1), (11, 2), (15, 1)
])
self.assertEqual(destroyed[9:18], [
(12, 2), (13, 2), (14, 2), (15, 2), (12, 3), (16, 4), (15, 4), (10, 4), (4, 4)
])
self.assertEqual(destroyed[18:27], [
(2, 4), (2, 3), (0, 2), (1, 2), (0, 1), (1, 1), (5, 2), (1, 0), (5, 1)
])
def test_destroy_example2(self):
test = clean("""
.#..##.###...#######
##.############..##.
.#.######.########.#
.###.#######.####.#.
#####.##.#.##.###.##
..#####..#.#########
####################
#.####....###.#.#.##
##.#################
#####.##.###..####..
..######..##.#######
####.##.####...##..#
.#####..#.######.###
##...#.##########...
#.##########.#######
.####.#.###.###.#.##
....##.##.###..#####
.#.#.###########.###
#.#.#.#####.####.###
###.##.####.##.#..##
""")
destroyed = destroyAsteroids(test)
self.assertEqual(destroyed[0], (11, 12))
self.assertEqual(destroyed[1], (12, 1))
self.assertEqual(destroyed[2], (12, 2))
self.assertEqual(destroyed[9], (12, 8))
self.assertEqual(destroyed[19], (16, 0))
self.assertEqual(destroyed[49], (16, 9))
self.assertEqual(destroyed[99], (10, 16))
self.assertEqual(destroyed[198], (9, 6))
self.assertEqual(destroyed[199], (8, 2))
self.assertEqual(destroyed[200], (10, 9))
self.assertEqual(destroyed[298], (11, 1))
self.assertEqual(len(destroyed), 299)
def test_part2(self):
test = slurp('inputs/q10')
destroyed = destroyAsteroids(test)
number200 = destroyed[199]
self.assertEqual(number200, (6, 12))