72 Commits

Author SHA1 Message Date
bc6f3dc8c6 2025 day 10 part 2 in Rust
Using a library. I'm not happy about it but also I have thought about
this enough.
2025-12-11 21:46:15 +01:00
a8a1c85498 2025 day 11 in Python 2025-12-11 09:44:39 +01:00
ff8b8dac0c 2025 day 10 part 1 in Rust 2025-12-10 08:39:51 +01:00
51ca5db42e 2025 day 9 part 2 in TS 2025-12-09 19:44:31 +01:00
e60bfbf8c4 2025 day 9 part 1 in TypeScript 2025-12-09 08:41:13 +01:00
768a3ff10b One pointless index removed 2025-12-08 22:07:02 +01:00
c3d3116f8a Improvise heap sort for partial sorting 2025-12-08 21:51:17 +01:00
13592a60ee 2025 day 8 in Go 2025-12-08 21:21:24 +01:00
be6c3d37ea Minor cleanup
Avoid repeatedly creating sets that live for almost no time, as well as
some readability fixes
2025-12-07 22:38:39 +01:00
ff2c86a11b Missing compilation instructions 2025-12-07 22:26:25 +01:00
a737a35227 2025 day 7 part 2 in Haskell 2025-12-07 16:51:24 +01:00
29e02c819e Implement 2025 day 7 part 1 in Haskell 2025-12-07 15:12:21 +01:00
a01eb547d1 2025 day 6 in PHP 2025-12-06 12:42:32 +01:00
1485eb3cd5 Implement 2025 day 5 in C 2025-12-05 08:52:58 +01:00
0a7ec71a97 Very ugly part 2 2025-12-04 22:35:12 +01:00
f2204e554b Moderately cursed 2025 day 4 part 1 2025-12-04 09:29:39 +01:00
6f4b02af33 Implement 2025 day 3 in bash 2025-12-03 09:54:40 +01:00
7e23cf94a6 2025 day 2 part 2 in Terraform 2025-12-02 13:39:20 +01:00
f132842b5c Forgot to add 2025-12-02 09:58:16 +01:00
027a7bdde6 Attempt at part two, OOM 2025-12-02 09:55:37 +01:00
2277721010 2025 day 2 part 1 2025-12-02 09:36:44 +01:00
5e9a24c8d7 Document Nix solution 2025-12-01 21:13:50 +01:00
1468c87347 Implement 2025 day 1 part 2 2025-12-01 20:47:21 +01:00
1dc59c18eb Solve 2025 day 1 part 1 2025-12-01 08:59:13 +01:00
dbcbd15103 Prepare 2025 2025-12-01 08:08:13 +01:00
b6aafa1b27 Slightly cleaner 2025-02-03 23:10:53 +01:00
1f7108be47 Euclid's algorithm 2025-02-03 23:02:24 +01:00
48fb8cf8c9 Remove unnecesary duplication 2025-02-03 20:33:49 +01:00
f9416db251 Implement day 8 2024 in Terraform 2025-01-29 00:43:14 +01:00
6941f2b2d2 Rely more on type coercion 2025-01-21 21:40:07 +01:00
dc92b65830 Inline loop 2024-12-27 21:45:36 +01:00
5c030d5272 Implement 2024 day 14 part 1 in Terraform 2024-12-27 21:14:28 +01:00
d07bb9235b Implement 2024 day 13 in terraform 2024-12-27 21:08:47 +01:00
b23676bf04 Implement 2024 day 21 part 2
Invent a unit test for it too, because why not, it's Christmas
2024-12-26 16:43:15 +01:00
40632c8114 Implement 2024 day 21 part 1 2024-12-26 15:55:06 +01:00
073b576fd8 Implement 2024 day 25 in Terraform 2024-12-26 15:13:46 +01:00
f3a3e1fca3 More numpy for speed 2024-12-25 10:34:40 +01:00
4a479c1646 Implement 2024 day 25 2024-12-25 10:25:48 +01:00
b41571949e Fix problem by adding more lookback 2024-12-24 21:17:49 +01:00
e949ce9932 Sort of functional implementation of 2024 day 24 2024-12-24 20:56:12 +01:00
48824288b0 Implement 2024 day 23
Is using networkx cheating? I don't think so. I've already solved
max-clique before, so I know I can do it
2024-12-23 08:21:45 +01:00
9e552c9b6a More inlining 2024-12-23 08:00:10 +01:00
329cd4e471 More optimizations 2024-12-22 14:11:39 +01:00
321d78d850 More loops to numpy 2024-12-22 12:15:11 +01:00
230b4ae028 Implement 2024 day 22 2024-12-22 12:03:44 +01:00
be2244eca9 Inline module
Because this is Terraform, and it's the ultimate optimisation technique
2024-12-20 20:15:30 +01:00
395463dc4a Implement 2024 day 20 2024-12-20 09:46:53 +01:00
fdaadfe184 Implement day 19 part 1 in Terraform 2024-12-19 23:24:11 +01:00
dd07090056 Poor man's trie 2024-12-19 22:35:55 +01:00
891a5ea50c Inline function to avoid passing arg 2024-12-19 09:13:37 +01:00
d4aad95f55 Implement 2024 day 19 2024-12-19 09:05:55 +01:00
caa2c9b6cf Implement 2024 day 18 2024-12-18 09:14:39 +01:00
4a8b9f9109 Implement 2024 day 17 part 2 2024-12-17 22:17:12 +01:00
6c0a49a5f3 Implement 2024 day 17 part 1 2024-12-17 09:55:07 +01:00
17017e7ab4 Formatting? 2024-12-17 09:34:43 +01:00
b23f24c567 Implement 2024 day 16 part 2 2024-12-16 18:16:05 +01:00
f0556c6afe Implement 2024 day 16 part 1 2024-12-16 08:58:42 +01:00
da7ee96b93 Slihghtly simplify 2024-12-16 08:33:02 +01:00
e90ee40880 Implement 2024 day 15 2024-12-15 14:05:31 +01:00
98983f622c It looks nice, not what I expected 2024-12-14 09:18:36 +01:00
4a7305d7eb Implement 2024 day 14 2024-12-14 09:11:16 +01:00
c8ab67d145 Implement 2024 day 13 2024-12-13 08:36:20 +01:00
044c971d00 Implement 2024 day 12 2024-12-12 22:18:32 +01:00
8308580284 Actually run part 2 in Terraform, madness 2024-12-11 23:18:56 +01:00
612c4d19ef Implement efficient algorithm in TF 2024-12-11 22:33:42 +01:00
2beccdd0e6 day 11 part 1 in terraform 2024-12-11 18:53:06 +01:00
023807a701 Implement 2024 day 11 2024-12-11 08:51:32 +01:00
3e709cc863 Implement 2024 day 10 2024-12-10 09:38:31 +01:00
d49fc8e769 Do day 7 RTL
That does indeed cut down branching significantly
2024-12-09 22:30:52 +01:00
cb7aedc4ba Implement 2024 day 9 2024-12-09 18:14:49 +01:00
dcb0f4514d Restructure tests 2024-12-08 10:33:17 +01:00
dfee3d4e11 Implement 2024 day 8 2024-12-08 10:17:29 +01:00
142 changed files with 5340 additions and 183 deletions

View File

@@ -1,43 +0,0 @@
on:
- push
name: Advent of Code 2024
jobs:
ci:
strategy:
matrix:
python-version:
- "3.12"
- "3.13"
name: Continuous Integration
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v3
with:
enable-cache: true
cache-dependency-glob: "2024/uv.lock"
- name: Check formatting
working-directory: "2024"
run: >
uv run ruff format --check
- name: Run lints
working-directory: "2024"
run: >
uv run ruff check
- name: Run tests
working-directory: "2024"
run: >
uv run pytest tests

15
2024/bonus/24todot.py Normal file
View File

@@ -0,0 +1,15 @@
import fileinput
print("digraph day24 {")
for line in fileinput.input():
parts = line.split(" ")
if len(parts) != 5:
continue
first, op, second, _, result = parts
print(f'{first}{second}{op} [label="{op}"];')
print(f"{first} -> {first}{second}{op} -> {result};")
print(f"{second} -> {first}{second}{op};")
print("}")

View File

