seq = ["L68", "L30", "R48", "L5", "R60", "L55", "L1", "L99", "R14", "L82"]TLDR
Advent of code is a website created by Eric Wastl, releasing a (increasingly challenging) programming puzzle each day. This year we will not be receiving the usual 25 days worth (I have never made it that far anyway), but it will instead be finishing on 12th December (see announcement on Reddit). This is a welcome change of pace for me.
Spoiler alert: super un-optimised, but hopefully readable Julia solutions ahead, so you may only want to look after you have had a go yourself. Let’s see how we go…
Play along: https://adventofcode.com
Day 1: a password from a decoy safe 🔓
So we have this safe, with a dial set to 50, and a sequence of turns to make, see below:
Here an “R” means we are turning the dial to the right, and an “L” means we are turning it to the left, followed by the number indicating how many turns. “L68” means turn the dial left 68 times. The dial wraps around, so if we turn left from 0, we go to 99, 98, and so on.
We are asked:
- part 1: how many times do we end up on zero after a turn?
- part 2: how many times do we turn through zero in total?
Floor division fld() will count how many times we lap around zero (and in which direction).
function get_passwords(seq::Vector{String}, dial_start::Int = 50, dial_mod::Int = 100)
dial = dial_start; pass_zeros = 0; end_zeros = 0
for turn ∈ seq
direction = turn[begin]; turns = parse(Int, turn[2:end])
if direction == 'R'
pass_zeros += fld(dial + turns, dial_mod)
dial += turns
elseif direction == 'L'
# we need to catch the case where the dial starts at 0 and not count that first turn
pass_zeros -= fld(dial - turns - 1, dial_mod) - fld(dial - 1, dial_mod)
dial -= turns
end
dial = mod(dial, dial_mod)
if dial == 0
end_zeros += 1
end
end
return (end_zeros=end_zeros, pass_zeros=pass_zeros)
endget_passwords (generic function with 3 methods)
A nice opener, and I did make a mistake in my first guess for part 2, forgetting the case where the dial starts on zero and we turn left. We shouldn’t count that first turn as going through zero, and so I had to add the start and end points, with that -1 offset.
It appears I wasn’t the only one…
[2025 Day 1 Part 2] Public Service Announcement
byu/StaticMoose inadventofcode
seq |> get_passwords(end_zeros = 3, pass_zeros = 6)
⭐⭐
Day 2: fake IDs at the gift shop 🎁
We’re given these ‘product IDs’ from the gift shop, and we need to sort through them to find which ones are invalid. Problematic IDs that we are looking for are made up exclusively of repeating sequences of numbers.
product_ids = ["11-22", "95-115", "998-1012", "1188511880-1188511890", "222220-222224",
"1698522-1698528","446443-446449","38593856-38593862","565653-565659",
"824824821-824824827","2121212118-2121212124"]In part 1, we were looking for a single repeated sequence, so the first half of the ID is the same as the second half. Then in part 2, the length of that sequence could change. Here’s my solution:
function is_invalid(id::Int; part_1::Bool = false)
id_string = string(id); midpoint = length(id_string) ÷ 2
if part_1
return id_string[begin:midpoint] == id_string[midpoint+1:end]
end
for pattern_length ∈ 1:midpoint
# a pattern more than half the length can't repeat
if id_string == id_string[begin:pattern_length] ^ (length(id_string) ÷ pattern_length)
# is the id made *only* of this repeated pattern?
return true
end
end
return false
endis_invalid (generic function with 1 method)
The puzzle itself felt simpler than yesterday, but only after I read the description a few times. I initially thought that we were going to be asking to find IDs that contained some (any) repeated sequence, but knowing that the entire ID had to be a repetition simplified things.
invalid_ids = Int[]
for id ∈ product_ids
parts = split(id, "-")
id_start = parse(Int, parts[begin]); id_end = parse(Int, parts[end])
for id ∈ id_start:id_end
if is_invalid(id)
push!(invalid_ids, id)
end
end
end
invalid_ids |> sum4174379265
BenchmarkTools.jl tells me this runs in 459.768 ms, with lots of allocations 606.35 MiB, presumably due to all of the string splitting, concatenation, and going back and forth to integers. I don’t think I’ll have time to think about smarter ways to do this, but I’m sure I’ll be forced to in the coming days. Happy to be brute-forcing for now.
My young children happened to wake me up super early today, so I will have scored highly on the leaderboard, but I’m in it for the puzzles, not the glory.
⭐⭐
Day 3: maximum joltage ⚡
We find batteries, each with their own joltage rating, arranged in banks (rows).
Going from left to right, which batteries should we connect up to get the maximum joltage possible from each row?
joltages = [
"987654321111111"
"811111111111119"
"234234234234278"
"818181911112111"
]For instance, if we were asked to connect 4 batteries in the third row, we can’t make a 4 digit number higher than 4478 (selecting batteries 3 (joltage = 4), 6 (joltage = 4), 14 (joltage = 7), and 15 (joltage = 8)).
In part 1 we were asked to select 2 batteries from each bank, and in part 2 we were to select 12. Here’s a solution:
function max_joltage_for_bank(bank::String; n_batteries::Int = 2)
digits = parse.(Int, collect(bank))
max_joltage = []; index = 1
for k ∈ 1:n_batteries
digits_needed = n_batteries - k
search_end = length(digits) - digits_needed
max_val = -1; max_idx = -1
for i ∈ index:search_end
if digits[i] > max_val
# keep track of the biggest digit in this range and it's index...
max_val = digits[i]; max_idx = i
end
end
# ...so we can update the new starting point
index = max_idx + 1
push!(max_joltage, max_val)
end
return max_joltage |> join |> x -> parse(Int, x)
endmax_joltage_for_bank (generic function with 1 method)
This is based on selecting the maximum digit we can, whilst still leaving space for however many remaining batteries we need to select.
joltages |> x -> max_joltage_for_bank.(x, n_batteries = 12) |> sum3121910778619
Don’t forget to then update your starting index accordingly, rather than just adding 1, otherwise you’ll keep selecting the same battery over and over - although seeing this helpded me fix it.
Check out some smarter solutions than mine:
-❄️- 2025 Day 3 Solutions -❄️-
byu/daggerdragon inadventofcode
⭐⭐
Day 4: Dunder Mifflin warehouse 📄
There are only so many types of AoC puzzles, and today I recognised bts of code I write every year. Namely, checking where I can safely move in in a grid while staying within bounds.
We’re given the layout of warehouse floor - specifically, where a reams of paper are located @, and where there is an empty space ..
grid = [
"..@@.@@@@."
"@@@.@.@.@@"
"@@@@@.@.@@"
"@.@@@@..@."
"@@.@@@@.@@"
".@@@@@@@.@"
".@.@.@.@@@"
"@.@@@.@@@@"
".@@@@@@@@."
"@.@.@@@.@."
]We are asked:
- part 1: If we can only move reams with less than 4 neighbouring reams - how many can we move?
- part 2: Once we’ve removed them, we might be able to move more. How many are left that can never be moved?
Here’s a solution:
function find_accessible(warehouse::Matrix{Char}; n_aux::Int = 4)
n_rows, n_cols = size(warehouse); labelled_warehouse = copy(warehouse)
# all 8 directions: (row_offset, col_offset)
directions = [
(-1, -1), (-1, 0), (-1, 1),
( 0, -1), ( 0, 1),
( 1, -1), ( 1, 0), ( 1, 1)
]
for row ∈ 1:n_rows
for col ∈ 1:n_cols
warehouse[row, col] != '@' && continue # we're only checking the papers
neighbouring_papers = 0
for (Δr, Δc) ∈ directions
check_row, check_col = row + Δr, col + Δc
if 1 ≤ check_row ≤ n_rows && 1 ≤ check_col ≤ n_cols # stay in the grid!
if warehouse[check_row, check_col] == '@'
neighbouring_papers += 1
end
end
end
if neighbouring_papers < n_aux
labelled_warehouse[row, col] = 'x'
end
end
end
return labelled_warehouse
endfind_accessible (generic function with 1 method)
grid |>
lines -> [lines[r][c] for r ∈ eachindex(lines), c ∈ 1:length(lines[1])] |>
warehouse -> count(==('x'), find_accessible(warehouse))13
Not super elegeant. I replace the reams that can be moved with an x and then count them. It was important to make this change to a copy of the warehouse grid, otherwise we would be changing the grid as we go, and thinking there are less neighbouring reams than there actually are.
…and for part 2, we can just keep calling this function until there are no more reams that can be moved:
function remove_all_reams(grid::Matrix{Char})
labelled_warehouse = copy(grid)
while true
new_labelled_warehouse = find_accessible(labelled_warehouse)
new_labelled_warehouse == labelled_warehouse && break
labelled_warehouse = new_labelled_warehouse
end
return count(==('x'), labelled_warehouse)
endloevely visualisation on the reddit of clearing out the warehouse:
[2025 Day 4 Part 2]
byu/EverybodyCodes inadventofcode
⭐⭐
Day 5: fresh ingredients 🥗
Part 2 got me today.
Our inputs are a list of ranges, followed by ingredient id’s.
ingredients_db = [
"3-5"
"10-14"
"16-20"
"12-18"
"1"
"5"
"8"
"11"
"17"
"32"
]An ingredient is fresh if its id falls within any of the ranges given. After a little pre-processing, we can collect all of the ranges that fresh ingredient fall into
…because we suspect we will need that for part 2, even though they end up going in a different, more complicated direction.
fresh_ingredients = Dict{Int, Vector{UnitRange{Int}}}()
for i ∈ ingredients
for r ∈ ranges
if i ∈ r
push!(get!(fresh_ingredients, i, UnitRange{Int}[]), r)
end
end
endand then returning the number of fresh ingredients.
fresh_ingredients |> length3
Part 2 instead asks to find the number of possible fresh ingredients i.e. how many unique ingredient IDs fall within any of the given ranges.
…and in the real input data, the ranges were large, and overlapped. So, in cases where there was overlap, I combined them into a single new range (with the smaller of the start values, and larger of the end values).
sorted_ranges = sort(ranges, by = first)
fresh_ingredients_merged = [sorted_ranges[begin]]
for range ∈ sorted_ranges[2:end]
# start a new range when the lower value is outside the largest value of the merge
if last(fresh_ingredients_merged[end]) < first(range)
push!(fresh_ingredients_merged, range)
else
# the start of the new range stays the same, but the end needs to be extended if it's bigger for the new range
new_max_range = [last(fresh_ingredients_merged[end]), last(range)] |> maximum
fresh_ingredients_merged[end] = first(fresh_ingredients_merged[end]):new_max_range
end
end…and then summed the lengths of the now non-overlapping ranges (without duplicate ingredient IDs):
[length(x) for x ∈ fresh_ingredients_merged] |> sum14
⭐⭐
Day 6: Cephalopod Maths 🐙
I googled cephalopod - it means a creature with a head and tentacles.
In the advent of code universe, they get maths homework, and this is what it looks like:
sums = [
"123 328 51 64 ",
" 45 64 387 23 ",
" 6 98 215 314",
"* + * + "
]This is asking us to apply the operator in the last row, to the numbers above it. In part 1, the numbers above the first (leftmost) * are 123, 45, 6. In part 2 we are told the numbers are written in coluns (not rows) so we instead have to multiply 356, 24, and 1.
So the difference is in fiddling the parsing of the numbers. Here’s a solution:
function solve_sums(lines::Vector{String}; part_2::Bool = false)
n_cols = lines[end] |> length
# need to break until the next sum when there is a column of spaces
no_sum = [all(line[c] == ' ' for line ∈ lines) for c ∈ 1:n_cols]
results = Int[]; col = 1
while col <= n_cols
# skip the separator columns that split the sums
while col <= n_cols && no_sum[col]
col += 1
end
col > n_cols && break
# otherwise we start a new sum
start_sum = col
# and keep going until the next separator
while col <= n_cols && !no_sum[col]
col += 1
end
# the operator is always in the first col of each sum, in the last row
op = lines[end][start_sum]
if part_2
# pt.2: each column is a number (read top-down)
numbers = Int[]
for nums_col ∈ start_sum:col-1
digits = [line[nums_col] for line ∈ lines[1:end-1] if line[nums_col] != ' ']
if !isempty(digits)
push!(numbers, parse(Int, String(digits)))
end
end
else
# pt.1: each row is a number, but they can have leading spaces
numbers = [parse(Int, strip(line[start_sum:col-1])) for line ∈ lines[1:end-1]]
end
# an excuse to use Julia metaprogramming :)
push!(results, op |> Symbol |> eval |> x -> reduce(x, numbers))
end
return results
endsolve_sums (generic function with 1 method)
Not completed on the day for the first time, but life is very hectic.
Oh, and an excuse to use Julia metaprogramming! We could have put in some conditional logic to take products when the operator is * and sums when it’s +, but instead we can directly convert to a symbol and evaluate it. Not at all required for this problem, but hey…
And then add up the results:
sums |> solve_sums |> sum4277556
I’m inferring from the below meme, but I guess some people created matrices for all the numbers in each sum, rather than just strip the whitespace 🤔
Neat - wish I did it that way 🤷🏻
[2025 Day 6 (Part 2)] The solution be like…
byu/HotTop7260 inadventofcode
⭐⭐
Day 7: Tachyon beam splitting ⚛️
After yesterday, we’ve got 12 ⭐’s on the board
we’re 1/2 way there [Jon Bon Jovi voice]
A beam of tachyons, | flows from a starting point, S down a grid. When it hits a splitter, ^, two beams emerge (one either side).
tachyon_manifold = [
".......S.......",
"...............",
".......^.......",
"...............",
"......^.^......",
"...............",
".....^.^.^.....",
"...............",
"....^.^...^....",
"...............",
"...^.^...^.^...",
"...............",
"..^...^.....^..",
"...............",
".^.^.^.^.^...^.",
"..............."
]In part 1, we are asked how many times the beam is split, and in part 2 we are asked how many parallel timelines (possible paths) exist for the beam.
Here’s my solution:
function analyse_beams(beam_rows::Vector{String})
n_rows = beam_rows |> length
n_cols = beam_rows[end] |> length
grid = [beam_rows[row][col] for row in 1:n_rows, col in 1:n_cols]
# find starting position
start_pos = findfirst(==('S'), grid)
start_row, start_col = start_pos[1], start_pos[2]
# initialise beams and timelines...
splits = 0; timelines = Dict(start_col => 1)
for row ∈ (start_row + 1):n_rows
# ...and then keep track of new ones
new_timelines = Dict{Int, Int}()
for (col, count) ∈ timelines
if grid[row, col] == '^'
# for part 1, we count the splits
splits += 1
# for part 2, we "do" the split and count the timelines (add beams to the left and right - after an in-bounds check)
col - 1 >= 1 && (new_timelines[col - 1] = get(new_timelines, col - 1, 0) + count)
col + 1 <= n_cols && (new_timelines[col + 1] = get(new_timelines, col + 1, 0) + count)
else
new_timelines[col] = get(new_timelines, col, 0) + count
end
end
timelines = new_timelines
end
return ("splits" => splits, "timelines" => timelines)
endanalyse_beams (generic function with 1 method)
This is a tidied up version, because I was initially separately tracking a variable beams, which I ended up separately recording in my part 2 dictionary. Also, I’ve got no idea if I needed the in-bounds checks (where I discount any beams that stray off the edges of the tachyon manifold).
tachyon_manifold |> analyse_beams("splits" => 21, "timelines" => Dict(5 => 10, 13 => 1, 15 => 1, 7 => 11, 11 => 2, 9 => 11, 12 => 1, 3 => 2, 1 => 1))
Day 8: ⏳
…
Citation
@online{di_francesco2025,
author = {Di Francesco, Domenic},
title = {Advent of Code 2025},
date = {2025-12-08},
url = {https://allyourbayes.com/posts/AoC/},
langid = {en}
}