\n",
"\n",
"# Advent of Code 2025: The AI LLM Edition\n",
"\n",
"*I enjoy doing the [**Advent of Code**](https://adventofcode.com/) (AoC) programming puzzles, and **my** solutions are [**over here**](Advent2025.ipynb).*\n",
"\n",
"*In **this** notebook I show some solutions by various AI Large Language Models: Gemini, Claude, and ChatGPT. In order to tell the difference between what the LLM generates and what I say, my comments (like this one) are in italics, and my prompts given to the AI LLMs are in **bold italics**. The LLM responses are in* regular roman font.\n",
"\n",
"*In order to really understand what's going on here, you'll have to look at the problem descriptions at [**Advent of Code**](https://adventofcode.com/2025) for each [**Day**](https://adventofcode.com/2025/day/1).*\n",
"\n",
"*Each day I'll choose a model and give it the prompt* \"***Write code to solve the following problem:***\" *followed by the full text of the AoC Part 1 problem description. Then I'll prompt again with* \"***There is a change to the specification:***\" *followed by the AoC Part 2 description.*\n",
"\n",
"*For brevity, I have removed some of the models' output, such as:*\n",
"- *Prose analysis of the problem and descriptions of the programs. (In most cases these were accurate and thorough!)*\n",
"- *The \"`#!/usr/bin/env python3`\" at the top of the file*\n",
"- *The \"`__main__`\" idiom for running code from the command line*\n",
"- *Usage examples on how to run the program on test input*\n",
"\n",
"\n",
"\n",
"# Day 0\n",
"\n",
"\n",
"\n",
"*On Day 0 I load my [**AdventUtils.ipynb**](AdventUtils.ipynb) and set the`current_year` so I can access my input files with `get_text(day_number)` and can use my `answer` function to verify whether the AI systems get the right answer.*"
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "34563e0b-09c5-4600-a455-3ff0f31b81a0",
"metadata": {},
"outputs": [],
"source": [
"%run AdventUtils.ipynb\n",
"current_year = 2025"
]
},
{
"cell_type": "markdown",
"id": "39657fac-2a12-43d5-ab79-024ebceeadc4",
"metadata": {},
"source": [
"# [Day 1](https://adventofcode.com/2025/day/1) Gemini 3 Pro\n",
"\n",
"*For **Part 1** I started with the Gemini 3 Pro Fast model, which produced this following code:*"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "a03354cc-951c-4609-80e1-a7e1a6d0c2bd",
"metadata": {},
"outputs": [],
"source": [
"def solve_safe_password(rotations):\n",
" \"\"\"\n",
" Simulates the safe dial rotations and counts how many times the dial\n",
" lands on 0.\n",
"\n",
" Args:\n",
" rotations: A list of strings, each representing a rotation (e.g., \"L68\").\n",
"\n",
" Returns:\n",
" The total number of times the dial points at 0 after a rotation.\n",
" \"\"\"\n",
" # The dial starts at 50\n",
" current_position = 50\n",
" zero_count = 0\n",
" \n",
" # The dial has numbers 0 through 99, so all math is modulo 100\n",
" MODULO = 100\n",
"\n",
" for rotation in rotations:\n",
" # 1. Parse the instruction\n",
" direction = rotation[0]\n",
" # Distance is the rest of the string converted to an integer\n",
" distance = int(rotation[1:])\n",
"\n",
" # 2. Determine the rotation value\n",
" if direction == 'R':\n",
" rotation_value = distance\n",
" elif direction == 'L':\n",
" # Left rotations subtract from the current position\n",
" rotation_value = -distance\n",
" else:\n",
" # Should not happen based on problem description\n",
" print(f\"Error: Unknown direction '{direction}' in rotation: {rotation}\")\n",
" continue\n",
"\n",
" # 3. Calculate the new position and apply Modulo 100\n",
" # The standard Python % operator handles negative numbers correctly\n",
" # for a positive modulus (e.g., (-1 - 99) % 100 == 0).\n",
" new_position = (current_position + rotation_value) % MODULO\n",
"\n",
" # 4. Check for Zero\n",
" if new_position == 0:\n",
" zero_count += 1\n",
" \n",
" # 5. Update Position\n",
" current_position = new_position\n",
" \n",
" # Optional: Print step-by-step for debugging/verification\n",
" # print(f\"Rotation {rotation}: new position = {new_position}, zero_count = {zero_count}\")\n",
"\n",
" return zero_count"
]
},
{
"cell_type": "markdown",
"id": "c5219309-4f2f-49b3-8208-8c7a3526e328",
"metadata": {},
"source": [
"*A bit verbose and over-commented, but pretty nice. Below I fetch the Day 1 data with `get_text(1)`, split it into lines (as expected by Gemini's `solve_safe_password` function), and verify that the answer is correct:*"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "c3ff875a-6267-47b8-a032-115a40bfd246",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 1.1: .0013 seconds, answer 1182 correct"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"rotations = get_text(1).splitlines()\n",
"\n",
"answer(puzzle=1.1, solution=1182, code=lambda: \n",
" solve_safe_password(rotations))"
]
},
{
"cell_type": "markdown",
"id": "47a04265-2dab-4a4c-9a24-e1c4c1b93c6b",
"metadata": {},
"source": [
"*For **Part 2** I gave Gemini Fast the updated instructions, and again it had a nice analysis and the following code:*"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "a1cf0e58-4567-4713-9d4b-e57f5c958800",
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"\n",
"def solve_new_safe_password(rotations):\n",
" \"\"\"\n",
" Simulates the safe dial rotations and counts every time the dial\n",
" points at 0, including during the rotation movement.\n",
"\n",
" Args:\n",
" rotations: A list of strings, each representing a rotation (e.g., \"L68\").\n",
"\n",
" Returns:\n",
" The total number of times the dial points at 0.\n",
" \"\"\"\n",
" # The dial starts at 50\n",
" current_position = 50\n",
" total_zero_crossings = 0\n",
" MODULO = 100\n",
"\n",
" for rotation in rotations:\n",
" direction = rotation[0]\n",
" distance = int(rotation[1:])\n",
" \n",
" S = current_position # Start Position\n",
" D = distance # Distance\n",
" \n",
" crossings_for_this_rotation = 0\n",
"\n",
" if direction == 'L':\n",
" # --- LEFT ROTATION ---\n",
" # Dial moves from S -> S-1 -> ... -> 0 -> 99 -> ...\n",
" \n",
" # 1. Distance from S to the first 0: S clicks\n",
" # If D is large enough to cover this distance, the first 0 is crossed.\n",
" \n",
" if D >= S:\n",
" # The first crossing (at position 0) is achieved\n",
" crossings_for_this_rotation += 1\n",
" \n",
" # 2. Remaining distance after first 0 crossing\n",
" remaining_distance = D - S\n",
" \n",
" # 3. Subsequent crossings: The dial crosses 0 every 100 clicks\n",
" # after the first one.\n",
" crossings_for_this_rotation += remaining_distance // MODULO\n",
" \n",
" elif direction == 'R':\n",
" # --- RIGHT ROTATION ---\n",
" # Dial moves from S -> S+1 -> ... -> 99 -> 0 -> 1 -> ...\n",
" \n",
" # 1. Distance from S to the first 0: 100 - S clicks\n",
" # (e.g., 50 to 0 is 50 clicks; 99 to 0 is 1 click)\n",
" distance_to_first_zero = MODULO - S\n",
" \n",
" if D >= distance_to_first_zero:\n",
" # The first crossing (at position 0) is achieved\n",
" crossings_for_this_rotation += 1\n",
" \n",
" # 2. Remaining distance after first 0 crossing\n",
" remaining_distance = D - distance_to_first_zero\n",
" \n",
" # 3. Subsequent crossings: The dial crosses 0 every 100 clicks\n",
" crossings_for_this_rotation += remaining_distance // MODULO\n",
" \n",
" # Add the crossings for this rotation to the total\n",
" total_zero_crossings += crossings_for_this_rotation\n",
"\n",
" # Calculate the final position to set up for the next rotation\n",
" if direction == 'R':\n",
" rotation_value = distance\n",
" else: # 'L'\n",
" rotation_value = -distance\n",
" \n",
" current_position = (current_position + rotation_value) % MODULO\n",
"\n",
" return total_zero_crossings"
]
},
{
"cell_type": "markdown",
"id": "f8e02f20-87b1-4bd1-9bdb-2bd187fc42cb",
"metadata": {},
"source": [
"*Note that the code does `import math` but then never uses the math module. (I've done that, but I try to eventually clean things up. Maybe that's the different between Gemini \"Fast\" and Gemini \"Thinking\" models.)*\n",
"\n",
"*Here I check whether the code is correct:*"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "a0b3e0fc-e044-4c1e-b4b6-6bc735145f57",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 1.2: .0019 seconds, answer 7509 WRONG; EXPECTED ANSWER IS 6907"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(1.2, 6907, lambda: \n",
" solve_new_safe_password(rotations))"
]
},
{
"cell_type": "markdown",
"id": "d640d8e4-6940-416d-93af-b55d82482a35",
"metadata": {},
"source": [
"*Too bad; the code has a **bug**. I switched to the Gemini 3 Pro \"Thinking\" model and gave the prompt \"**That's not quite right.**\" It responded with*\n",
"\n",
"It seems I made a subtle logic error in my previous calculation for the Right (R) rotation.\n",
"When the dial is at 0, the distance to the *next* zero is **100**, not 0.\n",
"\n",
"*which is exactly the error I made on *my* first attempt! Gemini produced the following updated code:*"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "196a6a93-e318-4a45-a023-9997fa0fb0f5",
"metadata": {},
"outputs": [],
"source": [
"def solve_safe_password_method_2(rotations):\n",
" \"\"\"\n",
" Simulates the safe dial rotations and counts every time the dial\n",
" points at 0 (Method 0x434C49434B).\n",
" \n",
" Correctly handles cases where the dial starts at 0.\n",
" \"\"\"\n",
" current_position = 50\n",
" total_zero_crossings = 0\n",
" MODULO = 100\n",
"\n",
" for rotation in rotations:\n",
" direction = rotation[0]\n",
" distance = int(rotation[1:])\n",
" \n",
" # Determine distance required to hit the *next* 0\n",
" if direction == 'L':\n",
" if current_position == 0:\n",
" dist_to_first_zero = 100\n",
" else:\n",
" dist_to_first_zero = current_position\n",
" \n",
" elif direction == 'R':\n",
" if current_position == 0:\n",
" dist_to_first_zero = 100\n",
" else:\n",
" dist_to_first_zero = 100 - current_position\n",
"\n",
" # Check if the rotation distance is enough to reach the first zero\n",
" if distance >= dist_to_first_zero:\n",
" # We hit 0 at least once\n",
" count = 1\n",
" \n",
" # Remove the distance traveled to the first zero\n",
" remaining_distance = distance - dist_to_first_zero\n",
" \n",
" # Add any subsequent full rotations (every 100 clicks hits 0 again)\n",
" count += remaining_distance // 100\n",
" \n",
" total_zero_crossings += count\n",
"\n",
" # Update position for next step\n",
" if direction == 'R':\n",
" current_position = (current_position + distance) % 100\n",
" else:\n",
" current_position = (current_position - distance) % 100\n",
"\n",
" return total_zero_crossings"
]
},
{
"cell_type": "markdown",
"id": "e05db80b-c56d-4e1f-988c-a09be8ca5a3e",
"metadata": {},
"source": [
"*This second try is indeed correct:*"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "021043ed-2ac5-4aaf-8171-985f0f5911f2",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 1.2: .0016 seconds, answer 6907 correct"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(1.2, 6907, lambda: \n",
" solve_safe_password_method_2(rotations))"
]
},
{
"cell_type": "markdown",
"id": "82fb1dca-1619-4ad7-9155-52fb4804470e",
"metadata": {},
"source": [
"# [Day 2](https://adventofcode.com/2025/day/2) Claude Opus 4.5\n",
"\n",
"*I gave Claude the instructions for **Part 1** and it wrote some code that produces the correct answer but prints a lot of unneccessary debugging output along the way. I prompted it to \"**Change the code to not print anything, just return the answer**\" and got the following:*"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "8eac98f3-b884-4d95-b38b-ea4365ec3004",
"metadata": {},
"outputs": [],
"source": [
"def find_invalid_ids_in_range(start, end):\n",
" invalid_ids = []\n",
" min_digits = len(str(start))\n",
" max_digits = len(str(end))\n",
" \n",
" for total_digits in range(min_digits, max_digits + 1):\n",
" if total_digits % 2 != 0:\n",
" continue\n",
" \n",
" half_digits = total_digits // 2\n",
" pattern_start = 1 if half_digits == 1 else 10 ** (half_digits - 1)\n",
" pattern_end = 10 ** half_digits - 1\n",
" for pattern in range(pattern_start, pattern_end + 1):\n",
" pattern_str = str(pattern)\n",
" invalid_id = int(pattern_str + pattern_str)\n",
" if start <= invalid_id <= end:\n",
" invalid_ids.append(invalid_id)\n",
" \n",
" return invalid_ids\n",
"\n",
"\n",
"def solve(input_str):\n",
" input_str = input_str.strip().replace('\\n', '').replace(' ', '').rstrip(',')\n",
" total = 0\n",
" for r in input_str.split(','):\n",
" if r:\n",
" start, end = map(int, r.split('-'))\n",
" total += sum(find_invalid_ids_in_range(start, end))\n",
" return total"
]
},
{
"cell_type": "markdown",
"id": "2bd0db00-952b-47e5-b787-b3887b7539f1",
"metadata": {},
"source": [
"*This code is overall rather nice, but conspicously lacks comments and doc strings. It uses the more efficient \"enumerate over the first half of the digit string\" strategy, but is not precise in narrowing down the range it enumerates over. For example, for the range \"999000-109000\", this code will enumerate the range (100, 1000), when it could enumerate just the range (999, 1000).*\n",
"\n",
"*I verified that the code gives the correct answer:*"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "a91845ec-ace7-482e-b0b5-8a620ef3461f",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 2.1: .1247 seconds, answer 23560874270 correct"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"id_ranges = get_text(2)\n",
"\n",
"answer(2.1, 23560874270, lambda:\n",
" solve(id_ranges))"
]
},
{
"cell_type": "markdown",
"id": "a31d006f-8cf2-4e4c-92d3-d7b7def22227",
"metadata": {},
"source": [
"*When given the **Part 2** instructions, Claude wrote the following code:*"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "f0dc176b-dd85-40a4-ac5c-dfa936a6a524",
"metadata": {},
"outputs": [],
"source": [
"def find_invalid_ids_in_range(start, end):\n",
" invalid_ids = set()\n",
" min_digits = len(str(start))\n",
" max_digits = len(str(end))\n",
" \n",
" for total_digits in range(min_digits, max_digits + 1):\n",
" # Pattern length can be 1 to total_digits//2 (need at least 2 repetitions)\n",
" for pattern_len in range(1, total_digits // 2 + 1):\n",
" if total_digits % pattern_len != 0:\n",
" continue\n",
" \n",
" repetitions = total_digits // pattern_len\n",
" if repetitions < 2:\n",
" continue\n",
" \n",
" pattern_start = 1 if pattern_len == 1 else 10 ** (pattern_len - 1)\n",
" pattern_end = 10 ** pattern_len - 1\n",
" \n",
" for pattern in range(pattern_start, pattern_end + 1):\n",
" pattern_str = str(pattern)\n",
" invalid_id = int(pattern_str * repetitions)\n",
" if start <= invalid_id <= end:\n",
" invalid_ids.add(invalid_id)\n",
" \n",
" return invalid_ids\n",
"\n",
"\n",
"def solve(input_str):\n",
" input_str = input_str.strip().replace('\\n', '').replace(' ', '').rstrip(',')\n",
" total = 0\n",
" for r in input_str.split(','):\n",
" if r:\n",
" start, end = map(int, r.split('-'))\n",
" total += sum(find_invalid_ids_in_range(start, end))\n",
" return total"
]
},
{
"cell_type": "markdown",
"id": "287ed190-e8c3-4aa8-a01b-b4ecd97327b5",
"metadata": {},
"source": [
"*Claude's code produces the correct answer:*"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "9c0049e6-a992-4aa8-a2d7-3ea748e361a6",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 2.2: .1325 seconds, answer 44143124633 correct"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(2.2, 44143124633, lambda:\n",
" solve(id_ranges))"
]
},
{
"cell_type": "markdown",
"id": "d3533d6a-d12f-4dbf-b0e8-9d878c9bc283",
"metadata": {},
"source": [
"# [Day 3](https://adventofcode.com/2025/day/3) ChatGPT 5.1 Auto\n",
"\n",
"*For **Part 1** ChatGPT gave a very brief analysis of the problem and then wrote a program that was designed to be called from the command line, using the `\"__main__\"` idiom. I told it \"**I don't want to run it like that, I want a function that I can pass the input text and have it return an int**\" and it produced this code (conspicuously lacking comments or doc strings):*"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "3aa266f3-50d0-4d8d-a464-4c74c52daa69",
"metadata": {},
"outputs": [],
"source": [
"def total_output_joltage(input_text: str) -> int:\n",
" def max_joltage_for_bank(bank: str) -> int:\n",
" max_first = -1\n",
" max_pair = 0\n",
" for ch in bank:\n",
" if not ch.isdigit():\n",
" continue\n",
" d = int(ch)\n",
" if max_first != -1:\n",
" pair = 10 * max_first + d\n",
" if pair > max_pair:\n",
" max_pair = pair\n",
" if d > max_first:\n",
" max_first = d\n",
" return max_pair\n",
"\n",
" total = 0\n",
" for line in input_text.strip().splitlines():\n",
" line = line.strip()\n",
" if line:\n",
" total += max_joltage_for_bank(line)\n",
" return total"
]
},
{
"cell_type": "markdown",
"id": "6c2da1ae-d915-4f37-9494-bbe427ef87ad",
"metadata": {},
"source": [
"I verified this is correct:"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "09bf306b-8762-4346-aff9-bcff33639c71",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 3.1: .0042 seconds, answer 17085 correct"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"banks = get_text(3)\n",
"\n",
"answer(3.1, 17085, lambda:\n",
" total_output_joltage(banks))"
]
},
{
"cell_type": "markdown",
"id": "4a07f37f-c5e3-4484-a7b1-2cae0ff5bd01",
"metadata": {},
"source": [
"*For **Part 2** ChatGPT did well (and for some reason included comments and doc strings this time):*"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "bdb8b4e4-bed0-48dc-a045-47cd4c6002fd",
"metadata": {},
"outputs": [],
"source": [
"def total_output_joltage(input_text: str, k: int = 12) -> int:\n",
" \"\"\"\n",
" Given the full puzzle input as a string, return the total maximum joltage\n",
" when, for each line (bank), you must choose exactly k digits in order to\n",
" form the largest possible number.\n",
" \"\"\"\n",
"\n",
" def max_joltage_for_bank(bank: str, k: int) -> int:\n",
" # Extract digits from the line\n",
" digits = [int(ch) for ch in bank if ch.isdigit()]\n",
" n = len(digits)\n",
" if k > n:\n",
" raise ValueError(f\"Bank has only {n} digits, cannot pick {k}.\")\n",
"\n",
" # We will remove exactly n - k digits to maximize the resulting sequence\n",
" removals = n - k\n",
" stack = []\n",
"\n",
" for d in digits:\n",
" # While we can still remove digits, and the last digit in the stack\n",
" # is smaller than the current digit, pop it to make the number larger.\n",
" while stack and removals > 0 and stack[-1] < d:\n",
" stack.pop()\n",
" removals -= 1\n",
" stack.append(d)\n",
"\n",
" # If we still have removals left, remove from the end\n",
" if removals > 0:\n",
" stack = stack[:-removals]\n",
"\n",
" # Now stack should have length n - original_removals == k\n",
" chosen = stack[:k]\n",
" return int(\"\".join(map(str, chosen)))\n",
"\n",
" total = 0\n",
" for line in input_text.strip().splitlines():\n",
" line = line.strip()\n",
" if not line:\n",
" continue\n",
" total += max_joltage_for_bank(line, k)\n",
"\n",
" return total"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "70bde9b9-beb1-4e9d-bef6-0f20fb958891",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 3.2: .0078 seconds, answer 169408143086082 correct"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(3.2, 169408143086082, lambda:\n",
" total_output_joltage(banks))"
]
},
{
"cell_type": "markdown",
"id": "00625b83-f56f-4fff-8d87-1e9cdbc02847",
"metadata": {},
"source": [
"# [Day 4](https://adventofcode.com/2025/day/4): Gemini 3 Pro\n",
"\n",
"*Gemini produced a solution to **Part 1** that is straightforward and efficient, although perhaps could use some abstraction (e.g. if they had a function to count neighbors, they wouldn't need the `continue`).*"
]
},
{
"cell_type": "code",
"execution_count": 16,
"id": "35bf1f30-07c7-4842-a6e3-e33fb874e779",
"metadata": {},
"outputs": [],
"source": [
"def count_accessible_rolls(grid_str):\n",
" \"\"\"\n",
" Parses a grid string and counts paper rolls (@) that have\n",
" fewer than 4 neighbors of the same type.\n",
" \"\"\"\n",
" # Parse input into a list of lists (2D grid)\n",
" # .strip() removes leading/trailing whitespace to handle clean input\n",
" lines = grid_str.strip().split('\\n')\n",
" grid = [list(line) for line in lines]\n",
" \n",
" rows = len(grid)\n",
" cols = len(grid[0])\n",
" \n",
" accessible_count = 0\n",
" \n",
" # Directions for 8 neighbors: (row_offset, col_offset)\n",
" directions = [\n",
" (-1, -1), (-1, 0), (-1, 1),\n",
" (0, -1), (0, 1),\n",
" (1, -1), (1, 0), (1, 1)\n",
" ]\n",
" \n",
" for r in range(rows):\n",
" for c in range(cols):\n",
" # We only care if the current spot is a paper roll\n",
" if grid[r][c] != '@':\n",
" continue\n",
" \n",
" neighbor_rolls = 0\n",
" \n",
" # Check all 8 neighbors\n",
" for dr, dc in directions:\n",
" nr, nc = r + dr, c + dc\n",
" \n",
" # Check boundaries\n",
" if 0 <= nr < rows and 0 <= nc < cols:\n",
" if grid[nr][nc] == '@':\n",
" neighbor_rolls += 1\n",
" \n",
" # The Condition: fewer than 4 adjacent rolls\n",
" if neighbor_rolls < 4:\n",
" accessible_count += 1\n",
" \n",
" return accessible_count"
]
},
{
"cell_type": "markdown",
"id": "1e12bc4c-8cc8-4c01-b4ad-5392b49642e6",
"metadata": {},
"source": [
"Here I verify that the code does produce the correct answer:"
]
},
{
"cell_type": "code",
"execution_count": 17,
"id": "5b54c152-ce26-4baf-8b51-d4a166c6c2e7",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 4.1: .0182 seconds, answer 1569 correct"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"grid = get_text(4)\n",
"\n",
"answer(4.1, 1569, lambda:\n",
" count_accessible_rolls(grid))"
]
},
{
"cell_type": "markdown",
"id": "76845e18-6209-4995-a825-568782b79d0d",
"metadata": {},
"source": [
"*For **Part 2**, Gemini chose the \"repeatedly remove acessible rolls\" approach rather than the more efficient (and arguable more elegant) \"remove a roll and check its neighbors\" approach.*"
]
},
{
"cell_type": "code",
"execution_count": 18,
"id": "16a1a0db-7501-41fd-a606-87fbb79273bd",
"metadata": {},
"outputs": [],
"source": [
"def count_total_removable_rolls(grid_str):\n",
" \"\"\"\n",
" Parses a grid string and iteratively removes paper rolls (@) \n",
" that have fewer than 4 neighbors of the same type.\n",
" Returns the total number of rolls removed.\n",
" \"\"\"\n",
" # Parse input into a list of lists (2D grid)\n",
" lines = grid_str.strip().split('\\n')\n",
" grid = [list(line) for line in lines]\n",
" \n",
" rows = len(grid)\n",
" cols = len(grid[0])\n",
" \n",
" total_removed_count = 0\n",
" \n",
" # Directions for 8 neighbors\n",
" directions = [\n",
" (-1, -1), (-1, 0), (-1, 1),\n",
" (0, -1), (0, 1),\n",
" (1, -1), (1, 0), (1, 1)\n",
" ]\n",
" \n",
" while True:\n",
" rolls_to_remove = []\n",
" \n",
" # 1. Identify all rolls that can be removed in this pass\n",
" for r in range(rows):\n",
" for c in range(cols):\n",
" if grid[r][c] != '@':\n",
" continue\n",
" \n",
" neighbor_rolls = 0\n",
" for dr, dc in directions:\n",
" nr, nc = r + dr, c + dc\n",
" if 0 <= nr < rows and 0 <= nc < cols:\n",
" if grid[nr][nc] == '@':\n",
" neighbor_rolls += 1\n",
" \n",
" # Condition: fewer than 4 adjacent rolls\n",
" if neighbor_rolls < 4:\n",
" rolls_to_remove.append((r, c))\n",
" \n",
" # 2. If nothing to remove, we are done\n",
" if not rolls_to_remove:\n",
" break\n",
" \n",
" # 3. Remove them (update grid and count)\n",
" total_removed_count += len(rolls_to_remove)\n",
" for r, c in rolls_to_remove:\n",
" grid[r][c] = '.' # Mark as empty space\n",
" \n",
" return total_removed_count"
]
},
{
"cell_type": "code",
"execution_count": 19,
"id": "b47c2e05-978a-4b22-aafc-e31ee1825387",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 4.2: .3946 seconds, answer 9280 correct"
]
},
"execution_count": 19,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(4.2, 9280, lambda:\n",
" count_total_removable_rolls(grid))"
]
},
{
"cell_type": "markdown",
"id": "81d88b41-9dec-404d-9bca-9e3ba5caa14d",
"metadata": {},
"source": [
"*I asked Gemini \"Can you do it more efficiently\" and it replied*\n",
"\n",
"The previous implementation scanned the entire grid repeatedly (`O(Rows * Cols * Iterations)`). I've replaced it with a Worklist/Queue algorithm that checks neighbors only when necessary. This reduces the complexity to be roughly proportional to the number of rolls, which is significantly faster for large grids.\n",
"\n",
"*This is the same approach that I used in my solution.*"
]
},
{
"cell_type": "code",
"execution_count": 20,
"id": "71bfe887-fbd4-4378-b37f-d0b88f9fa3e7",
"metadata": {},
"outputs": [],
"source": [
"def count_total_removable_rolls(grid_str):\n",
" \"\"\"\n",
" Parses a grid string and iteratively removes paper rolls (@) \n",
" that have fewer than 4 neighbors of the same type.\n",
" Returns the total number of rolls removed.\n",
" \n",
" Optimized to use a worklist (queue) approach instead of full grid scans.\n",
" \"\"\"\n",
" # Parse input into a list of lists (2D grid)\n",
" lines = grid_str.strip().split('\\n')\n",
" grid = [list(line) for line in lines]\n",
" \n",
" rows = len(grid)\n",
" cols = len(grid[0])\n",
" \n",
" total_removed_count = 0\n",
" \n",
" # Directions for 8 neighbors\n",
" directions = [\n",
" (-1, -1), (-1, 0), (-1, 1),\n",
" (0, -1), (0, 1),\n",
" (1, -1), (1, 0), (1, 1)\n",
" ]\n",
" \n",
" def count_neighbors(r, c):\n",
" count = 0\n",
" for dr, dc in directions:\n",
" nr, nc = r + dr, c + dc\n",
" if 0 <= nr < rows and 0 <= nc < cols:\n",
" if grid[nr][nc] == '@':\n",
" count += 1\n",
" return count\n",
"\n",
" # 1. Initial scan: Find all rolls that are immediately removable\n",
" queue = []\n",
" for r in range(rows):\n",
" for c in range(cols):\n",
" if grid[r][c] == '@':\n",
" if count_neighbors(r, c) < 4:\n",
" queue.append((r, c))\n",
" \n",
" # 2. Process the queue\n",
" # Using a list as a queue. Duplicates are handled by checking grid state.\n",
" head = 0\n",
" while head < len(queue):\n",
" r, c = queue[head]\n",
" head += 1\n",
" \n",
" # If already removed (e.g., added to queue multiple times), skip\n",
" if grid[r][c] != '@':\n",
" continue\n",
" \n",
" # Remove the roll\n",
" grid[r][c] = '.'\n",
" total_removed_count += 1\n",
" \n",
" # Check its neighbors\n",
" for dr, dc in directions:\n",
" nr, nc = r + dr, c + dc\n",
" if 0 <= nr < rows and 0 <= nc < cols:\n",
" # If neighbor is a roll, check if it became removable\n",
" if grid[nr][nc] == '@':\n",
" if count_neighbors(nr, nc) < 4:\n",
" queue.append((nr, nc))\n",
" \n",
" return total_removed_count"
]
},
{
"cell_type": "code",
"execution_count": 21,
"id": "f370ee38-67af-42a6-9ad3-cdeec2019ff3",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 4.2: .0938 seconds, answer 9280 correct"
]
},
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(4.2, 9280, lambda:\n",
" count_total_removable_rolls(grid))"
]
},
{
"cell_type": "markdown",
"id": "78434cfe-d728-453c-8f45-fc6b5fea18c3",
"metadata": {},
"source": [
"# [Day 5](https://adventofcode.com/2025/day/5): Claude Opus 4.5\n",
"\n",
"*Claude produces a straightforward program that solves **Part 1** just fine and demonstrates good use of abstraction. This time it has nice doc strings; for Day 2 it had none. Go figure.*"
]
},
{
"cell_type": "code",
"execution_count": 22,
"id": "e7ab7dac-9686-4a76-b83f-6779275c3519",
"metadata": {},
"outputs": [],
"source": [
"\"\"\"\n",
"Solution for the Fresh Ingredients puzzle.\n",
"Parses a database with fresh ingredient ID ranges and available ingredient IDs,\n",
"then counts how many available ingredients are fresh.\n",
"\"\"\"\n",
"\n",
"def parse_input(input_text):\n",
" \"\"\"Parse the input into ranges and ingredient IDs.\"\"\"\n",
" parts = input_text.strip().split('\\n\\n')\n",
" \n",
" # Parse fresh ranges\n",
" ranges = []\n",
" for line in parts[0].strip().split('\\n'):\n",
" start, end = map(int, line.split('-'))\n",
" ranges.append((start, end))\n",
" \n",
" # Parse available ingredient IDs\n",
" ingredient_ids = []\n",
" for line in parts[1].strip().split('\\n'):\n",
" ingredient_ids.append(int(line))\n",
" \n",
" return ranges, ingredient_ids\n",
"\n",
"\n",
"def is_fresh(ingredient_id, ranges):\n",
" \"\"\"Check if an ingredient ID falls within any fresh range.\"\"\"\n",
" for start, end in ranges:\n",
" if start <= ingredient_id <= end:\n",
" return True\n",
" return False\n",
"\n",
"\n",
"def count_fresh_ingredients(ranges, ingredient_ids):\n",
" \"\"\"Count how many ingredient IDs are fresh.\"\"\"\n",
" count = 0\n",
" for ingredient_id in ingredient_ids:\n",
" if is_fresh(ingredient_id, ranges):\n",
" count += 1\n",
" return count\n",
"\n",
"\n",
"def solve(input_text):\n",
" \"\"\"Solve the puzzle and return the count of fresh ingredients.\"\"\"\n",
" ranges, ingredient_ids = parse_input(input_text)\n",
" return count_fresh_ingredients(ranges, ingredient_ids)"
]
},
{
"cell_type": "markdown",
"id": "40fc662b-ecee-4abe-a23a-ca7786edd438",
"metadata": {},
"source": [
"*Here I feed the input to `solve` and verify that the answer is correct:*"
]
},
{
"cell_type": "code",
"execution_count": 23,
"id": "49f0bb9c-00c0-4983-ab26-d1cec1e692ac",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 5.1: .0056 seconds, answer 635 correct"
]
},
"execution_count": 23,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"input_text = get_text(5)\n",
"\n",
"answer(5.1, 635, lambda:\n",
" solve(input_text))"
]
},
{
"cell_type": "markdown",
"id": "dabe3fbc-0fa6-46d0-adfb-f6413f3a63d5",
"metadata": {},
"source": [
"*For **Part 2**, Claude chose to sort ranges, then merge them, then count the total of the sizes of the ranges. That's a reasonable approach–it involves an extra step of merging ranges (instead of just counting on the fly) but it could be useful to have the merged ranges.*"
]
},
{
"cell_type": "code",
"execution_count": 24,
"id": "b70269fb-3a0b-4728-9d60-421e3b35a877",
"metadata": {},
"outputs": [],
"source": [
"\"\"\"\n",
"Solution for the Fresh Ingredients puzzle - Part 2.\n",
"Count all unique ingredient IDs that are considered fresh by any range.\n",
"\"\"\"\n",
"\n",
"def parse_input(input_text):\n",
" \"\"\"Parse the input to get fresh ranges (ignore ingredient IDs section).\"\"\"\n",
" parts = input_text.strip().split('\\n\\n')\n",
" \n",
" # Parse fresh ranges (only first section matters now)\n",
" ranges = []\n",
" for line in parts[0].strip().split('\\n'):\n",
" start, end = map(int, line.split('-'))\n",
" ranges.append((start, end))\n",
" \n",
" return ranges\n",
"\n",
"\n",
"def merge_ranges(ranges):\n",
" \"\"\"Merge overlapping ranges to count unique IDs efficiently.\"\"\"\n",
" if not ranges:\n",
" return []\n",
" \n",
" # Sort ranges by start value\n",
" sorted_ranges = sorted(ranges)\n",
" \n",
" merged = [sorted_ranges[0]]\n",
" \n",
" for current_start, current_end in sorted_ranges[1:]:\n",
" last_start, last_end = merged[-1]\n",
" \n",
" # Check if current range overlaps or is adjacent to the last merged range\n",
" if current_start <= last_end + 1:\n",
" # Merge by extending the end if needed\n",
" merged[-1] = (last_start, max(last_end, current_end))\n",
" else:\n",
" # No overlap, add as new range\n",
" merged.append((current_start, current_end))\n",
" \n",
" return merged\n",
"\n",
"\n",
"def count_fresh_ids(ranges):\n",
" \"\"\"Count total unique fresh ingredient IDs.\"\"\"\n",
" merged = merge_ranges(ranges)\n",
" \n",
" total = 0\n",
" for start, end in merged:\n",
" # Count is inclusive: end - start + 1\n",
" total += end - start + 1\n",
" \n",
" return total\n",
"\n",
"\n",
"def solve(input_text):\n",
" \"\"\"Solve part 2 and return the count of all fresh ingredient IDs.\"\"\"\n",
" ranges = parse_input(input_text)\n",
" return count_fresh_ids(ranges)"
]
},
{
"cell_type": "code",
"execution_count": 25,
"id": "bc525b8f-8c03-4ac1-a52d-1ba2d8c1e5bb",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 5.2: .0002 seconds, answer 369761800782619 correct"
]
},
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(5.2, 369761800782619, lambda:\n",
" solve(input_text))"
]
},
{
"cell_type": "markdown",
"id": "b1503029-3a5f-4949-8502-75b051f78a23",
"metadata": {},
"source": [
"# [Day 6](https://adventofcode.com/2025/day/6): ChatGPT 5.1 Auto\n",
"\n",
"*For **Part 1** ChatGPT produced a program that is correct, but has poor abstraction, with one long 63-line function. (It also contains a pet peeve of mine: in lines 17–20 the pattern \"`if some_boolean: True else: False`\" can always be replaced with \"`some_boolean`\".)*"
]
},
{
"cell_type": "code",
"execution_count": 26,
"id": "fc4e8201-532f-47e8-aa95-e5dad13b8f4f",
"metadata": {},
"outputs": [],
"source": [
"import re\n",
"from functools import reduce\n",
"from operator import add, mul\n",
"\n",
"def solve(lines):\n",
" if not lines:\n",
" return 0\n",
"\n",
" # Preserve internal spaces; pad all lines to same width\n",
" width = max(len(line) for line in lines)\n",
" grid = [line.rstrip('\\n').ljust(width) for line in lines]\n",
" h = len(grid)\n",
"\n",
" # Identify separator columns: columns that are all spaces\n",
" sep = []\n",
" for c in range(width):\n",
" if all(grid[r][c] == ' ' for r in range(h)):\n",
" sep.append(True)\n",
" else:\n",
" sep.append(False)\n",
"\n",
" # Find ranges of columns that form individual problems\n",
" problem_ranges = []\n",
" in_problem = False\n",
" start = 0\n",
" for c in range(width):\n",
" if not sep[c]:\n",
" if not in_problem:\n",
" in_problem = True\n",
" start = c\n",
" else:\n",
" if in_problem:\n",
" in_problem = False\n",
" problem_ranges.append((start, c - 1))\n",
" if in_problem:\n",
" problem_ranges.append((start, width - 1))\n",
"\n",
" grand_total = 0\n",
"\n",
" # Process each problem\n",
" for c_start, c_end in problem_ranges:\n",
" numbers = []\n",
"\n",
" # All rows except the last contain numbers (potentially)\n",
" for r in range(h - 1):\n",
" segment = grid[r][c_start:c_end + 1]\n",
" for match in re.finditer(r'\\d+', segment):\n",
" numbers.append(int(match.group(0)))\n",
"\n",
" # Last row contains the operator (+ or *)\n",
" bottom_segment = grid[h - 1][c_start:c_end + 1]\n",
" if '+' in bottom_segment:\n",
" op = add\n",
" elif '*' in bottom_segment:\n",
" op = mul\n",
" else:\n",
" # No valid operator found; skip this malformed problem\n",
" continue\n",
"\n",
" if not numbers:\n",
" continue\n",
"\n",
" # Fold numbers using the chosen operator\n",
" result = reduce(op, numbers)\n",
" grand_total += result\n",
"\n",
" return grand_total"
]
},
{
"cell_type": "markdown",
"id": "54e76b17-4372-4074-b7f9-9439c44bc3c4",
"metadata": {},
"source": [
"*Here I interface my input data to ChatGPT's function and verify it is correct:*"
]
},
{
"cell_type": "code",
"execution_count": 27,
"id": "d6beae51-7205-4cae-b4d7-495da2953baf",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 6.1: .0078 seconds, answer 5877594983578 correct"
]
},
"execution_count": 27,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"lines = get_text(6).splitlines()\n",
"\n",
"answer(6.1, 5877594983578, lambda:\n",
" solve(lines))"
]
},
{
"cell_type": "markdown",
"id": "28de0612-03f3-4587-88cb-18ef9b10ee00",
"metadata": {},
"source": [
"*For **Part 2** we get an even-longer function, with even more complications. For example in \"Determine the operator\"; the 14 lines could be replaced with `op = (add if '+' in segment else mul)`. I think ChatGPT ws trying to be robust against errors in the input.*"
]
},
{
"cell_type": "code",
"execution_count": 28,
"id": "053c3971-9aa5-490a-a016-f303b391c5d8",
"metadata": {},
"outputs": [],
"source": [
"import re\n",
"from functools import reduce\n",
"from operator import add, mul\n",
"\n",
"def solve(lines):\n",
" if not lines:\n",
" return 0\n",
"\n",
" # Normalize lines to a rectangle, preserving internal spaces\n",
" width = max(len(line) for line in lines)\n",
" grid = [line.rstrip('\\n').ljust(width) for line in lines]\n",
" h = len(grid)\n",
"\n",
" # A separator column is all spaces -> separates problems\n",
" sep = []\n",
" for c in range(width):\n",
" if all(grid[r][c] == ' ' for r in range(h)):\n",
" sep.append(True)\n",
" else:\n",
" sep.append(False)\n",
"\n",
" # Find contiguous ranges of non-separator columns (each is a problem)\n",
" problem_ranges = []\n",
" in_problem = False\n",
" start = 0\n",
" for c in range(width):\n",
" if not sep[c]:\n",
" if not in_problem:\n",
" in_problem = True\n",
" start = c\n",
" else:\n",
" if in_problem:\n",
" in_problem = False\n",
" problem_ranges.append((start, c - 1))\n",
" if in_problem:\n",
" problem_ranges.append((start, width - 1))\n",
"\n",
" grand_total = 0\n",
"\n",
" for c_start, c_end in problem_ranges:\n",
" # Find the operator row for THIS problem slice:\n",
" # bottom-most row with + or * in [c_start, c_end]\n",
" op_row = None\n",
" for r in range(h):\n",
" segment = grid[r][c_start:c_end + 1]\n",
" if '+' in segment or '*' in segment:\n",
" op_row = r\n",
"\n",
" if op_row is None:\n",
" # No operator for this vertical slice -> skip\n",
" continue\n",
"\n",
" # Determine the operator\n",
" op_segment = grid[op_row][c_start:c_end + 1]\n",
" if '+' in op_segment and '*' in op_segment:\n",
" # If both appear (shouldn't in valid input), choose the leftmost\n",
" plus_idx = op_segment.find('+') if '+' in op_segment else float('inf')\n",
" mul_idx = op_segment.find('*') if '*' in op_segment else float('inf')\n",
" op = add if plus_idx < mul_idx else mul\n",
" elif '+' in op_segment:\n",
" op = add\n",
" elif '*' in op_segment:\n",
" op = mul\n",
" else:\n",
" # No operator actually present\n",
" continue\n",
"\n",
" # Build numbers column-by-column (each column is one number),\n",
" # reading digits from top to just above op_row\n",
" numbers = []\n",
" for c in range(c_start, c_end + 1):\n",
" digits = []\n",
" for r in range(op_row):\n",
" ch = grid[r][c]\n",
" if ch.isdigit():\n",
" digits.append(ch)\n",
" if digits:\n",
" num = int(''.join(digits))\n",
" numbers.append(num)\n",
"\n",
" if not numbers:\n",
" continue\n",
"\n",
" result = reduce(op, numbers)\n",
" grand_total += result\n",
"\n",
" return grand_total"
]
},
{
"cell_type": "markdown",
"id": "799fdb8a-0564-42f6-9d6a-f6604666afad",
"metadata": {},
"source": [
"*The program is correct:*"
]
},
{
"cell_type": "code",
"execution_count": 29,
"id": "e37018e7-0c82-4501-830e-dfe55efc3ad6",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 6.2: .0070 seconds, answer 11159825706149 correct"
]
},
"execution_count": 29,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(6.2, 11159825706149, lambda:\n",
" solve(lines))"
]
},
{
"cell_type": "markdown",
"id": "110a8177-d4d8-4a61-9f74-1ed6444ec38f",
"metadata": {},
"source": [
"# [Day 7](https://adventofcode.com/2025/day/7): Gemini 3 Pro\n",
"\n",
"*Gemini's code for **Part 1** is a bit verbose, but gets the job done.*"
]
},
{
"cell_type": "code",
"execution_count": 30,
"id": "adbbd7bc-1eca-4638-a551-470d27edae0d",
"metadata": {},
"outputs": [],
"source": [
"def solve_tachyon_manifold(puzzle_input):\n",
" # 1. Parse the Grid\n",
" grid = puzzle_input.splitlines()\n",
" grid = [line for line in grid if line] # Remove empty lines\n",
" \n",
" if not grid:\n",
" return 0\n",
"\n",
" height = len(grid)\n",
" width = max(len(row) for row in grid)\n",
" \n",
" # Pad rows to ensure uniform width\n",
" grid = [row.ljust(width, '.') for row in grid]\n",
"\n",
" # 2. Find Start Position 'S'\n",
" # We scan the first row (or all rows if S is buried, but usually it's top)\n",
" active_beams = set()\n",
" for y in range(height):\n",
" if 'S' in grid[y]:\n",
" active_beams.add(grid[y].index('S'))\n",
" # We assume beams start flowing from this row downwards\n",
" # so we break and start the simulation from this row\n",
" start_row = y\n",
" break\n",
" else:\n",
" return 0 # No start found\n",
"\n",
" split_count = 0\n",
"\n",
" # 3. Simulate Row by Row\n",
" # We iterate through the rows starting from the one containing 'S'\n",
" for y in range(start_row, height):\n",
" next_beams = set()\n",
" \n",
" # Check every column that currently has a beam\n",
" for x in active_beams:\n",
" cell = grid[y][x]\n",
" \n",
" if cell == '^':\n",
" # SPLITTER: \n",
" # 1. Increment total split count\n",
" split_count += 1\n",
" \n",
" # 2. Create new beams left and right\n",
" # Check boundaries to ensure we don't wrap around or error\n",
" if x - 1 >= 0:\n",
" next_beams.add(x - 1)\n",
" if x + 1 < width:\n",
" next_beams.add(x + 1)\n",
" \n",
" else:\n",
" # EMPTY SPACE (.) or START (S):\n",
" # The beam continues straight down\n",
" next_beams.add(x)\n",
" \n",
" # Update the active beams for the next row iteration\n",
" active_beams = next_beams\n",
" \n",
" # Optimization: If no beams are left, stop early\n",
" if not active_beams:\n",
" break\n",
"\n",
" return split_count"
]
},
{
"cell_type": "markdown",
"id": "f8de5310-3556-4150-a050-5303cd10702e",
"metadata": {},
"source": [
"*The solution is correct:*"
]
},
{
"cell_type": "code",
"execution_count": 31,
"id": "9ef96af9-87b7-4e9b-a5fb-e310bc8e8163",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 7.1: .0009 seconds, answer 1681 correct"
]
},
"execution_count": 31,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"puzzle_input = get_text(7)\n",
"\n",
"answer(7.1, 1681, lambda:\n",
" solve_tachyon_manifold(puzzle_input))"
]
},
{
"cell_type": "markdown",
"id": "c56012a6-b9e7-4053-84c1-79fa457436b1",
"metadata": {},
"source": [
"*For **Part 2**, Gemini gets the key ideas (although it is a bit old-fashioned to use a `defaultdict(int)` instead of a `Counter`).*"
]
},
{
"cell_type": "code",
"execution_count": 32,
"id": "3e05450f-0cdc-401f-99bc-17d7dfb83143",
"metadata": {},
"outputs": [],
"source": [
"from collections import defaultdict\n",
"\n",
"def solve_quantum_tachyon_manifold(puzzle_input):\n",
" # 1. Parse the Grid\n",
" grid = puzzle_input.splitlines()\n",
" grid = [line for line in grid if line]\n",
" \n",
" if not grid:\n",
" return 0\n",
"\n",
" height = len(grid)\n",
" width = max(len(row) for row in grid)\n",
" # Pad rows to ensure uniform width\n",
" grid = [row.ljust(width, '.') for row in grid]\n",
"\n",
" # 2. Find Start Position 'S'\n",
" # 'counts' maps column_index -> number_of_timelines_at_this_column\n",
" counts = defaultdict(int)\n",
" start_row = 0\n",
" found = False\n",
" \n",
" for y in range(height):\n",
" if 'S' in grid[y]:\n",
" start_x = grid[y].index('S')\n",
" counts[start_x] = 1 # We start with 1 timeline\n",
" start_row = y\n",
" found = True\n",
" break\n",
" \n",
" if not found:\n",
" return 0\n",
"\n",
" # 3. Simulate Row by Row\n",
" # We process the grid from the start row down to the bottom.\n",
" # Each iteration calculates where the particles end up in the *next* row.\n",
" for y in range(start_row, height):\n",
" next_counts = defaultdict(int)\n",
" \n",
" for x, num_timelines in counts.items():\n",
" # Determine what is at this position\n",
" # If the timeline has drifted out of bounds, treat it as empty space (.)\n",
" if 0 <= x < width:\n",
" char = grid[y][x]\n",
" else:\n",
" char = '.'\n",
"\n",
" if char == '^':\n",
" # SPLITTER:\n",
" # The timelines here split. The count moves to both left and right.\n",
" # If 5 timelines hit this splitter, 5 go left AND 5 go right.\n",
" next_counts[x - 1] += num_timelines\n",
" next_counts[x + 1] += num_timelines\n",
" else:\n",
" # EMPTY SPACE (.) or START (S):\n",
" # The timelines pass straight through.\n",
" next_counts[x] += num_timelines\n",
" \n",
" # Move to the next row\n",
" counts = next_counts\n",
"\n",
" # 4. Sum Total Timelines\n",
" # After processing the final row, 'counts' holds the number of timelines \n",
" # exiting the bottom of the manifold at each column.\n",
" return sum(counts.values())"
]
},
{
"cell_type": "markdown",
"id": "dc634316-8e9d-46d7-9380-f50345bf6149",
"metadata": {},
"source": [
"*The answer is correct:*"
]
},
{
"cell_type": "code",
"execution_count": 33,
"id": "555b979d-77a8-4bda-b59c-85ce82df2230",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 7.2: .0017 seconds, answer 422102272495018 correct"
]
},
"execution_count": 33,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(7.2, 422102272495018, lambda:\n",
" solve_quantum_tachyon_manifold(puzzle_input))"
]
},
{
"cell_type": "markdown",
"id": "7320594a-1284-4b9a-9a22-c6d2829b5e03",
"metadata": {},
"source": [
"*My friend Barry Hayes suggested I ask Gemini whose code it preferred, its or mine, So I asked Gemini: \"**How would you compare your code to this:\"***"
]
},
{
"cell_type": "code",
"execution_count": 34,
"id": "99d90004-938d-42ac-a5a5-86185129fe4f",
"metadata": {},
"outputs": [],
"source": [
"def count_timelines(manifold: List[str]) -> int:\n",
" \"\"\"How many possible paths are there to the final line of the manifold?\"\"\"\n",
" start = manifold[0].index('S')\n",
" beams = Counter({start: 1})\n",
" for line in manifold:\n",
" for b, n in list(beams.items()):\n",
" if line[b] == '^':\n",
" beams[b] -= n\n",
" beams[b - 1] += n\n",
" beams[b + 1] += n\n",
" return sum(beams.values())"
]
},
{
"cell_type": "markdown",
"id": "7a39ec7d-a27b-40ec-9d32-13f922cc764e",
"metadata": {},
"source": [
"*Here is Gemini's reply:*\n",
"\n",
"This is a great comparison. Your code is much more concise and \"Pythonic,\" using collections.Counter and in-place updates to reduce verbosity. It strips away a lot of the boilerplate that my solution included for robustness.\n",
"\n",
"- **Your Code**: Assumes 'S' is always on the first line (manifold[0]). If 'S' is buried in the middle of the grid, your code crashes or misses it.\n",
"- **My Code**: Scans the whole grid for 'S', which handles cases where the entry point isn't at the very top.\n",
"- **Your Code**: Relies on Python's robust dictionary handling to avoid index errors (if b-1 is negative, it just adds a key to the dict). This is clever, but effectively allows particles to wrap around or exist in \"negative space\" if not careful (though typically harmless for this specific puzzle input).\n",
"\n",
"*It seems to me that the instructions make it clear that there must be an 'S' on the top line, although technically it isn't stated explicitly. (If there is no 'S' on the top line, my code will return 0, not crash.) Similarly, the puzzle implies that beams can't go outside the borders, but Gemini is right that if the input file did have a splitter in the first or last column then there could be a problem that I don't check for.*\n",
"\n",
"*Next I asked Gemini: **Do you think your code could be improved with some more modularity/abstraction, such as having a separate function to get input?** It replied:*\n",
"\n",
"Yes, absolutely. Breaking the code down makes it much easier to read, test, and debug. If the input format changes (e.g., reading from a file vs. a raw string) or the simulation rules change (e.g., different splitter types), you only have to modify one specific function.\n",
"\n",
"Here is a refactored version that separates Input Parsing, State Initialization, and Simulation Logic."
]
},
{
"cell_type": "code",
"execution_count": 35,
"id": "624796aa-510a-4a6a-8c79-50395f654ee9",
"metadata": {},
"outputs": [],
"source": [
"from collections import Counter\n",
"from typing import List, Tuple\n",
"\n",
"def parse_manifold(input_text: str) -> List[str]:\n",
" \"\"\"\n",
" Cleans and converts raw input text into a grid of strings.\n",
" Handles stripping empty lines and padding if necessary.\n",
" \"\"\"\n",
" lines = [line for line in input_text.splitlines() if line]\n",
" if not lines:\n",
" return []\n",
" \n",
" # Optional: Pad lines to uniform width for safety\n",
" width = max(len(line) for line in lines)\n",
" return [line.ljust(width, '.') for line in lines]\n",
"\n",
"def find_start_position(grid: List[str]) -> Tuple[int, int]:\n",
" \"\"\"\n",
" Locates the 'S' character in the grid.\n",
" Returns (row_index, col_index).\n",
" \"\"\"\n",
" for r, row in enumerate(grid):\n",
" if 'S' in row:\n",
" return r, row.index('S')\n",
" raise ValueError(\"Start position 'S' not found in manifold.\")\n",
"\n",
"def process_row(beams: Counter, row_string: str) -> None:\n",
" \"\"\"\n",
" Updates the beam positions for a single row of the manifold.\n",
" Modifies the 'beams' Counter in-place using the subtraction logic.\n",
" \"\"\"\n",
" # Create a snapshot of items to iterate over, since we modify 'beams' in the loop\n",
" current_states = list(beams.items())\n",
" \n",
" for col, count in current_states:\n",
" # Boundary check ensures we don't access outside the string\n",
" if col < 0 or col >= len(row_string):\n",
" continue\n",
" \n",
" char = row_string[col]\n",
" \n",
" if char == '^':\n",
" # Splitter logic: Remove current stack, add to left and right\n",
" beams[col] -= count\n",
" beams[col - 1] += count\n",
" beams[col + 1] += count\n",
" # Else: beam passes through (no change needed for '.')\n",
"\n",
"def solve_quantum_manifold(input_text: str) -> int:\n",
" \"\"\"\n",
" Main orchestrator function.\n",
" \"\"\"\n",
" # 1. Get Input\n",
" grid = parse_manifold(input_text)\n",
" \n",
" # 2. Initialize State\n",
" try:\n",
" start_row, start_col = find_start_position(grid)\n",
" except ValueError:\n",
" return 0\n",
"\n",
" beams = Counter({start_col: 1})\n",
"\n",
" # 3. Run Simulation\n",
" # We slice the grid to start from the row containing 'S'\n",
" for row in grid[start_row:]:\n",
" process_row(beams, row)\n",
"\n",
" # 4. Output Result\n",
" return sum(beams.values())"
]
},
{
"cell_type": "markdown",
"id": "23e17286-9534-4da1-8ec9-26a4333b8c70",
"metadata": {},
"source": [
"*I like this refactored version better. It produces the correct answer:*"
]
},
{
"cell_type": "code",
"execution_count": 36,
"id": "a4a0b5b5-00a0-4488-8588-cb6f137be239",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 7.2: .0022 seconds, answer 422102272495018 correct"
]
},
"execution_count": 36,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(7.2, 422102272495018, lambda:\n",
" solve_quantum_manifold(puzzle_input))"
]
},
{
"cell_type": "markdown",
"id": "518c135a-6b5e-4602-b37f-c79c0573a9a1",
"metadata": {},
"source": [
"# [Day 8](https://adventofcode.com/2025/day/8): Claude Opus 4.5\n",
"\n",
"*Here's Claude's code for **Part 1**:*"
]
},
{
"cell_type": "code",
"execution_count": 37,
"id": "fce446d7-b72f-4ade-b783-b40f4212b43b",
"metadata": {},
"outputs": [],
"source": [
"\"\"\"\n",
"Solution for the Junction Box puzzle.\n",
"\n",
"Connect the 1000 pairs of junction boxes which are closest together.\n",
"Then multiply together the sizes of the three largest circuits.\n",
"\"\"\"\n",
"\n",
"import sys\n",
"from collections import defaultdict\n",
"\n",
"def parse_input(text):\n",
" \"\"\"Parse the input into a list of (x, y, z) coordinates.\"\"\"\n",
" boxes = []\n",
" for line in text.strip().split('\\n'):\n",
" if line.strip():\n",
" x, y, z = map(int, line.strip().split(','))\n",
" boxes.append((x, y, z))\n",
" return boxes\n",
"\n",
"def distance_squared(p1, p2):\n",
" \"\"\"Calculate squared Euclidean distance between two points.\"\"\"\n",
" return (p1[0] - p2[0])**2 + (p1[1] - p2[1])**2 + (p1[2] - p2[2])**2\n",
"\n",
"class UnionFind:\n",
" \"\"\"Union-Find data structure for tracking connected components.\"\"\"\n",
" def __init__(self, n):\n",
" self.parent = list(range(n))\n",
" self.rank = [0] * n\n",
" self.size = [1] * n\n",
" \n",
" def find(self, x):\n",
" if self.parent[x] != x:\n",
" self.parent[x] = self.find(self.parent[x])\n",
" return self.parent[x]\n",
" \n",
" def union(self, x, y):\n",
" px, py = self.find(x), self.find(y)\n",
" if px == py:\n",
" return False # Already in same circuit\n",
" if self.rank[px] < self.rank[py]:\n",
" px, py = py, px\n",
" self.parent[py] = px\n",
" self.size[px] += self.size[py]\n",
" if self.rank[px] == self.rank[py]:\n",
" self.rank[px] += 1\n",
" return True\n",
" \n",
" def get_component_sizes(self):\n",
" \"\"\"Get sizes of all connected components.\"\"\"\n",
" components = defaultdict(int)\n",
" for i in range(len(self.parent)):\n",
" root = self.find(i)\n",
" components[root] = self.size[root]\n",
" return list(components.values())\n",
"\n",
"def solve(boxes, num_connections=1000):\n",
" \"\"\"Connect the num_connections closest pairs and return product of 3 largest circuit sizes.\"\"\"\n",
" n = len(boxes)\n",
" \n",
" # Calculate all pairwise distances\n",
" distances = []\n",
" for i in range(n):\n",
" for j in range(i + 1, n):\n",
" dist_sq = distance_squared(boxes[i], boxes[j])\n",
" distances.append((dist_sq, i, j))\n",
" \n",
" # Sort by distance\n",
" distances.sort()\n",
" \n",
" # Use Union-Find to track circuits\n",
" uf = UnionFind(n)\n",
" \n",
" # Make the specified number of connections\n",
" connections_made = 0\n",
" for dist_sq, i, j in distances:\n",
" if connections_made >= num_connections:\n",
" break\n",
" # Attempt to connect (even if already in same circuit, it counts as a connection attempt)\n",
" uf.union(i, j)\n",
" connections_made += 1\n",
" \n",
" # Get component sizes and find the three largest\n",
" sizes = uf.get_component_sizes()\n",
" sizes.sort(reverse=True)\n",
" \n",
" # Multiply the three largest\n",
" result = 1\n",
" for i in range(min(3, len(sizes))):\n",
" result *= sizes[i]\n",
" \n",
" return result"
]
},
{
"cell_type": "markdown",
"id": "0a3f11c5-e193-41ad-b53c-4ca3aac11fc3",
"metadata": {},
"source": [
"*Here I parse the input and hand it to Claude's function, which computes the correct answer:*"
]
},
{
"cell_type": "code",
"execution_count": 38,
"id": "dd99ffbf-3304-49f5-8cfa-d5428edf0db7",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Puzzle 8.1: .8993 seconds, answer 24360 correct"
]
},
"execution_count": 38,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"boxes = parse(8, ints, show=0)\n",
"\n",
"answer(8.1, 24360, lambda:\n",
" solve(boxes, 1000))"
]
},
{
"cell_type": "markdown",
"id": "2b1df990-73d9-4cb1-aeca-3bad52c86c8a",
"metadata": {},
"source": [
"*Here is Claude's **Part 2** code:*"
]
},
{
"cell_type": "code",
"execution_count": 39,
"id": "9ed8312b-4455-4f2d-bef6-67097898aefd",
"metadata": {},
"outputs": [],
"source": [
"\"\"\"\n",
"Solution for the Junction Box puzzle - Part 2.\n",
"\n",
"Connect junction boxes until they're all in one circuit.\n",
"Find the last two junction boxes connected and multiply their X coordinates.\n",
"\"\"\"\n",
"\n",
"import sys\n",
"from collections import defaultdict\n",
"\n",
"def parse_input(text):\n",
" \"\"\"Parse the input into a list of (x, y, z) coordinates.\"\"\"\n",
" boxes = []\n",
" for line in text.strip().split('\\n'):\n",
" if line.strip():\n",
" x, y, z = map(int, line.strip().split(','))\n",
" boxes.append((x, y, z))\n",
" return boxes\n",
"\n",
"def distance_squared(p1, p2):\n",
" \"\"\"Calculate squared Euclidean distance between two points.\"\"\"\n",
" return (p1[0] - p2[0])**2 + (p1[1] - p2[1])**2 + (p1[2] - p2[2])**2\n",
"\n",
"class UnionFind:\n",
" \"\"\"Union-Find data structure for tracking connected components.\"\"\"\n",
" def __init__(self, n):\n",
" self.parent = list(range(n))\n",
" self.rank = [0] * n\n",
" self.size = [1] * n\n",
" self.num_components = n\n",
" \n",
" def find(self, x):\n",
" if self.parent[x] != x:\n",
" self.parent[x] = self.find(self.parent[x])\n",
" return self.parent[x]\n",
" \n",
" def union(self, x, y):\n",
" px, py = self.find(x), self.find(y)\n",
" if px == py:\n",
" return False # Already in same circuit\n",
" if self.rank[px] < self.rank[py]:\n",
" px, py = py, px\n",
" self.parent[py] = px\n",
" self.size[px] += self.size[py]\n",
" if self.rank[px] == self.rank[py]:\n",
" self.rank[px] += 1\n",
" self.num_components -= 1\n",
" return True\n",
"\n",
"def solve_part2(boxes):\n",
" \"\"\"Connect boxes until all in one circuit, return product of X coords of last connection.\"\"\"\n",
" n = len(boxes)\n",
" \n",
" # Calculate all pairwise distances\n",
" distances = []\n",
" for i in range(n):\n",
" for j in range(i + 1, n):\n",
" dist_sq = distance_squared(boxes[i], boxes[j])\n",
" distances.append((dist_sq, i, j))\n",
" \n",
" # Sort by distance\n",
" distances.sort()\n",
" \n",
" # Use Union-Find to track circuits\n",
" uf = UnionFind(n)\n",
" \n",
" # Keep connecting until all in one circuit\n",
" last_i, last_j = None, None\n",
" for dist_sq, i, j in distances:\n",
" if uf.union(i, j):\n",
" # This connection actually merged two circuits\n",
" last_i, last_j = i, j\n",
" if uf.num_components == 1:\n",
" # All boxes are now in one circuit!\n",
" break\n",
" \n",
" # Return product of X coordinates\n",
" x1 = boxes[last_i][0]\n",
" x2 = boxes[last_j][0]\n",
" print(f\"Last connection: {boxes[last_i]} and {boxes[last_j]}\")\n",
" print(f\"X coordinates: {x1} and {x2}\")\n",
" return x1 * x2"
]
},
{
"cell_type": "markdown",
"id": "fa637a95-9774-4d76-b2a4-c0f929f13b25",
"metadata": {},
"source": [
"*This code is correct. It does some extraneous printing, which is unneccessary, but not strictly incorrect.*"
]
},
{
"cell_type": "code",
"execution_count": 40,
"id": "64309157-e1d3-48fc-ba06-e5e66ba23026",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Last connection: (53148, 96207, 9295) and (41127, 88410, 10248)\n",
"X coordinates: 53148 and 41127\n"
]
},
{
"data": {
"text/plain": [
"Puzzle 8.2: .8707 seconds, answer 2185817796 correct"
]
},
"execution_count": 40,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"answer(8.2, 2185817796, lambda:\n",
" solve_part2(boxes))"
]
},
{
"cell_type": "markdown",
"id": "8aa26008-a652-4860-9c84-5ba4344d32f3",
"metadata": {},
"source": [
"# Summary\n",
"\n",
"*Overall, the models did well, producing code that gives the correct answer in a reasonable run time. Some of the code could be improved stylistically. (But I guess if you're vibe coding and not looking at the code, maybe that doesn't matter.) Here are the answers and the run times:*"
]
},
{
"cell_type": "code",
"execution_count": 41,
"id": "16ce8af4-a1da-4280-9078-116ced3e9941",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Puzzle 1.1: .0013 seconds, answer 1182 correct\n",
"Puzzle 1.2: .0016 seconds, answer 6907 correct\n",
"Puzzle 2.1: .1247 seconds, answer 23560874270 correct\n",
"Puzzle 2.2: .1325 seconds, answer 44143124633 correct\n",
"Puzzle 3.1: .0042 seconds, answer 17085 correct\n",
"Puzzle 3.2: .0078 seconds, answer 169408143086082 correct\n",
"Puzzle 4.1: .0182 seconds, answer 1569 correct\n",
"Puzzle 4.2: .0938 seconds, answer 9280 correct\n",
"Puzzle 5.1: .0056 seconds, answer 635 correct\n",
"Puzzle 5.2: .0002 seconds, answer 369761800782619 correct\n",
"Puzzle 6.1: .0078 seconds, answer 5877594983578 correct\n",
"Puzzle 6.2: .0070 seconds, answer 11159825706149 correct\n",
"Puzzle 7.1: .0009 seconds, answer 1681 correct\n",
"Puzzle 7.2: .0022 seconds, answer 422102272495018 correct\n",
"Puzzle 8.1: .8993 seconds, answer 24360 correct\n",
"Puzzle 8.2: .8707 seconds, answer 2185817796 correct\n",
"\n",
"Time in seconds: sum = 2.178, mean = .136, median = .007, max = .899\n"
]
}
],
"source": [
"summary(answers)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.15"
}
},
"nbformat": 4,
"nbformat_minor": 5
}