Translation of a joke into Python and JS.
Day 10 involved lots of fiddling around with numbers, me getting very confused, and the odd diagram of which way was up.
This involved some different data types. Screened locations were only on exact grid points. To keep the precision of exact points, I could use rational numbers to represent points (and screened points). I used the
V2 Rational vector type to represent an asteroid, and a
Set of them to represent the whole field.
To find the best site, I have to find the number of visible asteroids at each possible site. This returns both the best site and the number of visible asteroids.
bestVisible :: Bounds -> Asteroids -> (Position, Int) bestVisible bounds asteroids = maximumBy (comparing snd) $ S.toList $ S.map (visibleCount bounds asteroids) asteroids visibleCount :: Bounds -> Asteroids -> Position -> (Position, Int) visibleCount bounds asteroids origin = (origin, S.size $ visible bounds origin asteroids)
To find the visible asteroids from a site, I find all the points that are screened by the asteroids, and remove them from the set of all asteroids: what remains is just the set of visible asteroids.
visible :: Bounds -> Position -> Asteroids -> Asteroids visible bounds origin asteroids = S.delete origin $ S.difference asteroids screened where screened = allScreenings bounds origin asteroids
The set of all the screened points, is just the union of the sets of points screened by each asteroid.
allScreenings :: Bounds -> Position -> Asteroids -> Asteroids allScreenings bounds origin asteroids = S.foldl' (screenings bounds origin) S.empty asteroids
screenings does the work. I only have to worry about screened points that line up on grid points. That means that if an asteroid is five units left and two units below the origin, the interesting screened points are 10 left and 4 below, 15 left and 6 below, and so on. But if an asteroid is four units left and two units below, the interesting screened points are 6 left and 3 below, 8 left and 4 below, 10 left and 6 below, and so on.
But rather than thinking about common factors of the steps left and down, I can just take one step left and two fifths of a step down (in the 5, 2 case), or one step left and half a step down (in the 4, 2 case). This will generate more screened points than could possibly line up with asteroids, but as all I'm doing is finding the intersection between these screened points and the asteroids, I'll get the same answer anyway.
screenings does. It finds the difference between the origin and the target, and then finds the largest component of that step. It uses that to scale the
delta to be one step in that direction, and whatever fraction of a step in the other direction. It then just applies multiples of that
delta beyond the target, until it reaches the edge of the asteroid field.
screenings :: Bounds -> Position -> Asteroids -> Position -> Asteroids screenings bounds origin@(V2 ox oy) screened0 target@(V2 tx ty) | origin == target = screened0 | otherwise = S.union screened0 screened where maxComponent = max (abs (tx - ox)) (abs (ty - oy)) delta = V2 ((tx - ox) / maxComponent) ((ty - oy) / maxComponent) rawScreens = takeWhile (inBounds bounds) [target ^+^ n *^ delta | n <- [1..]] screened = S.fromList rawScreens inBounds :: Bounds -> Position -> Bool inBounds (maxX, maxY) (V2 x y) = (x >= 0) && (x <= (maxX % 1)) && (y >= 0) && (y <= (maxY % 1))
There is also some faffing around with
fromIntegral calls in the datafile reading code, to convert the machine-sized
Int values to the arbitrary-sized
Integer values needed in the rest of the program.
At this point, all the niceties of rational numbers disappeared. I did start by trying that route, having a virtual asteroid tracking around the perimeter of the field and the laser cannon pointing at it. But that missed some asteroids, so I dropped down into dirty
Float real numbers.
I created a
Map of targetting information, one element per asteroid. The value was the position of asteroid; the key was a pair (2-tuple) of its angular position (relative to the monitor satellite) and its distance. (I somehow managed to have the angle increasing clockwise; not what I was expecting.)
makeTargets :: Position -> Asteroids -> Targets makeTargets origin asteroids = S.foldl' addTarget M.empty asteroids where addTarget m t = M.insert (targetInfo origin t) t m targetInfo :: Position -> Position -> TargetInfo targetInfo origin target = (angle, range) where V2 dx dy = target - origin angle = atan2 (fromRational dy) (fromRational dx) range = norm (V2 (fromRational dy) (fromRational dx))
The targetting information was used with the current angle of laser. The next target is the asteroid with the smallest angle larger than the laser's angle; if there are several such targets, the laser shoots the nearest asteroid. As tuples order on first term then second term, sorting by the (angle, distance) pair sorts the asteroids in the correct targetting order.
possibleTargets finds all the targets clockwise of the laser's angle.
firstTarget finds the first such target.
targetNext handles the destruction of an asteroid, and updates the laser's angle to that of the just-destroyed asteroid. It also handles the branch cut situation where there are no more targets between the laser and the angle π radians; in that case, the angle is reset to -π radians and the zapping continues.
targetSequence :: Targets -> [Position] targetSequence targets = targetNext ((- pi / 2) - 0.001) targets targetNext :: Float -> Targets -> [Position] targetNext angle targets | M.null targets =  | M.null possibles = targetNext (- pi) targets | otherwise = (target:(targetNext angle' targets')) where possibles = possibleTargets angle targets ((targetAngle, targetRange), target) = firstTarget possibles targets' = M.delete (targetAngle, targetRange) targets angle' = targetAngle possibleTargets :: Float -> Targets -> Targets possibleTargets angle targets = M.filterWithKey (\(a, _) _ -> a > angle) targets firstTarget :: Targets -> (TargetInfo, Position) firstTarget targets = M.findMin targets
All of that description sounds very simple and straightforward, but I managed to tie myself into several knots along the way. The angular positions weren't doing what I expected, and I once had a situation where an asteroid was shielding asteroids closer than it as well as those further away.
showPattern function was helpful, generating a little map of interesting points. I used that to compare what my program thought was happening with what the AoC examples were showing.
The code is available here, and on Github. There's also an earlier version, which uses
V2 Int to store the asteroid locations, and then does some fiddling around inside
screenings to covert the
Rational and back again.