Skip to content

Argeento/advent-of-code-2022

Repository files navigation

Advent of Code 2022

My solutions for Advent of Code 2022 in TypeScript

Story

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.

Star fruits

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 journey

Day 1: Calorie Counting

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

Solution

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))

Day 2: Rock Paper Scissors

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

Solution

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))

Day 3: Rucksack Reorganization

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

Solution

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)

Day 4: Camp Cleanup

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

Solution

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)

Day 5: Supply Stacks

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

Solution

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(''))

Day 6: Tuning Trouble

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

Solution

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))

Day 7: No Space Left On Device

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

Solution

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)

Day 8: Treetop Tree House

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

Solution

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)

Day 9: Rope Bridge

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

Solution

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))

Day 10: Cathode-Ray Tube

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

Solution

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'))

Day 11: Monkey in the Middle

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

Solution

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)

Day 12: Hill Climbing Algorithm

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

Solution

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()

Day 13: Distress Signal

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

Solution

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)

Day 14: Regolith Reservoir

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

Solution

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)

Day 15: Beacon Exclusion Zone

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

Solution

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
  }
}

Day 16: Proboscidea Volcanium

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

Solution

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')

Day 17: Pyroclastic Flow

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

Solution

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)

Day 18: Boiling Boulders

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

Solution

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)

Day 19: Not Enough Minerals

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

Solution

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)

Day 20: Grove Positioning System

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

Solution

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))

Day 21: Monkey Math

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

Solution

// @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 (_) {}

Day 22: Monkey Map

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

Solution

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))

Day 23: Unstable Diffusion

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

Solution

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)
  }
}

Day 24: Blizzard Basin

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

Solution

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)

Day 25: Full of Hot Air

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

Solution

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))

How to run?

Requirements:

node v18.12.1

Install dependencies:

npm ci

Run solution:

npx ts-node src/<nr>/index.ts

Generate README.md:

npm run readme

Story illustrations

Midjourney and DALL-E

Thanks to the AoC team

Puzzles, Code, & Design: Eric Wastl

Beta Testing:

Community Managers: Danielle Lucek and Aneurysm9