import { readFileSync } from 'node:fs'; const input = readFileSync('input', 'utf-8'); const [mapData, pathData] = input.split('\n\n'); const F = { R: 0, D: 1, L: 2, U: 3, invert: (facing) => { switch (facing) { case F.R: return F.L; case F.D: return F.U; case F.L: return F.R; case F.U: return F.D; } }, rotateR: (facing) => { switch (facing) { case F.R: return F.D; case F.D: return F.L; case F.L: return F.U; case F.U: return F.R; } }, rotateL: (facing) => { switch (facing) { case F.R: return F.U; case F.U: return F.L; case F.L: return F.D; case F.D: return F.R; } } }; class Map { constructor(mapData) { this.data = mapData .split('\n') .filter(line => !!line.trim()); this.height = this.data.length; this.width = this.data.reduce((max, row) => Math.max(max, row.length), 0); this.portals = {}; } get startingPosition() { const pos = [1, 1]; while (this.get(...pos) !== '.') { pos[0]++; } return pos; } cubeMode() { if (this.width !== 150 || this.height !== 200) { throw new Error('Cube mode only works with full size map'); } this.portals = {}; const e1 = this._edge([51, 1], [100, 1]); const e2 = this._edge([101, 1], [150, 1]); const e3 = this._edge([51, 1], [51, 50]); const e4 = this._edge([150, 1], [150, 50]); const e5 = this._edge([101, 50], [150, 50]); const e6 = this._edge([51, 51], [51, 100]); const e7 = this._edge([100, 51], [100, 100]); const e8 = this._edge([1, 101], [50, 101]); const e9 = this._edge([1, 101], [1, 150]); const e10 = this._edge([100, 101], [100, 150]); const e11 = this._edge([51, 150], [100, 150]); const e12 = this._edge([1, 151], [1, 200]); const e13 = this._edge([50, 151], [50, 200]); const e14 = this._edge([1, 200], [50, 200]); this._portalEdge(e1, F.U, e12, F.R); this._portalEdge(e2, F.U, e14, F.U); this._portalEdge([...e3].reverse(), F.L, e9, F.R); this._portalEdge([...e4].reverse(), F.R, e10, F.L); this._portalEdge(e5, F.D, e7, F.L); this._portalEdge(e6, F.L, e8, F.D); this._portalEdge(e11, F.D, e13, F.L); } flatMode() { this.portals = {}; for (let row = 1; row <= this.height; row++) { let col = 1; while (this.get(col, row) === ' ') col++; const start = [col, row]; while (this.get(col + 1, row) !== ' ') col++; const end = [col, row]; this.portals[[start, F.L]] = [end, F.L]; this.portals[[end, F.R]] = [start, F.R]; } for (let col = 1; col <= this.width; col++) { let row = 1; while (this.get(col, row) === ' ') row++; const start = [col, row]; while (this.get(col, row + 1) !== ' ') row++; const end = [col, row]; this.portals[[start, F.U]] = [end, F.U]; this.portals[[end, F.D]] = [start, F.D]; } } get(col, row) { const x = col - 1; const y = row - 1; if (y < 0 || y >= this.data.length || x < 0 || x >= this.data[y].length) { return ' '; } return this.data[y][x]; } nextPos(pos, facing) { if (this.portals[[pos, facing]]) return this.portals[[pos, facing]]; return [[ pos[0] + (facing === F.R ? 1 : (facing === F.L ? -1 : 0)), pos[1] + (facing === F.D ? 1 : (facing === F.U ? -1 : 0)), ], facing]; } _portalEdge(fromEdge, fromFacing, toEdge, toFacing) { if (fromEdge.length !== toEdge.length) { throw new Error('fromEdge must be the same length as toEdge'); } for (let i = 0; i < fromEdge.length; i++) { const fromPos = fromEdge[i]; const toPos = toEdge[i]; this.portals[[fromPos, fromFacing]] = [toPos, toFacing]; this.portals[[toPos, F.invert(toFacing)]] = [fromPos, F.invert(fromFacing)]; } } _edge(start, end) { if (start[0] !== end[0] && start[1] !== end[1]) { throw new Error("Edges can't be diagonal"); } const colDiff = end[0] - start[0]; const colIncr = colDiff / (Math.abs(colDiff) || 1); const rowDiff = end[1] - start[1]; const rowIncr = rowDiff / (Math.abs(rowDiff) || 1); const edge = []; let col = start[0]; let row = start[1]; edge.push([col, row]); do { col += colIncr; do { row += rowIncr; edge.push([col, row]); } while (row !== end[1]) } while (col !== end[0]) return edge; } } class Walker { constructor(map) { this.map = map; this.facing = F.R; this.pos = this.map.startingPosition; } walk(paces) { for (let p = 0; p < paces; p++) { const [nextPos, nextFacing] = this.map.nextPos(this.pos, this.facing); let nextTile = this.map.get(...nextPos); if (nextTile === ' ') { throw new Error('Escaped Map!'); } if (nextTile === '#') { break; } this.pos = nextPos; this.facing = nextFacing; } } get password() { return ( this.pos[1] * 1000 + this.pos[0] * 4 + this.facing ); } rotateCounterclockwise() { this.facing = F.rotateL(this.facing); } rotateClockwise() { this.facing = F.rotateR(this.facing); } } const instructions = pathData .split('\n') .filter(line => !!line.trim()) .join('') .split(/(L|R)/) .filter(instruction => !!instruction.trim()) .map(instruction => { const num = parseInt(instruction); return isNaN(num) ? instruction : num }); function runInstructions(walker) { for (const instruction of instructions) { if (typeof instruction === 'number') { walker.walk(instruction); } else if (instruction === 'L') { walker.rotateCounterclockwise(); } else if (instruction === 'R') { walker.rotateClockwise(); } else { throw new Error(`Invalid instruction: ${instruction}`); } } } const map = new Map(mapData); map.flatMode(); const flatWalker = new Walker(map); runInstructions(flatWalker); console.log('The password for the flat map is:', flatWalker.password); map.cubeMode(); const cubeWalker = new Walker(map) runInstructions(cubeWalker); console.log('The password for the cube map is:', cubeWalker.password);