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.
290 lines
6.4 KiB
290 lines
6.4 KiB
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` |
|
);
|
|
|