-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
01a798f
commit 8a2ad25
Showing
5 changed files
with
451 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
# Day 4: Mull It Over | ||
|
||
<br> | ||
|
||
## 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](https://adventofcode.com/2019/day/10)! | ||
|
||
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 `.`: | ||
|
||
```txt | ||
..X... | ||
.SAMX. | ||
.A..A. | ||
XMAS.S | ||
.X.... | ||
``` | ||
|
||
The actual word search will be full of letters instead. For example: | ||
|
||
```txt | ||
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 `.`: | ||
|
||
```txt | ||
....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?** | ||
|
||
<br> | ||
|
||
## 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: | ||
|
||
```txt | ||
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-MAS`es have been kept instead: | ||
|
||
```txt | ||
.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?** |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { describe, it } from 'node:test'; | ||
import assert from 'node:assert'; | ||
import path from 'node:path'; | ||
import { countXMASAppearances, countXShapedMASAppearances } from './day-4'; | ||
|
||
describe('Day 4: Ceres Search', () => { | ||
const wordSearchFilePath = path.join(__dirname, 'word-search.txt'); | ||
|
||
it('Part 1: should count XMAS appearances', async () => { | ||
const expectedNumberOfXMASAppearances = 2462; // Verified for this dataset | ||
|
||
const numberOfXMASAppearances = await countXMASAppearances(wordSearchFilePath); | ||
|
||
assert.strictEqual(numberOfXMASAppearances, expectedNumberOfXMASAppearances); | ||
}); | ||
|
||
it('Part 2: should count X-shaped MAS appearances', async () => { | ||
const expectedNumberOfXShapedMASAppearances = 1877; // Verified for this dataset | ||
|
||
const numberOfXShapedMASAppearances = await countXShapedMASAppearances(wordSearchFilePath); | ||
|
||
assert.strictEqual(numberOfXShapedMASAppearances, expectedNumberOfXShapedMASAppearances); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
import fs from 'node:fs/promises'; | ||
|
||
/** | ||
* Read file | ||
*/ | ||
const readFile = async (filePath: string): Promise<string> => { | ||
const fileContents = await fs.readFile(filePath, { | ||
encoding: 'utf8', | ||
}); | ||
const normalizedFileContents = fileContents.trim().split(/\r?\n/).join('\n'); | ||
return normalizedFileContents; | ||
}; | ||
|
||
/** | ||
* Parse word search as grid | ||
*/ | ||
const parseWordSearchAsGrid = (wordSearchFileContents: string): Array<Array<string>> => { | ||
// Parse word search string into grid | ||
// Note: Sadly, we can only parse out a YX grid immediately | ||
const wordSearchYXGrid = wordSearchFileContents.split('\n').map((wordSearchLine) => { | ||
return wordSearchLine.split(''); | ||
}); | ||
|
||
// Transform YX grid into XY grid (mostly for it to be easier and more understandable to use) | ||
const wordSearchXYGrid: Array<Array<string>> = []; | ||
for (let y = 0; y < wordSearchYXGrid.length; y++) { | ||
for (let x = 0; x < wordSearchYXGrid[y].length; x++) { | ||
(wordSearchXYGrid[x] ??= [])[y] = wordSearchYXGrid[y][x]; | ||
} | ||
} | ||
|
||
// Done | ||
return wordSearchXYGrid; | ||
}; | ||
|
||
/** | ||
* Part 1: Count XMAS appearances | ||
*/ | ||
export const countXMASAppearances = async (wordSearchFilePath: string) => { | ||
// Get data | ||
const wordSearchFileContents = await readFile(wordSearchFilePath); | ||
const wordSearchGrid = parseWordSearchAsGrid(wordSearchFileContents); | ||
|
||
// Setup search parameters | ||
const searchTerm = 'XMAS'; | ||
// Setup all search directions (also covers reverse search terms) | ||
// Note: Coordinate system starts with 0-0 at top left, search directions run clockwise and start at the top | ||
const searchOffsets: Array<[x: number, y: number]> = [ | ||
[0, -1], // top | ||
[1, -1], // top-right | ||
[1, 0], // right | ||
[1, 1], // bottom-right | ||
[0, 1], // bottom | ||
[-1, 1], // bottom-left | ||
[-1, 0], // left | ||
[-1, -1], // top-left | ||
]; | ||
|
||
// Look at each coordinate ... | ||
const searchTermMatches: Array<Array<[x: number, y: number]>> = []; | ||
for (let x = 0; x < wordSearchGrid.length; x++) { | ||
for (let y = 0; y < wordSearchGrid[x].length; y++) { | ||
// Search into each direction ... | ||
for (let searchOffsetIndex = 0; searchOffsetIndex < searchOffsets.length; searchOffsetIndex++) { | ||
// Compare each search term character ... | ||
const searchTermMatch: Array<[x: number, y: number]> = []; | ||
for (let searchTermIndex = 0; searchTermIndex < searchTerm.length; searchTermIndex++) { | ||
// Find current coordinate | ||
const currentCoordinate: [x: number, y: number] = | ||
searchTermIndex === 0 | ||
? // Start with original coordinate for first character | ||
[x, y] | ||
: // Continue looking at the next coordinate based on previous match and search offset | ||
[ | ||
searchTermMatch[searchTermMatch.length - 1][0] + searchOffsets[searchOffsetIndex][0], | ||
searchTermMatch[searchTermMatch.length - 1][1] + searchOffsets[searchOffsetIndex][1], | ||
]; | ||
|
||
// Get character at coordinate | ||
// Note: Possibly undefined when going "off grid" | ||
const currentCharacter: string | undefined = wordSearchGrid[currentCoordinate[0]]?.[currentCoordinate[1]]; | ||
|
||
// Check whether the character matches the search term | ||
if (currentCharacter === searchTerm[searchTermIndex]) { | ||
searchTermMatch.push(currentCoordinate); | ||
} else { | ||
break; // Early exit | ||
} | ||
} | ||
|
||
// Accept match if all characters have been found | ||
if (searchTermMatch.length === searchTerm.length) { | ||
searchTermMatches.push(searchTermMatch); | ||
} | ||
} | ||
} | ||
} | ||
const numberOfSearchTermMatches = searchTermMatches.length; | ||
|
||
// !DEBUG: Print each match | ||
// for (let matchIndex = 0; matchIndex < searchTermMatches.length; matchIndex++) { | ||
// let searchTermResult = ''; | ||
// for (let coordinateIndex = 0; coordinateIndex < searchTermMatches[matchIndex].length; coordinateIndex++) { | ||
// searchTermResult += | ||
// wordSearchGrid[searchTermMatches[matchIndex][coordinateIndex][0]][searchTermMatches[matchIndex][coordinateIndex][1]]; | ||
// } | ||
// console.log(searchTermResult); | ||
// } | ||
|
||
// Done | ||
return numberOfSearchTermMatches; | ||
}; | ||
|
||
/** | ||
* Part 1: Count X-shaped MAS appearances | ||
*/ | ||
export const countXShapedMASAppearances = async (wordSearchFilePath: string) => { | ||
// Get data | ||
const wordSearchFileContents = await readFile(wordSearchFilePath); | ||
const wordSearchGrid = parseWordSearchAsGrid(wordSearchFileContents); | ||
|
||
// Setup search parameters | ||
const searchTerm = 'MAS'; | ||
// Setup X-shaped search directions (also covers reverse search terms) | ||
// Note: Coordinate system starts with 0-0 at top left, search directions run clockwise and start at the top-right | ||
const searchOffsets: Array<[x: number, y: number]> = [ | ||
[1, -1], // top-right | ||
[1, 1], // bottom-right | ||
[-1, 1], // bottom-left | ||
[-1, -1], // top-left | ||
]; | ||
|
||
// Look at each coordinate ... | ||
const searchTermMatches: Array<Array<[x: number, y: number]>> = []; | ||
for (let x = 0; x < wordSearchGrid.length; x++) { | ||
for (let y = 0; y < wordSearchGrid[x].length; y++) { | ||
// Search into each direction ... | ||
const searchTermMatchesForSearchOffsets: Array<Array<[x: number, y: number]>> = []; | ||
for (let searchOffsetIndex = 0; searchOffsetIndex < searchOffsets.length; searchOffsetIndex++) { | ||
// Compare each search term character ... | ||
const searchTermMatch: Array<[x: number, y: number]> = []; | ||
for (let searchTermIndex = 0; searchTermIndex < searchTerm.length; searchTermIndex++) { | ||
// Find current coordinate | ||
const currentCoordinate: [x: number, y: number] = | ||
searchTermIndex === 0 | ||
? // Start with search offset coordinate for first character | ||
[x + searchOffsets[searchOffsetIndex][0], y + searchOffsets[searchOffsetIndex][1]] | ||
: // Continue looking at the next coordinate based on previous match and reverse search offset | ||
[ | ||
searchTermMatch[searchTermMatch.length - 1][0] - searchOffsets[searchOffsetIndex][0], | ||
searchTermMatch[searchTermMatch.length - 1][1] - searchOffsets[searchOffsetIndex][1], | ||
]; | ||
|
||
// Get character at coordinate | ||
// Note: Possibly undefined when going "off grid" | ||
const currentCharacter: string | undefined = wordSearchGrid[currentCoordinate[0]]?.[currentCoordinate[1]]; | ||
|
||
// Check whether the character matches the search term | ||
if (currentCharacter === searchTerm[searchTermIndex]) { | ||
searchTermMatch.push(currentCoordinate); | ||
} else { | ||
break; // Early exit | ||
} | ||
} | ||
|
||
// Accept match if all characters have been found | ||
if (searchTermMatch.length === searchTerm.length) { | ||
searchTermMatchesForSearchOffsets.push(searchTermMatch); | ||
} | ||
} | ||
|
||
// Accept match if two matches along the diagonals (X-shape) have been found | ||
if (searchTermMatchesForSearchOffsets.length === 2) { | ||
searchTermMatches.push(searchTermMatchesForSearchOffsets.flat(1)); | ||
} | ||
} | ||
} | ||
const numberOfSearchTermMatches = searchTermMatches.length; | ||
|
||
// !DEBUG: Print each match | ||
// for (let matchIndex = 0; matchIndex < searchTermMatches.length; matchIndex++) { | ||
// let searchTermResult = ''; | ||
// for (let coordinateIndex = 0; coordinateIndex < searchTermMatches[matchIndex].length; coordinateIndex++) { | ||
// searchTermResult += | ||
// wordSearchGrid[searchTermMatches[matchIndex][coordinateIndex][0]][searchTermMatches[matchIndex][coordinateIndex][1]]; | ||
// } | ||
// console.log(searchTermResult); | ||
// } | ||
|
||
// Done | ||
return numberOfSearchTermMatches; | ||
}; |
Oops, something went wrong.