Skip to main content

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:

0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899

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: 0

In 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!

Want to receive updates straight in your inbox?

Subscribe to the newsletter

Comments