Skip to main content

Advent of Code - Day 01: Spinning Dials and Surprising Zeros

Jeppe Lillevang Salling 4 minutes read

Today’s puzzle turned out to be a great example of how a seemingly trivial problem hides a neat little twist — and how visualizing your algorithm can completely change how you think about it.

The setup: You’re given a giant dial with 100 positions (0–99). You start at position 50. Then you receive instructions like:

L10
R47
L3
...  

Each one telling you to rotate the dial left or right by a number of clicks.

Easy enough.

Until you look at what you’re actually asked to compute.

Part 1: Only Count the Landing Spots

Part 1 asks:

How many times do you land on position 0 after completing a full move?

Not during the spin — only the final position of each instruction matters.

That leads to a straightforward simulation:

  • Parse each instruction

  • Update the position with modular arithmetic

  • Count how many times the result is exactly zero

It’s simple, fast, and very “AoC warm-up” territory.

I generated a small animation so you can actually see the dial as each instruction resolves. Here’s Part 1 running through my input:

Animation of part 1 of advent of code 2025

Part 2: Every Click Counts

Part 2 changes the rules entirely:

Instead of only counting where you end, count every time the dial passes through position 0 — even during long spins.

This can blow up drastically. A single instruction like R5000000 might pass zero tens of thousands of times

A naive simulation would be:

for each move:
    for each click:
        pos = (pos ± 1) mod 100
        if pos == 0: count += 1  

…and that obviously does not scale.

Fortunately, after staring at the problem long enough (and a bit of scratch-paper modular math), you can derive a constant-time formula for each move:

  • The first time you’d hit 0 depends on your starting position and direction

  • After that, you hit 0 every full rotation

  • Number of hits = 1 + (remaining_distance // 100)

This makes Part 2 extremely fast.

But since pure math is boring, here’s the per-click animated version so you can see the dial spinning like a malfunctioning slot machine:

Animation of part 2 of advent of code 2025

And yes — the animation is absolutely useless for solving the puzzle efficiently, but extremely useful for understanding what’s going on.

On to tomorrow’s puzzle!

The Code

In my Github repo you can find my cleaned up python solution, an elixir version I made after the python solution and the code I used for making the visualization

Python solution:

from typing import Iterable, List, Tuple, Literal

Direction = Literal["L", "R"]
Move = Tuple[Direction, int]

DIAL_SIZE = 100
START_POS = 50


def parse_moves(raw: str) -> List[Move]:
    moves: List[Move] = []

    for line in raw.splitlines():
        line = line.strip()
        if not line:
            continue

        direction = line[0]
        if direction not in ("L", "R"):
            raise ValueError(f"Invalid direction: {direction}")

        distance = int(line[1:])
        moves.append((direction, distance))

    return moves


def move(pos: int, direction: Direction, distance: int) -> int:
    if direction == "R":
        return (pos + distance) % DIAL_SIZE
    else:  # "L"
        return (pos - distance) % DIAL_SIZE


def compute_password(moves: Iterable[Move]) -> int:
    pos = START_POS
    hits_zero = 0

    for direction, distance in moves:
        pos = move(pos, direction, distance)
        if pos == 0:
            hits_zero += 1

    return hits_zero


def count_zero_hits_during_rotation(pos: int, direction: Direction, distance: int) -> int:
    if distance <= 0:
        return 0

    if direction == "R":
        first_step = (DIAL_SIZE - pos) % DIAL_SIZE
    else:  # "L"
        first_step = pos % DIAL_SIZE

    # If first_step == 0, it means we'd hit 0 after a full turn
    if first_step == 0:
        first_step = DIAL_SIZE

    if distance < first_step:
        return 0

    return 1 + (distance - first_step) // DIAL_SIZE


def compute_password_method_click(moves: Iterable[Move]) -> int:
    pos = START_POS
    hits_zero = 0

    for direction, distance in moves:
        hits_zero += count_zero_hits_during_rotation(pos, direction, distance)
        pos = move(pos, direction, distance)

    return hits_zero


if __name__ == "__main__":
    with open("input", "r", encoding="utf-8") as f:
        raw = f.read()

    moves = parse_moves(raw)

    print(f"Part 1: {compute_password(moves)}")
    print(f"Part 2: {compute_password_method_click(moves)}")