1 Commits

Author SHA1 Message Date
adb09169f4 Merge eef637f006 into 1f41f4b35a 2024-11-22 09:46:14 +00:00
162 changed files with 53 additions and 6341 deletions

43
.github/workflows/2024.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
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

3
.gitignore vendored
View File

@@ -67,6 +67,3 @@ flamegraph.svg
# Ignore saved inputs # Ignore saved inputs
inputs/ inputs/
# Terraform dir
.terraform

View File

@@ -1,15 +0,0 @@
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

@@ -1,49 +0,0 @@
# Bonus Challenge
This part of the repo holds the bonus challenge for 2024: implement as much as possible in pure
Terraform.
## Usage
The infrastructure definitions expect the input files to be located at `../inputs/[\d]{2}.txt`.
After storing the input files there, the code should be runnable as follows:
```console
$ terraform init
< a lot of modules being installed >
$ terraform plan
Changes to Outputs:
+ day01_1 = 42
+ day01_2 = 12
+ day02_1 = …
You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.
```
Note that Terraform may freeze for tens of seconds while it's running the computations. This is
normal, and all Terraform code ought to think a little before doing anything. It would save people
some bad rollbacks.
## Development
I use [Terraform tests](https://developer.hashicorp.com/terraform/language/tests) to automatically
run my terraform code on the sample inputs. It's almost a normal development workflow.
The only rule is that the code should be all terraform, no cheating by shelling out to external
programs. Using providers is allowed, as long as the providers don't actually interact with external
systems and are reasonably self-contained. I will try to limit my use of those regardless.
## Why
DevOps will continue until morale improves. But really, a friend remarked that my Python solutions
were strangely normal for me, so I opted to use a language that I do use professionally.
Terraform is a unique beast. It can do a lot, but it is also very limited, and intentionally so.
There's a standard library of functions you might want to use, but all of them work in strange ways
and there isn't that much to begin with. You can never mutate any variables, you can only declare
new ones. You don't have recursion and your only source of loops are list- and map comprehensions,
or multiple instantiations or a module.
These make for a very constrained programming environment, and constrained programming is fun. It
makes you think outside the box.

View File

@@ -1,28 +0,0 @@
variable "input" {
type = string
}
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 : tonumber(line[0])]
right = [for line in local.lines_split : tonumber(line[1])]
left_sorted = sort(local.left)
right_sorted = sort(local.right)
diffs = [for i in range(length(local.left_sorted)) : abs(local.left_sorted[i] - local.right_sorted[i])]
counts = { for num in local.right : num => num... }
matching = [for left in local.left : left * try(length(local.counts[left]), 0)]
}
output "part1" {
value = sum(local.diffs)
}
output "part2" {
value = sum(local.matching)
}

View File

@@ -1,17 +0,0 @@
variable "report" {
type = list(number)
}
module "is_valid" {
source = "../is_valid"
count = length(var.report)
report = concat(
count.index > 0 ? slice(var.report, 0, count.index) : [],
try(slice(var.report, count.index + 1, length(var.report)), [])
)
}
output "valid" {
value = anytrue(module.is_valid[*].valid)
}

View File

@@ -1,14 +0,0 @@
variable "report" {
type = list(number)
}
locals {
delta = [for i in range(1, length(var.report)) : var.report[i] - var.report[i - 1]]
all_negative = alltrue([for d in local.delta : d <= -1 && d >= -3])
all_positive = alltrue([for d in local.delta : d >= 1 && d <= 3])
}
output "valid" {
value = local.all_negative || local.all_positive
}

View File

@@ -1,28 +0,0 @@
variable "input" {
type = string
}
locals {
reports = [for line in split("\n", trim(var.input, "\n")) : split(" ", line)]
}
module "part1_valid" {
source = "./is_valid"
count = length(local.reports)
report = local.reports[count.index]
}
module "part2_valid" {
source = "./is_savable"
count = length(local.reports)
report = local.reports[count.index]
}
output "part1" {
value = length([for i in range(length(local.reports)) : true if module.part1_valid[i].valid])
}
output "part2" {
value = length([for i in range(length(local.reports)) : true if module.part2_valid[i].valid])
}

