Day 17
This commit is contained in:
parent
d0e924934e
commit
bc6c2a9426
290
17/solution.mjs
Normal file
290
17/solution.mjs
Normal file
@ -0,0 +1,290 @@
|
||||
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`
|
||||
);
|
Loading…
Reference in New Issue
Block a user