Advent of Code 2025, part 1
My journey through AoC 2025, days 1-6
13 min read
Welcome to Advent of Code 2025 š
On this page I will be documenting my struggles, solutions and, sometimes visualizations for days of this challenge. Letās begin!
Boilerplate
Before we start for real though, thereās some repeating code to read the file that is passed as an argument:
import { readFile } from "node:fs/promises";
import { argv, exit } from "node:process";
const filename = argv[2];
if (!filename) {
console.error("expect filename");
exit(1);
}
const input = (await readFile(filename, "utf-8")).trim();
Every day begins with this boilerplate, so Iāll include it once at the beginning.
Day 1
The task is pretty straightforward (as I find it later), but I started over-complicating the solution, and it backfired quickly.
Iāve solved the first part mathematically:
const instructions = input.split("\n");
class Safe {
#dial: number;
cnt: number;
constructor(initial: number) {
this.#dial = initial;
this.cnt = 0;
}
turnLeft(amount: number) {
this.#dial = (((this.#dial - amount) % 100) + 100) % 100;
this.#checkCounter();
}
turnRight(amount: number) {
this.#dial = (this.#dial + amount) % 100;
this.#checkCounter();
}
#checkCounter() {
if (this.#dial === 0) {
this.cnt++;
}
}
}
const safe = new Safe(50);
for (const instruction of instructions) {
const dir = instruction.substring(0, 1);
const amount = parseInt(instruction.substring(1));
if (dir === "L") {
safe.turnLeft(amount);
} else {
safe.turnRight(amount);
}
}
console.log("part 1:", safe.cnt);
But for the second part⦠Well, I couldnāt figure out how to calculate everything essentially in one operation. So I switched back to simulating the whole process, and in doing so, Iāve simplified and refactored it a lot:
type Direction = "L" | "R";
class Safe {
#dial: number;
part1: number;
part2: number;
constructor(initial = 50) {
this.#dial = initial;
this.part1 = 0;
this.part2 = 0;
}
turn(direction: Direction, amount: number) {
const dir = direction === "L" ? -1 : 1;
for (let i = 0; i < amount; ++i) {
this.#dial += dir;
this.#dial %= 100;
if (!this.#dial) {
this.part2++;
}
}
if (!this.#dial) {
this.part1++;
}
}
}
const safe = new Safe();
for (const instruction of instructions) {
const direction = instruction.substring(0, 1) as Direction;
const amount = parseInt(instruction.substring(1));
safe.turn(direction, amount);
}
console.log("part 1:", safe.part1);
console.log("part 2:", safe.part2);
And this was quite a good starting point for visualization, that I set out to implement, after seeing some inspiring visuals on Reddit.
Here it is:
You can click āSimulateā to start an animation and āResetā to start over.
Day 2
The second day was more straightforward to me, and it begins with parsing, which is basically splitting ranges and splitting every range individually:
const ranges = input.split(",").map((r) => r.split("-").map(Number));
The first part of the puzzle is to find the āinvalidā numbers in the ranges that weāve just parsed, and sum them up. An invalid number is a number in which the sequence of digits repeats twice. For example 11, 123123 or 1122511225. Iāve decided to split every numberās string representation in the middle and just compare two parts. Hereās my implementation:
const invalid1: number[] = [];
for (const [start, end] of ranges) {
for (let n = start; n <= end; ++n) {
const s = n.toString();
if (s.length % 2 !== 0) {
continue;
}
const [left, right] = [
s.substring(0, s.length / 2),
s.substring(s.length / 2),
];
if (left === right) {
invalid1.push(n);
}
}
}
const part1 = invalid1.reduce((prev, curr) => prev + curr, 0);
console.log("part 1:", part1);
Additionally, Iām skipping the odd-length numbers, as they wonāt have repeating sequences anyway.
The second part was trickier though. Now, the sequences of numbers can repeat more than two times. For example, 121212 previously would have been considered valid, but with new rules, it will be considered invalid.
To implement this logic, Iām iterating over all possible sizes (from 1 and up to s.length / 2) and splitting the string into multiple parts. And to verify whether the number is invalid Iām comparing every part. Hereās my implementation:
const invalid2: number[] = [];
for (const [start, end] of ranges) {
for (let n = start; n <= end; ++n) {
const s = n.toString();
for (let size = 1; size <= Math.floor(s.length / 2); ++size) {
if (s.length % size !== 0) {
continue;
}
const parts: string[] = [];
for (let i = 0; i < s.length; i += size) {
parts.push(s.substring(i, i + size));
}
if (parts.every((p) => p === parts[0])) {
invalid2.push(n);
break;
}
}
}
}
const part2 = invalid2.reduce((prev, curr) => prev + curr, 0);
console.log("part 2:", part2);
You can notice a sneaky little break after weāve identified an invalid number, it is needed there to avoid counting the same number multiple times. For example, number 222222 can be split up as ['2','2','2','2','2','2'] or ['22','22','22'] or ['222','222'] which all satisfy our requirement. And in the first version Iāve pushed the same number multiple times š
Which led to over-counting.
Another way to solve this issue was to store all of the invalid numbers in a Set, as it allows only unique numbers. But this approach is much simpler, in my opinion.
Day 3
On this day, the task was really interesting: find consecutive maximums in array of digits. But letās start with parsing:
const banks = input.split("\n").map((line) => line.split("").map(Number));
Itās pretty straightforward - every line is transformed into array of numbers.
For the first idea, I immediately jumped into solving the problem with brute force approach, and it worked fine for 2 maximums that are required in the first part. But for the second part you need to find 12 maximums! Of course, you can write 12 for loops inside of one another š But I took a step back to re-think my brute-force solution and improve it to be linear.
The idea is to greedily find the maximum number possible, remember it, and then start looking for maximum but from the next index of the previous one. The main catch for me in this task was to limit the index that we are looking for. For example, if we are looking for the first number, we can look up to the batteries.length - 11 index, for second - up to batteries.length - 10 and when looking for 12th - up to the end of the array.
Hereās how I implemented both parts in one function:
function findTotalJoltage(size: number) {
const maximums: number[] = [];
for (const batteries of banks) {
let max = 0;
let maxIdx = -1;
for (let s = size - 1; s >= 0; --s) {
let maxInner = -1;
for (let i = maxIdx + 1; i < batteries.length - s; ++i) {
if (batteries[i] > maxInner) {
maxInner = batteries[i];
maxIdx = i;
}
}
max = max * 10 + maxInner;
}
maximums.push(max);
}
return maximums.reduce((prev, curr) => prev + curr, 0);
}
console.log("part 1:", findTotalJoltage(2));
console.log("part 2:", findTotalJoltage(12));
Thereās also a bonus trick here: instead of converting every digit back to string, concatenating and then parsing the whole result back to number, this line:
max = max * 10 + maxInner;
Will instead directly add a digit to the number!
Day 4
Fourth day was a breeze: I literally solved it in 10 minutes!
Letās take a look how, first by parsing the input:
const initialGrid = input.split("\n").map((line) => line.split(""));
Again, the parsing is straightforward - just split the lines and then split every character.
The core of the task is to iterate over all cells and identify whether the cell is accessible or not. Iām doing it with the help of a trusty directions array which just contains the coordinate differences for all 8 neighbors. And then Iām iterating over all neighbors of the given cell and counting rolls. Hereās how it looks:
const ROLL = "@";
const EMPTY = ".";
const directions = [
[-1, -1],
[-1, 0],
[-1, 1],
[0, -1],
[0, 1],
[1, -1],
[1, 0],
[1, 1],
];
function isAccessible(grid: string[][], row: number, col: number) {
const n = grid.length;
const m = grid[0].length;
let adjRolls = 0;
for (const [dr, dc] of directions) {
const [r, c] = [row + dr, col + dc];
if (r >= 0 && r < n && c >= 0 && c < m && grid[r][c] === ROLL) {
adjRolls++;
}
}
return adjRolls < 4;
}
Then, for the first part, itās only a matter of iterating over all cells in a grid and keeping a count of all accessible ones:
function countAccessibleRolls(grid: string[][]) {
const n = grid.length;
const m = grid[0].length;
let accessible = 0;
for (let row = 0; row < n; ++row) {
for (let col = 0; col < m; ++col) {
if (grid[row][col] !== ROLL) {
continue;
}
if (isAccessible(grid, row, col)) {
accessible++;
}
}
}
return accessible;
}
console.log("part 1", countAccessibleRolls(initialGrid));
For the second part thereās a twist: now we need to remove all accessible rolls until thereās no accessible rolls anymore. But the implementation is straightforward: iterate over all cells, save the coordinates that weāll be removing in set, and then remove them!
Hereās how it looks like:
function countRemovableRolls(grid: string[][]) {
const n = grid.length;
const m = grid[0].length;
let removable = 0;
while (countAccessibleRolls(grid) !== 0) {
const toRemove = new Set<string>();
for (let row = 0; row < n; ++row) {
for (let col = 0; col < m; ++col) {
if (grid[row][col] !== ROLL) {
continue;
}
if (isAccessible(grid, row, col)) {
toRemove.add(`${row}-${col}`);
}
}
}
for (const rollPos of toRemove) {
const [row, col] = rollPos.split("-").map(Number);
grid[row][col] = EMPTY;
removable++;
}
}
return removable;
}
console.log("part 2", countRemovableRolls(initialGrid));
The second part reuses both functions from part 1. And I built a little interactive playground below:
.
.
@
@
.
@
@
@
@
.
@
@
@
.
@
.
@
.
@
@
@
@
@
@
@
.
@
.
@
@
@
.
@
@
@
@
.
.
@
.
@
@
.
@
@
@
@
.
@
@
.
@
@
@
@
@
@
@
.
@
.
@
.
@
.
@
.
@
@
@
@
.
@
@
@
.
@
@
@
@
.
@
@
@
@
@
@
@
@
.
@
.
@
.
@
@
@
.
@
.
Accessible: 13
Removed: 0In this playground, accessible rolls are marked with red color. Every time when you click āRemoveā button, they will be removed, and the count of removed rolls will be increased by that amount. Count of accessible rolls will be recalculated. And, once you no longer have accessible rolls, āRemoveā button will be disabled. But you can click āResetā button to start over.
Day 5
First order of business - parsing. This time input is split into two parts; āfreshā ID ranges and available IDs. They have different formats: fresh IDs are pairs of numbers separated by ā-ā. Available IDs are represented simply as one number per line.
Hereās the code to implement parsing:
function parse(input: string) {
const [freshRangesStr, availableIngredientsStr] = input.split("\n\n");
const freshRanges = freshRangesStr
.split("\n")
.map((line) => line.split("-").map(Number));
freshRanges.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
const availableIngredients = availableIngredientsStr.split("\n").map(Number);
return { freshRanges, availableIngredients };
}
We need to find which of the available IDs are fresh. My first idea is to perform brute force: save all of the possible fresh IDs into a Set and iterate over all available products while identifying if they belong to a Set in O(1).
While sounding perfectly reasonable and working excellently on the example input, it fails on the full input. With an error that Iāve never seen before:
RangeError: Set maximum size exceeded
Turns out we canāt just put all of the numbers in a Set after all š
Thatās where the second idea comes into play: iterate over all available and inside iterate over all ranges, and if it falls within the range, increment the counter.
But thereās a slightly different problem: now Iām double-counting, because sometimes number falls into multiple ranges simultaneously š¤
But my LeetCode grind wasnāt for nothing after all - I remembered that I solved a similar problem before: 56. Merge Intervals
Hereās the code that merges ranges:
function merge(ranges: number[][]) {
const merged = [ranges[0]];
for (let i = 1; i < ranges.length; i++) {
const lastEnd = merged[merged.length - 1][1];
const [nextStart, nextEnd] = ranges[i];
if (nextStart <= lastEnd) {
merged[merged.length - 1][1] = Math.max(lastEnd, nextEnd);
} else {
merged.push(ranges[i]);
}
}
return merged;
}
The code above requires the intervals to be sorted already, thatās why that sneaky sort is included in the parse function š
Hereās how I implemented part 1 task:
function countFreshAvailable() {
const { freshRanges, availableIngredients } = parse(input);
const mergedFreshRanges = merge(freshRanges);
let cnt = 0;
for (const available of availableIngredients) {
for (const [start, end] of mergedFreshRanges) {
if (available >= start && available <= end) {
cnt++;
}
}
}
return cnt;
}
console.log("part 1", countFreshAvailable());
And with the merging implemented part 2 becomes even simpler:
function countAllFresh() {
const { freshRanges } = parse(input);
const mergedFreshRanges = merge(freshRanges);
let cnt = 0;
for (const [start, end] of mergedFreshRanges) {
cnt += end - start + 1;
}
return cnt;
}
console.log("part 2", countAllFresh());
Day 6
This day was tough š First part was relatively simple, hereās a parser to parse input for the first part:
function parse1(input: string) {
let lines = input.split("\n");
const parsed = lines
.slice(0, -1)
.map((line) => line.trim().split(/\s+/).map(Number));
const operations = lines[lines.length - 1].trim().split(/\s+/);
const nums: number[][] = [];
for (let i = 0; i < operations.length; ++i) {
const col: number[] = [];
for (let j = 0; j < parsed.length; ++j) {
col.push(parsed[j][i]);
}
nums.push(col);
}
return { nums, operations };
}
And the function that calculates the final results (after many iterations I made it so it works for the both parts):
function calculate({
nums,
operations,
}: {
nums: number[][];
operations: string[];
}) {
return nums
.map((col, idx) => {
let total = col[0];
const op = operations[idx];
for (let i = 1; i < col.length; ++i) {
if (op === "+") {
total += col[i];
} else if (op === "*") {
total *= col[i];
}
}
return total;
})
.reduce((prev, curr) => prev + curr, 0);
}
Hereās how Iām invoking everything:
console.log(calculate(parse1(input)));
But for the second part, parsing was tough. I had to take a break and go to the gym, and honestly - Iām thankful that I did. Because I was able to think about the problem more and āwriteā the pseudocode in the Notes on my phone. And after I got back to my laptop, I basically implemented my idea in like 15-20 minutes.
Hereās my implementation for the second part:
function parse2(input: string) {
let lines = input.split("\n");
const operations = lines[lines.length - 1].trim().split(/\s+/);
lines = lines.slice(0, -1);
const nums: number[][] = Array.from({ length: operations.length }, () => []);
let curr = 0;
for (let i = 0; i < operations.length; ++i) {
while (curr < lines[0].length) {
let num = "";
for (let j = 0; j < lines.length; ++j) {
num += lines[j][curr];
}
curr++;
num = num.trim();
if (!num) break;
nums[i].push(Number(num));
}
}
return { nums, operations };
}
And the invocation looks really similar:
console.log(calculate(parse2(input)));
Wrap up
Six days down, many more to go. If the cephalopods, rolls of papers, and safe dials are any indication, Advent of Code 2025 is just warming up. My brain may already be slightly overclocked ā but thatās part of the fun. See you in the next batch of puzzles!