Advent of Code 2024

Day 04 Ceres Search

Description

"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?

main.ts

import { readFileSync } from "fs";

function solve<T>(inputFile: string, solution: (input: string) => T): T {
    const data = readFileSync(inputFile, "utf-8").trim()
    const solved = solution(data)
    return solved
}

type Crossword = string[][]

function parse(input: string): Crossword {
    return input.split("\n").map(line => line.split(""))
}

type WordsParams = {
    crossword: Crossword
    length: number,
    startingAt: [number, number]
}

function forwardWord({ crossword, length, startingAt }: WordsParams): string | null {
    const word = crossword[startingAt[0]]
        .slice(startingAt[1], startingAt[1] + length)
        .join("")
    return word.length == length ? word : null
}

function backwardWord({ crossword, length, startingAt }: WordsParams): string | null {
    const word = crossword[startingAt[0]]
        .slice(startingAt[1] - length + 1, startingAt[1] + 1)
        .reverse()
        .join("")
    return word.length == length ? word : null
}

function forwardDiagonalWord({ crossword, length, startingAt }: WordsParams): string | null {
    let word = [];

    if (startingAt[0] + length > crossword.length - 1) {
        return null
    }

    for (let offset = 0; offset < length; offset++) {
        let row = startingAt[0] + offset
        let column = startingAt[1] + offset

        if (crossword[row][column] !== undefined) {
            word.push(crossword[row][column])
        }
    }

    return word.length == length ? word.join("") : null
}

function backwardDiagonalWord({ crossword, length, startingAt }: WordsParams): string | null {
    let word = [];

    if (startingAt[0] - length < 0) {
        return null
    }
    for (let offset = 0; offset < length; offset++) {
        let row = startingAt[0] - offset
        let column = startingAt[1] - offset

        if (crossword[row][column] !== undefined) {
            word.push(crossword[row][column])
        }
    }

    return word.length == length ? word.join("") : null
}

function wordsInCrossWord(params: WordsParams): string[] {
    return [forwardWord, backwardWord, backwardDiagonalWord, forwardDiagonalWord]
        .map(fn => fn(params))
        .filter(w => w !== null)
}

function wordCount(targetWord: string, params: WordsParams): number {
    return wordsInCrossWord(params).filter(a => a == targetWord).length
}


function part1(input: string): number {
    const crossword = parse(input)
    const targetWord = "XMAS"

    let count = 0;
    for (let row = 0; row < crossword.length; row++) {
        for (let col = 0; col < crossword[row].length; col++) {
            count += wordCount(targetWord, { crossword, length: targetWord.length, startingAt: [row, col] })
        }
    }

    return count
}

function part2(input: string): number {
    return 0
}

console.log("sample")
console.log(`Part 1: ${solve("src/day-04/sample-input.txt", part1)}`)
// console.log(`Part 2: ${solve("src/day-XX/sample-input.txt", part2)}`)

// console.log("final")
// console.log(`Part 1: ${solve("src/day-XX/input.txt", part1)}`)
// console.log(`Part 2: ${solve("src/day-XX/input.txt", part2)}`)