This year I’m going through the Advent of Code as usual. But then I’m going back through it with GenAI just to see how it’s working. For more context see the introduction to this series of posts

Part 1

Human Solution

Pretty straightforward problem to start things off. More or less just typed it out. One bug with the direction parsing: initially I wrote “R” instead of ‘R’ which distinguishes between a string and a char.

function part1(path, size=100, start=50)
    clicks = 0
    dial = start
    for line in readlines(path)
        dir = line[1] == 'R' ? 1 : -1
        mag = parse(Int, line[2:end])

        dial += dir * mag
        dial = mod(dial, size)

        clicks += (dial == 0)
    end
    return clicks
end

Gemini Solution

The LLM one-shot this which is not that surprising. It’s a little verbose but especially having solved the problem myself it’s intuitively obvious that the solution works. Much faster than me reading and then typing out my roughly identical solution.

function solve()
    # The dial starts by pointing at 50.
    current_position = 50
    zero_count = 0

    # Read the sequence of rotations from the input file.
    lines = readlines("input.txt")

    for line in lines
        # A rotation starts with an L or R.
        direction = line[1]
        # Then, the rotation has a distance value.
        distance = parse(Int, line[2:end])

        if direction == 'R'
            # Turning the dial to the right (toward higher numbers).
            current_position += distance
        elseif direction == 'L'
            # Turning the dial to the left (toward lower numbers).
            current_position -= distance
        end

        # Because the dial is a circle, use modular arithmetic.
        current_position = mod(current_position, 100)

        # The actual password is the number of times the dial is left
        # pointing at 0 after any rotation in the sequence.
        if current_position == 0
            zero_count += 1
        end
    end

    println("The password is: ", zero_count)
end

solve()

I’d probably edit this to be more or less what I did. One interesting point was that it errored out when it tried to run the script:

It appears the `julia` command is not available in this environment. This could be because Julia is not installed or its location is not in the system’s PATH.

It takes two to three minutes for the LLM to come back after those prompts. It’s not entirely clear how useful that time is; it’s short enough that I can’t quite focus on something else, but it’s long enough to be a substantial drag on time.

Part 2

The second problem seemed more likely to cause problems and indeed it did.

Human Solution

It pushed my brain a little bit more too; I needed to think through the cases especially the case where it starts at zero. Needed a couple test cases to be confident it was correct.

function part2(path, size=100, start=50)
    clicks = 0
    dial = start
    for line in readlines(path)
        dir = line[1] == 'R' ? 1 : -1
        mag = parse(Int, line[2:end])

        # Wrap around to avoid messing up the sign
        if dial == 0 && dir == -1
            dial = 100
        end

        # Deal with multiple rotations
        clicks += div(abs(mag) - 1, size)
        dial += dir * sign(mag) * mod(abs(mag), size)

        # Check the final rotation
        clicks += (dial > size || dial < 0)
        dial = mod(dial, size)
        clicks += (dial == 0)
    end

    return clicks
end

Gemini Solution

Not surprising given it was more complicated so it took about 5 minutes for the LLM to come up with a solution. And it came back with an incorrect answer.

# It's not just about where the dial lands, but how many times it passes 0.
# We can use integer division to solve this efficiently without simulating every click.

function solve_part2()
    # The dial still starts at 50.
    current_position = 50
    # This will be our password.
    total_zero_count = 0

    # Read the same instructions.
    lines = readlines("input.txt")

    for line in lines
        direction = line[1]
        distance = parse(Int, line[2:end])

        start_pos = current_position

        if direction == 'R'
            # For a right turn, the number of times we pass 0 is the number of
            # full 100-unit cycles we complete.
            # div(a, b) in Julia is equivalent to floor(a / b).
            crossings = div(start_pos + distance, 100) - div(start_pos, 100)
            total_zero_count += crossings
            current_position = mod(start_pos + distance, 100)
        elseif direction == 'L'
            # For a left turn, the logic is similar but with negative numbers.
            # The number of crossings is ceil(start/100) - ceil(end/100).
            # This can be expressed with floor as:
            # floor(-(start - distance) / 100) - floor(-start / 100)
            end_pos_unmod = start_pos - distance
            crossings = div(-end_pos_unmod, 100) - div(-start_pos, 100)
            total_zero_count += crossings
            current_position = mod(end_pos_unmod, 100)
        end
    end

    println("The new password is: ", total_zero_count)
end

solve_part2()

It’s not clear that not having solved it myself I would know what to make of this. So I just responded that it’s too low and to fix it. Shockingly that worked.

You are absolutely right to point that out. My apologies, there was a subtle bug in my previous script that would indeed cause the count to be too low.

The issue was in how I translated the mathematical `floor` function into Julia code. I used the `div` function, which performs truncating division (rounding towards zero). However, the correct logic requires floor division (always rounding down). For the left rotations, this difference is critical and was causing some zero-crossings to be missed.

The correct Julia function for floor division is `fld`.

I have corrected the script to use `fld` instead of `div`. This will now accurately count all the zero crossings as per the problem description.

I will now update the `gemini_part2.jl` file with the corrected code. Please run it again.

# It's not just about where the dial lands, but how many times it passes 0.
# We can use integer division to solve this efficiently without simulating every click.

function solve_part2_fixed()
    # The dial still starts at 50.
    current_position = 50
    # This will be our password.
    total_zero_count = 0

    # Read the same instructions.
    lines = readlines("input.txt")

    for line in lines
        direction = line[1]
        distance = parse(Int, line[2:end])

        start_pos = current_position

        if direction == 'R'
            end_pos_unmod = start_pos + distance
            # fld(a, b) performs floor division, which is what we need.
            # The previous use of div() was incorrect as it truncates towards zero,
            # causing undercounts for negative number scenarios in left turns.
            # While div works for positive numbers, fld is used here for correctness and consistency.
            crossings = fld(end_pos_unmod, 100) - fld(start_pos, 100)
            total_zero_count += crossings
            current_position = mod(end_pos_unmod, 100)
        elseif direction == 'L'
            end_pos_unmod = start_pos - distance
            # The formula for left turns relies on floor division with negative numbers,
            # so using fld() is critical for a correct result.
            crossings = fld(-end_pos_unmod, 100) - fld(-start_pos, 100)
            total_zero_count += crossings
            current_position = mod(end_pos_unmod, 100)
        end
    end

    println("The new password is: ", total_zero_count)
end

solve_part2_fixed()

On a stylistic note I find the suffixing adorable: I presume if this kept on going it’d end up as solve_part2_fixed_final_v2. Unlike the first part I don’t think I could review this myself without solving the problem. The comments are particularly unhelpful especially as they refer to previous versions of the code which is irrelevant now. I suppose I could have gone back and asked it to revise the comments?

With the back and forth it’s not clear that this was indeed faster than me especially given that I would have to grok the solution myself before feeling confident about submitting. One thing that would be helpful though is that it could automate some of the unit tests that I was informally using myself to check my own logic.

The incorrect answer is particularly interesting in the context of data analysis. To some extent I’d probably have only caught that it was incorrect when I found an incorrect answer. There’s no such guardrails for data analysis: it’d simply be wrong and hard to discover unless I either failed to replicate it (with some known correct code) or it gave such an incorrect answer that I could intuit something went wrong.

It’s interesting to see the costs. At the end you get a summary: Agent active time was 12 minutes and 9 seconds. It sent 157431 input tokens to 2.5 pro and got back 2337 output tokens. Of that 61957 tokens were served from cache and thus are cheaper. At the current pricing of \$1.25 per million input tokens and \$10 per million tokens output tokens that’s less than a quarter.