163 lines
3.6 KiB
JavaScript
163 lines
3.6 KiB
JavaScript
import { readFileSync } from 'node:fs';
|
|
|
|
const input = readFileSync('input', 'utf-8');
|
|
|
|
// Direction
|
|
const D = {
|
|
R: 0,
|
|
D: 1,
|
|
L: 2,
|
|
U: 3
|
|
};
|
|
|
|
class Blizzard {
|
|
constructor(width, height, x, y, direction) {
|
|
this.width = width;
|
|
this.height = height;
|
|
this.x = x;
|
|
this.y = y;
|
|
this.direction = direction;
|
|
}
|
|
|
|
posAtTime(t) {
|
|
switch (this.direction) {
|
|
case D.R: return [(this.x + t) % this.width, this.y];
|
|
case D.D: return [this.x, (this.y + t) % this.height];
|
|
case D.L: return [
|
|
this.width - 1 - (this.width - 1 - this.x + t) % this.width,
|
|
this.y
|
|
];
|
|
case D.U: return [
|
|
this.x,
|
|
this.height - 1 - (this.height - 1 - this.y + t) % this.height,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
const initialState = input
|
|
.split('\n')
|
|
.filter(line => !!line.trim() && !/#.*#.*#/.test(line))
|
|
.map(line => line.replace(/^#(.*)#$/, '$1'));
|
|
|
|
const height = initialState.length;
|
|
const width = initialState[0].length;
|
|
|
|
const blizzards = initialState
|
|
.flatMap((line, y) => line
|
|
.split('')
|
|
.flatMap((c, x) => {
|
|
let direction;
|
|
switch (c) {
|
|
case '>': direction = D.R; break;
|
|
case 'v': direction = D.D; break;
|
|
case '<': direction = D.L; break;
|
|
case '^': direction = D.U; break;
|
|
case '.': return [];
|
|
default: throw new Error(`Invalid character: ${c}`);
|
|
}
|
|
return [new Blizzard(width, height, x, y, direction)];
|
|
})
|
|
);
|
|
|
|
function lcm(a, b) {
|
|
const larger = Math.max(a, b);
|
|
const smaller = Math.min(a, b);
|
|
|
|
for (let m = 1; ; m++) {
|
|
const v = larger * m;
|
|
if (v % smaller === 0) {
|
|
return v;
|
|
}
|
|
}
|
|
}
|
|
|
|
class Frame {
|
|
constructor(blizzards, width, height, t) {
|
|
this.width = width;
|
|
this.height = height;
|
|
this.t = t;
|
|
this.data = new Array(height).fill().map(_ => '.'.repeat(width));
|
|
for (const blizzard of blizzards) {
|
|
const [x, y] = blizzard.posAtTime(t);
|
|
this.data[y] = (
|
|
this.data[y].slice(0, x) +
|
|
'#' +
|
|
this.data[y].slice(x + 1)
|
|
);
|
|
}
|
|
}
|
|
|
|
getPos(x, y) {
|
|
// Start
|
|
if (x === 0 && y === -1) {
|
|
return '.';
|
|
}
|
|
if (x === width - 1 && y === this.height) {
|
|
return '.';
|
|
}
|
|
return this.data[y]?.[x] ?? '#';
|
|
}
|
|
|
|
movesFromPos(x, y) {
|
|
return [
|
|
[x, y],
|
|
[x + 1, y],
|
|
[x, y + 1],
|
|
[x - 1, y],
|
|
[x, y - 1]
|
|
].filter(pos =>
|
|
this.getPos(...pos) !== '#'
|
|
);
|
|
}
|
|
|
|
print() {
|
|
console.log(this.data.join('\n'));
|
|
}
|
|
}
|
|
|
|
const nFrames = lcm(width, height);
|
|
const frames = new Array(nFrames)
|
|
.fill()
|
|
.map((_, t) => new Frame(blizzards, width, height, t));
|
|
|
|
function fastestRoute(start, end, timeOffset = 0) {
|
|
let positions = [start];
|
|
for (let t = timeOffset + 1; ; t++) {
|
|
const frame = frames[t % nFrames];
|
|
|
|
const newPositions = [];
|
|
|
|
for (const [x, y] of positions) {
|
|
const moves = frame.movesFromPos(x, y);
|
|
if (moves.find(move => move[0] === end[0] && move[1] === end[1])) {
|
|
return t;
|
|
}
|
|
newPositions.push(...frame.movesFromPos(x, y));
|
|
}
|
|
|
|
// Remove duplicates and assign to positions
|
|
positions = newPositions.filter((pos1, i, arr) =>
|
|
arr.findLastIndex(
|
|
pos2 => pos1[0] === pos2[0] && pos1[1] === pos2[1]
|
|
) === i
|
|
);
|
|
}
|
|
}
|
|
|
|
const start = [0, -1];
|
|
const end = [width - 1, height];
|
|
|
|
const fastestTime = fastestRoute(start, end);
|
|
console.log('The fastest time to navigate the blizzards is:', fastestTime);
|
|
|
|
const fastestTimeWithSnacks = fastestRoute(
|
|
start,
|
|
end,
|
|
fastestRoute(end, start, fastestTime)
|
|
);
|
|
console.log(
|
|
'The fastest time with a return trip for snacks is:',
|
|
fastestTimeWithSnacks
|
|
);
|