View File

@@ -1,19 +0,0 @@
variable "input" {
type = string
}
locals {
filtered = replace(var.input, "/(?s)don't\\(\\).*?do\\(\\)/", "")
filtered2 = replace(local.filtered, "/(?s)don't\\(\\).*/", "")
muls = regexall("mul\\((\\d+),(\\d+)\\)", var.input)
filtered_muls = regexall("mul\\((\\d+),(\\d+)\\)", local.filtered2)
}
output "part1" {
value = sum([for mul in local.muls : tonumber(mul[1]) * tonumber(mul[0])])
}
output "part2" {
value = sum([for mul in local.filtered_muls : tonumber(mul[1]) * tonumber(mul[0])])
}

View File

@@ -1,30 +0,0 @@
variable "grid" {
type = list(string)
}
variable "x" {
type = number
}
variable "y" {
type = number
}
locals {
found_a = substr(var.grid[var.y], var.x, 1) == "A"
c1 = substr(var.grid[var.y - 1], var.x - 1, 1)
c2 = substr(var.grid[var.y - 1], var.x + 1, 1)
c3 = substr(var.grid[var.y + 1], var.x + 1, 1)
c4 = substr(var.grid[var.y + 1], var.x - 1, 1)
d1 = "${local.c1}${local.c3}"
d2 = "${local.c2}${local.c4}"
found_d1 = contains(["SM", "MS"], local.d1)
found_d2 = contains(["SM", "MS"], local.d2)
}
output "found" {
value = local.found_a && local.found_d1 && local.found_d2
}

View File

@@ -1,27 +0,0 @@
variable "grid" {
type = list(string)
}
variable "x" {
type = number
}
variable "y" {
type = number
}
variable "dx" {
type = number
}
variable "dy" {
type = number
}
locals {
word = join("", [for i in range(4) : var.x + i * var.dx >= 0 ? try(substr(var.grid[var.y + i * var.dy], var.x + i * var.dx, 1), "F") : "F"])
}
output "found" {
value = contains(["SAMX", "XMAS"], local.word) ? 1 : 0
}

View File

@@ -1,59 +0,0 @@
variable "grid" {
type = list(string)
}
variable "index" {
type = number
}
variable "width" {
type = number
}
variable "height" {
type = number
}
locals {
x = var.index % var.width
y = floor(var.index / var.width)
directions = {
"DL" = [-1, 1]
"D" = [0, 1]
"DR" = [1, 1]
}
should_check_x_mas = local.x >= 1 && local.y >= 1 && local.x < var.width - 1 && local.y < var.height - 1
}
module "check_xmas" {
source = "./check_xmas"
for_each = local.directions
grid = var.grid
x = local.x
y = local.y
dx = each.value[0]
dy = each.value[1]
}
module "check_x_mas" {
source = "./check_x_mas"
count = local.should_check_x_mas ? 1 : 0
grid = var.grid
y = local.y
x = local.x
}
output "xmas" {
value = sum([for _, v in module.check_xmas : v.found])
}
output "x_mas" {
value = try(module.check_x_mas[0].found, false) ? 1 : 0
}

View File

@@ -1,31 +0,0 @@
variable "input" {
type = string
}
locals {
grid = split("\n", chomp(var.input))
height = length(local.grid)
width = length(local.grid[0])
lr = length(regexall("XMAS", var.input))
rl = length(regexall("SAMX", var.input))
}
module "check_point" {
source = "./check_point"
count = local.width * local.height
width = local.width
height = local.height
grid = local.grid
index = count.index
}
output "part1" {
value = sum(module.check_point[*].xmas) + local.lr + local.rl
}
output "part2" {
value = sum(module.check_point[*].x_mas)
}

View File

@@ -1,25 +0,0 @@
variable "input" {
type = string
}
locals {
parts = split("\n\n", chomp(var.input))
rules = [for rule_line in split("\n", local.parts[0]) : [for v in split("|", rule_line) : tonumber(v)]]
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)]]
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(local.scores[*])
}

View File

@@ -1,52 +0,0 @@
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

@@ -1,52 +0,0 @@
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]]
)
}

