diff --git a/day10.go b/day10.go new file mode 100644 index 0000000..dd11cf1 --- /dev/null +++ b/day10.go @@ -0,0 +1,213 @@ +package aoc2019 + +import ( + "io" + "io/ioutil" + "math" + "os" + "sort" + + "github.com/pkg/errors" +) + +type day10MonitorGrid struct { + asteroidMap []byte + width int +} + +func (d day10MonitorGrid) asteroidCount() int { + var count int + for _, c := range d.asteroidMap { + if c == '#' { + count++ + } + } + + return count +} + +func (d day10MonitorGrid) clone() *day10MonitorGrid { + var grid = &day10MonitorGrid{asteroidMap: make([]byte, len(d.asteroidMap)), width: d.width} + for i, c := range d.asteroidMap { + grid.asteroidMap[i] = c + } + return grid +} + +func (d day10MonitorGrid) getAsteroidPositions() []int { + var knownPositions []int + for i, c := range d.asteroidMap { + if c != '#' { + // Not an asteroid, don't care + continue + } + + knownPositions = append(knownPositions, i) + } + return knownPositions +} + +func (d day10MonitorGrid) getCleanedGrid(x, y int) *day10MonitorGrid { + // Clone the map to work on + var grid = d.clone() + + // Collect positions of all known asteroids + var knownPositions = grid.getAsteroidPositions() + + // Mark observer (does not count into observable asteroids) + grid.asteroidMap[grid.coordToPos(x, y)] = '@' + + // Iterate all positions and remove covered (invisible) asteroids + for _, pos := range knownPositions { + var aX, aY = d.posToCoord(pos) + if grid.isObstructed(x, y, aX, aY) { + grid.asteroidMap[pos] = '-' + } + } + + return grid +} + +func (d *day10MonitorGrid) isObstructed(observX, observY, x, y int) bool { + var distX, distY = x - observX, y - observY + + if distX == 0 && distY == 0 { + // No steps, observer equals asteroid, needless calculation + return false + } + + var ( + div = int(math.Abs(float64(greatestCommonDivisor(int64(distX), int64(distY))))) + stepX, stepY = distX / div, distY / div + ) + + for i := 1; i < math.MaxInt64; i++ { + var rPosX, rPosY = observX + stepX*i, observY + stepY*i + + if rPosX < 0 || rPosX >= d.width || rPosY < 0 || rPosY >= len(d.asteroidMap)/d.width { + // Position outside grid, stop searching + panic(errors.Errorf("Observed position ran out of bounds (obsX=%d obsY=%d x=%d y=%d div=%d stepX=%d stepY=%d)", observX, observY, x, y, div, stepX, stepY)) + } + + if rPosX == x && rPosY == y { + return false + } + + if d.asteroidMap[d.coordToPos(rPosX, rPosY)] == '#' { + return true + } + } + + panic(errors.Errorf("Unreachable end was reached")) +} + +func (d day10MonitorGrid) coordToPos(x, y int) int { return y*d.width + x } +func (d day10MonitorGrid) posToCoord(pos int) (int, int) { return pos % d.width, pos / d.width } +func (d day10MonitorGrid) step2deg(x, y int) float64 { + rad := math.Atan2(float64(x), float64(y)) + deg := rad * (180 / math.Pi) + return 180 + -1*deg +} + +func day10ReadAsteroidMap(in io.Reader) (*day10MonitorGrid, error) { + var grid = &day10MonitorGrid{} + + raw, err := ioutil.ReadAll(in) + if err != nil { + return nil, errors.Wrap(err, "Unable to read asteroid map") + } + + for _, c := range raw { + if c == '\n' { + if grid.width == 0 { + grid.width = len(grid.asteroidMap) + } + // Skip newlines for our representation + continue + } + + grid.asteroidMap = append(grid.asteroidMap, c) + } + + return grid, nil +} + +func solveDay10Part1Coordinate(inFile string) (*day10MonitorGrid, int, error) { + f, err := os.Open(inFile) + if err != nil { + return nil, 0, errors.Wrap(err, "Unable to open input file") + } + defer f.Close() + + grid, err := day10ReadAsteroidMap(f) + if err != nil { + return nil, 0, errors.Wrap(err, "Unable to read asteroid map") + } + + var ( + bestMonitorPos int + bestAsteroidCount int + ) + for _, pos := range grid.getAsteroidPositions() { + var aX, aY = grid.posToCoord(pos) + rGrid := grid.getCleanedGrid(aX, aY) + + if c := rGrid.asteroidCount(); c > bestAsteroidCount { + bestMonitorPos = pos + bestAsteroidCount = c + } + } + + return grid, bestMonitorPos, nil +} + +func solveDay10Part1(inFile string) (int, error) { + grid, bestMonitorPos, err := solveDay10Part1Coordinate(inFile) + if err != nil { + return 0, err + } + + var aX, aY = grid.posToCoord(bestMonitorPos) + return grid.getCleanedGrid(aX, aY).asteroidCount(), nil +} + +func solveDay10Part2(inFile string) (int, error) { + grid, bestMonitorPos, err := solveDay10Part1Coordinate(inFile) + if err != nil { + return 0, err + } + + var ( + mX, mY = grid.posToCoord(bestMonitorPos) + destroyed []int + ) + + // Mark monitor / laser -- cannot be destroyed + grid.asteroidMap[bestMonitorPos] = 'M' + + // Gradually destroy asteroids + for grid.asteroidCount() > 0 { + asteroidsInSight := grid.getCleanedGrid(mX, mY).getAsteroidPositions() + var degPos [][2]int + for _, pos := range asteroidsInSight { + var aX, aY = grid.posToCoord(pos) + + degPos = append(degPos, [2]int{ + pos, + int(grid.step2deg(aX-mX, aY-mY) * 1000000), // Degree to asteroid in 6-digit precision + }) + } + + // Sort by degree low-to-high -- represents order of destruction + sort.Slice(degPos, func(i, j int) bool { return degPos[i][1] < degPos[j][1] }) + + for _, dp := range degPos { + grid.asteroidMap[dp[0]] = '*' // Mark asteroids destroyed + destroyed = append(destroyed, dp[0]) + } + } + + destr200X, destr200Y := grid.posToCoord(destroyed[199]) + + return 100*destr200X + destr200Y, nil +} diff --git a/day10_input.txt b/day10_input.txt new file mode 100644 index 0000000..38fb895 --- /dev/null +++ b/day10_input.txt @@ -0,0 +1,23 @@ +.###..#......###..#...# +#.#..#.##..###..#...#.# +#.#.#.##.#..##.#.###.## +.#..#...####.#.##..##.. +#.###.#.####.##.####### +..#######..##..##.#.### +.##.#...##.##.####..### +....####.####.######### +#.########.#...##.####. +.#.#..#.#.#.#.##.###.## +#..#.#..##...#..#.####. +.###.#.#...###....###.. +###..#.###..###.#.###.# +...###.##.#.##.#...#..# +#......#.#.##..#...#.#. +###.##.#..##...#..#.#.# +###..###..##.##..##.### +###.###.####....######. +.###.#####.#.#.#.#####. +##.#.###.###.##.##..##. +##.#..#..#..#.####.#.#. +.#.#.#.##.##########..# +#####.##......#.#.####. diff --git a/day10_test.go b/day10_test.go new file mode 100644 index 0000000..474f086 --- /dev/null +++ b/day10_test.go @@ -0,0 +1,86 @@ +package aoc2019 + +import ( + "strings" + "testing" +) + +func TestDay10ReadAsteroidMap(t *testing.T) { + grid, err := day10ReadAsteroidMap(strings.NewReader(".#..#\n.....\n#####\n....#\n...##")) + if err != nil { + t.Fatalf("Asteroid map parser failed: %s", err) + } + + if grid.width != 5 { + t.Errorf("Wrong width detected: exp=5 got=%d", grid.width) + } + + if l := len(grid.asteroidMap); l != 25 { + t.Errorf("Wrong length of asteroid map detected: exp=25 got=%d", l) + } + + if c := grid.asteroidCount(); c != 10 { + t.Errorf("Wrong number of asteroids detected: exp=10 got=%d", c) + } +} + +func TestDay10GetCleanedGrid(t *testing.T) { + grid, err := day10ReadAsteroidMap(strings.NewReader(".#..#\n.....\n#####\n....#\n...##")) + if err != nil { + t.Fatalf("Asteroid map parser failed: %s", err) + } + + for expCount, coords := range map[int][2]int{ + 5: {4, 2}, + 6: {0, 2}, + 7: {1, 0}, + 8: {3, 4}, + } { + rGrid := grid.getCleanedGrid(coords[0], coords[1]) + + if c := rGrid.asteroidCount(); c != expCount { + t.Errorf("Wrong number of asteroids detected: exp=%d got=%d (grid=%s)", expCount, c, rGrid.asteroidMap) + } + } +} + +func TestDay10StepToDeg(t *testing.T) { + // Is a function on a day10MonitorGrid, grid itself is not used + grid, err := day10ReadAsteroidMap(strings.NewReader(".#..#\n.....\n#####\n....#\n...##")) + if err != nil { + t.Fatalf("Asteroid map parser failed: %s", err) + } + + for expDeg, vals := range map[float64][2]int{ + 0: {0, -5}, + 45: {5, -5}, + 90: {5, 0}, + 135: {5, 5}, + 180: {0, 5}, + 225: {-5, 5}, + 270: {-5, 0}, + 315: {-5, -5}, + } { + if d := grid.step2deg(vals[0], vals[1]); d != expDeg { + t.Errorf("Step to Degree yield unexpected result for %+v: exp=%.2f got=%.2f", vals, expDeg, d) + } + } +} + +func TestCalculateDay10_Part1(t *testing.T) { + count, err := solveDay10Part1("day10_input.txt") + if err != nil { + t.Fatalf("Day 10 solver failed: %s", err) + } + + t.Logf("Solution Day 10 Part 1: %d", count) +} + +func TestCalculateDay10_Part2(t *testing.T) { + res, err := solveDay10Part2("day10_input.txt") + if err != nil { + t.Fatalf("Day 10 solver failed: %s", err) + } + + t.Logf("Solution Day 10 Part 2: %d", res) +} diff --git a/helpers.go b/helpers.go index addc0b2..737a750 100644 --- a/helpers.go +++ b/helpers.go @@ -2,6 +2,15 @@ package aoc2019 import "math" +func greatestCommonDivisor(a, b int64) int64 { + for b != 0 { + t := b + b = a % b + a = t + } + return a +} + func manhattenDistance(x1, y1, x2, y2 int) int { return int(math.Abs(float64(x1-x2)) + math.Abs(float64(y1-y2))) } diff --git a/helpers_test.go b/helpers_test.go index fc7954d..cedee9a 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -2,6 +2,18 @@ package aoc2019 import "testing" +func TestGreatestCommonDivisor(t *testing.T) { + for expDivisor, vals := range map[int64][2]int64{ + 1: {2, 3}, + 2: {4, 6}, + -5: {-5, 15}, + } { + if r := greatestCommonDivisor(vals[0], vals[1]); r != expDivisor { + t.Errorf("Unexpected divisor for values %+v: exp=%d got=%d", vals, expDivisor, r) + } + } +} + func TestManhattenDistance(t *testing.T) { for expDist, p := range map[int][4]int{ 2: {0, 0, 0, 2},