@@ -6,8 +6,8 @@ locals {
cleaned_input = replace(var.input, "/ +/", " ")
lines = split("\n", trim(local.cleaned_input, "\n"))
lines_split = [for line in local.lines : split(" ", line)]
left = [for line in local.lines_split : parseint(line[0], 10)]
right = [for line in local.lines_split : parseint(line[1], 10)]
left = [for line in local.lines_split : tonumber(line[0])]
right = [for line in local.lines_split : tonumber(line[1])]
left_sorted = sort(local.left)
right_sorted = sort(local.right)

View File

@@ -3,7 +3,7 @@ variable "input" {
}
locals {
reports = [for line in split("\n", trim(var.input, "\n")) : [for num in split(" ", line) : parseint(num, 10)]]
reports = [for line in split("\n", trim(var.input, "\n")) : split(" ", line)]
}
module "part1_valid" {

View File

@@ -1,21 +0,0 @@
variable "update" {
type = list(number)
}
variable "disallow_rules" {
type = map(list(number))
}
locals {
not_disallowed = alltrue([
for i in range(1, length(var.update)) :
!contains(
flatten([for j in range(i) : lookup(var.disallow_rules, var.update[j], [])]),
var.update[i]
)
])
}
output "valid" {
value = local.not_disallowed ? var.update[floor(length(var.update) / 2)] : 0
}

View File

@@ -8,16 +8,18 @@ locals {
disallow_rules = { for rule in local.rules : rule[1] => rule[0]... }
updates = [for update_line in split("\n", local.parts[1]) : [for v in split(",", update_line) : tonumber(v)]]
}
module "is_valid" {
source = "./is_correct"
count = length(local.updates)
update = local.updates[count.index]
disallow_rules = local.disallow_rules
scores = [
for update in local.updates :
alltrue([
for i in range(1, length(update)) :
!contains(
flatten([for j in range(i) : lookup(local.disallow_rules, update[j], [])]),
update[i]
)
]) ? update[floor(length(update) / 2)] : 0]
}
output "part1" {
value = sum(module.is_valid[*].valid)
value = sum(local.scores[*])
}

View File

@@ -0,0 +1,52 @@
variable "width" {
type = number
}
variable "height" {
type = number
}
variable "antennae" {
type = list(tuple([number, number]))
}
locals {
pairs = concat([
for i in range(length(var.antennae)) :
[
for j in range(i + 1, length(var.antennae)) : [var.antennae[i], var.antennae[j]]
]
]...)
}
module "pair" {
source = "./pair"
count = length(local.pairs)
first = local.pairs[count.index][0]
second = local.pairs[count.index][1]
width = var.width
height = var.height
}
output "nodes1" {
value = setunion([
for i in range(length(local.pairs)) :
[
for v in module.pair[i].nodes1 :
v
if v[0] >= 0 && v[0] < var.width && v[1] >= 0 && v[1] < var.height
]
]...)
}
output "nodes2" {
value = setunion([
for i in range(length(local.pairs)) :
[
for v in module.pair[i].nodes2 :
v
if v[0] >= 0 && v[0] < var.width && v[1] >= 0 && v[1] < var.height
]
]...)
}

View File

@@ -0,0 +1,52 @@
variable "first" {
type = tuple([number, number])
}
variable "second" {
type = tuple([number, number])
}
variable "width" {
type = number
}
variable "height" {
type = number
}
locals {
dx = var.second[0] - var.first[0]
dy = var.second[1] - var.first[1]
# sort() doesn't work as it turns the numbers into strings
gcd0 = abs(local.dx) < abs(local.dy) ? [abs(local.dx), abs(local.dy)] : [abs(local.dy), abs(local.dx)]
# Do as many iterations as necessary.
gcd1 = local.gcd0[0] == 0 ? local.gcd0 : [local.gcd0[1] % local.gcd0[0], local.gcd0[0]]
gcd2 = local.gcd1[0] == 0 ? local.gcd1 : [local.gcd1[1] % local.gcd1[0], local.gcd1[0]]
gcd3 = local.gcd2[0] == 0 ? local.gcd2 : [local.gcd2[1] % local.gcd2[0], local.gcd2[0]]
gcd4 = local.gcd3[0] == 0 ? local.gcd3 : [local.gcd3[1] % local.gcd3[0], local.gcd3[0]]
gcd5 = local.gcd4[0] == 0 ? local.gcd4 : [local.gcd4[1] % local.gcd4[0], local.gcd4[0]]
gcd6 = local.gcd5[0] == 0 ? local.gcd5 : [local.gcd5[1] % local.gcd5[0], local.gcd5[0]]
gcd7 = local.gcd6[0] == 0 ? local.gcd6 : [local.gcd6[1] % local.gcd6[0], local.gcd6[0]]
gcd8 = local.gcd7[0] == 0 ? local.gcd7 : [local.gcd7[1] % local.gcd7[0], local.gcd7[0]]
gcd9 = local.gcd8[0] == 0 ? local.gcd8 : [local.gcd8[1] % local.gcd8[0], local.gcd8[0]]
gcd10 = local.gcd9[0] == 0 ? local.gcd9 : [local.gcd9[1] % local.gcd9[0], local.gcd9[0]]
# 10 iterations should cover numbers up to 55, which is more than the width/height
gcd = local.gcd10[1]
}
output "nodes1" {
value = [
[var.first[0] - local.dx, var.first[1] - local.dy],
[var.second[0] + local.dx, var.second[1] + local.dy],
]
}
output "nodes2" {
value = concat(
[for i in range(max(var.width, var.height)) : [var.first[0] - i * local.dx / local.gcd, var.first[1] - i * local.dy / local.gcd]],
[for i in range(max(var.width, var.height)) : [var.second[0] + i * local.dx / local.gcd, var.second[1] + i * local.dy / local.gcd]]
)
}

39
2024/bonus/day08/main.tf Normal file
View File

@@ -0,0 +1,39 @@
variable "input" {
type = string
}
locals {
lines = split("\n", chomp(var.input))
height = length(local.lines)
width = length(local.lines[0])
antennae = concat([
for y in range(local.height) :
[
for x in range(local.width) :
[substr(local.lines[y], x, 1), x, y]
if substr(local.lines[y], x, 1) != "."
]
]...)
by_freq = {
for antenna in local.antennae :
antenna[0] => [antenna[1], antenna[2]]...
}
}
module "freq" {
source = "./freq"
for_each = local.by_freq
width = local.width
height = local.height
antennae = each.value
}
output "part1" {
value = length(setunion([for _, v in module.freq : v.nodes1]...))
}
output "part2" {
value = length(setunion([for _, v in module.freq : v.nodes2]...))
}

24
2024/bonus/day11/main.tf Normal file
View File

@@ -0,0 +1,24 @@
variable "input" {
type = string
}
locals {
nums = [for s in split(" ", chomp(var.input)) : tonumber(s)]
grouped = { for num in local.nums : num => 1... }
total = { for k, v in local.grouped : k => sum(v) }
}
module "step1" {
source = "./step"
prev = local.total
}
output "part1" {
value = sum(values(module.step25.next))
}
output "part2" {
value = sum(values(module.step75.next))
}

View File

@@ -0,0 +1,32 @@
variable "prev" {
type = map(number)
}
locals {
by_value = flatten([
for num, count in var.prev : (
tonumber(num) == 0
? [{ number = 1, amount = count }]
: (
length(tostring(num)) % 2 == 0
? [
{
number = tonumber(substr(tostring(num), 0, length(tostring(num)) / 2)),
amount = count
},
{
number = tonumber(substr(tostring(num), length(tostring(num)) / 2, length(tostring(num)) / 2)),
amount = count,
},
]
: [{ number = 2024 * num, amount = count }]
)
)
])
grouped = { for kv in local.by_value : kv.number => kv.amount... }
}
output "next" {
value = { for num, groups in local.grouped : num => sum(groups) }
}

446
2024/bonus/day11/steps.tf Normal file
View File

@@ -0,0 +1,446 @@
# This file was generated using a nasty bash for loop. Edit at your own peril.
module "step2" {
source = "./step"
prev = module.step1.next
}
module "step3" {
source = "./step"
prev = module.step2.next
}
module "step4" {
source = "./step"
prev = module.step3.next
}
module "step5" {
source = "./step"
prev = module.step4.next
}
module "step6" {
source = "./step"
prev = module.step5.next
}
module "step7" {
source = "./step"
prev = module.step6.next
}
module "step8" {
source = "./step"
prev = module.step7.next
}
module "step9" {
source = "./step"
prev = module.step8.next
}
module "step10" {
source = "./step"
prev = module.step9.next
}
module "step11" {
source = "./step"
prev = module.step10.next
}
module "step12" {
source = "./step"
prev = module.step11.next
}
module "step13" {
source = "./step"
prev = module.step12.next
}
module "step14" {
source = "./step"
prev = module.step13.next
}
module "step15" {
source = "./step"
prev = module.step14.next
}
module "step16" {
source = "./step"
prev = module.step15.next
}
module "step17" {
source = "./step"
prev = module.step16.next
}
module "step18" {
source = "./step"
prev = module.step17.next
}
module "step19" {
source = "./step"
prev = module.step18.next
}
module "step20" {
source = "./step"
prev = module.step19.next
}
module "step21" {
source = "./step"
prev = module.step20.next
}
module "step22" {
source = "./step"
prev = module.step21.next
}
module "step23" {
source = "./step"
prev = module.step22.next
}
module "step24" {
source = "./step"
prev = module.step23.next
}
module "step25" {
source = "./step"
prev = module.step24.next
}
module "step26" {
source = "./step"
prev = module.step25.next
}
module "step27" {
source = "./step"
prev = module.step26.next
}
module "step28" {
source = "./step"
prev = module.step27.next
}
module "step29" {
source = "./step"
prev = module.step28.next
}
module "step30" {
source = "./step"
prev = module.step29.next
}
module "step31" {
source = "./step"
prev = module.step30.next
}
module "step32" {
source = "./step"
prev = module.step31.next
}
module "step33" {
source = "./step"
prev = module.step32.next
}
module "step34" {
source = "./step"
prev = module.step33.next
}
module "step35" {
source = "./step"
prev = module.step34.next
}
module "step36" {
source = "./step"
prev = module.step35.next
}
module "step37" {
source = "./step"
prev = module.step36.next
}
module "step38" {
source = "./step"
prev = module.step37.next
}
module "step39" {
source = "./step"
prev = module.step38.next
}
module "step40" {
source = "./step"
prev = module.step39.next
}
module "step41" {
source = "./step"
prev = module.step40.next
}
module "step42" {
source = "./step"
prev = module.step41.next
}
module "step43" {
source = "./step"
prev = module.step42.next
}
module "step44" {
source = "./step"
prev = module.step43.next
}
module "step45" {
source = "./step"
prev = module.step44.next
}
module "step46" {
source = "./step"
prev = module.step45.next
}
module "step47" {
source = "./step"
prev = module.step46.next
}
module "step48" {
source = "./step"
prev = module.step47.next
}
module "step49" {
source = "./step"
prev = module.step48.next
}
module "step50" {
source = "./step"
prev = module.step49.next
}
module "step51" {
source = "./step"
prev = module.step50.next
}
module "step52" {
source = "./step"
prev = module.step51.next
}
module "step53" {
source = "./step"
prev = module.step52.next
}
module "step54" {
source = "./step"
prev = module.step53.next
}
module "step55" {
source = "./step"
prev = module.step54.next
}
module "step56" {
source = "./step"
prev = module.step55.next
}
module "step57" {
source = "./step"
prev = module.step56.next
}
module "step58" {
source = "./step"
prev = module.step57.next
}
module "step59" {
source = "./step"
prev = module.step58.next
}
module "step60" {
source = "./step"
prev = module.step59.next
}
module "step61" {
source = "./step"
prev = module.step60.next
}
module "step62" {
source = "./step"
prev = module.step61.next
}
module "step63" {
source = "./step"
prev = module.step62.next
}
module "step64" {
source = "./step"
prev = module.step63.next
}
module "step65" {
source = "./step"
prev = module.step64.next
}
module "step66" {
source = "./step"
prev = module.step65.next
}
module "step67" {
source = "./step"
prev = module.step66.next
}
module "step68" {
source = "./step"
prev = module.step67.next
}
module "step69" {
source = "./step"
prev = module.step68.next
}
module "step70" {
source = "./step"
prev = module.step69.next
}
module "step71" {
source = "./step"
prev = module.step70.next
}
module "step72" {
source = "./step"
prev = module.step71.next
}
module "step73" {
source = "./step"
prev = module.step72.next
}
module "step74" {
source = "./step"
prev = module.step73.next
}
module "step75" {
source = "./step"
prev = module.step74.next
}

31
2024/bonus/day13/main.tf Normal file
View File

@@ -0,0 +1,31 @@
variable "input" {
type = string
}
locals {
machines = regexall(
"Button A: X\\+(\\d+), Y\\+(\\d+)\nButton B: X\\+(\\d+), Y\\+(\\d+)\nPrize: X=(\\d+), Y=(\\d+)",
var.input
)
}
module "solve1" {
source = "./solve"
machines = local.machines
}
module "solve2" {
source = "./solve"
machines = [
for machine in local.machines :
[machine[0], machine[1], machine[2], machine[3], 10000000000000 + machine[4], 10000000000000 + machine[5]]
]
}
output "part1" {
value = module.solve1.solutions
}
output "part2" {
value = module.solve2.solutions
}

View File

@@ -0,0 +1,34 @@
variable "machines" {
type = list(list(number))
}
locals {
a_substitutions = [
for machine in var.machines :
[-machine[2] / machine[0], machine[4] / machine[0]]
]
b_equations = [
for i in range(length(var.machines)) :
[
var.machines[i][3] + local.a_substitutions[i][0] * var.machines[i][1],
var.machines[i][5] - local.a_substitutions[i][1] * var.machines[i][1]
]
]
b = [for eq in local.b_equations : floor(eq[1] / eq[0] + 0.5)]
a = [
for i in range(length(var.machines)) :
floor((var.machines[i][4] - local.b[i] * var.machines[i][2]) / var.machines[i][0] + 0.5)
]
}
output "solutions" {
value = sum([
for i in range(length(var.machines)) :
3 * local.a[i] + local.b[i]
if var.machines[i][0] * local.a[i] + var.machines[i][2] * local.b[i] == var.machines[i][4]
&& var.machines[i][1] * local.a[i] + var.machines[i][3] * local.b[i] == var.machines[i][5]
])
}

33
2024/bonus/day14/main.tf Normal file
View File

@@ -0,0 +1,33 @@
variable "input" {
type = string
}
variable "width" {
type = number
default = 101
}
variable "height" {
type = number
default = 103
}
locals {
lines = regexall("p=(-?\\d+),(-?\\d+) v=(-?\\d+),(-?\\d+)", var.input)
positions = [
for line in local.lines :
[
((line[0] + 100 * line[2]) % var.width + var.width) % var.width,
((line[1] + 100 * line[3]) % var.height + var.height) % var.height,
]
]
q1 = length([for pos in local.positions : pos if pos[0] < floor(var.width / 2) && pos[1] < floor(var.height / 2)])
q2 = length([for pos in local.positions : pos if pos[0] > floor(var.width / 2) && pos[1] < floor(var.height / 2)])
q3 = length([for pos in local.positions : pos if pos[0] < floor(var.width / 2) && pos[1] > floor(var.height / 2)])
q4 = length([for pos in local.positions : pos if pos[0] > floor(var.width / 2) && pos[1] > floor(var.height / 2)])
}
output "part1" {
value = local.q1 * local.q2 * local.q3 * local.q4
}

13
2024/bonus/day19/main.tf Normal file
View File

@@ -0,0 +1,13 @@
variable "input" {
type = string
}
locals {
parts = split("\n\n", chomp((var.input)))
patterns = replace(local.parts[0], ", ", "|")
valid = [for line in split("\n", local.parts[1]) : line if length(regexall("^(${local.patterns})+$", line)) > 0]
}
output "part1" {
value = length(local.valid)
}

23
2024/bonus/day25/main.tf Normal file
View File

@@ -0,0 +1,23 @@
variable "input" {
type = string
}
locals {
blocks = split("\n\n", chomp(var.input))
heights = [
for block in local.blocks : [
for i in range(5) : length([
for line in split("\n", block) : line if substr(line, i, 1) == "#"
])
]
]
locks = [for i in range(length(local.blocks)) : local.heights[i] if startswith(local.blocks[i], "#####")]
keys = [for i in range(length(local.blocks)) : local.heights[i] if !startswith(local.blocks[i], "#####")]
combined = concat([for lock in local.locks : [for key in local.keys : [for i in range(5) : lock[i] + key[i] <= 7]]]...)
}
output "part1" {
value = length([for combination in local.combined : combination if alltrue(combination)])
}

View File

@@ -65,3 +65,69 @@ module "day05" {
output "day05_1" {
value = module.day05.part1
}
module "day08" {
source = "./day08"
input = file("../inputs/08.txt")
}
output "day08_1" {
value = module.day08.part1
}
output "day08_2" {
value = module.day08.part2
}
module "day11" {
source = "./day11"
input = file("../inputs/11.txt")
}
output "day11_1" {
value = module.day11.part1
}
output "day11_2" {
value = module.day11.part2
}
module "day13" {
source = "./day13"
input = file("../inputs/13.txt")
}
output "day13_1" {
value = module.day13.part1
}
output "day13_2" {
value = module.day13.part2
}
module "day14" {
source = "./day14"
input = file("../inputs/14.txt")
}
output "day14_1" {
value = module.day14.part1
}
module "day19" {
source = "./day19"
input = file("../inputs/19.txt")
}
output "day19_1" {
value = module.day19.part1
}
module "day25" {
source = "./day25"
input = file("../inputs/25.txt")
}
output "day25_1" {
value = module.day25.part1
}

View File

@@ -130,3 +130,113 @@ run "day5_1" {
error_message = "Part1 output is wrong"
}
}
run "day08" {
command = plan
module {
source = "./day08"
}
variables {
input = file("../tests/samples/08.txt")
}
assert {
condition = output.part1 == 14
error_message = "Part1 output is wrong"
}
assert {
condition = output.part2 == 34
error_message = "Part1 output is wrong"
}
}
run "day11" {
command = plan
module {
source = "./day11"
}
variables {
input = "125 17"
}
assert {
condition = output.part1 == 55312
error_message = "Part1 output is wrong"
}
}
run "day13" {
command = plan
module {
source = "./day13"
}
variables {
input = file("../tests/samples/13.txt")
}
assert {
condition = output.part1 == 480
error_message = "Part1 output is wrong"
}
}
run "day14" {
command = plan
module {
source = "./day14"
}
variables {
input = file("../tests/samples/14.txt")
height = 7
width = 11
}
assert {
condition = output.part1 == 12
error_message = "Part1 output is wrong"
}
}
run "day19" {
command = plan
module {
source = "./day19"
}
variables {
input = file("../tests/samples/19.txt")
}
assert {
condition = output.part1 == 6
error_message = "Part1 output is wrong"
}
}
run "day25" {
command = plan
module {
source = "./day25"
}
variables {
input = file("../tests/samples/25.txt")
}
assert {
condition = output.part1 == 3
error_message = "Part1 output is wrong"
}
}

View File

@@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"click>=8.1.7",
"networkx>=3.4.2",
"numpy>=2.1.2",
]

View File

@@ -0,0 +1,85 @@
from collections import deque
import numpy
from . import SeparateRunner
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
grid = numpy.array(
[[int(v) for v in line] for line in input.strip().split("\n")]
)
width, height = grid.shape
start_x, start_y = numpy.nonzero(grid == 0)
todo = []
reachable = 0
for sx, sy in zip(start_x, start_y):
todo.append((sx, sy))
ways = numpy.zeros_like(grid, dtype=bool)
def enqueue(x: int, y: int, val: int) -> None:
if grid[x, y] == val + 1:
if not ways[x, y]:
todo.append((x, y))
ways[x, y] += True
while todo:
x, y = todo.pop()
val = grid[x, y]
if val == 9:
reachable += 1
continue
if x > 0:
enqueue(x - 1, y, val)
if y > 0:
enqueue(x, y - 1, val)
if x < width - 1:
enqueue(x + 1, y, val)
if y < height - 1:
enqueue(x, y + 1, val)
return reachable
@classmethod
def part2(cls, input: str) -> int:
grid = numpy.array(
[[int(v) for v in line] for line in input.strip().split("\n")]
)
ways = numpy.zeros_like(grid)
width, height = grid.shape
start_x, start_y = numpy.nonzero(grid == 9)
ways[grid == 9] = 1
todo = deque((x, y) for x, y in zip(start_x, start_y))
def enqueue(x: int, y: int, val: int, cur: int) -> None:
if grid[x, y] == val - 1:
if ways[x, y] == 0:
todo.append((x, y))
ways[x, y] += cur
while todo:
x, y = todo.popleft()
val = grid[x, y]
cur = ways[x, y]
if x > 0:
enqueue(x - 1, y, val, cur)
if y > 0:
enqueue(x, y - 1, val, cur)
if x < width - 1:
enqueue(x + 1, y, val, cur)
if y < height - 1:
enqueue(x, y + 1, val, cur)
return ways[grid == 0].sum()

View File

@@ -0,0 +1,50 @@
import functools
from collections import defaultdict
from . import CombinedRunner
@functools.cache
def blink_num(num: int) -> tuple[int, ...]:
if num == 0:
return (1,)
num_str = str(num)
num_len = len(num_str)
if num_len % 2 == 0:
half = num_len // 2
return (int(num_str[:half]), int(num_str[half:]))
return (num * 2024,)
def step(nums: dict[int, int]) -> dict[int, int]:
result = defaultdict(int)
for num, count in nums.items():
for transformed in blink_num(num):
result[transformed] += count
return result
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> tuple[int, int]:
nums = [int(val) for val in input.strip().split(" ")]
counts = defaultdict(int)
for num in nums:
counts[num] += 1
for _ in range(25):
counts = step(counts)
part1 = sum(counts.values())
for _ in range(50):
counts = step(counts)
return part1, sum(counts.values())

101
2024/src/aoc/days/day12.py Normal file
View File

@@ -0,0 +1,101 @@
import numpy
from . import CombinedRunner
DIRECTIONS = [
(-1, 0),
(1, 0),
(0, -1),
(0, 1),
]
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> tuple[int, int]:
grid = numpy.array(list(map(list, input.strip().split("\n"))))
score = 0
score2 = 0
for y in range(grid.shape[0]):
for x in range(grid.shape[1]):
if grid[y, x] == ".":
continue
search = grid[y, x]
grid[y, x] = "."
todo = [(y, x)]
cluster = {(y, x)}
def enqueue(y, x):
if grid[y, x] == search:
grid[y, x] = "."
todo.append((y, x))
cluster.add((y, x))
while todo:
cy, cx = todo.pop()
if cx > 0:
enqueue(cy, cx - 1)
if cy > 0:
enqueue(cy - 1, cx)
if cx < grid.shape[1] - 1:
enqueue(cy, cx + 1)
if cy < grid.shape[0] - 1:
enqueue(cy + 1, cx)
side_length = sum(
sum((cy + dy, cx + dx) not in cluster for dy, dx in DIRECTIONS)
for cy, cx in cluster
)
corners = 0
for cy, cx in cluster:
# Outer corners
corners += (cy, cx - 1) not in cluster and (
cy - 1,
cx,
) not in cluster
corners += (cy, cx + 1) not in cluster and (
cy - 1,
cx,
) not in cluster
corners += (cy, cx - 1) not in cluster and (
cy + 1,
cx,
) not in cluster
corners += (cy, cx + 1) not in cluster and (
cy + 1,
cx,
) not in cluster
# Inner corners
corners += (
(cy, cx - 1) in cluster
and (cy - 1, cx) in cluster
and (cy - 1, cx - 1) not in cluster
)
corners += (
(cy, cx + 1) in cluster
and (cy - 1, cx) in cluster
and (cy - 1, cx + 1) not in cluster
)
corners += (
(cy, cx - 1) in cluster
and (cy + 1, cx) in cluster
and (cy + 1, cx - 1) not in cluster
)
corners += (
(cy, cx + 1) in cluster
and (cy + 1, cx) in cluster
and (cy + 1, cx + 1) not in cluster
)
score += side_length * len(cluster)
score2 += corners * len(cluster)
return (score, score2)

View File

@@ -0,0 +1,43 @@
import re
import numpy
from . import CombinedRunner
NASTY_REGEX = r"""Button A: X\+(\d+), Y\+(\d+)
Button B: X\+(\d+), Y\+(\d+)
Prize: X=(\d+), Y=(\d+)"""
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> tuple[int, int]:
machines = re.findall(NASTY_REGEX, input)
cost_to_win = 0
cost_to_win2 = 0
scale = 10000000000000
for machine in machines:
ax, ay, bx, by, px, py = map(int, machine)
X = numpy.array([[ax, bx], [ay, by]])
B = numpy.array([px, py])
B2 = numpy.array([px + scale, py + scale])
A = numpy.linalg.solve(X, B)
A2 = numpy.linalg.solve(X, B2)
a_press, b_press = map(round, A)
a_press2, b_press2 = map(round, A2)
if a_press * ax + b_press * bx == px and a_press * ay + b_press * by == py:
cost_to_win += 3 * a_press + b_press
if (
a_press2 * ax + b_press2 * bx == px + scale
and a_press2 * ay + b_press2 * by == py + scale
):
cost_to_win2 += 3 * a_press2 + b_press2
return cost_to_win, cost_to_win2

View File

@@ -0,0 +1,80 @@
import itertools
import math
import re
import sys
import numpy
from . import SeparateRunner
NASTY_REGEX = re.compile(r"p=(-?\d+),(-?\d+) v=(-?\d+),(-?\d+)")
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str, width: int = 101, height: int = 103) -> int:
points = NASTY_REGEX.findall(input)
quadrants = [0] * 4
x_middle = width // 2
y_middle = height // 2
for point in points:
x, y, dx, dy = map(int, point)
rx = ((x + dx * 100) % width + width) % width
ry = ((y + dy * 100) % height + height) % height
match rx:
case _ if rx < x_middle:
xq = 0
case _ if rx > x_middle:
xq = 1
case _:
continue
match ry:
case _ if ry < y_middle:
yq = 0
case _ if ry > y_middle:
yq = 1
case _:
continue
quadrants[2 * yq + xq] += 1
return math.prod(quadrants)
@classmethod
def part2(cls, input: str) -> int:
width = 101
height = 103
points = NASTY_REGEX.findall(input)
points_fast = numpy.array([list(map(int, point)) for point in points])
positions = points_fast[:, 0:2]
velocities = points_fast[:, 2:]
target = len(velocities)
# Assumption: when the easter egg happens, no robots overlap, and this is the
# only time this happens. There is no reason this should work but it does.
mod_base = numpy.array([width, height])
for i in itertools.count(1):
positions += velocities
positions %= mod_base
positions += mod_base
positions %= mod_base
if len(numpy.unique(positions, axis=0)) == target:
grid = [[" "] * width for _ in range(height)]
for x, y in positions:
grid[y][x] = "#"
tree = "\n".join(map((lambda x: "".join(x)), grid))
print(tree, file=sys.stderr)
return i