View File

@@ -1,39 +0,0 @@
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]...))
}

View File

@@ -1,24 +0,0 @@
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

@@ -1,32 +0,0 @@
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) }
}

View File

@@ -1,446 +0,0 @@
# 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
}

View File

@@ -1,31 +0,0 @@
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

@@ -1,34 +0,0 @@
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]
])
}

View File

@@ -1,33 +0,0 @@
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
}

View File

@@ -1,13 +0,0 @@
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)
}

View File

@@ -1,23 +0,0 @@
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

@@ -1,133 +0,0 @@
terraform {
}
module "day01" {
source = "./day01"
input = file("../inputs/01.txt")
}
output "day01_1" {
value = module.day01.part1
}
output "day01_2" {
value = module.day01.part2
}
module "day02" {
source = "./day02"
input = file("../inputs/02.txt")
}
output "day02_1" {
value = module.day02.part1
}
output "day02_2" {
value = module.day02.part2
}
module "day03" {
source = "./day03"
input = file("../inputs/03.txt")
}
output "day03_1" {
value = module.day03.part1
}
output "day03_2" {
value = module.day03.part2
}
# Don't run this, it runs forever (6 minutes) and requires a lot of memory (~5.5GB) to execute to
# boot. Trust me, it works.
# module "day04" {
# source = "./day04"
# input = file("../inputs/04.txt")
# }
# output "day04_1" {
# value = module.day04.part1
# }
# output "day04_2" {
# value = module.day04.part2
# }
module "day05" {
source = "./day05"
input = file("../inputs/05.txt")
}
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

@@ -1,242 +0,0 @@
run "day1" {
command = plan
module {
source = "./day01"
}
variables {
input = file("../tests/samples/01.txt")
}
assert {
condition = output.part1 == 11
error_message = "Part1 output is wrong"
}
assert {
condition = output.part2 == 31
error_message = "Part2 output is wrong"
}
}
run "day2" {
command = plan
module {
source = "./day02"
}
variables {
input = file("../tests/samples/02.txt")
}
assert {
condition = output.part1 == 2
error_message = "Part1 output is wrong"
}
assert {
condition = output.part2 == 4
error_message = "Part2 output is wrong"
}
}
run "day3_1" {
command = plan
module {
source = "./day03"
}
variables {
input = "xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))"
}
assert {
condition = output.part1 == 161
error_message = "Part1 output is wrong"
}
}
run "day3_2" {
command = plan
module {
source = "./day03"
}
variables {
input = "xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))"
}
assert {
condition = output.part2 == 48
error_message = "Part2 output is wrong"
}
}
run "day4_small" {
command = plan
module {
source = "./day04"
}
variables {
input = file("../tests/samples/04.1.txt")
}
assert {
condition = output.part1 == 4
error_message = "Part1 output is wrong"
}
}
run "day4" {
command = plan
module {
source = "./day04"
}
variables {
input = file("../tests/samples/04.2.txt")
}
assert {
condition = output.part1 == 18
error_message = "Part1 output is wrong"
}
assert {
condition = output.part2 == 9
error_message = "Part2 output is wrong"
}
}
run "day5_1" {
command = plan
module {
source = "./day05"
}
variables {
input = file("../tests/samples/05.txt")
}
assert {
condition = output.part1 == 143
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

View File

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

View File

@@ -1,6 +1,5 @@
import datetime import datetime
import time import time
from typing import IO
import click import click
@@ -20,13 +19,12 @@ from aoc import days
"-t", "--time", "timing", is_flag=True, help="Print elapsed time afterwards" "-t", "--time", "timing", is_flag=True, help="Print elapsed time afterwards"
) )
@click.argument("day", required=True) @click.argument("day", required=True)
def main(day: int, timing: bool, data: IO[str]) -> None: def main(day: int, timing: bool, data: str) -> None:
runner_class = days.get_runner(day) runner_class = days.get_runner(day)
contents = data.read()
start = time.perf_counter_ns() start = time.perf_counter_ns()
part1, part2 = runner_class.run_both(contents) part1, part2 = runner_class.run_both(data)
if timing: if timing:
elapsed = time.perf_counter_ns() - start elapsed = time.perf_counter_ns() - start

