Advent of Code 2024 Week 1

Christof Reimers

2311 words, 12 mins

Here we are, once again. Advent of Code is back at it again and I'm taking part. Last year I stopped after 11 days and didn't feel like writing a post about it. But this year I changed a few things.

Preamble

This years Advent of Code started with me not starting it. I did not do Day 1 on December 1st. But in the evening on December 2nd I eventually came around and did it. I was a bit in a hurry and slapped something together in the TypeScript Playground, meaning no fancy reading the input, no fancy tests, no fancy nothing, not even git (which is madness).

This shitty approach has now become somewhat my way of doing things this year: a little bit shit and sloppy. But I wouldn't say the code is shit, I would say it's (at least a little bit) ✨art✨.

In this post I will showcase my solutions and say something (probably not much) about each solution.

Self imposed rules

Over the past week a specific way of doing things revealed itself, again and again: Writing everything in one line.

Some might think: Isn't that not unreadable and easy? Just remove all the linebreaks...

But that is not the route I took. I talking more about a "logical" line. Meaning no place where I would usually place a semicolon or a curly bracket. This comes with two kinds of problems.

Functions

Functions have an obvious solution: Lambads. If I don't reuse that piece of code I also don't use a function. Makes the code less readable, but that is somewhat the point.

1function x(param) {
2 <some code>
3}

becomes

1const x = param => <some code>;

Well, something isn't looking right there.

Assignments

Assignments without creating a new line are impossible. Is what I would have said a week ago. Then I stumbled across IIFEs.

1const x = <some value>;
2<some code using x>

becomes

1(x => <some code using x>)(<some value>)

While it reverses the read order of things, it eliminates the need for a linebreak.

Array methods

One thing I always find interesting is how "easy" Advent of Code solutions are to write using the methods of JavaScript arrays. That specifically means map, reduce, filter, some and so on. These combined with lambdas and IIFEs should result in the perfect toolset to write unreadable one liners with horrible performance. Excuse me, I meant, to write ✨art✨.

The only exception to this is a field called input which contains a long multiline string, which is obviously the input from the website. This field is in the same local scope as the code for each day, so that when the IIFE for a day gets executed, the input is ready to be used.

The week

As you will see the code evolved a bit and I didn't stick 100% to the rules, especially on day 6. And with the evolution of the code, the rules also changed and evolved and so I would say without going back and "prettifying" the previous days, not every day is a "perfect" solution.

Day 1: Historian Hysteria

1(
2 (
3 solveA: (arr0: number[], arr1: number[]) => number,
4 solveB: (arr0: number[], arr1: number[]) => number,
5 input: number[][]
6 ) =>
7 ((arr0, arr1) =>
8 console.log(`Part A: ${solveA(arr0, arr1)}\nPart B: ${solveB(arr0, arr1)}`))(
9 input.map((i) => i[0]).sort((a, b) => a - b),
10 input.map((i) => i[1]).sort((a, b) => a - b)
11 )
12)(
13 (arr0, arr1) =>
14 arr0.reduce((prev, curr, i) => prev + Math.abs(curr - arr1[i]), 0),
15 (arr0, arr1) =>
16 arr0
17 .map((v0) => v0 * arr1.filter((v1) => v1 === v0).length)
18 .reduce((prev, curr) => prev + curr),
19 input
20 .trim()
21 .split("\n")
22 .map((l) => l.split(" ").map((v) => +v))
23);

Here you can see the whole shebang in all it's glory. As you can see, I am actually using TypeScript, which should prevent me from doing stupid mistakes. But it doesn't prevent me from writing stupid code. The first day was really quite easy, so let's continue with day 2.

Day 2: Red-Nosed Reports