161
2024/src/aoc/days/day15.py Normal file
View File

@@ -0,0 +1,161 @@
import itertools
import numpy
from . import SeparateRunner
def parse_input(data: str) -> tuple[numpy.array, str]:
grid, steps = data.split("\n\n")
grid_split = numpy.array([list(line) for line in grid.split("\n")])
steps = "".join(steps.split("\n"))
return grid_split, steps
def convert_dir(c: str) -> tuple[int, int]:
match c:
case "^":
return 0, -1
case ">":
return 1, 0
case "<":
return -1, 0
case "v":
return 0, 1
case other:
raise ValueError(f"Invalid movement: {other}")
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> None:
grid, steps = parse_input(input)
y, x = numpy.where(grid == "@")
x, y = x[0], y[0]
for c in steps:
dx, dy = convert_dir(c)
match grid[y + dy, x + dx]:
case "#":
continue
case "O":
crashed = False
for dist in itertools.count(2):
match grid[y + dist * dy, x + dist * dx]:
case "O":
continue
case "#":
crashed = True
break
case _:
crashed = False
break
if crashed:
continue
grid[y + dist * dy, x + dist * dx] = "O"
case _:
pass
grid[y, x] = "."
x += dx
y += dy
grid[y, x] = "@"
stones = numpy.where(grid == "O")
return sum(100 * y + x for y, x in zip(*stones))
@classmethod
def part2(cls, input: str) -> None:
input = input.replace(".", "..")
input = input.replace("#", "##")
input = input.replace("O", "[]")
input = input.replace("@", "@.")
grid, steps = parse_input(input)
y, x = numpy.where(grid == "@")
x, y = x[0], y[0]
for c in steps:
dx, dy = convert_dir(c)
match grid[y + dy, x + dx]:
case "#":
continue
case "]" | "[":
crashed = False
if dy == 0:
# easy case: just move linearly
for dist in itertools.count(2):
match grid[y, x + dist * dx]:
case "[" | "]":
continue
case "#":
crashed = True
break
case _:
break
if crashed:
continue
# shuffle all grid points one over
for steps in range(dist, 1, -1):
grid[y, x + dx * steps] = grid[y, x + dx * (steps - 1)]
else:
if grid[y + dy, x] == "[":
to_check = {x, x + 1}
else:
to_check = {x, x - 1}
moving_stones = [to_check]
for dist in itertools.count(2):
to_check_next = set()
for cx in to_check:
match grid[y + dist * dy, cx]:
case "#":
crashed = True
break
case "[":
to_check_next.add(cx)
to_check_next.add(cx + 1)
case "]":
to_check_next.add(cx)
to_check_next.add(cx - 1)
case _:
continue
if crashed or not to_check_next:
break
moving_stones.append(to_check_next)
to_check = to_check_next
if crashed:
continue
for steps in range(len(moving_stones), 0, -1):
dist = steps + 1
for cx in moving_stones[steps - 1]:
grid[y + dy * dist, cx] = grid[y + dy * (dist - 1), cx]
grid[y + dy * (dist - 1), cx] = "."
case _:
pass
grid[y, x] = "."
x += dx
y += dy
grid[y, x] = "@"
stones = numpy.where(grid == "[")
return sum(100 * y + x for y, x in zip(*stones))