View File

@@ -1,27 +1,13 @@
from collections import defaultdict
from io import StringIO
from typing import Any from typing import Any
import numpy from . import SeparateRunner
from . import CombinedRunner
class DayRunner(CombinedRunner): class DayRunner(SeparateRunner):
@classmethod @classmethod
def run_both(cls, data: str) -> tuple[Any, Any]: def part1(cls, _data: str) -> Any:
nums = numpy.loadtxt(StringIO(data), dtype=numpy.int32) return "Hello"
left = nums[..., 0] @classmethod
right = nums[..., 1] def part2(cls, _data: str) -> Any:
return "world!"
left.sort()
right.sort()
diff = numpy.abs(left - right).sum()
counts: defaultdict[int, int] = defaultdict(int)
for val in right:
counts[val] += 1
return diff, sum(counts[v] * v for v in left)

View File

@@ -1,85 +0,0 @@
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

@@ -1,50 +0,0 @@
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())

View File

@@ -1,101 +0,0 @@
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

@@ -1,43 +0,0 @@
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

@@ -1,80 +0,0 @@
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

View File

@@ -1,161 +0,0 @@
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

@@ -1,76 +0,0 @@
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

@@ -1,89 +0,0 @@
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

@@ -1,70 +0,0 @@
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

@@ -1,41 +0,0 @@
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

@@ -1,47 +0,0 @@
import numpy
from . import SeparateRunner
def _safe(nums: numpy.ndarray) -> bool:
steps = nums[1:] - nums[:-1]
if numpy.all(steps > 0):
return numpy.all((steps >= 1) & (steps <= 3))
elif numpy.all(steps < 0):
return numpy.all((steps <= -1) & (steps >= -3))
else:
return False
def is_safe(line: str) -> bool:
nums = numpy.fromstring(line, dtype=numpy.int32, sep=" ")
return _safe(nums)
def is_savable(line: str) -> bool:
nums = numpy.fromstring(line, dtype=numpy.int32, sep=" ")
return any(
_safe(numpy.concatenate((nums[:i], nums[i + 1 :]), axis=None))
for i in range(len(nums))
)
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, data: str) -> int:
lines = data.strip().split("\n")
safe = sum(1 for line in lines if is_safe(line))
return safe
@classmethod
def part2(cls, data: str) -> int:
lines = data.strip().split("\n")
safe = sum(1 for line in lines if is_savable(line))
return safe

View File

@@ -1,82 +0,0 @@
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)
)

View File

@@ -1,159 +0,0 @@
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

@@ -1,41 +0,0 @@
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

@@ -1,42 +0,0 @@
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))

View File

@@ -1,133 +0,0 @@
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

@@ -1,29 +0,0 @@
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

@@ -1,25 +0,0 @@
import re
from . import SeparateRunner
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, data: str) -> int:
return sum(int(a) * int(b) for a, b in re.findall(r"mul\((\d+),(\d+)\)", data))
@classmethod
def part2(cls, data: str) -> int:
do = True
total = 0
for op, a, b in re.findall(r"(don't\(\)|do\(\)|mul\((\d+),(\d+)\))", data):
match op:
case "do()":
do = True
case "don't()":
do = False
case _:
if do:
total += int(a) * int(b)
return total

View File

@@ -1,69 +0,0 @@
import numpy
from . import SeparateRunner
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
grid = numpy.array(list(map(list, input.strip().split("\n"))))
found = 0
directions = [
(-1, -1),
(-1, 0),
(-1, 1),
(0, -1),
(0, 1),
(1, -1),
(1, 0),
(1, 1),
]
word = "XMAS"
for y in range(grid.shape[0]):
for x in range(grid.shape[1]):
if grid[y, x] != "X":
continue
for dx, dy in directions:
end_x = x + 3 * dx
end_y = y + 3 * dy
if (
end_x < 0
or end_x >= grid.shape[1]
or end_y < 0
or end_y >= grid.shape[0]
):
continue
if all(
grid[y + i * dy, x + i * dx] == c for i, c in enumerate(word)
):
found += 1
return found
@classmethod
def part2(cls, input: str) -> int:
grid = numpy.array(list(map(list, input.strip().split("\n"))))
found = 0
magic = ord("M") ^ ord("S")
for y in range(1, grid.shape[0] - 1):
for x in range(1, grid.shape[1] - 1):
if grid[y, x] != "A":
continue
first_diag = ord(grid[y - 1, x - 1]) ^ ord(grid[y + 1, x + 1])
secnd_diag = ord(grid[y - 1, x + 1]) ^ ord(grid[y + 1, x - 1])
if first_diag == magic and secnd_diag == magic:
found += 1
return found

