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