View File

@@ -0,0 +1,76 @@
import heapq
import numpy
from . import CombinedRunner
TURNS = (
(-1, 1),
(1, -1),
)
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> tuple[int, int]:
grid = numpy.array([list(line) for line in input.strip().split("\n")])
y, x = numpy.where(grid == "S")
x, y = x[0], y[0]
todo = [(0, x, y, 1, 0)]
best = {
(x, y, 1, 0): (0, []),
}
def enqueue(dist, x, y, dx, dy, cx, cy, cdx, cdy):
if grid[y, x] == "#":
return
if (x, y, dx, dy) not in best or best[x, y, dx, dy][0] > dist:
best[x, y, dx, dy] = (dist, [(cx, cy, cdx, cdy)])
heapq.heappush(todo, (dist, x, y, dx, dy))
elif best[x, y, dx, dy][0] == dist:
best[x, y, dx, dy][1].append((cx, cy, cdx, cdy))
shortest_dist = None
finishes = set()
while todo:
dist, x, y, dx, dy = heapq.heappop(todo)
if best[x, y, dx, dy][0] < dist:
continue
if shortest_dist is not None and shortest_dist < dist:
break
if grid[y, x] == "E":
shortest_dist = dist
finishes.add((x, y, dx, dy))
enqueue(dist + 1, x + dx, y + dy, dx, dy, x, y, dx, dy)
enqueue(dist + 2001, x - dx, y - dy, dx, dy, x, y, dx, dy)
for tx, ty in TURNS:
ndx = dy * ty
ndy = dx * ty
enqueue(dist + 1001, x + ndx, y + ndy, ndx, ndy, x, y, dx, dy)
assert shortest_dist is not None, "Should find a path to the exit"
visited_tiles = {(x, y) for x, y, _, _ in finishes}
todo2 = [f for f in finishes]
visited_states = set(todo2)
while todo2:
state = todo2.pop()
for prev in best[state][1]:
if prev not in visited_states:
visited_states.add(prev)
visited_tiles.add((prev[0], prev[1]))
todo2.append(prev)
return shortest_dist, len(visited_tiles)

View File

@@ -0,0 +1,89 @@
import re
from . import SeparateRunner
def run_program(
register_a: int, register_b: int, register_c: int, program: list[int]
) -> list[int]:
ip = 0
out = []
def combo(index: int) -> int:
match program[index]:
case 0:
return 0
case 1:
return 1
case 2:
return 2
case 3:
return 3
case 4:
return register_a
case 5:
return register_b
case 6:
return register_c
while ip < len(program):
match program[ip]:
case 0: # adv
register_a = register_a // 2 ** combo(ip + 1)
case 1: # bxl
register_b ^= program[ip + 1]
case 2: # bst
register_b = combo(ip + 1) & 0x7
case 3: # jnz
if register_a != 0:
ip = program[ip + 1]
continue
case 4: # bxc
register_b ^= register_c
case 5: # out
out.append(combo(ip + 1) & 7)
case 6: # bdv
register_b = register_a // 2 ** combo(ip + 1)
case 7: # cdv
register_c = register_a // 2 ** combo(ip + 1)
ip += 2
return out
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> str:
numbers = re.findall(r"\d+", input)
register_a, register_b, register_c = map(int, numbers[:3])
program = list(map(int, numbers[3:]))
out = run_program(register_a, register_b, register_c, program)
return ",".join(map(str, out))
@classmethod
def part2(cls, input: str) -> int:
numbers = re.findall(r"\d+", input)
_, register_b, register_c = map(int, numbers[:3])
program = list(map(int, numbers[3:]))
cur = [0]
# It came to me in a dream
for entry in reversed(program):
next_gen = []
for num in cur:
num *= 8
for n in range(8):
output = run_program(num + n, register_b, register_c, program)
result = output[0]
if result == entry:
next_gen.append(num + n)
cur = next_gen
return cur[0]

