Learning Python with Advent of Code Walkthroughs

Dazbo's Advent of Code solutions, written in Python

Safe Dial

Advent of Code 2025 - Day 1

Day 1: Secret Entrance

Useful Links

Concepts and Packages Demonstrated

Modular ArithmeticInteger Division

Problem Intro

Welcome to Advent of Code 2025! I normally expect the first day to be a warm-up, but this one was a little tricky. Edge cases that were not included in the example input caught me out for a while.

We need to get into a safe to retrieve a password. The safe has a dial with numbers 0 through 99 arranged in a circle. The dial starts pointing at 50. As you turn the dial, it makes a small click at each number position.

The input contains rotation instructions, one per line. Each instruction starts with:

For example:

L68
L30
R48
L5
R60

The dial wraps around: turning left from 0 goes to 99, and turning right from 99 goes to 0.

Example rotations:

Part 1

What’s the actual password to open the door?

The password is the number of times the dial ends at position 0 after any rotation.

This is straightforward - we just need to track the dial position and count how many times we land on 0 after completing each rotation.

Solution Approach

We can use modular arithmetic to handle the circular dial:

Here’s the core logic:

def part1(data: list[str], start: int = 50, clicks: int = 100):
    zero_counter = 0
    curr_pos = start

    for instruction in data:
        direction = instruction[0]
        steps = int(instruction[1:])
        
        # Convert LEFT to equivalent RIGHT rotation
        if direction == "L":
            steps = clicks - steps
        
        curr_pos = (curr_pos + steps) % clicks
        if curr_pos == 0:
            zero_counter += 1

    return zero_counter

Why convert LEFT to RIGHT? This simplifies the logic. A left rotation of n steps is equivalent to a right rotation of 100 - n steps on a dial with 100 positions.

Part 2

Using password method 0x434C49434B, what is the password to open the door?

Now we need to count every click that lands on 0, not just the final position after each rotation. This includes:

For example, R1000 from position 50 would cross 0 ten times before returning to 50.

Solution 1: Simulation (The “Obvious” Approach)

The most straightforward way to solve this is to simply simulate every single click of the dial. If we’re told to rotate 60 times, we update the position 60 times, checking for 0 at each step.

def part2(data: list[str], start: int = 50, dial_nums: int = 100) -> int:
    zero_counter = 0
    curr_pos = start

    for instruction in data:
        direction = instruction[0]
        clicks = int(instruction[1:])
        
        for _ in range(clicks): # simulate EVERY click
            if direction == "R":
                curr_pos = (curr_pos + 1) % dial_nums
            else:  # direction == "L"
                curr_pos = (curr_pos - 1) % dial_nums
            
            if curr_pos == 0:
                zero_counter += 1
        
    return zero_counter

This works perfectly fine for the given input. The dial only has 100 positions, and the number of rotations isn’t huge. It runs in about 0.02 seconds.

Solution 2: Math (The “Optimized” Approach)

But what if the dial had a billion positions? Or if we had to rotate billions of times? The simulation approach is O(N) where N is the total number of clicks. We can do better - O(1) per instruction - using math.

For RIGHT rotations: We can calculate how many times we wrap around.

zero_counter += (curr_pos + clicks) // dial_nums
curr_pos = (curr_pos + clicks) % dial_nums

For LEFT rotations: This is slightly trickier to handle the wrap-around logic correctly.

if direction == "L":
    new_pos = (curr_pos - clicks) % dial_nums
    
    if curr_pos == 0:
        # Starting at 0: only count complete loops (not the starting position)
        zero_counter += clicks // dial_nums
    elif clicks > curr_pos:
        # We cross 0 at least once during the CCW rotation
        zero_counter += ((clicks - curr_pos - 1) // dial_nums) + 1
        # If we also END on 0, count that final landing
        if new_pos == 0:
            zero_counter += 1
    elif clicks == curr_pos:
        # We land exactly on 0
        zero_counter += 1
    
    curr_pos = new_pos

This approach runs in 0.002 seconds - an order of magnitude faster!

The Bug Hunt

Getting the math solution correct for Part 2 was a journey! Here’s what happened:

Initial Attempts

  1. First try Too Low: Wasn’t counting all zero crossings correctly
  2. Second try Too High: Overcounting zeros when starting at position 0
  3. Third try Still Wrong: Undercounting LEFT rotations ending at 0

Root Cause

The formula ((steps - curr_pos - 1) // clicks) + 1 correctly counts how many times we cross the 0 boundary, but when we end exactly on 0, that final landing wasn’t being counted.

The Fix

Added a check after calculating crossings for LEFT rotations:

elif steps > curr_pos:
    zero_counter += ((steps - curr_pos - 1) // clicks) + 1
    # If we also END on 0, count that final landing
    if new_pos == 0:
        zero_counter += 1

Results

The results look something like this:

Part 1: 1081
Part 2: 6689
Execution time: 0.002 seconds

Both parts run in just 2 milliseconds with the optimized solution!