Advent-of-Code-2022/17/solution.mjs

291 lines
6.4 KiB
JavaScript
Raw Normal View History

2022-12-20 15:22:09 -08:00
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`
);