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.
 

162 lines
3.6 KiB

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