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