View File

@@ -0,0 +1,70 @@
from collections import deque
from . import SeparateRunner
def parse_input(data: str) -> list[tuple[int, int]]:
return [tuple(map(int, line.split(","))) for line in data.strip().split("\n")]
def find_exit(fallen: set[tuple[int, int]], width: int, height: int) -> int | None:
todo = deque([(0, 0, 0)])
best = {(0, 0): 0}
def enqueue(dist: int, x: int, y: int):
# print(f"trying {x},{y}")
if (x, y) in fallen:
return
if (x, y) not in best or best[x, y] > dist:
best[x, y] = dist
todo.append((dist, x, y))
while todo:
dist, x, y = todo.popleft()
# print(x, y)
if x == width - 1 and y == height - 1:
return dist
if x > 0:
enqueue(dist + 1, x - 1, y)
if x + 1 < width:
enqueue(dist + 1, x + 1, y)
if y > 0:
enqueue(dist + 1, x, y - 1)
if y + 1 < height:
enqueue(dist + 1, x, y + 1)
class DayRunner(SeparateRunner):
@classmethod
def part1(
cls, input: str, width: int = 71, height: int = 71, limit: int = 1024
) -> int:
falling = parse_input(input)
return find_exit(set(falling[:limit]), width, height)
@classmethod
def part2(cls, input: str, width: int = 71, height: int = 71) -> str:
falling = parse_input(input)
lower = 0
upper = len(falling)
while lower < upper:
mid = lower + (upper - lower) // 2
if find_exit(set(falling[:mid]), width, height) is not None:
lower = mid + 1
else:
upper = mid
first_blocker = falling[lower - 1]
return f"{first_blocker[0]},{first_blocker[1]}"

View File

@@ -0,0 +1,41 @@
import collections
import functools
from . import CombinedRunner
def parse_input(data: str) -> tuple[list[str], list[str]]:
patterns, designs = data.strip().split("\n\n")
return patterns.split(", "), designs.split("\n")
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> int:
patterns, designs = parse_input(input)
by_prefix = collections.defaultdict(list)
for prefix in patterns:
by_prefix[prefix[0]].append(prefix)
possible = 0
ways = 0
@functools.cache
def is_possible(design: str) -> bool:
if not design:
return 1
else:
return sum(
is_possible(design[len(prefix) :])
for prefix in by_prefix[design[0]]
if design.startswith(prefix)
)
for design in designs:
if (solve := is_possible(design)) > 0:
possible += 1
ways += solve
return possible, ways

View File

@@ -0,0 +1,82 @@
import itertools
import numpy
from . import SeparateRunner
DIRECTIONS = [
(-1, 0),
(1, 0),
(0, -1),
(0, 1),
]
CHEATS = [
(-2, 0),
(2, 0),
(0, -2),
(0, 2),
]
def parse_path(input: str) -> dict[tuple[int, int], int]:
grid = numpy.array(list(map(list, input.strip().split("\n"))))
ys, xs = numpy.nonzero(grid == "S")
sx, sy = int(xs[0]), int(ys[0])
nx, ny = sx, sy
path = {
(sx, sy): 0,
}
while grid[ny, nx] != "E":
x, y = nx, ny
for dx, dy in DIRECTIONS:
if grid[y + dy, x + dx] == "#" or (x + dx, y + dy) in path:
continue
nx = x + dx
ny = y + dy
break
path[nx, ny] = len(path)
return path
def get_savings(a: tuple[tuple[int, int], int], b: tuple[tuple[int, int], int]) -> int:
(ax, ay), ad = a
(bx, by), bd = b
dist = abs(bx - ax) + abs(by - ay)
if dist <= 20:
return bd - ad - dist
else:
return 0
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str, limit: int = 100) -> int:
path = parse_path(input)
total = 0
for (px, py), dist in path.items():
for dx, dy in CHEATS:
if (other := path.get((px + dx, py + dy))) is not None:
savings = dist - other - 2
if savings >= limit:
total += 1
return total
@classmethod
def part2(cls, input: str, limit: int = 100) -> int:
path = parse_path(input)
return sum(
get_savings(a, b) >= limit
for a, b in itertools.combinations(path.items(), 2)
)

159
2024/src/aoc/days/day21.py Normal file
View File

@@ -0,0 +1,159 @@
import functools
from collections import Counter, defaultdict
from . import SeparateRunner
NUMPAD = {
"A": (3, 2),
"0": (3, 1),
"1": (2, 0),
"2": (2, 1),
"3": (2, 2),
"4": (1, 0),
"5": (1, 1),
"6": (1, 2),
"7": (0, 0),
"8": (0, 1),
"9": (0, 2),
}
DIRPAD = {
"A": (0, 2),
"^": (0, 1),
"<": (1, 0),
"v": (1, 1),
">": (1, 2),
}
@functools.cache
def shortest_numpad(from_: str, to: str) -> list[str]:
inverse = set(NUMPAD.values())
ay, ax = NUMPAD[from_]
by, bx = NUMPAD[to]
dx, dy = bx - ax, by - ay
sx = "<" if dx < 0 else ">"
sy = "^" if dy < 0 else "v"
if dx > 0 and (by, ax) in inverse or (ay, bx) not in inverse:
return abs(dy) * sy + abs(dx) * sx + "A"
else:
return abs(dx) * sx + abs(dy) * sy + "A"
@functools.cache
def shortest_dirpad(from_: str, to: str) -> str:
inverse = set(DIRPAD.values())
ay, ax = DIRPAD[from_]
by, bx = DIRPAD[to]
dx, dy = bx - ax, by - ay
sx = "<" if dx < 0 else ">"
sy = "^" if dy < 0 else "v"
if dx > 0 and (by, ax) in inverse or (ay, bx) not in inverse:
return abs(dy) * sy + abs(dx) * sx + "A"
else:
return abs(dx) * sx + abs(dy) * sy + "A"
def encode_shortest_numpad(code: str) -> str:
pos = "A"
res = ""
for c in code:
res += shortest_numpad(pos, c)
# print(c, res)
pos = c
return res
def encode_shortest_dirpad(code: str) -> str:
pos = "A"
res = ""
for c in code:
if pos != c:
res += shortest_dirpad(pos, c)
else:
res += "A"
pos = c
return res
def decode(code: str, pad: dict[str, tuple[int, int]]) -> str:
result = ""
inverse = {v: k for k, v in pad.items()}
y, x = pad["A"]
for i, c in enumerate(code):
match c:
case "A":
result += inverse[y, x]
case "^":
y -= 1
case "v":
y += 1
case "<":
x -= 1
case ">":
x += 1
if (y, x) not in inverse:
raise ValueError(
f"""Moved off the board {x, y}, after processing {c}.
Path so far: {result} (from {code[:i]})"""
)
return result
def count_steps(path: str, count: int) -> dict[str, int]:
cur = "A"
counts = defaultdict(int)
for c in path:
step = shortest_dirpad(cur, c)
cur = c
counts[step] += count
return counts
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
result = 0
for code in input.strip().split("\n"):
numpad = encode_shortest_numpad(code)
robot1 = encode_shortest_dirpad(numpad)
robot2 = encode_shortest_dirpad(robot1)
result += int(code[:-1]) * len(robot2)
return result
@classmethod
def part2(cls, input: str, robots=25) -> int:
result = 0
for code in input.strip().split("\n"):
numpad = encode_shortest_numpad(code)
keypresses = Counter([numpad])
for _ in range(robots + 1):
new_presses = Counter()
for subroute, count in keypresses.items():
new_presses.update(count_steps(subroute, count))
keypresses = new_presses
result += int(code[:-1]) * keypresses.total()
return result

View File

@@ -0,0 +1,41 @@
from collections import defaultdict
import numpy
from numpy.lib.stride_tricks import sliding_window_view
from . import CombinedRunner
def advance(secrets: numpy.array) -> numpy.array:
new_secrets = (secrets ^ (secrets << 6)) & 0xFFFFFF
new_secrets ^= new_secrets >> 5
new_secrets ^= new_secrets << 11
new_secrets &= 0xFFFFFF
return new_secrets
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> tuple[int, int]:
secrets = numpy.fromstring(input, dtype=int, sep="\n")
progression = [secrets]
for _ in range(2000):
secrets = advance(secrets)
progression.append(secrets)
field = numpy.stack(progression, axis=-1) % 10
delta = field[:, 1:] - field[:, :-1]
windows = sliding_window_view(delta, 4, axis=1)
per_signal = defaultdict(int)
for row_scores, row_deltas in zip(field, windows):
unique, positions = numpy.unique(row_deltas, return_index=True, axis=0)
for key, bananas in zip(unique, row_scores[positions + 4]):
per_signal[tuple(key)] += bananas
return secrets.sum(), max(per_signal.values())

View File

@@ -0,0 +1,42 @@
from collections import defaultdict
import networkx
from . import SeparateRunner
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
edges = defaultdict(set)
for line in input.strip().split("\n"):
a, b = line.split("-")
edges[a].add(b)
edges[b].add(a)
found = set()
for a, out in edges.items():
if a[0] != "t":
continue
for b in out:
for c in edges[b]:
if c in out:
found.add(tuple(sorted([a, b, c])))
return len(found)
@classmethod
def part2(cls, input: str) -> str:
graph = networkx.Graph()
for line in input.strip().split("\n"):
a, b = line.split("-")
graph.add_edge(a, b)
cliques = networkx.find_cliques(graph)
max_clique = max(cliques, key=len)
return ",".join(sorted(max_clique))

133
2024/src/aoc/days/day24.py Normal file
View File

