If you haven’t already, I recommend referring to my introduction post to AOC2025 here, and you can see my solution here.

Puzzle

The Advent of Code story starts out with us arriving in the North Pole and working to save Christmas by setting up all the decorations by December 12th. In order to begin decorating, we need to gain access to the Secret Entrance, and to do that we need to figure out the door’s password. The password is hidden in a safe, and a cheat sheet (puzzle input) has been provided that allows access to the safe. The safe has a classic circular dial with values ranging from 0 to 99.

Safe Dial

The puzzle input is formatted as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
L68
L30
R48
L5
R60
L55
L1
L99
R14
L82

Each line is made up of 2 chunks.

  1. Direction
  2. Value

For example, L68 means turn the dial LEFT 68 positions, and R14 means turn the dial RIGHT 14. It’s important to remember that if you try to go higher than 99 the dial loops back to 0, and vice versa. Also, the dial always starts facing 50.

Part 1

In Part 1, the password to the secret entrance is the number of times the dial STOPS on 0. Using the above example would give:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    -> 50 (starting position)
L68 -> 82 
L30 -> 52
R48 -> 0 (1)
L5  -> 95
R60 -> 55
L55 -> 0 (2)
L1  -> 99
L99 -> 0 (3)
R14 -> 14
L82 -> 32

This means that the Secret Door password would be 3.

Solution Steps

  1. Parse the input file into lines
  2. Parse each line into its parts:
    • Direction
    • Steps
  3. Move dial to correct position
  4. Check if dial is on 0
  5. Handle edge conditions

Parsing Input

Zig’s Standard Library has a lot of helpful functions. In order to tokenize each line from the file, I used the functionsplitScalar. This will return string slices we can then iterate over.

1
2
3
4
var lines = std.mem.splitScalar(u8, input, '\n');
while (lines.next()) |line| {
    // Do stuff with each line
}

Parsing Lines

Because the parsed lines returned from splitScalar are string slices, we can use these slices to easily separate the line into its respective chunks. Since the direction will always be the first character, we can pull it out and use a switch statement.

1
2
3
4
5
switch (line[0]) {
        'R' => // Turn dial RIGHT,
        'L' => // Turn dial LEFT,
        else => return error.InvalidDirection,
    }

To get the number of steps from the remainder of the line’s string slice, we can utilize another Zig Standard Library function, parseInt.

1
var steps = try std.fmt.parseInt(i32, line[1..], 10)

Move Dial to Position

With all of the relevant information pulled from the input, we can move to the actual solution logic. If we treat moving RIGHT as clockwise and increasing the value, and LEFT as counterclockwise and decreasing the value, we can simply add or subtract the steps from our current position.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
var lines = std.mem.splitScalar(u8, input, '\n');
while (lines.next()) |line| {
    const ticks = try std.fmt.parseInt(i32, line[1..], 10);
    switch (line[0]) {
        'R' => lock_value += ticks,
        'L' => lock_value -= ticks,
        else => return error.InvalidDirection,
    }

    // Handle wrap-around
    if (lock_value < 0) {
        lock_value += 100;
    } else if (lock_value >= 100) {
        lock_value -= 100;
    }
}

Check Dial Position

At the end of each iteration, we need to check if the dial is at position 0 and add that to our final result for the password if that’s the case.

1
2
3
if (lock_value == 0) {
    code += 1;
}

Handle Edge Conditions

Just implementing the above solution would work for the example set. However, what do we do if the number of steps given is greater than 100? For example, what if the line was R10000? Luckily, due to the wrapping nature of the dial, every 100 steps is equivalent to 0, which means we can mod the value with 100. This changes our Position code to:

1
const ticks = @mod(try std.fmt.parseInt(i32, line[1..], 10), 100);

Part 1 Solution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pub fn part1(input: []const u8) !i32 {
    var code: i32 = 0;
    var lock_value: i32 = STARTING_LOCK_VALUE;
    var lines = std.mem.splitScalar(u8, input, '\n');
    while (lines.next()) |line| {
        const ticks = @mod(try std.fmt.parseInt(i32, line[1..], 10), 100);
        switch (line[0]) {
            'R' => lock_value += ticks,
            'L' => lock_value -= ticks,
            else => return error.InvalidDirection,
        }

        // Handle wrap-around
        if (lock_value < 0) {
            lock_value += 100;
        } else if (lock_value >= 100) {
            lock_value -= 100;
        }

        // Check for code match
        if (lock_value == 0) {
            code += 1;
        }
    }
    return code;
}

Part 2

After completing Part 1, we are informed that the password for the Secret Entrance is incorrect. The correct password is found not only when the dial stops on 0 but also every time it passes over 0. That would change the example input’s password from 3 to 6.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    -> 50 // starting position
L68 -> 82 (1) // passes over 0 once
L30 -> 52
R48 -> 0 (2) // dial stops on 0
L5  -> 95
R60 -> 55 (3) // passes over 0 once
L55 -> 0 (4) // dial stops on 0
L1  -> 99
L99 -> 0 (5) // dial stops on 0
R14 -> 14
L82 -> 32 (6) // passes over 0 once

Logical Changes

The core functions of our solution are very similar to Part 1. However, there are two main differences:

  1. We cannot just use mod to simplify directions that have values greater than 100.
  2. We need a way to count every time we pass 0, not just when we land on it.

In order to meet both of these requirements, I decided to brute-force the solution. Now, instead of using math to jump from one position to the next after each instruction, I manually increment or decrement each step. This works! Although it is probably not the most efficient method, it is extremely easy to follow the flow of the code through each scenario—similar to how you would solve it manually.

Part 2 Solution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pub fn part2(input: []const u8) !usize {
    var code: usize = 0;
    var lock_value: i32 = STARTING_LOCK_VALUE;
    var lines = std.mem.splitScalar(u8, input, '\n');
    while (lines.next()) |line| {
        const ticks = try std.fmt.parseInt(usize, line[1..], 10);

        for (0..ticks) |_| {
            if (lock_value == 0) {
                code += 1;
            }
            switch (line[0]) {
                'R' => lock_value += 1,
                'L' => lock_value -= 1,
                else => return error.InvalidDirection,
            }

            if (lock_value < 0) {
                lock_value += 100;
            } else if (lock_value >= 100) {
                lock_value -= 100;
            }
        }
    }
    return code;
}