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