1((solveA, solveB, input: number[][]) =>
2 console.log(`Part A: ${solveA(input)}\nPart B: ${solveB(input)}`))(
3 (input: number[][]) =>
4 input.filter(
5 (rep) =>
6 rep
7 .slice(1)
8 .map((v, i) => rep[i] - v)
9 .reduce(
10 (prev, curr) => [
11 prev[0] &&
12 Math.abs(curr) >= 1 &&
13 Math.abs(curr) <= 3 &&
14 (prev[1] === 0 || prev[1] === Math.sign(curr)),
15 prev[1] === 0 ? Math.sign(curr) : prev[1],
16 ],
17 [true, 0]
18 )[0]
19 ).length,
20 (input: number[][]) =>
21 input.filter(
22 (rep) =>
23 rep
24 .slice(1)
25 .map((v, i) => rep[i] - v)
26 .reduce(
27 (prev, curr) => {
28 if (!prev[0]) {
29 return prev;
30 }
31 const hasSign = prev[1] !== 0;
32 const nextSign = !hasSign ? Math.sign(curr) : prev[1];
33 let valid =
34 Math.abs(curr) >= 1 &&
35 Math.abs(curr) <= 3 &&
36 (!hasSign || prev[1] === Math.sign(curr));
37 let damped = prev[2];
38 if (!valid && !damped) {
39 damped = true;
40 valid = true;
41 }
42 return [valid, nextSign, damped];
43 },
44 [true, 0, false]
45 )[0]
46 ).length,
47 input
48 .trim()
49 .split("\n")
50 .map((l) => l.split(" ").map((v) => +v))
51);

Here you can see one of the days where I didn't fully commit to the rules. But still being the first day, these rules didn't exist yet. I actually went back to make it less readable!

This was also just getting me started, so nothing special here.

Day 3: Mull It Over

1((solve, inputA: string[] | null, inputB: string[] | null) =>
2 console.log(`Part A: ${solve(inputA)}\nPart B: ${solve(inputB)}`))(
3 (input: string[] | null) =>
4 input
5 ?.map((mul) =>
6 mul
7 .slice(4, -1)
8 .split(",")
9 .map((v) => +v)
10 )
11 .map((mul) => mul[0] * mul[1])
12 .reduce((prev, curr) => prev + curr, 0),
13 input.match(/mul(d{1,3},d{1,3})/g),
14 input
15 .split("do()")
16 .map((dos) => dos.split("don't()")[0])
17 .join("")
18 .match(/mul(d{1,3},d{1,3})/g)
19);

This is quite the short one. As you can see I used a fancy regex to find all valid instructions and then parsed them out. I think I also solved the second part quite elegantly, splitting the input beforehand on dos, and splitting the dos on donts, discarding everything after the first entry. This leaves only the sections between a do and a dont to check with the regex.

1((
2 transpose: (matrix: string[][]) => string[][],
3 diagonals: (matrix: string[][]) => string[][],
4 flipX: (matrix: string[][]) => string[][]
5) =>
6 ((solveA, solveB, input) =>
7 console.log(`Part A: ${solveA(input)}\nPart B: ${solveB(input)}`))(
8 (matrix: string[][]) =>
9 [
10 ...matrix,
11 ...transpose(matrix),
12 ...diagonals(matrix),
13 ...diagonals(flipX(matrix)),
14 ]
15 .map((line) => line.join(""))
16 .map(
17 (line) =>
18 (line.match(/XMAS/g)?.length ?? 0) +
19 (line.match(/SAMX/g)?.length ?? 0)
20 )
21 .reduce((p, c) => p + c, 0),
22 (matrix: string[][]) =>
23 [
24 matrix,
25 transpose(matrix),
26 flipX(matrix),
27 flipX(transpose(matrix)),
28 ].reduce(
29 (prev, mat) =>
30 mat.reduce(
31 (prev, row, y) =>
32 row.reduce(
33 (prev, _, x) =>
34 +[/M.S/, /.A./, /M.S/].every((reg, i) =>
35 reg.test(mat[y + i]?.slice(x, x + 3).join(""))
36 ) + prev,
37 0
38 ) + prev,
39 0
40 ) + prev,
41 0
42 ),
43 input
44 .trim()
45 .split("\n")
46 .map((line) => line.split(""))
47 ))(
48 (matrix) =>
49 matrix[0].map((_, colIndex) => matrix.map((row) => row[colIndex])),
50 (matrix) =>
51 Array(matrix.length + matrix[0].length)
52 .fill(0)
53 .map((_, dIdx) =>
54 matrix.reduce(
55 (arr, row, y) => [...row.filter((_, x) => x + y === dIdx), ...arr],
56 []
57 )
58 ),
59 (matrix) => matrix.map((line) => line.reduce((p, c) => [c, ...p], [] as string[]))
60);

This one was a doozy. As you might (or might not) see, I am transforming the whole board to check rows with a regex. Also instead of just finding all As for the second part and checking its corners, I bruteforce myself through the whole board. Nobody ever said this should be efficient, right?

With this one, I even pass the helper functions to the IIFE to keep it in one line.

