My solutions for Advent of Code 2022 in TypeScript
Santa's reindeer typically eat regular reindeer food, but they need a lot of magical energy to deliver presents on Christmas. For that, their favorite snack is a special type of star fruit that only grows deep in the jungle. The Elves have brought you on their annual expedition to the grove where the fruit grows.
To supply enough magical energy, the expedition needs to retrieve a minimum of fifty stars by December 25th. Although the Elves assure you that the grove has plenty of fruit, you decide to grab any fruit you see along the way, just in case.
Day | Quest | Part 1 | Part 2 |
---|---|---|---|
1 | Calorie Counting | ⭐ | ⭐ |
2 | Rock Paper Scissors | ⭐ | ⭐ |
3 | Rucksack Reorganization | ⭐ | ⭐ |
4 | Camp Cleanup | ⭐ | ⭐ |
5 | Supply Stacks | ⭐ | ⭐ |
6 | Tuning Trouble | ⭐ | ⭐ |
7 | No Space Left On Device | ⭐ | ⭐ |
8 | Treetop Tree House | ⭐ | ⭐ |
9 | Rope Bridge | ⭐ | ⭐ |
10 | Cathode-Ray Tube | ⭐ | ⭐ |
11 | Monkey in the Middle | ⭐ | ⭐ |
12 | Hill Climbing Algorithm | ⭐ | ⭐ |
13 | Distress Signal | ⭐ | ⭐ |
14 | Regolith Reservoir | ⭐ | ⭐ |
15 | Beacon Exclusion Zone | ⭐ | ⭐ |
16 | Proboscidea Volcanium | ⭐ | ⭐ |
17 | Pyroclastic Flow | ⭐ | ⭐ |
18 | Boiling Boulders | ⭐ | ⭐ |
19 | Not Enough Minerals | ⭐ | ⭐ |
20 | Grove Positioning System | ⭐ | ⭐ |
21 | Monkey Math | ⭐ | ⭐ |
22 | Monkey Map | ⭐ | ⭐ |
23 | Unstable Diffusion | ⭐ | ⭐ |
24 | Blizzard Basin | ⭐ | ⭐ |
25 | Full of Hot Air | ⭐ | ⭐ |
The jungle must be too overgrown and difficult to navigate in vehicles or access from the air; the Elves' expedition traditionally goes on foot. As your boats approach land, the Elves begin taking inventory of their supplies. One important consideration is food - in particular, the number of Calories each Elf is carrying.
Quest: adventofcode.com/2022/day/1
import { add } from 'lodash'
import { desc, getLines } from '../utils'
const lines = getLines(__dirname)
const elves: number[] = [0]
for (const line of lines) {
if (line === '') {
elves.unshift(0)
} else {
elves[0] += parseInt(line)
}
}
elves.sort(desc)
console.log('Part 1:', elves[0])
console.log('Part 2:', elves.slice(0, 3).reduce(add))
The Elves begin to set up camp on the beach. To decide whose tent gets to be closest to the snack storage, a giant Rock Paper Scissors tournament is already in progress.
Quest: adventofcode.com/2022/day/2
import { add } from 'lodash'
import { getLines } from '../utils'
const lines = getLines(__dirname).map(x => x.replace(' ', ''))
const points: Record<string, number[]> = {
AX: [4, 3],
AY: [8, 4],
AZ: [3, 8],
BX: [1, 1],
BY: [5, 5],
BZ: [9, 9],
CX: [7, 2],
CY: [2, 6],
CZ: [6, 7],
}
console.log('Part 1:', lines.map(s => points[s][0]).reduce(add))
console.log('Part 2:', lines.map(s => points[s][1]).reduce(add))
One Elf has the important job of loading all of the rucksacks with supplies for the jungle journey. Unfortunately, that Elf didn't quite follow the packing instructions, and so a few items now need to be rearranged.
Quest: adventofcode.com/2022/day/3
import { getLines } from '../utils'
import { chunk, intersection, add } from 'lodash'
const rucksacks = getLines(__dirname).map(x => [...x])
function itemToPriority(item: string) {
const code = item.charCodeAt(0)
return code - (code > 90 ? 96 : 38)
}
function commonItem(rucksacks: string[][]) {
return intersection(...rucksacks)[0]
}
function divideRucksack(rucksack: string[]) {
return chunk(rucksack, rucksack.length / 2)
}
const prioritySum = rucksacks
.map(divideRucksack)
.map(commonItem)
.map(itemToPriority)
.reduce(add)
const badgeSum = chunk(rucksacks, 3)
.map(commonItem)
.map(itemToPriority)
.reduce(add)
console.log('part 1:', prioritySum)
console.log('part 2:', badgeSum)
Space needs to be cleared before the last supplies can be unloaded from the ships, and so several Elves have been assigned the job of cleaning up sections of the camp.
Quest: adventofcode.com/2022/day/4
import { intersection, range } from 'lodash'
import { getLines } from '../utils'
const pairs = getLines(__dirname).map(line =>
line.split(',').map(range => range.split('-').map(Number))
)
let fullyOverlapping = 0
let anyOverlapping = 0
for (const pair of pairs) {
const ranges = pair.map(elf => range(elf[0], elf[1] + 1))
const overlaps = intersection(...ranges).length
const shorterAssignment = Math.min(...ranges.map(x => x.length))
if (overlaps === shorterAssignment) fullyOverlapping += 1
if (overlaps > 0) anyOverlapping += 1
}
console.log('Part 1:', fullyOverlapping)
console.log('Part 2:', anyOverlapping)
The expedition can depart as soon as the final supplies have been unloaded from the ships. Supplies are stored in stacks of marked crates, but because the needed supplies are buried under many other crates, the crates need to be rearranged.
Quest: adventofcode.com/2022/day/5
import { cloneDeep, last } from 'lodash'
import { getLines } from '../utils'
const CRATE_MOVER_9000 = 'CrateMover 9000'
const CRATE_MOVER_9001 = 'CrateMover 9001'
type Step = { move: number; from: number; to: number }
// Read file
const lines = getLines(__dirname)
const numberOfStacks = (lines[0].length + 1) / 4
const heightOfStacks = lines.findIndex(line => line[1] === '1')
const stacks: string[][] = new Array(numberOfStacks).fill(0).map(() => [])
for (let y = 0; y < heightOfStacks; y++) {
for (let x = 0; x < numberOfStacks; x++) {
const crate = lines[y][1 + x * 4]
if (crate !== ' ') stacks[x].unshift(crate)
}
}
const steps: Step[] = lines.slice(heightOfStacks + 2).map(line => {
const [move, from, to] = line.match(/(\d+)/g)!.map(Number)
return { move, from: from - 1, to: to - 1 }
})
// Part 1, 2:
const stacksPart2 = cloneDeep(stacks)
function rearrange(stacks: string[][], step: Step, craneModel: string) {
const crates = stacks[step.from].splice(-step.move)
if (craneModel === CRATE_MOVER_9000) crates.reverse()
stacks[step.to].push(...crates)
}
for (const step of steps) {
rearrange(stacks, step, CRATE_MOVER_9000)
rearrange(stacksPart2, step, CRATE_MOVER_9001)
}
console.log('Part 1:', stacks.map(last).join(''))
console.log('Part 2:', stacksPart2.map(last).join(''))
The preparations are finally complete; you and the Elves leave camp on foot and begin to make your way toward the star fruit grove.
As you move through the dense undergrowth, one of the Elves gives you a handheld device. He says that it has many fancy features, but the most important one to set up right now is the communication system.
However, because he's heard you have significant experience dealing with signal-based systems, he convinced the other Elves that it would be okay to give you their one malfunctioning device - surely you'll have no problem fixing it.
Quest: adventofcode.com/2022/day/6
import { range } from 'lodash'
import { getLines } from '../utils'
const signal = getLines(__dirname)[0]
function findStartIndex(signal: string, markerLength: number) {
let regex = '(.)'
for (let i = 2; i <= markerLength; i++) {
const negativeLookAhead = range(1, i).map(n => `\\${n}`)
regex += `(?!(?:${negativeLookAhead.join('|')}))(.)`
}
return signal.match(new RegExp(regex))!.index! + markerLength
}
console.log('Part 1:', findStartIndex(signal, 4))
console.log('Part 2:', findStartIndex(signal, 14))
You can hear birds chirping and raindrops hitting leaves as the expedition proceeds. Occasionally, you can even hear much louder sounds in the distance; how big do the animals get out here, anyway?
The device the Elves gave you has problems with more than just its communication system. You try to run a system update:
$ system-update --please --pretty-please-with-sugar-on-top
Error: No space left on device
Perhaps you can delete some files to make space for the update?
Quest: adventofcode.com/2022/day/7
import { add } from 'lodash'
import { asc, getLines } from '../utils'
const lines = getLines(__dirname)
const fs: Record<string, number> = {}
const path: string[] = []
for (const line of lines) {
if (/ cd /.test(line)) {
const arg = line.slice(5)
arg === '..' ? path.pop() : path.push(arg)
} else if (/^\d/.test(line)) {
let tmp = ''
path.forEach(dir => {
tmp += dir
fs[tmp] ??= 0
fs[tmp] += parseInt(line)
})
}
}
const sum = Object.values(fs)
.filter(size => size < 100_000)
.reduce(add)
const size = Object.values(fs)
.sort(asc)
.find(size => fs['/'] - size <= 40_000_000)
console.log('Part 1:', sum)
console.log('Part 2:', size)
The expedition comes across a peculiar patch of tall trees all planted carefully in a grid. The Elves explain that a previous expedition planted these trees as a reforestation effort. Now, they're curious if this would be a good location for a tree house.
Quest: adventofcode.com/2022/day/8
import { multiply } from 'lodash'
import { Cartesian } from '../Cartesian'
import { getArray2d } from '../utils'
const trees = new Cartesian(getArray2d(__dirname).map(line => line.map(Number)))
let maxScore = -Infinity
let visibleTrees = 0
trees.forEach((tree, x, y) => {
const col = trees.cols[x]
const row = trees.rows[y]
const dirs = [
col.slice(0, y).reverse(), // bottom
col.slice(y + 1), // top
row.slice(0, x).reverse(), // left
row.slice(x + 1), // right
]
const isVisible = dirs.some(dir => tree > Math.max(...dir))
const scenicScore = dirs
.map(dir => dir.findIndex(t => t >= tree) + 1 || dir.length)
.reduce(multiply, 1)
if (scenicScore > maxScore) maxScore = scenicScore
if (isVisible) visibleTrees += 1
})
console.log('Part 1', visibleTrees)
console.log('Part 2', maxScore)
This rope bridge creaks as you walk along it. You aren't sure how old it is, or whether it can even support your weight.
It seems to support the Elves just fine, though. The bridge spans a gorge which was carved out by the massive river far below you.
You step carefully; as you do, the ropes stretch and twist. You decide to distract yourself by modeling rope physics; maybe you can even figure out where not to step.
Quest: adventofcode.com/2022/day/9
import { range, values, last } from 'lodash'
import { getLines } from '../utils'
const dirs = getLines(__dirname)
.map(line => line.split(' '))
.map(([dir, steps]) => dir.repeat(+steps))
.join('')
function move(knot: Point, dir: string) {
if (dir === 'R') knot.x++
if (dir === 'L') knot.x--
if (dir === 'U') knot.y++
if (dir === 'D') knot.y--
}
function follow(follower: Point, leader: Point) {
const deltaX = leader.x - follower.x
const deltaY = leader.y - follower.y
const distance = Math.hypot(deltaX, deltaY)
if (distance < 2) return
// prettier-ignore
const dir = Math.abs(deltaX) > Math.abs(deltaY)
? deltaX > 0 ? 'R' : 'L'
: deltaY > 0 ? 'U' : 'D'
move(follower, dir)
if (distance > 2) {
const align = 'UD'.includes(dir) ? 'x' : 'y'
follower[align] += Math.sign(align === 'x' ? deltaX : deltaY)
}
}
function tailPositions(dirs: string, ropeLength: number) {
const positions = new Set<string>().add('0,0')
const rope: Point[] = range(ropeLength).map(() => ({ x: 0, y: 0 }))
for (const dir of dirs) {
move(rope[0], dir)
for (let i = 1; i < rope.length; i++) {
follow(rope[i], rope[i - 1])
}
positions.add(values(last(rope)).join())
}
return positions.size
}
console.log('Part 1:', tailPositions(dirs, 2))
console.log('Part 2:', tailPositions(dirs, 10))
You avoid the ropes, plunge into the river, and swim to shore.
The Elves yell something about meeting back up with them upriver, but the river is too loud to tell exactly what they're saying. They finish crossing the bridge and disappear from view.
Situations like this must be why the Elves prioritized getting the communication system on your handheld device working. You pull it out of your pack, but the amount of water slowly draining from a big crack in its screen tells you it probably won't be of much immediate use.
Quest: adventofcode.com/2022/day/10
import { getLines, toNumber, inRange, divisible } from '../utils'
let x = 1
let sum = 0
let cycle = 0
let crt = ''
getLines(__dirname).forEach(line => {
exec()
if (line.startsWith('add')) {
exec()
x += toNumber(line)
}
})
function exec() {
if (divisible(cycle + 21, 40)) sum += (cycle + 1) * x
crt += inRange(x - 1, cycle % 40, x + 1) ? '#' : '.'
cycle++
}
console.log('Part 1:', sum)
console.log(crt.match(/.{40}/g)!.join('\n'))
As you finally start making your way upriver, you realize your pack is much lighter than you remember. Just then, one of the items from your pack goes flying overhead. Monkeys are playing Keep Away with your missing things!
To get your stuff back, you need to be able to predict where the monkeys will throw your items. After some careful observation, you realize the monkeys operate based on how worried you are about each item.
Quest: adventofcode.com/2022/day/11
import { multiply, range } from 'lodash'
import { desc, getInput, getLcm, toNumber, toNumbers } from '../utils'
type CalcWorry = (worry: number) => number
const input = getInput(__dirname)
.split('\n\n')
.map(x => x.split('\n'))
function getMonkeys(calcWorry: CalcWorry) {
const lcm = getLcm(input.map(x => x[3]).map(toNumber))
const monkeys = input.map(line => ({
items: toNumbers(line[1]),
divisible: toNumber(line[3]),
targetTrue: toNumber(line[4]),
targetFalse: toNumber(line[5]),
inspects: 0,
operation(old: number): number {
return eval(line[2].slice(19))
},
throwItems() {
this.inspects += this.items.length
this.items.map(item => {
const worry = calcWorry(this.operation(item)) % lcm
const test = worry % this.divisible === 0
const target = test ? this.targetTrue : this.targetFalse
monkeys[target].items.push(worry)
})
this.items.length = 0
},
}))
return monkeys
}
function business(rounds: number, calcWorry: CalcWorry) {
const monkeys = getMonkeys(calcWorry)
range(rounds).forEach(() => {
monkeys.forEach(monkey => monkey.throwItems())
})
return monkeys
.map(monkey => monkey.inspects)
.sort(desc)
.slice(0, 2)
.reduce(multiply)
}
const part1 = business(20, x => Math.floor(x / 3))
const part2 = business(10_000, x => x)
console.log('Part 1', part1)
console.log('Part 2', part2)
You try contacting the Elves using your handheld device, but the river you're following must be too low to get a decent signal.
Quest: adventofcode.com/2022/day/12
import EasyStar from 'easystarjs'
import { range } from 'lodash'
import { getArray2d, loop2d } from '../utils'
const input = getArray2d(__dirname)
const start = { x: 0, y: 0 }
const end = { x: 0, y: 0 }
const lowestPoints: Point[] = []
const pathMap: number[][] = range(input.length).map(() =>
range(input[0].length).map(() => 0)
)
const map = new EasyStar.js()
map.setGrid(pathMap)
map.setAcceptableTiles([0])
function toHeight(letter?: string) {
return letter?.charCodeAt(0) ?? 99
}
loop2d(input, (y, x) => {
if (input[y][x] === 'S') {
start.x = x
start.y = y
input[y][x] = 'a'
}
if (input[y][x] === 'E') {
end.x = x
end.y = y
input[y][x] = 'z'
}
if (input[y][x] === 'a') {
lowestPoints.push({ x, y })
}
const dirs: EasyStar.Direction[] = []
const current = toHeight(input[y][x])
const up = toHeight(input[y - 1]?.[x])
const down = toHeight(input[y + 1]?.[x])
const left = toHeight(input[y][x - 1])
const right = toHeight(input[y][x + 1])
if (current - up < 2) dirs.push(EasyStar.TOP)
if (current - down < 2) dirs.push(EasyStar.BOTTOM)
if (current - left < 2) dirs.push(EasyStar.LEFT)
if (current - right < 2) dirs.push(EasyStar.RIGHT)
map.setDirectionalCondition(x, y, dirs)
})
function getLength(start: Point, end: Point) {
return new Promise<number>(resolve => {
map.findPath(start.x, start.y, end.x, end.y, steps => {
resolve(steps ? steps.length - 1 : Infinity)
})
})
}
getLength(start, end).then(path => {
console.log('Part 1:', path)
})
Promise.all(lowestPoints.map(p => getLength(p, end))).then(paths =>
console.log('Part 2:', Math.min(...paths))
)
map.calculate()
You climb the hill and again try contacting the Elves. However, you instead receive a signal you weren't expecting: a distress signal.
Your handheld device must still not be working properly; the packets from the distress signal got decoded out of order. You'll need to re-order the list of received packets to decode the message.
Quest: adventofcode.com/2022/day/13
import { isArray } from 'lodash'
import { getInput, getLines } from '../utils'
type Packet = number | Packet[]
enum Comparison {
EQUAL = 0,
LESS_THAN = -1,
GREATER_THAN = 1,
}
const signal = getInput(__dirname)
.split('\n\n')
.map(packets => packets.split('\n').map(eval) as Packet[])
.map(([left, right]) => ({ left, right }))
function compare(left: Packet, right: Packet): Comparison {
if (isArray(left) && isArray(right)) {
const length = Math.min(left.length, right.length)
for (let i = 0; i < length; i++) {
const comparison = compare(left[i], right[i])
if (comparison !== Comparison.EQUAL) {
return comparison
}
}
if (left[length] === undefined && right[length] === undefined) {
return Comparison.EQUAL
}
if (left[length] === undefined) {
return Comparison.LESS_THAN
}
return Comparison.GREATER_THAN
}
if (isArray(left)) {
return compare(left, [right])
}
if (isArray(right)) {
return compare([left], right)
}
if (left < right) {
return Comparison.LESS_THAN
} else if (left === right) {
return Comparison.EQUAL
}
return Comparison.GREATER_THAN
}
let sum = 0
signal.forEach((packets, i) => {
if (compare(packets.left, packets.right) === Comparison.LESS_THAN) {
sum += i + 1
}
})
console.log('Part 1:', sum)
const signal2 = getLines(__dirname)
.filter(x => x)
.map(eval)
signal2.push([[2]], [[6]])
signal2.sort(compare)
const a = signal2.findIndex(x => JSON.stringify(x) == '[[2]]') + 1
const b = signal2.findIndex(x => JSON.stringify(x) == '[[6]]') + 1
console.log('Part 2:', a * b)
The distress signal leads you to a giant waterfall! Actually, hang on - the signal seems like it's coming from the waterfall itself, and that doesn't make any sense. However, you do notice a little path that leads behind the waterfall.
Correction: the distress signal leads you behind a giant waterfall! There seems to be a large cave system here, and the signal definitely leads further inside.
As you begin to make your way deeper underground, you feel the ground rumble for a moment. Sand begins pouring into the cave! If you don't quickly figure out where the sand is going, you could quickly become trapped!
Quest: adventofcode.com/2022/day/14
import { chunk, last } from 'lodash'
import { getLines, toKeys, key, createArray, toNumbers, asc } from '../utils'
enum Unit {
AIR,
ROCK,
SAND,
}
const rocks = getLines(__dirname)
.map(toNumbers)
.map(p => chunk(p, 2).map(toKeys('x', 'y')))
const maxX = Math.max(...rocks.flatMap(p => p.map(key('x'))))
const maxY = Math.max(...rocks.flatMap(p => p.map(key('y'))))
const cave = createArray(maxY + 2, maxX * 2, Unit.AIR)
// Create rocks
rocks.forEach(path => {
for (let i = 1; i < path.length; i++) {
const [from, to] = path.slice(i - 1, i + 1)
const [minY, maxY] = [from.y, to.y].sort(asc)
const [minX, maxX] = [from.x, to.x].sort(asc)
for (let y = minY; y <= maxY; y++) {
for (let x = minX; x <= maxX; x++) {
cave[y][x] = Unit.ROCK
}
}
}
})
function drop(y: number, x: number): Point {
const down = cave[y + 1]?.[x]
const downLeft = cave[y + 1]?.[x - 1]
const downRight = cave[y + 1]?.[x + 1]
if (down === Unit.AIR) {
return drop(y + 1, x)
} else if (downLeft === Unit.AIR) {
return drop(y + 1, x - 1)
} else if (downRight === Unit.AIR) {
return drop(y + 1, x + 1)
}
return { x, y }
}
let p1 = 0
let p2 = 0
while (cave[0][500] !== Unit.SAND) {
const { y, x } = drop(0, 500)
cave[y][x] = Unit.SAND
if (!last(cave)?.includes(Unit.SAND)) {
p1 += 1
}
p2 += 1
}
console.log('Part 1:', p1)
console.log('Part 2:', p2)
You feel the ground rumble again as the distress signal leads you to a large network of subterranean tunnels. You don't have time to search them all, but you don't need to: your pack contains a set of deployable sensors that you imagine were originally built to locate lost Elves.
The sensors aren't very powerful, but that's okay; your handheld device indicates that you're close enough to the source of the distress signal to use them. You pull the emergency sensor system out of your pack, hit the big button on top, and the sensors zoom off down the tunnels.
Quest: adventofcode.com/2022/day/15
import { arrayUnion } from 'interval-operations'
import { getLines, getManhattanDistance, toNumbers } from '../utils'
const P1_Y = 2_000_000
const P2_LIMIT = 4_000_000
const p1Ranges: [number, number][] = []
const p2Cols: Record<number, [number, number][]> = {}
getLines(__dirname).map(line => {
const [sensorX, sensorY, beaconX, beaconY] = toNumbers(line)
const sensor = { x: sensorX, y: sensorY }
const beacon = { x: beaconX, y: beaconY }
const radius = getManhattanDistance(sensor, beacon)
const p1Range: number[] = []
for (let i = -radius; i <= radius; i++) {
const x = sensor.x + i
const y1 = sensor.y + radius - Math.abs(i)
const y2 = sensor.y + Math.abs(i) - radius
if (y1 === P1_Y || y2 === P1_Y) {
p1Range.push(x)
}
if (
(y1 >= 0 || y2 >= 0) &&
(y1 <= P2_LIMIT || y2 <= P2_LIMIT) &&
x <= P2_LIMIT &&
x >= 0
) {
p2Cols[x] ??= []
p2Cols[x].push(y1 < y2 ? [y1, y2] : [y2, y1])
}
}
if (p1Range.length === 1) {
p1Range.push(p1Range[0])
}
if (p1Range.length === 2) {
p1Ranges.push(p1Range as [number, number])
}
})
const p1 = arrayUnion(p1Ranges)[0] as number[]
console.log('Part 1:', p1[1] - p1[0])
for (const x in p2Cols) {
const ranges = arrayUnion(p2Cols[x])
if (ranges.length > 1) {
const y = +ranges[0][1] + 1
console.log('Part 2:', +x * P2_LIMIT + y)
break
}
}
The sensors have led you to the origin of the distress signal: yet another handheld device, just like the one the Elves gave you. However, you don't see any Elves around; instead, the device is surrounded by elephants! They must have gotten lost in these tunnels, and one of the elephants apparently figured out how to turn on the distress signal.
The ground rumbles again, much stronger this time. What kind of cave is this, exactly? You scan the cave with your handheld device; it reports mostly igneous rock, some ash, pockets of pressurized gas, magma... this isn't just a cave, it's a volcano!
You need to get the elephants out of here, quickly. Your device estimates that you have 30 minutes before the volcano erupts, so you don't have time to go back out the way you came in.
Quest: adventofcode.com/2022/day/16
import { getLines, toNumber } from '../utils'
import Graph from 'node-dijkstra'
import combinations from 'combinations'
import memoizee from 'memoizee'
console.time('time')
type Costs = Record<string, Record<string, number>>
type Valve = {
name: string
flow: number
tunnels: string[]
}
const flowRateMap: Record<string, number> = {}
const valves = getLines(__dirname).map(line => {
const [name, ...tunnels] = line.match(/[A-Z]{2}/g)!
const flow = toNumber(line)
flowRateMap[name] = flow
return { name, flow, tunnels }
})
const tunnelsRoute = new Graph()
valves.forEach(valve => {
const edges = valve.tunnels.reduce(
(obj, tunnel) => ({ ...obj, [tunnel]: 1 }),
{}
)
tunnelsRoute.addNode(valve.name, edges)
})
const aaValve = valves.find(valve => valve.name === 'AA')!
const everyFlowValve = valves.filter(valve => valve.flow)
const getPathWeight = memoizee((from: string, to: string) => {
const path = tunnelsRoute.path(from, to) as string[]
return path.length - 1
})
function createCosts(valves: Valve[]) {
valves = [aaValve, ...valves]
const costs: Record<string, Record<string, number>> = {}
for (let i = 0; i < valves.length; i++) {
const from = valves[i]
const restValves = valves.filter(v => v !== from)
for (const to of restValves) {
costs[from.name] ??= {}
costs[from.name][to.name] = getPathWeight(from.name, to.name)
}
}
return costs
}
function best(
costs: Costs,
time: number,
current: string = 'AA',
open: string[] = []
): number {
if (time === 0) return 0
const results = Object.keys(costs[current])
.filter(destination => {
return !open.includes(destination) && time > costs[current][destination]
})
.map(dest => {
const remaining = time - costs[current][dest] - 1
const pressure = remaining * flowRateMap[dest]
return pressure + best(costs, remaining, dest, [dest, ...open])
})
return Math.max(0, ...results)
}
const p1Costs = createCosts(everyFlowValve)
console.log('Part 1:', best(p1Costs, 30))
console.timeLog('time')
const allPerm = combinations(everyFlowValve, 3, everyFlowValve.length / 2)
let max = -Infinity
for (const player of allPerm) {
const elephant = everyFlowValve.filter(valve => !player.includes(valve))
const sum = best(createCosts(player), 26) + best(createCosts(elephant), 26)
if (sum > max) {
max = sum
}
}
console.log('Part 2:', max)
console.timeEnd('time')
Your handheld device has located an alternative exit from the cave for you and the elephants. The ground is rumbling almost continuously now, but the strange valves bought you some time. It's definitely getting warmer in here, though.
The tunnels eventually open into a very tall, narrow chamber. Large, oddly-shaped rocks are falling into the chamber from above, presumably due to all the rumbling. If you can't work out where the rocks will fall next, you might be crushed!
Quest: adventofcode.com/2022/day/17
import { findLastIndex } from 'lodash'
import { findCycles } from '../find-cycles'
import { getInput } from '../utils'
type Shape = DeepReadonly<{ chars: string[]; width: number; height: number }>
type Rock = Point & Readonly<{ shape: Shape }>
type Cave = string[][]
const jetPattern = [...getInput(__dirname).trim()] as Readonly<('<' | '>')[]>
const shapes = [
['####'],
['.#.', '###', '.#.'],
['###', '..#', '..#'],
['#', '#', '#', '#'],
['##', '##'],
] as const
function putInCave(rock: Rock, cave: Cave) {
for (let y = 0; y < rock.shape.height; y++) {
for (let x = 0; x < rock.shape.width; x++) {
const char = rock.shape.chars[y][x]
if (char === '#') {
cave[rock.y + y][rock.x + x] = char
}
}
}
}
function getShape(shapeNr: number): Shape {
const shape = shapes[shapeNr % shapes.length]
return {
chars: shape,
width: shape[0].length,
height: shape.length,
}
}
function canPushRock(rock: Rock, cave: Cave, dir: -1 | 1) {
let canPush = true
loop: for (let y = 0; y < rock.shape.height; y++) {
for (let x = 0; x < rock.shape.width; x++) {
const char = rock.shape.chars[y][x]
const newY = rock.y + y
const newX = rock.x + x + dir
cave[newY] ??= [...'.......']
if (newX < 0 || newX > 6 || (char === '#' && cave[newY][newX] === '#')) {
canPush = false
break loop
}
}
}
return canPush
}
function canRockFall(rock: Rock, cave: Cave) {
let canFall = true
loop: for (let y = 0; y < rock.shape.height; y++) {
for (let x = 0; x < rock.shape.width; x++) {
const char = rock.shape.chars[y][x]
const newY = rock.y + y - 1
const newX = rock.x + x
cave[newY] ??= [...'.......']
if (newY < 0 || (char === '#' && cave[newY][newX] === '#')) {
canFall = false
break loop
}
}
}
return canFall
}
function dropRock(rock: Rock, cave: Cave, jetNr: number): number {
const dir = jetPattern[jetNr] === '<' ? -1 : 1
jetNr %= jetPattern.length
jetNr += 1
if (canPushRock(rock, cave, dir)) {
rock.x += dir
}
if (canRockFall(rock, cave)) {
rock.y -= 1
return dropRock(rock, cave, jetNr)
}
putInCave(rock, cave)
return jetNr
}
function getCaveHeight(cave: Cave) {
return findLastIndex(cave, line => line.includes('#')) + 1
}
function createCave(options: { height?: number; rocks?: number }) {
const cave: Cave = []
let jetNr = 0
let i = 0
while (true) {
const height = getCaveHeight(cave)
const dropPoint = getCaveHeight(cave) + 3
const rock = { x: 2, y: dropPoint, shape: getShape(i) }
jetNr = dropRock(rock, cave, jetNr)
i++
if (options.rocks) {
if (options.rocks === i) {
break
}
}
if (options.height) {
if (height > options.height) {
break
}
}
}
return {
cave,
rocks: i,
height: getCaveHeight(cave),
}
}
console.log('Part 1', createCave({ rocks: 2022 }).height)
const { startIndex: start, length: cycleHeight } = findCycles(
createCave({ rocks: 5000 }).cave.map(x => x.join(''))
)[0]
const allRocks = 1e12
const rocksBeforeCycle = createCave({ height: start }).rocks
const rocksAfterFirstCycle = createCave({ height: start + cycleHeight }).rocks
const rocksToCreateCycle = rocksAfterFirstCycle - rocksBeforeCycle
const repeats = Math.floor((allRocks - rocksBeforeCycle) / rocksToCreateCycle)
const rocksLeft = allRocks - repeats * rocksToCreateCycle
const heightWithoutCycles = createCave({ rocks: rocksLeft }).height
console.log('Part 2', heightWithoutCycles + cycleHeight * repeats)
ou and the elephants finally reach fresh air. You've emerged near the base of a large volcano that seems to be actively erupting! Fortunately, the lava seems to be flowing away from you and toward the ocean.
Bits of lava are still being ejected toward you, so you're sheltering in the cavern exit a little longer. Outside the cave, you can see the lava landing in a pond and hear it loudly hissing as it solidifies.
Depending on the specific compounds in the lava and speed at which it cools, it might be forming obsidian! The cooling rate should be based on the surface area of the lava droplets, so you take a quick scan of a droplet as it flies past you
Quest: adventofcode.com/2022/day/18
import { add, negate } from 'lodash'
import { getLines, toNumbers } from '../utils'
import Graph from 'node-dijkstra'
import memoizee from 'memoizee'
const cubes = getLines(__dirname).map(toNumbers)
const lavaMap: Record<string, boolean> = {}
cubes.forEach(cube => {
lavaMap[cube.toString()] = true
})
const isLava = (point: number[]) => !!lavaMap[point.toString()]
const isAir = negate(isLava)
const countLavaSides = memoizee(([x, y, z]: number[]) => {
let sides = 0
if (isLava([x + 1, y, z])) sides++
if (isLava([x - 1, y, z])) sides++
if (isLava([x, y + 1, z])) sides++
if (isLava([x, y - 1, z])) sides++
if (isLava([x, y, z + 1])) sides++
if (isLava([x, y, z - 1])) sides++
return sides
})
const p1Surface = cubes
.map(countLavaSides)
.map(sides => 6 - sides)
.reduce(add)
console.log('Part 1:', p1Surface)
const airGraph = new Graph()
const volumeSide = Math.max(...cubes.flatMap(x => x)) + 2
for (let z = 0; z < volumeSide; z++) {
for (let y = 0; y < volumeSide; y++) {
for (let x = 0; x < volumeSide; x++) {
const point = [x, y, z]
if (isAir(point)) {
const routes: Record<string, 1> = {}
if (isAir([x + 1, y, z])) routes[[x + 1, y, z].toString()] = 1
if (isAir([x - 1, y, z])) routes[[x - 1, y, z].toString()] = 1
if (isAir([x, y + 1, z])) routes[[x, y + 1, z].toString()] = 1
if (isAir([x, y - 1, z])) routes[[x, y - 1, z].toString()] = 1
if (isAir([x, y, z + 1])) routes[[x, y, z + 1].toString()] = 1
if (isAir([x, y, z - 1])) routes[[x, y, z - 1].toString()] = 1
airGraph.addNode(point.toString(), routes)
}
}
}
}
const freeAirPoint = [volumeSide - 1, volumeSide - 1, volumeSide - 1].toString()
let insideEdges = 0
for (let z = 0; z < volumeSide; z++) {
for (let y = 0; y < volumeSide; y++) {
for (let x = 0; x < volumeSide; x++) {
const point = [x, y, z]
if (isAir(point) && !airGraph.path(point.toString(), freeAirPoint)) {
insideEdges += countLavaSides(point)
}
}
}
}
console.log('Part 2:', p1Surface - insideEdges)
Your scans show that the lava did indeed form obsidian!
The wind has changed direction enough to stop sending lava droplets toward you, so you and the elephants exit the cave. As you do, you notice a collection of geodes around the pond. Perhaps you could use the obsidian to create some geode-cracking robots and break them open?
Quest: adventofcode.com/2022/day/19
import { add, multiply } from 'lodash'
import { desc, getLines, toKeys, toNumbers } from '../utils'
const blueprints = getLines(__dirname)
.map(toNumbers)
.map(
toKeys(
'id',
'oreRobotOreCost',
'clayRobotOreCost',
'obsidianRobotOreCost',
'obsidianRobotClayCost',
'geodeRobotOreCost',
'geodeRobotObsidianCost'
)
)
type Blueprint = typeof blueprints[number]
function createState() {
return {
ore: 0,
oreRobots: 1,
clay: 0,
clayRobots: 0,
obsidian: 0,
obsidianRobots: 0,
geode: 0,
geodeRobots: 0,
waiting: 0,
}
}
type State = Readonly<ReturnType<typeof createState>>
type RealitiesMap = Record<string, boolean>
function tick(
state: State,
print: Blueprint,
minute: number,
realitiesMap: RealitiesMap
): State[] {
if (minute > 20 && state.clayRobots === 0) return []
if (minute > 25 && state.obsidianRobots === 0) return []
if (state.oreRobots > 20) return []
if (state.clayRobots > 20) return []
const possibleStates: State[] = []
const newState: State = {
...state,
ore: state.ore + state.oreRobots,
clay: state.clay + state.clayRobots,
obsidian: state.obsidian + state.obsidianRobots,
geode: state.geode + state.geodeRobots,
waiting: state.waiting + 1,
}
if (
state.ore >= print.geodeRobotOreCost &&
state.obsidian >= print.geodeRobotObsidianCost
) {
const tmp: State = {
...newState,
ore: newState.ore - print.geodeRobotOreCost,
obsidian: newState.obsidian - print.geodeRobotObsidianCost,
geodeRobots: newState.geodeRobots + 1,
waiting: 0,
}
const json = JSON.stringify(tmp)
if (!realitiesMap[json]) {
realitiesMap[json] = true
possibleStates.push(tmp)
}
return possibleStates
}
if (
state.ore >= print.obsidianRobotOreCost &&
state.clay >= print.obsidianRobotClayCost
) {
const tmp: State = {
...newState,
ore: newState.ore - print.obsidianRobotOreCost,
clay: newState.clay - print.obsidianRobotClayCost,
obsidianRobots: newState.obsidianRobots + 1,
waiting: 0,
}
const json = JSON.stringify(tmp)
if (!realitiesMap[json]) {
realitiesMap[json] = true
possibleStates.push(tmp)
}
return possibleStates
}
if (newState.waiting < 6) {
const json = JSON.stringify(newState)
if (!realitiesMap[json]) {
realitiesMap[json] = true
possibleStates.push({ ...newState })
}
} else {
return []
}
if (state.ore >= print.clayRobotOreCost) {
const tmp: State = {
...newState,
ore: newState.ore - print.clayRobotOreCost,
clayRobots: newState.clayRobots + 1,
waiting: 0,
}
const json = JSON.stringify(tmp)
if (!realitiesMap[json]) {
realitiesMap[json] = true
possibleStates.push(tmp)
}
}
if (state.ore >= print.oreRobotOreCost) {
const tmp: State = {
...newState,
ore: newState.ore - print.oreRobotOreCost,
oreRobots: newState.oreRobots + 1,
waiting: 0,
}
const json = JSON.stringify(tmp)
if (!realitiesMap[json]) {
realitiesMap[json] = true
possibleStates.push(tmp)
}
}
return possibleStates
}
function getMaxGeodes(blueprint: Blueprint, time: number) {
let realities: State[] = [createState()]
const realitiesMap: RealitiesMap = {}
for (let minute = 1; minute <= time; minute++) {
realities = realities.flatMap(state =>
tick(state, blueprint, minute, realitiesMap)
)
}
return realities.sort((a, b) => desc(a.geode, b.geode))[0].geode
}
const p1Sum = blueprints
.map(blueprint => blueprint.id * getMaxGeodes(blueprint, 24))
.reduce(add)
const p2Product = blueprints
.slice(0, 3)
.map(blueprint => getMaxGeodes(blueprint, 32))
.reduce(multiply, 1)
console.log('Part 1:', p1Sum)
console.log('Part 2:', p2Product)
It's finally time to meet back up with the Elves. When you try to contact them, however, you get no reply. Perhaps you're out of range?
You know they're headed to the grove where the star fruit grows, so if you can figure out where that is, you should be able to meet back up with them.
Fortunately, your handheld device has a file (your puzzle input) that contains the grove's coordinates! Unfortunately, the file is encrypted - just in case the device were to fall into the wrong hands.
Maybe you can decrypt it?
Quest: adventofcode.com/2022/day/20
import { getLines, toKeys, toNumbers } from '../utils'
const input = getLines(__dirname).map(toNumbers).map(toKeys('value'))
const refsOrder = [...input]
function getSum(mixes: number, multiplier: number) {
const refs = [...input]
for (let i = 0; i < mixes; i++) {
for (let j = 0; j < refs.length; j++) {
const index = refs.indexOf(refsOrder[j])
const ref = refs.splice(index, 1)[0]
let newIndex = (index + ref.value * multiplier) % refs.length
refs.splice(newIndex, 0, ref)
if (newIndex === 0) refs.push(refs.shift()!)
}
}
let sum = 0
let zeroIndex = refs.findIndex(ref => ref.value === 0)
for (let i = 1000; i <= 3000; i += 1000) {
sum += refs[(zeroIndex + i) % refs.length].value * multiplier
}
return sum
}
console.log('Part 1:', getSum(1, 1))
console.log('Part 2:', getSum(10, 811589153))
The monkeys are back! You're worried they're going to try to steal your stuff again, but it seems like they're just holding their ground and making various monkey noises at you.
Eventually, one of the elephants realizes you don't speak monkey and comes over to interpret. As it turns out, they overheard you talking about trying to find the grove; they can show you a shortcut if you answer their riddle.
(Part 2 in /src/21/p2.ts
file)
Quest: adventofcode.com/2022/day/21
// @ts-nocheck
import { getLines } from '../utils'
// prettier-ignore
while (!global.root || console.log(root))
for (let x of getLines(__dirname))
try { eval('global.' + x.replace(...':=')) } catch (_) {}
The monkeys take you on a surprisingly easy trail through the jungle. They're even going in roughly the right direction according to your handheld device's Grove Positioning System.
As you walk, the monkeys explain that the grove is protected by a force field. To pass through the force field, you have to enter a password; doing so involves tracing a specific path on a strangely-shaped board.
At least, you're pretty sure that's what you have to do; the elephants aren't exactly fluent in monkey.
The monkeys give you notes that they took when they last saw the password entered
Quest: adventofcode.com/2022/day/22
import { findIndex, findLastIndex, negate } from 'lodash'
import { getInput, key, match } from '../utils'
type Player = Point & { facing: 0 | 90 | 180 | 270 }
enum Tile {
EMPTY = ' ',
OPEN = '.',
WALL = '#',
}
enum Turn {
R = 'R',
L = 'L',
}
enum Face {
TOP = 'TOP',
BOTTOM = 'BOTTOM',
FRONT = 'FRONT',
LEFT = 'LEFT',
BACK = 'BACK',
RIGHT = 'RIGHT',
NONE = 'NONE',
}
enum Dir {
UP = 0,
RIGHT = 90,
DOWN = 180,
LEFT = 270,
}
const CUBE_WIDTH = 50
const input = getInput(__dirname).split('\n\n')
const map = input[0].split('\n').map(x => [...x]) as Tile[][]
const mapWidth = Math.max(...map.map(key('length')))
const mapHeight = map.length
for (let y = 0; y < mapHeight; y++) {
for (let x = 0; x < mapWidth; x++) {
map[y][x] ??= Tile.EMPTY
}
}
const commands = input[1]
.trim()
.match(/(?:\d+|[A-Z]+)/g)!
.map(x => (/\d/.test(x) ? parseInt(x) : x)) as [number, Turn]
const isEmpty = (tile: Tile) => tile === Tile.EMPTY || tile === undefined
const isNotEmpty = negate(isEmpty)
function changeDir(player: Player, turn: Turn) {
player.facing += turn === Turn.L ? 270 : 90
player.facing %= 360
}
function p1Move(player: Player, distance: number) {
let newY = player.y
let newX = player.x
for (let i = 0; i < distance; i++) {
if (player.facing === 0) newY--
if (player.facing === 90) newX++
if (player.facing === 180) newY++
if (player.facing === 270) newX--
const deltaX = newX - player.x
const deltaY = newY - player.y
if (isEmpty(map[newY]?.[newX])) {
if (deltaX < 0) {
newX = findLastIndex(map[newY], isNotEmpty)
}
if (deltaX > 0) {
newX = findIndex(map[newY], isNotEmpty)
}
if (deltaY > 0) {
newY = findIndex(map, row => isNotEmpty(row[newX]))
}
if (deltaY < 0) {
newY = findLastIndex(map, row => isNotEmpty(row[newX]))
}
}
if (map[newY][newX] === Tile.WALL) {
break
}
player.x = newX
player.y = newY
}
}
const faces = [
[Face.NONE, Face.FRONT, Face.RIGHT],
[Face.NONE, Face.BOTTOM, Face.NONE],
[Face.LEFT, Face.BACK, Face.NONE],
[Face.TOP, Face.NONE, Face.NONE],
]
function getFace(y: number, x: number): Face {
const faceX = Math.floor(x / CUBE_WIDTH)
const faceY = Math.floor(y / CUBE_WIDTH)
return faces[faceY][faceX]
}
function getOnMapPosition(face: Face, y: number, x: number): Point {
let faceY = faces.findIndex(f => f.includes(face))
let faceX = faces[faceY].findIndex(f => f === face)
return { y: y + faceY * CUBE_WIDTH, x: x + faceX * CUBE_WIDTH }
}
function p2Move(player: Player, distance: number) {
let face = getFace(player.y, player.x)
let newFacing = player.facing
let faceX = player.x % CUBE_WIDTH
let faceY = player.y % CUBE_WIDTH
for (let i = 0; i < distance; i++) {
if (newFacing === 0) faceY--
if (newFacing === 90) faceX++
if (newFacing === 180) faceY++
if (newFacing === 270) faceX--
if (faceY < 0) {
match(face, {
[Face.FRONT]() {
face = Face.TOP
faceY = faceX
faceX = 0
newFacing = Dir.RIGHT
},
[Face.BOTTOM]() {
face = Face.FRONT
faceY = CUBE_WIDTH - 1
},
[Face.BACK]() {
face = Face.BOTTOM
faceY = CUBE_WIDTH - 1
},
[Face.LEFT]() {
face = Face.BOTTOM
faceY = faceX
faceX = 0
newFacing = Dir.RIGHT
},
[Face.RIGHT]() {
face = Face.TOP
faceY = CUBE_WIDTH - 1
},
[Face.TOP]() {
face = Face.LEFT
faceY = CUBE_WIDTH - 1
},
[Face.NONE]() {
throw 1
},
})
}
if (faceY === CUBE_WIDTH) {
match(face, {
[Face.FRONT]() {
face = Face.BOTTOM
faceY = 0
},
[Face.BOTTOM]() {
face = Face.BACK
faceY = 0
},
[Face.BACK]() {
face = Face.TOP
faceY = faceX
faceX = CUBE_WIDTH - 1
newFacing = Dir.LEFT
},
[Face.LEFT]() {
face = Face.TOP
faceY = 0
},
[Face.RIGHT]() {
face = Face.BOTTOM
faceY = faceX
faceX = CUBE_WIDTH - 1
newFacing = Dir.LEFT
},
[Face.TOP]() {
face = Face.RIGHT
faceY = 0
},
[Face.NONE]() {
throw 1
},
})
}
if (faceX === CUBE_WIDTH) {
match(face, {
[Face.FRONT]() {
face = Face.RIGHT
faceX = 0
},
[Face.BOTTOM]() {
face = Face.RIGHT
faceX = faceY
faceY = CUBE_WIDTH - 1
newFacing = Dir.UP
},
[Face.BACK]() {
face = Face.RIGHT
faceY = CUBE_WIDTH - faceY - 1
faceX = CUBE_WIDTH - 1
newFacing = Dir.LEFT
},
[Face.LEFT]() {
face = Face.BACK
faceX = 0
},
[Face.RIGHT]() {
face = Face.BACK
faceY = CUBE_WIDTH - faceY - 1
faceX = CUBE_WIDTH - 1
newFacing = Dir.LEFT
},
[Face.TOP]() {
face = Face.BACK
faceX = faceY
faceY = CUBE_WIDTH - 1
newFacing = Dir.UP
},
[Face.NONE]() {
throw 1
},
})
}
if (faceX < 0) {
match(face, {
[Face.FRONT]() {
face = Face.LEFT
faceY = CUBE_WIDTH - faceY - 1
faceX = 0
newFacing = Dir.RIGHT
},
[Face.BOTTOM]() {
face = Face.LEFT
faceX = faceY
faceY = 0
newFacing = Dir.DOWN
},
[Face.BACK]() {
face = Face.LEFT
faceX = CUBE_WIDTH - 1
},
[Face.LEFT]() {
face = Face.FRONT
faceY = CUBE_WIDTH - faceY - 1
faceX = 0
newFacing = Dir.RIGHT
},
[Face.RIGHT]() {
face = Face.FRONT
faceX = CUBE_WIDTH - 1
},
[Face.TOP]() {
face = Face.FRONT
faceX = faceY
faceY = 0
newFacing = Dir.DOWN
},
[Face.NONE]() {
throw 1
},
})
}
const { x, y } = getOnMapPosition(face, faceY, faceX)
if (map[y][x] === Tile.WALL) {
break
}
player.x = x
player.y = y
player.facing = newFacing
}
}
function getPassword(player: Player) {
changeDir(player, Turn.L)
return 1000 * (player.y + 1) + 4 * (player.x + 1) + player.facing / 90
}
function createPlayer(): Player {
return {
y: 0,
x: map[0].findIndex(point => point === Tile.OPEN),
facing: 90,
}
}
const p1Player = createPlayer()
const p2Player = createPlayer()
commands.forEach(command => {
if (typeof command === 'number') {
p1Move(p1Player, command)
p2Move(p2Player, command)
} else {
changeDir(p1Player, command)
changeDir(p2Player, command)
}
})
console.log('Part 1', getPassword(p1Player))
console.log('Part 2', getPassword(p2Player))
You enter a large crater of gray dirt where the grove is supposed to be. All around you, plants you imagine were expected to be full of fruit are instead withered and broken. A large group of Elves has formed in the middle of the grove.
"...but this volcano has been dormant for months. Without ash, the fruit can't grow!"
You look up to see a massive, snow-capped mountain towering above you.
"It's not like there are other active volcanoes here; we've looked everywhere."
"But our scanners show active magma flows; clearly it's going somewhere."
They finally notice you at the edge of the grove, your pack almost overflowing from the random star fruit you've been collecting. Behind you, elephants and monkeys explore the grove, looking concerned. Then, the Elves recognize the ash cloud slowly spreading above your recent detour.
"Why do you--" "How is--" "Did you just--"
Before any of them can form a complete question, another Elf speaks up: "Okay, new plan. We have almost enough fruit already, and ash from the plume should spread here eventually. If we quickly plant new seedlings now, we can still make it to the extraction point. Spread out!"
Quest: adventofcode.com/2022/day/23
import { asc, getLines, match } from '../utils'
type Elf = Point & { propose: Point | null; name: string }
enum Dir {
N = 'N',
S = 'S',
E = 'E',
W = 'W',
}
const elves: Elf[] = []
const dirs = [Dir.E, Dir.N, Dir.S, Dir.W] as const
getLines(__dirname).forEach((line, y) => {
line.split('').forEach((char, x) => {
if (char === '#') elves.push({ x, y, propose: null, name: `${x}|${y}` })
})
})
function dirGeneratorFactory() {
const tmpDirs = [...dirs]
return function () {
tmpDirs.push(tmpDirs.shift()!)
return [...tmpDirs]
}
}
const nextDirs = dirGeneratorFactory()
const isElfOnPosition = ({ x, y }: Point) => {
return elves.some(elf => elf.x === x && elf.y === y)
}
function isAnyElfOnPositions(positions: Point[]) {
return positions.some(isElfOnPosition)
}
function getPopsNumberOnPosition({ x, y }: Point) {
return elves.filter(elf => elf.propose?.x === x && elf.propose.y === y).length
}
for (let i = 0; true; i++) {
const dirs = nextDirs()
// propose
let elvesOnRightPosition = 0
elfLoop: for (const elf of elves) {
elf.propose = null
if (
!isAnyElfOnPositions([
{ x: elf.x, y: elf.y - 1 },
{ x: elf.x, y: elf.y + 1 },
{ x: elf.x - 1, y: elf.y },
{ x: elf.x + 1, y: elf.y },
{ x: elf.x - 1, y: elf.y - 1 },
{ x: elf.x - 1, y: elf.y + 1 },
{ x: elf.x + 1, y: elf.y - 1 },
{ x: elf.x + 1, y: elf.y + 1 },
])
) {
elvesOnRightPosition++
continue
}
for (const dir of dirs) {
match(dir, {
N() {
if (
!isAnyElfOnPositions([
{ x: elf.x, y: elf.y - 1 },
{ x: elf.x - 1, y: elf.y - 1 },
{ x: elf.x + 1, y: elf.y - 1 },
])
) {
elf.propose = { x: elf.x, y: elf.y - 1 }
}
},
S() {
if (
!isAnyElfOnPositions([
{ x: elf.x, y: elf.y + 1 },
{ x: elf.x - 1, y: elf.y + 1 },
{ x: elf.x + 1, y: elf.y + 1 },
])
) {
elf.propose = { x: elf.x, y: elf.y + 1 }
}
},
E() {
if (
!isAnyElfOnPositions([
{ x: elf.x + 1, y: elf.y },
{ x: elf.x + 1, y: elf.y - 1 },
{ x: elf.x + 1, y: elf.y + 1 },
])
) {
elf.propose = { x: elf.x + 1, y: elf.y }
}
},
W() {
if (
!isAnyElfOnPositions([
{ x: elf.x - 1, y: elf.y - 1 },
{ x: elf.x - 1, y: elf.y },
{ x: elf.x - 1, y: elf.y + 1 },
])
) {
elf.propose = { x: elf.x - 1, y: elf.y }
}
},
})
if (elf.propose !== null) {
continue elfLoop
}
}
}
if (elvesOnRightPosition === elves.length) {
console.log('Part 2:', i + 1)
break
}
// move
for (let elf of elves) {
if (elf.propose === null) continue
if (getPopsNumberOnPosition(elf.propose) === 1) {
elf.x = elf.propose.x
elf.y = elf.propose.y
}
}
if (i === 9) {
elves.sort((a, b) => asc(a.x, b.x))
const maxLeft = elves[0].x
const maxRight = elves[elves.length - 1].x
elves.sort((a, b) => asc(a.y, b.y))
const maxTop = elves[0].y
const maxBottom = elves[elves.length - 1].y
const area = (maxRight - maxLeft + 1) * (maxBottom - maxTop + 1)
console.log('Part 1:', area - elves.length)
}
}
With everything replanted for next year (and with elephants and monkeys to tend the grove), you and the Elves leave for the extraction point.
Partway up the mountain that shields the grove is a flat, open area that serves as the extraction point. It's a bit of a climb, but nothing the expedition can't handle.
At least, that would normally be true; now that the mountain is covered in snow, things have become more difficult than the Elves are used to.
As the expedition reaches a valley that must be traversed to reach the extraction site, you find that strong, turbulent winds are pushing small blizzards of snow and sharp ice around the valley. It's a good thing everyone packed warm clothes! To make it across safely, you'll need to find a way to avoid them.
Quest: adventofcode.com/2022/day/24
import { cloneDeep } from 'lodash'
import { getLines, match } from '../utils'
type Blizzard = Point3d & { dir: string }
const input: Blizzard[] = []
getLines(__dirname).forEach((line, y) => {
line.split('').forEach((dir, x) => {
if ('><v^'.includes(dir)) input.push({ x, y, z: 0, dir })
})
})
const str = <T extends Point>(b: T) => `${b.x},${b.y}`
const map = getLines(__dirname)
const layers: Blizzard[][] = [cloneDeep(input)]
const mapHeight = map.length
const mapWidth = map[0].length
const start = { x: 1, y: 0 }
const end = { x: mapWidth - 2, y: mapHeight - 1 }
function getNextBlizzardsPositions() {
return input.map(blizzard => {
blizzard.z++
match(blizzard.dir, {
v() {
blizzard.y += 1
if (blizzard.y === mapHeight - 1) blizzard.y = 1
},
'^'() {
blizzard.y -= 1
if (blizzard.y === 0) blizzard.y = mapHeight - 2
},
'<'() {
blizzard.x -= 1
if (blizzard.x === 0) blizzard.x = mapWidth - 2
},
'>'() {
blizzard.x += 1
if (blizzard.x === mapWidth - 1) blizzard.x = 1
},
})
return blizzard
})
}
function isBlizzard(x: number, y: number, z: number) {
return layers[z].some(b => b.x === x && b.y === y)
}
function findWay(start: Point, end: Point, time: number) {
let player: Record<string, boolean> = { [str(start)]: true }
let minutes = 0
for (let z = time; true; z++) {
while (layers.length <= z) layers.push(getNextBlizzardsPositions())
const possibleMoves: Record<string, boolean> = {}
Object.keys(player).forEach(coords => {
const [x, y] = coords.split(',').map(Number)
if (!isBlizzard(x, y, z)) {
possibleMoves[str({ x, y })] = true
}
if (
x < mapWidth - 2 &&
y !== 0 &&
y !== mapHeight - 1 &&
!isBlizzard(x + 1, y, z)
) {
possibleMoves[str({ x: x + 1, y })] = true
}
if (x > 1 && y !== 0 && y !== mapHeight - 1 && !isBlizzard(x - 1, y, z)) {
possibleMoves[str({ x: x - 1, y })] = true
}
if (
(y < mapHeight - 2 || (y === mapHeight - 2 && x === end.x)) &&
!isBlizzard(x, y + 1, z)
) {
possibleMoves[str({ x, y: y + 1 })] = true
}
if ((y > 1 || (y === 1 && x === end.x)) && !isBlizzard(x, y - 1, z)) {
possibleMoves[str({ x, y: y - 1 })] = true
}
})
if (Object.keys(possibleMoves).some(coords => coords === str(end))) {
break
}
minutes++
player = possibleMoves
}
return minutes
}
const firstTripTime = findWay(start, end, 0)
const tripBackTime = findWay(end, start, firstTripTime)
const backToGoalTime = findWay(start, end, firstTripTime + tripBackTime)
console.log('Part 1:', firstTripTime)
console.log('Part 2:', firstTripTime + tripBackTime + backToGoalTime)
As the expedition finally reaches the extraction point, several large hot air balloons drift down to meet you. Crews quickly start unloading the equipment the balloons brought: many hot air balloon kits, some fuel tanks, and a fuel heating machine.
The fuel heating machine is a new addition to the process. When this mountain was a volcano, the ambient temperature was more reasonable; now, it's so cold that the fuel won't work at all without being warmed up first.
Quest: adventofcode.com/2022/day/25
import { add } from 'lodash'
import { getLines } from '../utils'
const input = getLines(__dirname)
function toDec(snafu: string) {
return [...snafu]
.reverse()
.map(char => char.replace('-', '-1').replace('=', '-2'))
.map((char, i) => parseInt(char) * 5 ** i)
.reduce(add)
}
function toSnafu(nr: number) {
const snafu = [...nr.toString(5)].map(Number).reverse()
snafu.forEach((x, i) => {
if (x > 2) {
snafu[i] -= 5
snafu[i + 1] ??= 0
snafu[i + 1] += 1
}
})
return snafu
.reverse()
.map(x => '=-012'[x + 2])
.join('')
}
const fuel = input.map(toDec).reduce(add)
console.log('Part 1:', toSnafu(fuel))
Requirements:
node v18.12.1
Install dependencies:
npm ci
Run solution:
npx ts-node src/<nr>/index.ts
Generate README.md
:
npm run readme
Midjourney and DALL-E
Puzzles, Code, & Design: Eric Wastl
Beta Testing:
- Tim Giannetti
- Ben Lucek
- JP Burke
- Aneurysm9
- Andrew Skalski
Community Managers: Danielle Lucek and Aneurysm9