Day 13/2021 - Transparent Origami

Day thirteen of AoC 2021 (available at https://adventofcode.com/2021/day/13) marks the halfway point of the contest. We're activating a thermal imaging camera by folding a sheet of transparent paper marked with dots.

The input has two sections: coordinates of dots, and fold instructions. The folds are either along an x-axis (vertical fold, left) or y-axis (horizontal fold, up).

Parsing Two Sections

The input has dots, a blank line, then folds. My takeWhile helper comes in handy:

interface Dot {
    x: number;
    y: number;
}

interface Fold {
    direction: "x" | "y"; 
    amount: number;
}

const processInput = async (day: number) => {
    const input = await readInputLines(day);
    const dots = input.takeWhile(line => line !== "").map(line => {
        const [x, y] = line.split(",").map(n => parseInt(n, 10));
        return { x, y };
    });
    
    const folds = input.slice(dots.length + 1).map(line => {
        const direction = line[11] as "x" | "y";
        const amount = parseInt(line.slice(13), 10);
        return { direction, amount };
    });
    
    return { dots, folds };
};

The fold parsing uses substring extraction rather than regex. Each fold instruction is fold along x=123 or fold along y=45. Character 11 is always the direction (x or y), and character 13 onwards is the number. Regex would work, but this is simple enough that substring access is cleaner.

The as "x" | "y" cast tells TypeScript we know what we're doing. It can't verify at compile time that character 11 will always be x or y, but we can.

The Folding Math

When you fold paper, points on the near side stay put, and points on the far side mirror across the fold line.

The puzzle guarantees that dots never appear exactly on a fold line. This is important — it means we can skip that edge case entirely.

function foldDot(fold: Fold, dot: Dot): Dot {
    if (fold.direction === "x") {
        if (dot.x === fold.amount) {
            throw new Error(`Dot at the fold line x = ${fold.amount}`);
        }
        if (dot.x < fold.amount) {
            return dot;
        }
        const x = 2 * fold.amount - dot.x;
        return { x, y: dot.y };
    } else {
        if (dot.y === fold.amount) {
            throw new Error(`Dot at the fold line y = ${fold.amount}`);
        }
        if (dot.y < fold.amount) {
            return dot;
        }
        const y = 2 * fold.amount - dot.y;
        return { x: dot.x, y };
    }
}

The formula 2 * fold.amount - dot.x handles the reflection. If you fold at position 5 and have a dot at position 7, it moves to position 3. At position 9, it moves to position 1. The math works out cleanly.

I verified this analytically during the solve: if a dot is at position 7 and we fold at 5, the difference is 2, so the new position is 5 - 2 = 3. Generalizing: fold - (dot - fold) = 2 * fold - dot.

Part One: Single Fold

Part one asks for the number of unique dots after just the first fold:

const partOne = (input: Input, debug: boolean) => {
    const fold = input.folds[0];
    const dots = input.dots.map(dot => foldDot(fold, dot));
    
    const uniqueDots = [...new Set(dots.map(dot => dot.x + "," + dot.y))];
    return uniqueDots.length;
};

The uniqueness check converts dots to strings and uses a Set. JavaScript objects don't have value equality, so {x: 1, y: 2} and {x: 1, y: 2} are different objects. String serialization gives us the comparison we need.

Part Two: Reveal the Code

Part two applies all folds, then asks us to read the result. This is one of those puzzles where the answer is literally spelled out — we need to render the final dot pattern and read the letters:

const partTwo = (input: Input, debug: boolean) => {
    let dots = input.dots.map(dot => ({...dot}));
    
    for (const fold of input.folds) {
        dots = dots.map(dot => foldDot(fold, dot));
    }
    
    const maxx = dots.max(dot => dot.x);
    const maxy = dots.max(dot => dot.y);
    
    const matrix = Array(maxy + 1).fill(0).map(() => Array(maxx + 1).fill(0));
    for (const dot of dots) {
        matrix[dot.y][dot.x] = 1;
    }
    printMatrix(matrix, x => x ? "#" : " ");
    return 0;
};

Note the spread operator ({...dot}) to copy the dots — I don't want to mutate the original input in case I need to inspect it later.

One gotcha: I initially had the x and y flipped when building the matrix. Copilot helpfully "fixed" my code in exactly the wrong direction. The matrix is indexed as matrix[y][x] (row first, then column), which is the opposite of how we usually think about x/y coordinates.

The final output rendered as FJAHJGAH — sounds like Klingon, but it activated the thermal camera.

Performance

Both parts run nearly instantly. We could optimize by deduplicating dots between folds, but with only 12 folds and a manageable number of dots, there's no need. Most of the execution time is actually spent printing the final grid.

Takeaways

  1. Abuse stated guarantees: The puzzle says dots never appear on fold lines. Instead of handling that edge case gracefully, I threw an error. If the guarantee is violated, I want to know immediately.

  2. Substring parsing has its place: Regex is powerful, but for fixed-format strings like fold along x=7, character indexing is faster to write and easier to read.

  3. Work through the math: Rather than guess-and-check the reflection formula, taking a minute to derive it analytically (2 * fold - position) made the implementation straightforward.

  4. Watch your axes: Matrix indexing ([row][col]) and coordinate systems (x, y) are natural opposites. This will bite you at least once per Advent of Code.

The full solution is available in the repository.