import { readFileSync } from 'node:fs'; const input = readFileSync('input', 'utf-8'); function inclusiveRange(from, to) { const difference = to - from; const distance = Math.abs(difference); const add = difference / (distance || 1); return new Array(distance + 1).fill().map((_, i) => from + i * add) } const rockCoords = input .split('\n') .filter(Boolean) .flatMap(path => path .split(' -> ') .map(coordinates => coordinates .split(',') .map(coordinate => parseInt(coordinate)) ) // Separate into pairs of coordinates that represent a line .flatMap((coord, i, coords) => i === 0 ? [] : [[coords[i - 1], coord]]) // Fill in gaps between coordinates, will include duplicates .flatMap(([from, to]) => [ ...inclusiveRange(from[0], to[0]).map(x => [x, from[1]]), ...inclusiveRange(from[1], to[1]).map(y => [from[0], y]) ]) ); class InfinitePage { constructor() { this.ox = null; this.oy = null; this.lines = []; } get(x, y) { return this.lines[y - (this.oy ?? y)]?.[x - (this.ox ?? x)] || ' '; } set(x, y, v) { const c = v?.length ? v[0] : ' '; if (this.ox === null) this.ox = x; if (this.oy === null) this.oy = y; const height = this.lines.length; const width = this.lines[0]?.length ?? 0; const expandLeft = Math.max(0, this.ox - x); const expandUp = Math.max(0, this.oy - y); const expandRight = Math.max(0, (x - this.ox + 1) - width); const expandDown = Math.max(0, (y - this.oy + 1) - height); this.expand(expandUp, expandRight, expandDown, expandLeft); const ax = x - this.ox; const ay = y - this.oy; const line = this.lines[ay]; this.lines[ay] = line.slice(0, ax) + c + line.slice(ax + 1); } expand(up, right, down, left) { const width = this.lines[0]?.length ?? 0; if (up + right + down + left > 0) { this.lines = [ ...new Array(up).fill(' '.repeat(width)), ...this.lines, ...new Array(down).fill(' '.repeat(width)) ].map(line => ' '.repeat(left) + line + ' '.repeat(right)); } this.ox -= left; this.oy -= up; } print() { console.log(this.lines.join('\n')); } getBounds() { return { minX: this.ox, maxX: this.ox + (this.lines[0]?.length ?? 1) - 1, minY: this.oy, maxY: this.oy + (this.lines.length || 1) - 1 }; } } function getCave() { const cave = new InfinitePage(); for (const coord of rockCoords) { cave.set(...coord, '#'); } return cave; } // Sand simulation function nextSandPos(x, y, canMoveTo) { const fallPriority = [ {x: 0, y: 1}, {x: -1, y: 1}, {x: 1, y: 1} ]; for (const fall of fallPriority) { const nx = x + fall.x; const ny = y + fall.y; if (canMoveTo(nx, ny)) { return [nx, ny]; } } } function addUnitOfSand(cave, canMoveTo) { let x = 500; let y = 0; while (true) { let nextPos = nextSandPos(x, y, canMoveTo); if (!nextPos) { cave.set(x, y, 'o'); break; } ([x, y] = nextPos); } }; class EndSimulationError extends Error {} function simulateSand(cave, canMoveTo) { let sandCount = 0; while (true) { try { addUnitOfSand(cave, canMoveTo); sandCount++; } catch (err) { if (!(err instanceof EndSimulationError)) { throw err; } break; } } return sandCount; } const cave1 = getCave(); const bounds = cave1.getBounds(); const count1 = simulateSand( cave1, (x, y) => { if (x < bounds.minX || x > bounds.maxX || y < 0 || y > bounds.maxY) { throw new EndSimulationError(); } return cave1.get(x, y) === ' '; } ); console.log(`First simulation ended after ${count1} units of sand`); const cave2 = getCave(); const count2 = simulateSand( cave2, (x, y) => { if (cave2.get(500, 0) === 'o') { throw new EndSimulationError(); } return y < bounds.maxY + 2 && cave2.get(x, y) === ' '; } ) console.log(`Second simulation ended after ${count2} units of sand`);