291 lines
6.4 KiB
JavaScript
291 lines
6.4 KiB
JavaScript
|
import { readFileSync } from 'node:fs';
|
||
|
|
||
|
const input = readFileSync('input', 'utf-8');
|
||
|
|
||
|
|
||
|
const shapePatterns = `
|
||
|
#### # # # ##
|
||
|
### # # ##
|
||
|
# ### #
|
||
|
#
|
||
|
`;
|
||
|
|
||
|
function isTouching(coordA, coordB) {
|
||
|
return (
|
||
|
(coordA[0] === coordB[0] && Math.abs(coordA[1] - coordB[1]) === 1) ||
|
||
|
(coordA[1] === coordB[1] && Math.abs(coordA[0] - coordB[0]) === 1)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function isEqual(coordA, coordB) {
|
||
|
return coordA[0] === coordB[0] && coordA[1] === coordB[1];
|
||
|
}
|
||
|
|
||
|
function getAllConnected(coords, coordA) {
|
||
|
const connected = coords.filter(coordB => isTouching(coordA, coordB));
|
||
|
if (!connected.length) {
|
||
|
return [coordA];
|
||
|
}
|
||
|
const notConnected = coords.filter(coordC =>
|
||
|
!isEqual(coordC, coordA) &&
|
||
|
!connected.find(coordD => isEqual(coordC, coordD))
|
||
|
);
|
||
|
return [
|
||
|
coordA,
|
||
|
...connected
|
||
|
.flatMap(coordE => getAllConnected(notConnected, coordE))
|
||
|
.filter((coordF, i, connectedCoords) => connectedCoords.findLastIndex(
|
||
|
coordG => isEqual(coordF, coordG)
|
||
|
) === i)
|
||
|
];
|
||
|
}
|
||
|
|
||
|
class Shape {
|
||
|
constructor(coords, id) {
|
||
|
this.id = id;
|
||
|
|
||
|
if (!coords.length) {
|
||
|
throw new Error('Empty shape coords');
|
||
|
}
|
||
|
|
||
|
// Normalize coords
|
||
|
const minX = coords.reduce((min, [x, _]) => Math.min(min, x), Infinity);
|
||
|
const minY = coords.reduce((min, [_, y]) => Math.min(min, y), Infinity);
|
||
|
|
||
|
coords = coords.map(([x, y]) => [x - minX, y - minY]);
|
||
|
this.coords = coords;
|
||
|
|
||
|
this.width = 1 + coords.reduce((max, [x, _]) => Math.max(max, x), 0);
|
||
|
this.height = 1 + coords.reduce((max, [_, y]) => Math.max(max, y), 0);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getShapes(patterns) {
|
||
|
let shapeCoords = patterns
|
||
|
.split('\n')
|
||
|
.flatMap((line, y) => line
|
||
|
.split('')
|
||
|
.flatMap((c, x) => c === '#' ? [[x, y]] : [])
|
||
|
);
|
||
|
|
||
|
const shapes = [];
|
||
|
while (shapeCoords.length) {
|
||
|
const shape = getAllConnected(shapeCoords, shapeCoords[0]);
|
||
|
shapeCoords = shapeCoords.filter(coordA => !shape.find(coordB =>
|
||
|
isEqual(coordA, coordB)
|
||
|
));
|
||
|
shapes.push(shape);
|
||
|
}
|
||
|
|
||
|
return shapes.map((coords, i) => new Shape(coords, i));
|
||
|
}
|
||
|
|
||
|
|
||
|
class Chamber {
|
||
|
constructor(width) {
|
||
|
this.width = width;
|
||
|
this.rows = [];
|
||
|
this.placements = [];
|
||
|
}
|
||
|
|
||
|
get height() {
|
||
|
return this.rows.length;
|
||
|
}
|
||
|
|
||
|
get rockCount() {
|
||
|
return this.placements.length;
|
||
|
}
|
||
|
|
||
|
// Test if a shape will collide with anything at the given coordinates
|
||
|
collides(shape, x, y) {
|
||
|
for (const coord of shape.coords) {
|
||
|
const gX = x + coord[0];
|
||
|
const gY = y - coord[1];
|
||
|
|
||
|
if (gX < 0 || gX > this.width - 1 || gY < 0) {
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
if (gY > this.rows.length - 1) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (this.rows[gY][gX] !== ' ') {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Add a shape at the given coordinates
|
||
|
add(shape, x, y) {
|
||
|
const nRowsAdded = Math.max(0, y + 1 - this.rows.length);
|
||
|
if (nRowsAdded > 0) {
|
||
|
this.rows.push(...new Array(nRowsAdded).fill(' '.repeat(this.width)));
|
||
|
}
|
||
|
|
||
|
const lastY = this.placements.length ?
|
||
|
this.placements[this.placements.length - 1].y :
|
||
|
0;
|
||
|
|
||
|
this.placements.push({
|
||
|
shapeId: shape.id,
|
||
|
x,
|
||
|
y,
|
||
|
relativeY: y - lastY
|
||
|
});
|
||
|
|
||
|
for (const coord of shape.coords) {
|
||
|
const gX = x + coord[0];
|
||
|
const gY = y - coord[1];
|
||
|
this.rows[gY] = (
|
||
|
this.rows[gY].slice(0, gX) +
|
||
|
'#' +
|
||
|
this.rows[gY].slice(gX + 1)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
print() {
|
||
|
for (let i = this.rows.length - 1; i >= 0; i--) {
|
||
|
console.log(this.rows[i]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const shapes = getShapes(shapePatterns);
|
||
|
|
||
|
function* jetGenerator() {
|
||
|
while (true) {
|
||
|
for (const jet of input.split('').filter(c => /[<>]/.test(c))) {
|
||
|
yield jet === '<' ? -1 : 1;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function* rockGenerator() {
|
||
|
while (true) {
|
||
|
for (const shape of shapes) {
|
||
|
yield shape;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
const chamber = new Chamber(7);
|
||
|
|
||
|
const jets = jetGenerator();
|
||
|
const rocks = rockGenerator();
|
||
|
|
||
|
|
||
|
function processRock() {
|
||
|
const rock = rocks.next().value;
|
||
|
|
||
|
let x = 2;
|
||
|
let y = chamber.height + 2 + rock.height;
|
||
|
|
||
|
while (true) {
|
||
|
const jet = jets.next().value;
|
||
|
if (!chamber.collides(rock, x + jet, y)) {
|
||
|
x += jet;
|
||
|
}
|
||
|
|
||
|
if (!chamber.collides(rock, x, y - 1)) {
|
||
|
y -= 1;
|
||
|
} else {
|
||
|
chamber.add(rock, x, y);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
while (chamber.rockCount < 2022) {
|
||
|
processRock();
|
||
|
}
|
||
|
|
||
|
console.log(
|
||
|
`The tower will be: ${chamber.height} units tall after 2022 rocks`
|
||
|
);
|
||
|
|
||
|
|
||
|
function findRepetition(source, isEqual) {
|
||
|
let halfSize = Math.floor(source.length / 2);
|
||
|
|
||
|
while (halfSize > 0) {
|
||
|
let skip = false;
|
||
|
for (let i = 0; i < halfSize; i++) {
|
||
|
const first = source[source.length - (2 * halfSize) + i];
|
||
|
const second = source[source.length - halfSize + i];
|
||
|
if (!isEqual(first, second)) {
|
||
|
skip = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
if (!skip) {
|
||
|
return halfSize;
|
||
|
}
|
||
|
halfSize--;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add rocks until a period of repetition can be identified
|
||
|
function findPeriodOfRepetition() {
|
||
|
while (true) {
|
||
|
const periodOfRepetition = findRepetition(chamber.placements, (p1, p2) =>
|
||
|
p1.shapeId === p2.shapeId &&
|
||
|
p1.x === p2.x &&
|
||
|
p1.relativeY === p2.relativeY
|
||
|
);
|
||
|
if (periodOfRepetition !== undefined) {
|
||
|
return periodOfRepetition;
|
||
|
}
|
||
|
// Add 1000 rocks
|
||
|
for (let i = 0; i < 1000; i++) {
|
||
|
processRock();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const periodOfRepetition = findPeriodOfRepetition();
|
||
|
let changeInHeight;
|
||
|
|
||
|
// Confirm period of repetition
|
||
|
for (let round = 0; round < 5; round++) {
|
||
|
const initialHeight = chamber.height;
|
||
|
|
||
|
// Add period of repetition rocks
|
||
|
for (let i = 0; i < periodOfRepetition; i++) {
|
||
|
processRock();
|
||
|
}
|
||
|
|
||
|
const change = chamber.height - initialHeight;
|
||
|
if (changeInHeight === undefined) {
|
||
|
changeInHeight = change;
|
||
|
} else {
|
||
|
if (changeInHeight !== change) {
|
||
|
throw new Error(
|
||
|
`Unable to confirm period of repetition, expected change in height ` +
|
||
|
`of: ${changeInHeight}, got ${change}`
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const rockTarget = 1000000000000;
|
||
|
|
||
|
const rocksRemaining = rockTarget - chamber.rockCount;
|
||
|
const multiple = Math.floor(rocksRemaining / periodOfRepetition);
|
||
|
const remainder = rocksRemaining % periodOfRepetition;
|
||
|
|
||
|
// Add remaining rocks
|
||
|
for (let i = 0; i < remainder; i++) {
|
||
|
processRock();
|
||
|
}
|
||
|
|
||
|
const targetRockHeight = chamber.height + multiple * changeInHeight;
|
||
|
|
||
|
console.log(
|
||
|
`The tower will be: ${targetRockHeight} units tall after ${rockTarget} rocks`
|
||
|
);
|