Day 5: Print Queue

1((solveA, solveB, input: [[number, number][], number[][]]) =>
2 console.log(`Part A: ${solveA(input)}\nPart B: ${solveB(input)}`))(
3 ([rules, updates]: [[number, number][], number[][]]) =>
4 updates
5 .filter(
6 (update) =>
7 ![
8 rules.filter((rule) => rule.every((num) => update.includes(num))),
9 ].some(
10 (rules) =>
11 !update.every((num, i) =>
12 rules
13 .filter((rule) => rule.includes(num))
14 .every(
15 (rule) =>
16 (rule[0] === num && update.indexOf(rule[1]) > i) ||
17 (rule[1] === num && update.indexOf(rule[0]) < i)
18 )
19 )
20 )
21 )
22 .map((arr) => arr[Math.floor(arr.length / 2)])
23 .reduce((p, c) => p + c, 0),
24 ([rules, updates]: [[number, number][], number[][]]) =>
25 updates
26 .filter(
27 (update) =>
28 ![
29 rules.filter((rule) => rule.every((num) => update.includes(num))),
30 ].some(
31 (rules) =>
32 !update.some((num, i) =>
33 rules
34 .filter((rule) => rule.includes(num))
35 .some(
36 (rule) =>
37 (rule[0] === num && update.indexOf(rule[1]) < i) ||
38 (rule[1] === num && update.indexOf(rule[0]) > i)
39 )
40 )
41 )
42 )
43 .map((update) =>
44 update.sort((a, b) =>
45 ((rule) => (rule ? rule.indexOf(a) - rule.indexOf(b) : 0))(
46 rules.find((rule) => rule.includes(a) && rule.includes(b))
47 )
48 )
49 )
50 .map((arr) => arr[Math.floor(arr.length / 2)])
51 .reduce((p, c) => p + c, 0),
52 input
53 .split("\n\n")
54 .map((val) => val.split("\n"))
55 .map((val, i) =>
56 i === 0
57 ? val.map((rule) => rule.split("|"))
58 : val.map((update) => update.split(","))
59 )
60 .map((section) =>
61 section.map((line) => line.map((num) => +num))
62 ) as [[number, number][], number[][]]
63);

Here, something funny happened. I didn't read the instructions right and fumbled around for at least half an hour, wondering why it didn't work. Well, I should learn to read I guess.

The actual solution is once again quite easy and I can make use of JavaScripts array sorting method.

Day 6: Guard Gallivant

1type Vec = {
2 x: number;
3 y: number;
4};
5
6type Field = {
7 blocked: boolean;
8 guard: string;
9 visited: string;
10};
11
12type Guard = {
13 pos: Vec;
14 dir: number;
15};
16
17const directions = [
18 {
19 x: 0,
20 y: -1,
21 },
22 {
23 x: 1,
24 y: 0,
25 },
26 {
27 x: 0,
28 y: 1,
29 },
30 {
31 x: -1,
32 y: 0,
33 },
34];
35
36function addVec(a: Vec, b: Vec): Vec {
37 return {
38 x: a.x + b.x,
39 y: a.y + b.y,
40 };
41}
42
43function retrieve<T>(arr: T[][], pos: Vec): T | undefined {
44 const line = arr[pos.y];
45 if (!line) {
46 return undefined;
47 }
48 return line[pos.x];
49}
50
51function insideBoard<T>(arr: T[][], pos: Vec): boolean {
52 return (
53 0 <= pos.y && pos.y < arr.length && 0 <= pos.x && pos.x < arr[0].length
54 );
55}
56
57function getBoard(text: string): Field[][] {
58 return text.split("\n").map((line) =>
59 line.split("").map((c) => ({
60 blocked: c == "#",
61 visited: "^>v<".includes(c) ? c : "",
62 guard: "^>v<".includes(c) ? c : "",
63 }))
64 );
65}
66
67function getGuard(arr: Field[][]): Guard {
68 return arr.reduce(
69 (p, line, y) => [
70 ...p,
71 ...line.reduce(
72 (p, c, x) =>
73 c.guard ? [...p, { pos: { x, y }, dir: "^>v<".indexOf(c.guard) }] : p,
74 [] as { pos: Vec; dir: number }[]
75 ),
76 ],
77 [] as { pos: Vec; dir: number }[]
78 )[0];
79}
80
81let board = getBoard(input);
82let guard = getGuard(board);
83
84do {
85 const nextPosition = addVec(guard.pos, directions[guard.dir]);
86 const nextDirection = (guard.dir + 1) % directions.length;
87 if (retrieve(board, nextPosition)?.blocked) {
88 guard.dir = nextDirection;
89 } else {
90 guard.pos = nextPosition;
91 const nextField = retrieve(board, nextPosition);
92 if (nextField && !nextField.visited.includes("^>v<"[guard.dir])) {
93 nextField.visited += "^>v<"[guard.dir];
94 }
95 }
96} while (insideBoard(board, guard.pos));
97
98const visitedFields = board.reduce(
99 (p, line, y) => [
100 ...p,
101 ...line.reduce(
102 (p, val, x) => (val.visited.length > 0 ? [...p, { x, y }] : p),
103 [] as Vec[]
104 ),
105 ],
106 [] as Vec[]
107);
108
109console.log(visitedFields.length);
110
111const obstacles: Vec[] = [];
112
113for (const pos of visitedFields) {
114 board = getBoard(input);
115 guard = getGuard(board);
116 board[pos.y][pos.x].blocked = true;
117
118 let notlooped = true;
119 do {
120 const nextPosition = addVec(guard.pos, directions[guard.dir]);
121 const nextDirection = (guard.dir + 1) % directions.length;
122 if (retrieve(board, nextPosition)?.blocked) {
123 guard.dir = nextDirection;
124 } else {
125 guard.pos = nextPosition;
126 const nextField = retrieve(board, nextPosition);
127 if (nextField) {
128 if (!nextField.visited.includes("^>v<"[guard.dir])) {
129 nextField.visited += "^>v<"[guard.dir];
130 } else {
131 notlooped = false;
132 }
133 }
134 }
135 } while (insideBoard(board, guard.pos) && notlooped);
136 if (!notlooped) {
137 obstacles.push(pos);
138 }
139}
140console.log(obstacles.length);

