You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

171 lines
3.9 KiB

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