257 lines
6.1 KiB
JavaScript
257 lines
6.1 KiB
JavaScript
|
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);
|