@@ -0,0 +1,133 @@
import functools
import re
from . import SeparateRunner
def parse_input(input: str) -> tuple[dict[str, int], dict[str, tuple[str, str, str]]]:
variable_part, rules_part = input.strip().split("\n\n")
variables = {}
for line in variable_part.splitlines():
variable, value = line.split(": ")
variables[variable] = int(value)
rules = {}
for first, op, second, result in re.findall(
r"(\w+) (XOR|OR|AND) (\w+) -> (\w+)", rules_part
):
rules[result] = (first, op, second)
return variables, rules
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
variables, rules = parse_input(input)
@functools.cache
def get_value(variable: str) -> int:
if variable in variables:
return variables[variable]
first, op, second = rules[variable]
first_v = get_value(first)
second_v = get_value(second)
match op:
case "AND":
return first_v & second_v
case "OR":
return first_v | second_v
case "XOR":
return first_v ^ second_v
result = 0
for variable in reversed(sorted(rules)):
if not variable.startswith("z"):
continue
result = result * 2 + get_value(variable)
return result
@classmethod
def part2(cls, input: str) -> str:
variables, rules = parse_input(input)
max_bit = int(
max(variable for variable in rules if variable.startswith("z"))[1:]
)
def find_invalid(output: str, pattern) -> set[str]:
if pattern is None:
return set()
if output in rules:
left, op, right = rules[output]
elif output == pattern:
return set()
else:
return {output}
pop, pleft, pright = pattern
if op != pop:
return {output}
wrong_normal = find_invalid(left, pleft) | find_invalid(right, pright)
wrong_mirror = find_invalid(left, pright) | find_invalid(right, pleft)
least_wrong = min(wrong_mirror, wrong_normal, key=len)
return least_wrong
assert max_bit >= 3
# First one is a half adder, that's a simple pattern
invalid = find_invalid("z00", ["XOR", "x00", "y00"])
# Second one is missing a reference to the before-previous adder, so it's a
# slightly different patterns
invalid |= find_invalid(
"z01", ["XOR", ["AND", "x00", "y00"], ["XOR", "x01", "y01"]]
)
# Needed a second lookback to determine that `ktp` is valid
invalid |= find_invalid(
"z02",
[
"XOR",
["XOR", "x02", "y02"],
[
"OR",
["AND", "x01", "y01"],
["AND", ["XOR", "x01", "y01"], ["AND", "x00", "y00"]],
],
],
)
for n in range(3, max_bit):
xcurr = f"x{n:02}"
ycurr = f"y{n:02}"
zcurr = f"z{n:02}"
xprev = f"x{n-1:02}"
yprev = f"y{n-1:02}"
invalid |= find_invalid(
zcurr,
[
"XOR",
["XOR", xcurr, ycurr],
[
"OR",
["AND", xprev, yprev],
["AND", ["XOR", xprev, yprev], ["OR", None, None]],
],
],
)
# This code somehow believes `ktp` is invalid, but it's fine on closer
# inspection. Will figure that out later.
return ",".join(sorted(invalid))

View File

@@ -0,0 +1,29 @@
import numpy
from . import CombinedRunner
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> tuple[int, None]:
blocks = input.strip().split("\n\n")
keys = []
locks = []
for block in blocks:
grid = numpy.array(list(map(list, block.splitlines())))
heights = numpy.count_nonzero(grid == "#", axis=0)
if block.startswith("#####"):
locks.append(heights)
else:
keys.append(heights)
locks = numpy.stack(locks, axis=0)
fitting = sum(
numpy.count_nonzero(numpy.all((key + locks) <= 7, axis=1)) for key in keys
)
return fitting, None

View File

@@ -11,35 +11,39 @@ def parse_input(input: str) -> tuple[int, list[int]]:
return result
def is_possible(target: int, nums: list[int], cur: int) -> bool:
if cur == target and not nums:
return True
if cur > target or not nums:
def is_possible(target: int, nums: list[int]) -> bool:
if target == 0:
return not nums
if not nums or target < 0:
return False
head = nums[0]
remainder = nums[1:]
tail = nums[-1]
remainder = nums[:-1]
return is_possible(target, remainder, cur + head) or is_possible(
target, remainder, cur * head
return is_possible(target - tail, remainder) or (
target % tail == 0 and is_possible(target // tail, remainder)
)
def is_possible2(target: int, nums: list[int], cur: int) -> bool:
if cur == target and not nums:
return True
if cur > target or not nums:
def is_possible2(target: int, nums: list[int]) -> bool:
if target == 0:
return not nums
if not nums or target < 0:
return False
head = nums[0]
remainder = nums[1:]
tail = nums[-1]
remainder = nums[:-1]
target_str = str(target)
tail_str = str(tail)
return (
is_possible2(target, remainder, cur + head)
or is_possible2(target, remainder, cur * head)
or is_possible2(target, remainder, int(f"{cur}{head}"))
is_possible2(target - tail, remainder)
or (target % tail == 0 and is_possible2(target // tail, remainder))
or (
target_str.endswith(tail_str)
and is_possible2(int(target_str[: -len(str(tail_str))]), remainder)
)
)
@@ -48,14 +52,10 @@ class DayRunner(SeparateRunner):
def part1(cls, input: str) -> int:
lines = parse_input(input)
return sum(
target for target, nums in lines if is_possible(target, nums[1:], nums[0])
)
return sum(target for target, nums in lines if is_possible(target, nums))
@classmethod
def part2(cls, input: str) -> int:
lines = parse_input(input)
return sum(
target for target, nums in lines if is_possible2(target, nums[1:], nums[0])
)
return sum(target for target, nums in lines if is_possible2(target, nums))

60
2024/src/aoc/days/day8.py Normal file
View File

@@ -0,0 +1,60 @@
import itertools
import math
from collections import defaultdict
import numpy
from . import CombinedRunner
def simplify(vec: numpy.array) -> numpy.array:
if vec[0] == 0:
return numpy.array([0, 1])
elif vec[1] == 0:
return numpy.array([0, 1])
else:
div = math.gcd(*vec)
return vec // div
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> int:
grid = input.strip().split("\n")
height = len(grid)
width = len(grid[0])
antennae = defaultdict(list)
for y, line in enumerate(grid):
for x, c in enumerate(line):
if c != ".":
antennae[c].append(numpy.array([x, y]))
antinodes = set()
antinodes2 = set()
def in_bounds(node: numpy.array) -> bool:
return 0 <= node[0] < width and 0 <= node[1] < height
def add(node: numpy.array):
if in_bounds(node):
antinodes.add(tuple(node))
def walk(start: numpy.array, step: numpy.array):
for pos in itertools.count(start, step):
if in_bounds(pos):
antinodes2.add(tuple(pos))
else:
break
for values in antennae.values():
for a, b in itertools.combinations(values, 2):
add(2 * a - b)
add(2 * b - a)
step = simplify(b - a)
walk(b, step)
walk(a, -step)
return len(antinodes), len(antinodes2)

107
2024/src/aoc/days/day9.py Normal file
View File

@@ -0,0 +1,107 @@
import heapq
from . import SeparateRunner
def file_checksum(file_id: int, start: int, length: int) -> int:
return file_id * length * (2 * start + length - 1) // 2
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
files = []
empty = []
pos = 0
for c in input.strip():
val = int(c)
if len(files) == len(empty):
files.append((pos, val))
else:
empty.append((pos, val))
pos += val
checksum = 0
for start, length in empty:
while files and length > 0:
file_start, file_len = files.pop()
if file_start < start:
files.append((file_start, file_len))
break
file_id = len(files)
infill = min(file_len, length)
checksum += file_checksum(file_id, start, infill)
start += infill
if infill != file_len:
files.append((file_start, file_len - infill))
length -= infill
else:
continue
break
for file_id, (file_start, file_len) in enumerate(files):
checksum += file_checksum(file_id, file_start, file_len)
return checksum
@classmethod
def part2(cls, input: str) -> int:
files = []
empty = [[] for _ in range(10)]
pos = 0
is_file = True
for c in input.strip():
val = int(c)
if is_file:
files.append((pos, val))
is_file = False
else:
# No need for heappush, as we're appending values in order
empty[val].append(pos)
is_file = True
pos += val
checksum = 0
while files:
start, length = files.pop()
file_id = len(files)
best = None
best_heap = None
for i, heap in enumerate(empty[length:]):
if not heap or heap[0] > start:
continue
if best is None or best > heap[0]:
best = heap[0]
best_heap = i + length
if best is None:
# No room to move left, count score at current position
checksum += file_checksum(file_id, start, length)
else:
checksum += file_checksum(file_id, best, length)
heapq.heappop(empty[best_heap])
if length < best_heap:
remainder = best_heap - length
heapq.heappush(empty[remainder], best + length)
return checksum

View File

@@ -0,0 +1,8 @@
import os
def get_data(day: int, sub: int | None = None) -> str:
basename = f"{day:02d}" if sub is None else f"{day:02d}.{sub}"
sample = os.path.dirname(__file__) + f"/samples/{basename}.txt"
with open(sample, mode="rt", encoding="utf-8") as f:
return f.read()

12
2024/tests/samples/08.txt Normal file
View File

@@ -0,0 +1,12 @@
............
........0...
.....0......
.......0....
....0.......
......A.....
............
............
........A...
.........A..
............
............

View File

@@ -0,0 +1,8 @@
89010123
78121874
87430965
96549874
45678903
32019012
01329801
10456732

View File

@@ -0,0 +1,4 @@
AAAA
BBCD
BBCC
EEEC

View File

@@ -0,0 +1,5 @@
OOOOO
OXOXO
OOOOO
OXOXO
OOOOO

View File

@@ -0,0 +1,10 @@
RRRRIICCFF
RRRRIICCCF
VVRRRCCFFF
VVRCCCJFFF
VVVVCJJCFE
VVIVCCJJEE
VVIIICJJEE
MIIIIIJJEE
MIIISIJEEE
MMMISSJEEE

15
2024/tests/samples/13.txt Normal file
View File

@@ -0,0 +1,15 @@
Button A: X+94, Y+34
Button B: X+22, Y+67
Prize: X=8400, Y=5400
Button A: X+26, Y+66
Button B: X+67, Y+21
Prize: X=12748, Y=12176
Button A: X+17, Y+86
Button B: X+84, Y+37
Prize: X=7870, Y=6450
Button A: X+69, Y+23
Button B: X+27, Y+71
Prize: X=18641, Y=10279

12
2024/tests/samples/14.txt Normal file
View File

@@ -0,0 +1,12 @@
p=0,4 v=3,-3
p=6,3 v=-1,-3
p=10,3 v=-1,2
p=2,0 v=2,-1
p=0,0 v=1,3
p=3,0 v=-2,-2
p=7,6 v=-1,-3
p=3,0 v=-1,-2
p=9,3 v=2,3
p=7,3 v=-1,2
p=2,4 v=2,-3
p=9,5 v=-3,-3

View File

@@ -0,0 +1,21 @@
##########
#..O..O.O#
#......O.#
#.OO..O.O#
#..O@..O.#
#O#..O...#
#O..O..O.#
#.OO.O.OO#
#....O...#
##########
<vv>^<v^>v>^vv^v>v<>v^v<v<^vv<<<^><<><>>v<vvv<>^v^>^<<<><<v<<<v^vv^v>^
vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<<v<^v>^<^^>>>^<v<v
><>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^<v>v^^<^^vv<
<<v<^>>^^^^>>>v^<>vvv^><v<<<>^^^vv^<vvv>^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
^><^><>>><>^^<<^^v>>><^<v>^<vv>>v>>>^v><>^v><<<<v>>v<v<v>vvv>^<><<>^><
^>><>^v<><^vvv<^^<><v<<<<<><^v<<<><<<^^<v<^^^><^>>^<v^><<<^>>^v<v^v<v^
>^>>^v>vv>^<<^v<>><<><<v<<v><>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^
<><^^>^^^<><vvvvv^v<v<<>^v<v>v<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>
^^>vv<^v^v<vv>^<><v<^v>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><<v>
v^^>>><<^^<>>^v^<v^vv<>v^<<>^<^v^v><^<<<><<^<v><v<>vv>>v><v^<vv<>v^<<^

View File

@@ -0,0 +1,10 @@
########
#..O.O.#
##@.O..#
#...O..#
#.#.O..#
#...O..#
#......#
########
<^^>>>vv<v>>v<<

View File

@@ -0,0 +1,9 @@
#######
#...#.#
#.....#
#..OO@#
#..O..#
#.....#
#######
<vv<<^^<<^^

View File

@@ -0,0 +1,15 @@
###############
#.......#....E#
#.#.###.#.###.#
#.....#.#...#.#
#.###.#####.#.#
#.#.#.......#.#
#.#.#####.###.#
#...........#.#
###.#.#####.#.#
#...#.....#.#.#
#.#.#.###.#.#.#
#.....#...#.#.#
#.###.#.#.#.#.#
#S..#.....#...#
###############

View File

@@ -0,0 +1,17 @@
#################
#...#...#...#..E#
#.#.#.#.#.#.#.#.#
#.#.#.#...#...#.#
#.#.#.#.###.#.#.#
#...#.#.#.....#.#
#.#.#.#.#.#####.#
#.#...#.#.#.....#
#.#.#####.#.###.#
#.#.#.......#...#
#.#.###.#####.###
#.#.#...#.....#.#
#.#.#.#####.###.#
#.#.#.........#.#
#.#.#.#########.#
#S#.............#
#################

View File

@@ -0,0 +1,5 @@
Register A: 729
Register B: 0
Register C: 0
Program: 0,1,5,4,3,0

View File

@@ -0,0 +1,5 @@
Register A: 2024
Register B: 0
Register C: 0
Program: 0,3,5,4,3,0

25
2024/tests/samples/18.txt Normal file
View File

@@ -0,0 +1,25 @@
5,4
4,2
4,5
3,0
2,1
6,3
2,4
1,5
0,6
3,3
2,6
5,1
1,2
5,5
2,5
6,5
1,4
0,4
6,4
1,1
6,1
1,0
0,5
1,6
2,0

10
2024/tests/samples/19.txt Normal file
View File

@@ -0,0 +1,10 @@
r, wr, b, g, bwu, rb, gb, br
brwrr
bggr
gbbr
rrbgbr
ubwu
bwurrg
brgr
bbrgwb

15
2024/tests/samples/20.txt Normal file
View File

@@ -0,0 +1,15 @@
###############
#...#...#.....#
#.#.#.#.#.###.#
#S#...#.#.#...#
#######.#.#.###
#######.#.#...#
#######.#.###.#
###..E#...#...#
###.#######.###
#...###...#...#
#.#####.#.###.#
#.#...#.#.#...#
#.#.#.#.#.#.###
#...#...#...###
###############

View File

@@ -0,0 +1,5 @@
029A
980A
179A
456A
379A

View File

@@ -0,0 +1,4 @@
1
10
100
2024

View File

@@ -0,0 +1,4 @@
1
2
3
2024

32
2024/tests/samples/23.txt Normal file
View File

@@ -0,0 +1,32 @@
kh-tc
qp-kh
de-cg
ka-co
yn-aq
qp-ub
cg-tb
vc-aq
tb-ka
wh-tc
yn-cg
kh-ub
ta-co
de-co
tc-td
tb-wq
wh-td
ta-ka
td-qp
aq-cg
wq-ub
ub-vc
de-ta
wq-aq
wq-vc
wh-yn
ka-de
kh-ta
co-tc
wh-qp
tb-vc
td-yn

47
2024/tests/samples/24.txt Normal file
View File

@@ -0,0 +1,47 @@
x00: 1
x01: 0
x02: 1
x03: 1
x04: 0
y00: 1
y01: 1
y02: 1
y03: 1
y04: 1
ntg XOR fgs -> mjb
y02 OR x01 -> tnw
kwq OR kpj -> z05
x00 OR x03 -> fst
tgd XOR rvg -> z01
vdt OR tnw -> bfw
bfw AND frj -> z10
ffh OR nrd -> bqk
y00 AND y03 -> djm
y03 OR y00 -> psh
bqk OR frj -> z08
tnw OR fst -> frj
gnj AND tgd -> z11
bfw XOR mjb -> z00
x03 OR x00 -> vdt
gnj AND wpb -> z02
x04 AND y00 -> kjc
djm OR pbm -> qhw
nrd AND vdt -> hwm
kjc AND fst -> rvg
y04 OR y02 -> fgs
y01 AND x02 -> pbm
ntg OR kjc -> kwq
psh XOR fgs -> tgd
qhw XOR tgd -> z09
pbm OR djm -> kpj
x03 XOR y03 -> ffh
x00 XOR y04 -> ntg
bfw OR bqk -> z06
nrd XOR fgs -> wpb
frj XOR qhw -> z04
bqk OR frj -> z07
y03 OR x01 -> nrd
hwm AND bqk -> z03
tgd XOR rvg -> z12
tnw OR pbm -> gnj

39
2024/tests/samples/25.txt Normal file
View File

@@ -0,0 +1,39 @@
#####
.####
.####
.####
.#.#.
.#...
.....
#####
##.##
.#.##
...##
...#.
...#.
.....
.....
#....
#....
#...#
#.#.#
#.###
#####
.....
.....
#.#..
###..
###.#
###.#
#####
.....
.....
.....
#....
#.#..
#.#.#
#####

11
2024/tests/test_day01.py Normal file
View File

@@ -0,0 +1,11 @@
from aoc.days.day1 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(1)) == 11
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(1)) == 31

View File

@@ -1,12 +1,6 @@
import os
from aoc.days.day2 import DayRunner, is_savable
def get_data() -> str:
sample = os.path.dirname(__file__) + "/samples/02.txt"
with open(sample, mode="rt", encoding="utf-8") as f:
return f.read()
from . import get_data
def test_individual_samples() -> None:
@@ -19,8 +13,8 @@ def test_individual_samples() -> None:
def test_sample_part1() -> None:
assert DayRunner.part1(get_data()) == 2
assert DayRunner.part1(get_data(2)) == 2
def test_sample_part2() -> None:
assert DayRunner.part2(get_data()) == 4
assert DayRunner.part2(get_data(2)) == 4

20
2024/tests/test_day04.py Normal file
View File

@@ -0,0 +1,20 @@
import pytest
from aoc.days.day4 import DayRunner
from . import get_data
@pytest.mark.parametrize(
"input,answer",
[
(get_data(4, 1), 4),
(get_data(4, 2), 18),
],
)
def test_sample_part1(input: str, answer: int) -> None:
assert DayRunner.part1(input) == answer
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(4, 2)) == 9

15
2024/tests/test_day05.py Normal file
View File

@@ -0,0 +1,15 @@
from aoc.days.day5 import DayRunner
from . import get_data
def test_sample_part1() -> None:
data = get_data(5)
assert DayRunner.part1(data) == 143
def test_sample_part2() -> None:
data = get_data(5)
assert DayRunner.part2(data) == 123

View File

@@ -1,17 +1,11 @@
import os
from aoc.days.day7 import DayRunner
def get_data() -> str:
sample = os.path.dirname(__file__) + "/samples/07.txt"
with open(sample, mode="rt", encoding="utf-8") as f:
return f.read()
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data()) == 3749
assert DayRunner.part1(get_data(7)) == 3749
def test_sample_part2() -> None:
assert DayRunner.part2(get_data()) == 11387
assert DayRunner.part2(get_data(7)) == 11387

