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