Ben Ashton
1 year ago
2 changed files with 291 additions and 0 deletions
File diff suppressed because one or more lines are too long
@ -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