On this day, I definitely broke all of my own rules. I don't think the effort it would take is worth it, you can really see how this is by far the longest one. Of course there are a few type declarations at the beginning, but still.

While the first part was really trivial, the second part took me longer than I imagined. I ended up checking all cells the guard visits and if blocking it would result in a loop (Meaning he reaches a cell he already visited, going in the same direction). I guess that is a smart way to brute force things.

Day 7: Bridge Repair

1((
2 solve: (
3 input: {
4 result: number;
5 numbers: number[];
6 }[],
7 operators: string[]
8 ) => number,
9 input: {
10 result: number;
11 numbers: number[];
12 }[]
13) =>
14 console.log(
15 `Part A: ${solve(input, ["+", "*"])}\nPart B: ${solve(input, ["+", "*", "||"])}`
16 ))(
17 (input, operators) =>
18 input.reduce(
19 (p, { result, numbers }) =>
20 Array(Math.pow(operators.length, numbers.length - 1))
21 .fill(0)
22 .some(
23 (_, i) =>
24 result ===
25 ((ops) =>
26 numbers
27 .slice(1)
28 .map((n, i) => ({ n, o: ops[i] }))
29 .reduce(
30 (p, { n, o }) =>
31 o === "+" ? p + n : o === "*" ? p * n : +`${p}${n}`,
32 numbers[0]
33 ))(
34 Number(i)
35 .toString(operators.length)
36 .padStart(numbers.length - 1, "0")
37 .split("")
38 .map((v) => operators[+v])
39 )
40 )
41 ? p + result
42 : p,
43 0
44 ),
45 input.split("\n").map((line) =>
46 (([res, numbers]) => ({
47 result: +res,
48 numbers: numbers?.split(" ").map((num) => +num),
49 }))(line.split(": "))
50 )
51);

I gotta say, today was definitely the most fun I had so far. The whole thing is really just about binary counting. The code in line 34-38 is really just converting number into base 2 or 3 and then mapping this number onto the operators, resulting in a set of operators to use for each equation.

I actually solved this using a version where I created an array with all possible combinations of operators but this definitely saves a bit of memory.

And why am I basically brute forcing stuff, again?

Closing words (for this week)

While I either had in mind to not take part or to do it fancy, I did not expect to do it this sloppily.

But taking part in that way is better than not taking part. Also this week had a weird focus on brute forcing, or at least brute forcing in a smart way. I think I found these puzzles way easier than the one from the last years, maybe my perspective is just skewed.

Anyway, hopefully till next week!