Skip to content

Commit

Permalink
Add Day 4
Browse files Browse the repository at this point in the history
  • Loading branch information
dominique-mueller committed Dec 4, 2024
1 parent 01a798f commit 8a2ad25
Show file tree
Hide file tree
Showing 5 changed files with 451 additions and 1 deletion.
93 changes: 93 additions & 0 deletions day-4/README.md
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?**
24 changes: 24 additions & 0 deletions day-4/day-4.test.ts
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);
});
});
192 changes: 192 additions & 0 deletions day-4/day-4.ts
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;
};
Loading

0 comments on commit 8a2ad25

Please sign in to comment.