View File

@@ -1,50 +0,0 @@
import functools
from collections import defaultdict
from . import CombinedRunner
def parse_input(input: str) -> tuple[set[tuple[int, int]], list[list[int]]]:
first, second = input.strip().split("\n\n")
rules = {tuple(int(x) for x in line.split("|")) for line in first.split("\n")}
updates = [[int(x) for x in line.split(",")] for line in second.split("\n")]
return rules, updates
def is_correct(update: list[int], must_after: dict[int, set[int]]) -> bool:
forbidden = set()
for entry in update:
if entry in forbidden:
return False
forbidden |= must_after.get(entry, set())
return True
class DayRunner(CombinedRunner):
@classmethod
def run_both(cls, input: str) -> int:
rules, updates = parse_input(input)
must_after = defaultdict(set)
for before, after in rules:
must_after[after].add(before)
correct = 0
corrected = 0
key = functools.cmp_to_key(lambda a, b: -1 if (a, b) in rules else 1)
for update in updates:
if is_correct(update, must_after):
correct += update[len(update) // 2]
else:
update.sort(key=key)
corrected += update[len(update) // 2]
return correct, corrected

View File

@@ -1,105 +0,0 @@
import numpy
from . import SeparateRunner
def does_loop(
grid: numpy.array,
x: int,
y: int,
dx: int,
dy: int,
visited: set[tuple[int, int, int, int]],
) -> bool:
try:
while True:
while y + dy >= 0 and x + dx >= 0 and grid[y + dy, x + dx] == "#":
dx, dy = -dy, dx
x += dx
y += dy
if x < 0 or y < 0:
return False
pos = (x, y, dx, dy)
if pos in visited:
return True
else:
visited.add(pos)
except IndexError:
return False
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
grid = input.strip().split("\n")
for y, line in enumerate(grid):
if (x := line.find("^")) != -1:
break
dx = 0
dy = -1
visited = {(x, y)}
try:
while True:
nx = x + dx
ny = y + dy
if grid[ny][nx] == "#":
dx, dy = -dy, dx
else:
x, y = nx, ny
visited.add((x, y))
except IndexError:
pass
return len(visited)
@classmethod
def part2(cls, input: str) -> int:
grid = numpy.array(list(map(list, input.strip().split("\n"))))
y, x = numpy.where(grid == "^")
y = y[0]
x = x[0]
dx = 0
dy = -1
loops = 0
visited = {(x, y, dx, dy)}
tiles_visited = {(x, y)}
try:
while True:
while y + dy >= 0 and x + dx >= 0 and grid[y + dy, x + dx] == "#":
dx, dy = -dy, dx
nx = x + dx
ny = y + dy
if nx < 0 or ny < 0:
break
if (nx, ny) not in tiles_visited:
# check for a loop
grid[ny, nx] = "#"
if does_loop(grid, x, y, dx, dy, visited.copy()):
loops += 1
grid[ny, nx] = "L"
else:
grid[ny, nx] = "."
x, y = nx, ny
tiles_visited.add((x, y))
visited.add((x, y, dx, dy))
except IndexError:
pass
return loops

View File

@@ -1,61 +0,0 @@
from . import SeparateRunner
def parse_input(input: str) -> tuple[int, list[int]]:
result = []
for line in input.strip().split("\n"):
test, nums = line.split(": ")
result.append((int(test), list(map(int, nums.split(" ")))))
return result
def is_possible(target: int, nums: list[int]) -> bool:
if target == 0:
return not nums
if not nums or target < 0:
return False
tail = nums[-1]
remainder = nums[:-1]
return is_possible(target - tail, remainder) or (
target % tail == 0 and is_possible(target // tail, remainder)
)
def is_possible2(target: int, nums: list[int]) -> bool:
if target == 0:
return not nums
if not nums or target < 0:
return False
tail = nums[-1]
remainder = nums[:-1]
target_str = str(target)
tail_str = str(tail)
return (
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)
)
)
class DayRunner(SeparateRunner):
@classmethod
def part1(cls, input: str) -> int:
lines = parse_input(input)
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))

