From 05460f77028b6ab65deb643bd49ed0dfbb3e09a6 Mon Sep 17 00:00:00 2001 From: Shaun Burdick Date: Wed, 4 Dec 2024 15:17:15 -0500 Subject: [PATCH] Day 4: Over engineered! --- README.md | 1 + day-4/README.md | 82 ++++++++++++++++++++ day-4/main.go | 189 +++++++++++++++++++++++++++++++++++++++++++++ day-4/main_test.go | 165 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 437 insertions(+) create mode 100644 day-4/README.md create mode 100644 day-4/main.go create mode 100644 day-4/main_test.go diff --git a/README.md b/README.md index c9fff1e..d48288e 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Each day will be setup as a separate folder. - [Day 1](/day-1/) - Historian Hysteria - [Day 2](/day-2/) - Red-Nosed Reports - [Day 3](/day-3/) - Mull It Over +- [Day 4](/day-4/) - Ceres Search ## Environment Setup diff --git a/day-4/README.md b/day-4/README.md new file mode 100644 index 0000000..d040c5f --- /dev/null +++ b/day-4/README.md @@ -0,0 +1,82 @@ +# Day 4 - Ceres Search + +## Part 1 + +"Looks like the Chief's not here. Next!" One of The Historians pulls out a device and pushes the only button on it. After a brief flash, you recognize the interior of the Ceres monitoring station! + +As the search for the Chief continues, a small Elf who lives on the station tugs on your shirt; she'd like to know if you could help her with her word search (your puzzle input). She only has to find one word: XMAS. + +This word search allows words to be horizontal, vertical, diagonal, written backwards, or even overlapping other words. It's a little unusual, though, as you don't merely need to find one instance of XMAS - you need to find all of them. Here are a few ways XMAS might appear, where irrelevant characters have been replaced with .: + +``` +..X... +.SAMX. +.A..A. +XMAS.S +.X.... +``` + +The actual word search will be full of letters instead. For example: + +``` +MMMSXXMASM +MSAMXMSMSA +AMXSXMAAMM +MSAMASMSMX +XMASAMXAMM +XXAMMXXAMA +SMSMSASXSS +SAXAMASAAA +MAMMMXMMMM +MXMXAXMASX +``` + +In this word search, XMAS occurs a total of 18 times; here's the same word search again, but where letters not involved in any XMAS have been replaced with .: + +``` +....XXMAS. +.SAMXMS... +...S..A... +..A.A.MS.X +XMASAMX.MM +X.....XA.A +S.S.S.S.SS +.A.A.A.A.A +..M.M.M.MM +.X.X.XMASX +``` + +Take a look at the little Elf's word search. How many times does XMAS appear? + +## Part 2 + +The Elf looks quizzically at you. Did you misunderstand the assignment? + +Looking for the instructions, you flip over the word search to find that this isn't actually an XMAS puzzle; it's an X-MAS puzzle in which you're supposed to find two MAS in the shape of an X. One way to achieve that is like this: + +``` +M.S +.A. +M.S +``` + +Irrelevant characters have again been replaced with . in the above diagram. Within the X, each MAS can be written forwards or backwards. + +Here's the same example from before, but this time all of the X-MASes have been kept instead: + +``` +.M.S...... +..A..MSMS. +.M.S.MAA.. +..A.ASMSM. +.M.S.M.... +.......... +S.S.S.S.S. +.A.A.A.A.. +M.M.M.M.M. +.......... +``` + +In this example, an X-MAS appears 9 times. + +Flip the word search from the instructions back over to the word search side and try again. How many times does an X-MAS appear? diff --git a/day-4/main.go b/day-4/main.go new file mode 100644 index 0000000..4e0a436 --- /dev/null +++ b/day-4/main.go @@ -0,0 +1,189 @@ +package main + +import ( + _ "embed" + "flag" + "fmt" + "log" + "strings" + + file "github.com/shaunburdick/advent-of-code-2024/lib" +) + +var input string + +func init() { + // do this in init (not main) so test file has same input + inputFile, err := file.LoadRelativeFile("input.txt") + if err != nil { + log.Println(err) + } + + input = strings.TrimRight(inputFile, "\n") +} + +func main() { + var part int + flag.IntVar(&part, "part", 1, "part 1 or 2") + flag.Parse() + fmt.Println("Running part", part) + + if part == 1 { + ans := part1(input) + fmt.Println("Output:", ans) + } else { + ans := part2(input) + fmt.Println("Output:", ans) + } +} + +const MAS = "MAS" +const XMAS = "X" + MAS + +func part1(input string) int { + parsed := parseInput(input) + foundWords := 0 + // start traversing the grid + for y, row := range parsed { + for x := range row { + coord := Coords{x, y} + foundWords += xmasSearch(parsed, "", coord, AllDirections) + } + } + + return foundWords +} + +func part2(input string) int { + parsed := parseInput(input) + grid := Grid{parsed} + foundMas := 0 + // start traversing the grid + for y, row := range grid.Data { + if y == 0 || y == len(grid.Data)-1 { + // skip edges + continue + } + + for x, char := range row { + if x == 0 || x == len(row)-1 { + // skip edges + continue + } + if char == 'A' { + coord := Coords{x, y} + wing1 := string(grid.CharAt(coord.Direction(NorthWest))) + "A" + string(grid.CharAt(coord.Direction(SouthEast))) + wing2 := string(grid.CharAt(coord.Direction(NorthEast))) + "A" + string(grid.CharAt(coord.Direction(SouthWest))) + + if (wing1 == "MAS" || wing1 == "SAM") && (wing2 == "MAS" || wing2 == "SAM") { + foundMas += 1 + } + } + } + } + + return foundMas +} + +type Grid struct { + Data []string +} + +func (g Grid) CharAt(c Coords) rune { + return rune(g.Data[c.y][c.x]) +} + +type Coords struct { + x int + y int +} + +type Direction int + +const ( + NorthWest Direction = iota + North + NorthEast + West + East + SouthWest + South + SouthEast +) + +var AllDirections []Direction = []Direction{ + NorthWest, + North, + NorthEast, + West, + East, + SouthWest, + South, + SouthEast, +} + +// Generate a list of valid coordinates from the existing one +// Will only return valid positive values +func (c Coords) Directions() []Coords { + directions := []Coords{} + + for _, dir := range AllDirections { + newCoord := c.Direction(dir) + // check for bounds + if newCoord.x >= 0 && newCoord.y >= 0 { + directions = append(directions, newCoord) + } + } + + return directions +} + +func (c Coords) Direction(d Direction) Coords { + switch d { + case NorthWest: + return Coords{x: c.x - 1, y: c.y - 1} + case North: + return Coords{x: c.x, y: c.y - 1} + case NorthEast: + return Coords{x: c.x + 1, y: c.y - 1} + case West: + return Coords{x: c.x - 1, y: c.y} + case East: + return Coords{x: c.x + 1, y: c.y} + case SouthWest: + return Coords{x: c.x - 1, y: c.y + 1} + case South: + return Coords{x: c.x, y: c.y + 1} + case SouthEast: + return Coords{x: c.x + 1, y: c.y + 1} + default: + return c + } +} + +func xmasSearch(grid []string, currentWord string, coords Coords, directions []Direction) int { + foundWords := 0 + newWord := currentWord + string(grid[coords.y][coords.x]) + if newWord == XMAS { + // we match! + return 1 + } else if len(newWord) >= len(XMAS) || !strings.HasPrefix(XMAS, newWord) { + // we don't match! + return 0 + } else { + // we partially match, keep looking! + for _, direction := range directions { + newCoord := coords.Direction(direction) + // check for bounds + if newCoord.x >= 0 && newCoord.y >= 0 && newCoord.y < len(grid) && newCoord.x < len(grid[newCoord.y]) { + foundWords += xmasSearch(grid, newWord, newCoord, []Direction{direction}) + } + } + } + + return foundWords +} + +func parseInput(input string) (ans []string) { + return strings.Split(input, "\n") +} diff --git a/day-4/main_test.go b/day-4/main_test.go new file mode 100644 index 0000000..128a33b --- /dev/null +++ b/day-4/main_test.go @@ -0,0 +1,165 @@ +package main + +import ( + "reflect" + "testing" + + file "github.com/shaunburdick/advent-of-code-2024/lib" +) + +type TestDeclaration struct { + name string + input string + want int + run bool +} + +var example1 = `MMMSXXMASM +MSAMXMSMSA +AMXSXMAAMM +MSAMASMSMX +XMASAMXAMM +XXAMMXXAMA +SMSMSASXSS +SAXAMASAAA +MAMMMXMMMM +MXMXAXMASX` + +func Test_day4_part1(t *testing.T) { + tests := []TestDeclaration{ + { + name: "example", + input: example1, + want: 18, + run: true, + }, + { + name: "actual", + input: input, + want: 2358, + run: file.ExistsRelativeFile("input.txt"), + }, + } + for _, tt := range tests { + if tt.run { + t.Run(tt.name, func(t *testing.T) { + if got := part1(tt.input); got != tt.want { + t.Errorf("part1() = %v, want %v", got, tt.want) + } + }) + } + } +} + +func Benchmark_day4_part1(b *testing.B) { + for i := 0; i < b.N; i++ { + part1(example1) + } +} + +var example2 = `MMMSXXMASM +MSAMXMSMSA +AMXSXMAAMM +MSAMASMSMX +XMASAMXAMM +XXAMMXXAMA +SMSMSASXSS +SAXAMASAAA +MAMMMXMMMM +MXMXAXMASX` + +func Test_day4_part2(t *testing.T) { + tests := []TestDeclaration{ + { + name: "example", + input: example2, + want: 9, + run: true, + }, + { + name: "actual", + input: input, + want: 1737, + run: file.ExistsRelativeFile("input.txt"), + }, + } + for _, tt := range tests { + if tt.run { + t.Run(tt.name, func(t *testing.T) { + if got := part2(tt.input); got != tt.want { + t.Errorf("part2() = %v, want %v", got, tt.want) + } + }) + } + } +} + +func Benchmark_day4_part2(b *testing.B) { + for i := 0; i < b.N; i++ { + part2(example2) + } +} + +func Test_Coords_Directions(t *testing.T) { + t.Run("0,0", func(t *testing.T) { + expected := []Coords{ + { + x: 1, + y: 0, + }, + { + x: 0, + y: 1, + }, + { + x: 1, + y: 1, + }, + } + result := Coords{0, 0}.Directions() + if !reflect.DeepEqual(expected, result) { + t.Error(result) + } + }) + + t.Run("5,3", func(t *testing.T) { + expected := []Coords{ + { + x: 4, + y: 2, + }, + { + x: 5, + y: 2, + }, + { + x: 6, + y: 2, + }, + { + x: 4, + y: 3, + }, + { + x: 6, + y: 3, + }, + { + x: 4, + y: 4, + }, + { + x: 5, + y: 4, + }, + { + x: 6, + y: 4, + }, + } + result := Coords{5, 3}.Directions() + if !reflect.DeepEqual(expected, result) { + t.Error(result) + } + }) +}