11
2024/tests/test_day08.py Normal file
View File

@@ -0,0 +1,11 @@
from aoc.days.day8 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(8)) == 14
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(8)) == 34

11
2024/tests/test_day09.py Normal file
View File

@@ -0,0 +1,11 @@
from aoc.days.day9 import DayRunner
SAMPLE = "2333133121414131402"
def test_sample_part1() -> None:
assert DayRunner.part1(SAMPLE) == 1928
def test_sample_part2() -> None:
assert DayRunner.part2(SAMPLE) == 2858

View File

@@ -1,17 +0,0 @@
import os
from aoc.days.day1 import DayRunner
def get_data() -> str:
sample = os.path.dirname(__file__) + "/samples/01.txt"
with open(sample, mode="rt", encoding="utf-8") as f:
return f.read()
def test_sample_part1() -> None:
assert DayRunner.part1(get_data()) == 11
def test_sample_part2() -> None:
assert DayRunner.part2(get_data()) == 31

7
2024/tests/test_day10.py Normal file
View File

@@ -0,0 +1,7 @@
from aoc.days.day10 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(10)) == 36

7
2024/tests/test_day11.py Normal file
View File

@@ -0,0 +1,7 @@
from aoc.days.day11 import DayRunner
SAMPLE = "125 17"
def test_sample_part1() -> None:
assert DayRunner.part1(SAMPLE) == 55312

21
2024/tests/test_day12.py Normal file
View File

@@ -0,0 +1,21 @@
import pytest
from aoc.days.day12 import DayRunner
from . import get_data
@pytest.mark.parametrize(
"data,result",
[(get_data(12, 1), 140), (get_data(12, 2), 772), (get_data(12, 3), 1930)],
)
def test_sample_part1(data: str, result: int) -> None:
assert DayRunner.part1(data) == result
@pytest.mark.parametrize(
"data,result",
[(get_data(12, 1), 80), (get_data(12, 2), 436), (get_data(12, 3), 1206)],
)
def test_sample_part2(data: str, result: int) -> None:
assert DayRunner.part2(data) == result

7
2024/tests/test_day13.py Normal file
View File

@@ -0,0 +1,7 @@
from aoc.days.day13 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(13)) == 480

7
2024/tests/test_day14.py Normal file
View File

@@ -0,0 +1,7 @@
from aoc.days.day14 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(14), width=11, height=7) == 12

27
2024/tests/test_day15.py Normal file
View File

@@ -0,0 +1,27 @@
import pytest
from aoc.days.day15 import DayRunner
from . import get_data
@pytest.mark.parametrize(
"data,result",
[
(get_data(15, 1), 10092),
(get_data(15, 2), 2028),
],
)
def test_sample_part1(data: str, result: int) -> None:
assert DayRunner.part1(data) == result
@pytest.mark.parametrize(
"data,result",
[
(get_data(15, 1), 9021),
(get_data(15, 3), 618),
],
)
def test_sample_part2(data: str, result: int) -> None:
assert DayRunner.part2(data) == result