View File

@@ -1,60 +0,0 @@
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)

View File

@@ -1,107 +0,0 @@
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

@@ -1,8 +0,0 @@
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()

View File

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

View File

@@ -1,6 +0,0 @@
7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9

View File

@@ -1,5 +0,0 @@
..X...
.SAMX.
.A..A.
XMAS.S
.X....

View File

@@ -1,10 +0,0 @@
MMMSXXMASM
MSAMXMSMSA
AMXSXMAAMM
MSAMASMSMX
XMASAMXAMM
XXAMMXXAMA
SMSMSASXSS
SAXAMASAAA
MAMMMXMMMM
MXMXAXMASX

View File

@@ -1,28 +0,0 @@
47|53
97|13
97|61
97|47
75|29
61|13
75|53
29|13
97|29
53|29
61|53
97|53
61|29
47|13
75|47
97|75
47|61
75|61
47|29
75|13
53|13
75,47,61,53,29
97,61,53,29,13
75,29,13
75,97,47,61,53
61,13,29
97,13,75,29,47

View File

@@ -1,10 +0,0 @@
....#.....
.........#
..........
..#.......
.......#..
..........
.#..^.....
........#.
#.........
......#...

View File

@@ -1,9 +0,0 @@
190: 10 19
3267: 81 40 27
83: 17 5
156: 15 6
7290: 6 8 6 15
161011: 16 10 13
192: 17 8 14
21037: 9 7 18 13
292: 11 6 16 20

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +0,0 @@
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

View File

@@ -1,12 +0,0 @@
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

@@ -1,21 +0,0 @@
##########
#..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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,25 +0,0 @@
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,32 +0,0 @@
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

View File

@@ -1,47 +0,0 @@
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

View File

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

View File

@@ -1,11 +0,0 @@
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,20 +0,0 @@
from aoc.days.day2 import DayRunner, is_savable
from . import get_data
def test_individual_samples() -> None:
assert is_savable("7 6 4 2 1")
assert not is_savable("1 2 7 8 9")
assert not is_savable("9 7 6 2 1")
assert is_savable("1 3 2 4 5")
assert is_savable("8 6 4 4 1")
assert is_savable("1 3 6 7 9")
def test_sample_part1() -> None:
assert DayRunner.part1(get_data(2)) == 2
def test_sample_part2() -> None:
assert DayRunner.part2(get_data(2)) == 4

View File

@@ -1,12 +0,0 @@
from aoc.days.day3 import DayRunner
SAMPLE_1 = "xmul(2,4)%&mul[3,7]!@^do_not_mul(5,5)+mul(32,64]then(mul(11,8)mul(8,5))"
SAMPLE_2 = "xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5))"
def test_sample_part1() -> None:
assert DayRunner.part1(SAMPLE_1) == 161
def test_sample_part2() -> None:
assert DayRunner.part2(SAMPLE_2) == 48

View File

@@ -1,20 +0,0 @@
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

View File

@@ -1,15 +0,0 @@
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 +0,0 @@
import os
from aoc.days.day6 import DayRunner
def get_data() -> str:
sample = os.path.dirname(__file__) + "/samples/06.txt"
with open(sample, mode="rt", encoding="utf-8") as f:
return f.read()
def test_sample_part1() -> None:
assert DayRunner.part1(get_data()) == 41
def test_sample_part2() -> None:
assert DayRunner.part2(get_data()) == 6

View File

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

View File

@@ -1,11 +0,0 @@
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

View File

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

View File

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

View File

@@ -1,21 +0,0 @@
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

View File

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

View File

@@ -1,7 +0,0 @@
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

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