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