27
2024/tests/test_day16.py Normal file
View File

@@ -0,0 +1,27 @@
import pytest
from aoc.days.day16 import DayRunner
from . import get_data
@pytest.mark.parametrize(
"data,result",
[
(get_data(16, 1), 7036),
(get_data(16, 2), 11048),
],
)
def test_sample_part1(data: str, result: int) -> None:
assert DayRunner.part1(data) == result
@pytest.mark.parametrize(
"data,result",
[
(get_data(16, 1), 45),
(get_data(16, 2), 64),
],
)
def test_sample_part2(data: str, result: int) -> None:
assert DayRunner.part2(data) == result

11
2024/tests/test_day17.py Normal file
View File

@@ -0,0 +1,11 @@
from aoc.days.day17 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(17, 1)) == "4,6,3,5,6,3,5,2,1,0"
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(17, 2)) == 117440

11
2024/tests/test_day18.py Normal file
View File

@@ -0,0 +1,11 @@
from aoc.days.day18 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(18), width=7, height=7, limit=12) == 22
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(18), width=7, height=7) == "6,1"

11
2024/tests/test_day19.py Normal file
View File

@@ -0,0 +1,11 @@
from aoc.days.day19 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(19)) == 6
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(19)) == 16

11
2024/tests/test_day20.py Normal file
View File

@@ -0,0 +1,11 @@
from aoc.days.day20 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(20), limit=1) == 44
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(20), limit=50) == 285

55
2024/tests/test_day21.py Normal file
View File

@@ -0,0 +1,55 @@
import pytest
from aoc.days.day21 import (
DayRunner,
encode_shortest_dirpad,
encode_shortest_numpad,
)
from . import get_data
def test_encode_shortest_numpad() -> None:
assert encode_shortest_numpad("029A") in (
"<A^A>^^AvvvA",
"<A^A^>^AvvvA",
"<A^A^^>AvvvA",
)
def test_encode_shortest_dirpad() -> None:
numpad_encoded = encode_shortest_numpad("029A")
assert len(encode_shortest_dirpad(numpad_encoded)) == len(
"v<<A>>^A<A>AvA<^AA>A<vAAA>^A"
)
@pytest.mark.parametrize(
"code,answer",
[
(
"029A",
"<vA<AA>>^AvAA<^A>A<v<A>>^AvA^A<vA>^A<v<A>^A>AAvA^A<v<A>A>^AAAvA<^A>A",
),
("980A", "<v<A>>^AAAvA^A<vA<AA>>^AvAA<^A>A<v<A>A>^AAAvA<^A>A<vA>^A<A>A"),
(
"179A",
"<v<A>>^A<vA<A>>^AAvAA<^A>A<v<A>>^AAvA^A<vA>^AA<A>A<v<A>A>^AAAvA<^A>A",
),
("456A", "<v<A>>^AA<vA<A>>^AAvAA<^A>A<vA>^A<A>A<vA>^A<A>A<v<A>A>^AAvA<^A>A"),
("379A", "<v<A>>^AvA^A<vA<AA>>^AAvA<^A>AAvA^A<vA>^AA<A>A<v<A>A>^AAAvA<^A>A"),
],
)
def test_encode_shortest_dirpad_twice(code: str, answer: str) -> None:
numpad_encoded = encode_shortest_numpad(code)
robot1 = encode_shortest_dirpad(numpad_encoded)
robot2 = encode_shortest_dirpad(robot1)
assert len(robot2) == len(answer)
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(21)) == 126384
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(21), robots=2) == 126384

11
2024/tests/test_day22.py Normal file
View File

@@ -0,0 +1,11 @@
from aoc.days.day22 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(22, 1)) == 37327623
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(22, 2)) == 23

11
2024/tests/test_day23.py Normal file
View File

@@ -0,0 +1,11 @@
from aoc.days.day23 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(23)) == 7
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(23)) == "co,de,ka,ta"

7
2024/tests/test_day24.py Normal file
View File

@@ -0,0 +1,7 @@
from aoc.days.day24 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(24)) == 2024

7
2024/tests/test_day25.py Normal file
View File

@@ -0,0 +1,7 @@
from aoc.days.day25 import DayRunner
from . import get_data
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(25)) == 3

View File

@@ -1,26 +0,0 @@
import os
import pytest
from aoc.days.day4 import DayRunner
def get_data(which: int) -> str:
sample = os.path.dirname(__file__) + f"/samples/04.{which}.txt"
with open(sample, mode="rt", encoding="utf-8") as f:
return f.read()
@pytest.mark.parametrize(
"input,answer",
[
(get_data(1), 4),
(get_data(2), 18),
],
)
def test_sample_part1(input: str, answer: int) -> None:
assert DayRunner.part1(input) == answer
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(2)) == 9

View File

@@ -1,21 +0,0 @@
import os
from aoc.days.day5 import DayRunner
def get_data() -> str:
sample = os.path.dirname(__file__) + "/samples/05.txt"
with open(sample, mode="rt", encoding="utf-8") as f:
return f.read()
def test_sample_part1() -> None:
data = get_data()
assert DayRunner.part1(data) == 143
def test_sample_part2() -> None:
data = get_data()
assert DayRunner.part2(data) == 123

13
2024/uv.lock generated
View File

@@ -7,6 +7,7 @@ version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "click" },
{ name = "networkx" },
{ name = "numpy" },
]
@@ -20,6 +21,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "click", specifier = ">=8.1.7" },
{ name = "networkx", specifier = ">=3.4.2" },
{ name = "numpy", specifier = ">=2.1.2" },
]
@@ -35,7 +37,7 @@ name = "click"
version = "8.1.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "platform_system == 'Windows'" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
wheels = [
@@ -92,6 +94,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 },
]
[[package]]
name = "networkx"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263 },
]
[[package]]
name = "numpy"
version = "2.1.2"

5
2025/README.md Normal file
View File

@@ -0,0 +1,5 @@
# Advent of Code 2025
New year, new challenge. With the shorter year, I'm going to try to use a different language every
day, restricting myself to programming languages my employer uses in production. Let's call it the
**Full Stack Challenge**.

17
2025/day01/README.md Normal file
View File

@@ -0,0 +1,17 @@
# Day 01: Nix
Nix is a functional language made for the Nix package manager. As
To run the solution program, start the `nix` repl with the solution program and call the `solve`
function with the path to the input file.
```console
$ nix repl --option max-call-depth 10000 --file solve.nix
nix-repl> solve ./sample.txt
```
Some observations:
- The `max-call-depth` needs to be bumped to at least 10k for the main input files, otherwise you
hit the limit in the recursion.
- The standard library is lacking several basics but most of those you can build yourself

10
2025/day01/sample.txt Normal file
View File

@@ -0,0 +1,10 @@
L68
L30
R48
L5
R60
L55
L1
L99
R14
L82

37
2025/day01/solve.nix Normal file
View File

@@ -0,0 +1,37 @@
{
solve = input_file:
let
data = builtins.readFile input_file;
lines = builtins.filter (s: s != "" && builtins.isString s) (builtins.split "\n" data);
# Basic maths because the standard library doesn't have it
mod = a: b: a - (a / b) * b;
abs = n: if n < 0 then -n else n;
recurse = list: position: score1: score2:
let
first = builtins.head list;
# This is not guaranteed to work but it's good enough
num_part = builtins.substring 1 999 first;
num = builtins.fromJSON num_part;
delta = mod (if (builtins.substring 0 1 first) == "R" then num else -num) 100;
next = mod (position + delta + 100) 100;
score = if next == 0 then 1 else 0;
circles = num / 100;
passed = if position == 0 then
0
else
if delta < 0 && delta + position <= 0 || delta > 0 && delta + position >= 100 then
1
else
0;
in
if list == [] then
[score1 score2]
else
recurse (builtins.tail list) next (score + score1) (score2 + passed + circles);
in
recurse lines 50 0 0;
}

29
2025/day02/README.md Normal file
View File

@@ -0,0 +1,29 @@
# Day 02: Terraform
The code assumes an input file at `../inputs/02.txt`. Other than that, simply try to run as follows:
```console
$ terraform init
Initializing the backend...
Initializing modules...
Initializing provider plugins...
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
$ terraform plan
….
Changes to Outputs:
+ part1 = secret
+ part2 = also secret
You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.
```

29
2025/day02/main.tf Normal file
View File

@@ -0,0 +1,29 @@
locals {
input = file("../inputs/02.txt")
ranges = split(",", chomp(local.input))
min_max = [for r in local.ranges : split("-", r)]
}
module "check_range" {
source = "./range"
count = length(local.min_max)
min = local.min_max[count.index][0]
max = local.min_max[count.index][1]
}
module "check_range2" {
source = "./range2"
count = length(local.min_max)
min = local.min_max[count.index][0]
max = local.min_max[count.index][1]
}
output "part1" {
value = sum(module.check_range[*].invalid_sum)
}
output "part2" {
value = sum(module.check_range2[*].invalid_sum)
}

View File

@@ -0,0 +1,15 @@
variable "part" {
type = number
}
variable "repetitions" {
type = number
}
locals {
repeated = [for _ in range(var.repetitions) : tostring(var.part)]
}
output "full" {
value = join("", local.repeated)
}

46
2025/day02/range/main.tf Normal file
View File

@@ -0,0 +1,46 @@
variable "min" {
type = number
}
variable "max" {
type = number
}
variable "repetitions" {
type = number
default = 2
}
locals {
digits = floor(length(tostring(var.max)) / var.repetitions)
maximum = substr(tostring(var.max), 0, local.digits)
real_maximum = length(tostring(var.max)) % var.repetitions == 0 ? tonumber(local.maximum) : pow(10, local.digits)
minimum_prefix = substr(tostring(var.min), 0, length(tostring(var.min)) - local.digits * (var.repetitions - 1))
minimum = local.minimum_prefix == "" ? 1 : tonumber(local.minimum_prefix)
count = max(local.real_maximum - local.minimum + 1, 1)
can_work = anytrue([for n in range(length(tostring(var.min)), length(tostring(var.max)) + 1) : n % var.repetitions == 0])
}
// This "candidates" module ought really be a list comprehension from range, but Terraform does not
// allow you to create ranges longer than 1024.
module "candidates" {
source = "./item"
count = local.can_work ? local.count : 0
part = count.index + local.minimum
repetitions = var.repetitions
}
locals {
invalid = [for n in module.candidates[*].full : n if n >= var.min && n <= var.max]
}
output "invalid_sum" {
value = length(local.invalid) > 0 ? sum(local.invalid) : 0
}
output "invalid" {
value = toset(local.invalid)
}

Some files were not shown because too many files have changed in this diff Show More