day18.js
· 7.2 KiB · JavaScript
Raw
const input = require("fs").readFileSync(0).toString();
const mazeSize = 71;
const bytesReadPartOne = 1024;
/**
* @param {string} input
* @param {number} size
* @param {number} simulateFirstXBytes
* @returns {string[][]}
*/
const simulateMapState = (input, size, simulateFirstXBytes) => {
const grid = [...new Array(size).keys()].map(() => [...new Array(size).keys()].map(() => '.'));
/** @type {Coords[]} */
const inputPredictions = input.split("\n").map((c) => ({ x: parseInt(c.split(',')[0]), y: parseInt(c.split(',')[1]) }));
for (let i = 0; i < simulateFirstXBytes; i += 1) {
grid[inputPredictions[i].y][inputPredictions[i].x] = '#';
}
return grid;
}
/**
* @typedef {object} Coords
* @property {number} x
* @property {number} y
*/
class Escapee {
static DIRECTIONS = {
'<': { x: -1, y: 0, },
'^': { x: 0, y: -1, },
'>': { x: 1, y: 0, },
'v': { x: 0, y: 1, },
};
/** @type {string[][]} */
map = [];
/** @type {Coords} */
pos = {
x: 0,
y: 0,
};
/** @type {Coords} */
end = {
x: 0,
y: 0,
}
/** @type {keyof Escapee.DIRECTIONS} */
direction;
/** @type {string[]} */
path = [];
/** @type {number} */
score;
/**
* @param {string[][]} maze
* @param {Coords} position
* @param {Coords} end
* @param {keyof Escapee.DIRECTIONS} direction
* @param {string[]} path
*/
constructor(maze, x, y, endX, endY, direction = '>', path = [], score = 0) {
this.map = maze;
this.pos.x = x;
this.pos.y = y;
this.end.x = endX;
this.end.y = endY;
this.direction = direction;
this.path = path;
this.score = score;
}
/** @returns {Escapee} */
clone() {
return new Escapee(
this.map,
this.pos.x,
this.pos.y,
this.end.x,
this.end.y,
this.direction,
[...this.path],
this.score,
);
}
/**
* @param {boolean} ccw Counter-clockwise
* @returns {Escapee}
*/
rotate(ccw = false) {
const keys = Object.keys(Escapee.DIRECTIONS);
let index = keys.indexOf(this.direction);
index += !ccw ? 1 : -1;
if (index >= keys.length) {
index = 0;
}
if (index < 0) {
index = keys.length - 1;
}
this.direction = keys[index];
return this;
}
/**
* Move in the current facing direction.
* @returns {boolean|Escapee} True if reached end, false if impossible move or existing move, itself otherwise.
*/
move() {
let newX = this.pos.x + Escapee.DIRECTIONS[this.direction].x;
let newY = this.pos.y + Escapee.DIRECTIONS[this.direction].y;
if (newX < 0 || newX >= this.map.length || newY < 0 || newY >= this.map.length || this.map[newY][newX] === '#') {
return false;
}
if (this.path.some(r => r.startsWith(`${newX},${newY},`))) {
return false;
}
const key = `${newX},${newY},${this.direction}`;
this.path.push(key);
this.score += 1;
if (newX === this.end.x && newY === this.end.y) {
this.path.pop();
return true;
}
this.pos.x = newX;
this.pos.y = newY;
return this;
}
visualize() {
const maze = this.map.map(y => [...y]);
for (const path of this.path) {
const [x, y, dir] = path.split(",");
maze[y][x] = dir;
}
return maze.map(y => y.join("")).join("\n");
}
}
const map = simulateMapState(
input,
mazeSize,
bytesReadPartOne,
);
const ourEscapee = new Escapee(map, 0, 0, map.length-1, map.length-1);
const logger = new class {
successes = 0;
failures = 0;
queued = 1;
last = 0;
ops = 0;
bytes = bytesReadPartOne;
lOps = 0;
/** @param {Escapee} input */
write(input) {
const curTs = new Date().getTime();
if (curTs - this.last > 1000) {
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
const parts = [];
parts.push(`queued=${this.queued}`);
parts.push(`successes=${this.successes}`);
parts.push(`failures=${this.failures}`);
parts.push(`current_escapee_steps=${input.score}`);
parts.push(`bytes=${this.bytes}`);
parts.push(`ops/s=${this.ops - this.lOps}`);
this.lOps = this.ops;
process.stdout.write(parts.join(' '));
this.last = curTs;
}
}
}
const queue = new class {
/** @type {{element: Escapee, priority: number}[]} */
elements = [];
/**
* @param {Escapee} element
* @param {number} priority
*/
enqueue(element, priority) {
this.elements.push({ element, priority });
this.elements.sort((a, b) => a.priority - b.priority);
}
/** @returns {Escapee} */
dequeue() {
return this.elements.shift().element;
}
/** @returns {boolean} */
isEmpty() {
return this.elements.length === 0;
}
}
/**
* @param {boolean} stopOnFirst
* @returns {Escapee[]}
*/
const solveMaze = (stopOnFirst = false) => {
/** @type {Escapee[]} */
const successful = [];
queue.enqueue(ourEscapee, 0);
/** @type {Map<`${number},${number}.${keyof Escapee.DIRECTIONS}`, number>} */
const minScores = new Map();
while (!queue.isEmpty()) {
const Escapee = queue.dequeue();
logger.queued = queue.elements.length;
logger.ops += 1;
logger.write(Escapee);
const stateKey = `${Escapee.pos.x},${Escapee.pos.y},${Escapee.direction}`;
if (minScores.has(stateKey) && minScores.get(stateKey) <= Escapee.score) {
continue;
}
minScores.set(stateKey, Escapee.score);
const newEscapees = [
Escapee.clone(),
Escapee.clone().rotate(),
Escapee.clone().rotate(true),
];
for (const splitEscapee of newEscapees) {
const result = splitEscapee.move();
if (result === true) {
logger.successes += 1;
successful.push(splitEscapee);
if (stopOnFirst) {
return successful;
}
continue;
}
if (result === false) {
logger.failures += 1;
continue;
}
queue.enqueue(splitEscapee, splitEscapee.score);
}
}
return successful;
}
const successfulEscapees = solveMaze();
const answerOne = Math.min(...successfulEscapees.map(r => r.score));
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
console.log(
`Part One: ${answerOne}`
);
for (let i = bytesReadPartOne; i <= input.split("\n").length; i += 1) {
logger.bytes = i;
const mapAtTime = simulateMapState(
input,
mazeSize,
i,
);
ourEscapee.map = mapAtTime;
queue.enqueue(ourEscapee, 0);
const result = solveMaze(true);
if (result.length === 0) {
process.stdout.clearLine(0);
process.stdout.cursorTo(0);
console.log(`Part Two: ${input.split("\n")[i-1]}`);
break;
}
}
1 | const input = require("fs").readFileSync(0).toString(); |
2 | const mazeSize = 71; |
3 | const bytesReadPartOne = 1024; |
4 | |
5 | /** |
6 | * @param {string} input |
7 | * @param {number} size |
8 | * @param {number} simulateFirstXBytes |
9 | * @returns {string[][]} |
10 | */ |
11 | const simulateMapState = (input, size, simulateFirstXBytes) => { |
12 | const grid = [...new Array(size).keys()].map(() => [...new Array(size).keys()].map(() => '.')); |
13 | /** @type {Coords[]} */ |
14 | const inputPredictions = input.split("\n").map((c) => ({ x: parseInt(c.split(',')[0]), y: parseInt(c.split(',')[1]) })); |
15 | for (let i = 0; i < simulateFirstXBytes; i += 1) { |
16 | grid[inputPredictions[i].y][inputPredictions[i].x] = '#'; |
17 | } |
18 | return grid; |
19 | } |
20 | |
21 | /** |
22 | * @typedef {object} Coords |
23 | * @property {number} x |
24 | * @property {number} y |
25 | */ |
26 | |
27 | class Escapee { |
28 | static DIRECTIONS = { |
29 | '<': { x: -1, y: 0, }, |
30 | '^': { x: 0, y: -1, }, |
31 | '>': { x: 1, y: 0, }, |
32 | 'v': { x: 0, y: 1, }, |
33 | }; |
34 | |
35 | /** @type {string[][]} */ |
36 | map = []; |
37 | |
38 | /** @type {Coords} */ |
39 | pos = { |
40 | x: 0, |
41 | y: 0, |
42 | }; |
43 | |
44 | /** @type {Coords} */ |
45 | end = { |
46 | x: 0, |
47 | y: 0, |
48 | } |
49 | |
50 | /** @type {keyof Escapee.DIRECTIONS} */ |
51 | direction; |
52 | |
53 | /** @type {string[]} */ |
54 | path = []; |
55 | |
56 | /** @type {number} */ |
57 | score; |
58 | |
59 | /** |
60 | * @param {string[][]} maze |
61 | * @param {Coords} position |
62 | * @param {Coords} end |
63 | * @param {keyof Escapee.DIRECTIONS} direction |
64 | * @param {string[]} path |
65 | */ |
66 | constructor(maze, x, y, endX, endY, direction = '>', path = [], score = 0) { |
67 | this.map = maze; |
68 | this.pos.x = x; |
69 | this.pos.y = y; |
70 | this.end.x = endX; |
71 | this.end.y = endY; |
72 | this.direction = direction; |
73 | this.path = path; |
74 | this.score = score; |
75 | } |
76 | |
77 | /** @returns {Escapee} */ |
78 | clone() { |
79 | return new Escapee( |
80 | this.map, |
81 | this.pos.x, |
82 | this.pos.y, |
83 | this.end.x, |
84 | this.end.y, |
85 | this.direction, |
86 | [...this.path], |
87 | this.score, |
88 | ); |
89 | } |
90 | |
91 | /** |
92 | * @param {boolean} ccw Counter-clockwise |
93 | * @returns {Escapee} |
94 | */ |
95 | rotate(ccw = false) { |
96 | const keys = Object.keys(Escapee.DIRECTIONS); |
97 | let index = keys.indexOf(this.direction); |
98 | index += !ccw ? 1 : -1; |
99 | if (index >= keys.length) { |
100 | index = 0; |
101 | } |
102 | if (index < 0) { |
103 | index = keys.length - 1; |
104 | } |
105 | this.direction = keys[index]; |
106 | return this; |
107 | } |
108 | |
109 | /** |
110 | * Move in the current facing direction. |
111 | * @returns {boolean|Escapee} True if reached end, false if impossible move or existing move, itself otherwise. |
112 | */ |
113 | move() { |
114 | let newX = this.pos.x + Escapee.DIRECTIONS[this.direction].x; |
115 | let newY = this.pos.y + Escapee.DIRECTIONS[this.direction].y; |
116 | |
117 | if (newX < 0 || newX >= this.map.length || newY < 0 || newY >= this.map.length || this.map[newY][newX] === '#') { |
118 | return false; |
119 | } |
120 | |
121 | if (this.path.some(r => r.startsWith(`${newX},${newY},`))) { |
122 | return false; |
123 | } |
124 | |
125 | const key = `${newX},${newY},${this.direction}`; |
126 | |
127 | this.path.push(key); |
128 | |
129 | this.score += 1; |
130 | if (newX === this.end.x && newY === this.end.y) { |
131 | this.path.pop(); |
132 | return true; |
133 | } |
134 | this.pos.x = newX; |
135 | this.pos.y = newY; |
136 | return this; |
137 | } |
138 | |
139 | visualize() { |
140 | const maze = this.map.map(y => [...y]); |
141 | for (const path of this.path) { |
142 | const [x, y, dir] = path.split(","); |
143 | maze[y][x] = dir; |
144 | } |
145 | return maze.map(y => y.join("")).join("\n"); |
146 | } |
147 | } |
148 | |
149 | const map = simulateMapState( |
150 | input, |
151 | mazeSize, |
152 | bytesReadPartOne, |
153 | ); |
154 | |
155 | const ourEscapee = new Escapee(map, 0, 0, map.length-1, map.length-1); |
156 | |
157 | const logger = new class { |
158 | successes = 0; |
159 | failures = 0; |
160 | queued = 1; |
161 | last = 0; |
162 | ops = 0; |
163 | bytes = bytesReadPartOne; |
164 | lOps = 0; |
165 | /** @param {Escapee} input */ |
166 | write(input) { |
167 | const curTs = new Date().getTime(); |
168 | if (curTs - this.last > 1000) { |
169 | process.stdout.clearLine(0); |
170 | process.stdout.cursorTo(0); |
171 | const parts = []; |
172 | parts.push(`queued=${this.queued}`); |
173 | parts.push(`successes=${this.successes}`); |
174 | parts.push(`failures=${this.failures}`); |
175 | parts.push(`current_escapee_steps=${input.score}`); |
176 | parts.push(`bytes=${this.bytes}`); |
177 | parts.push(`ops/s=${this.ops - this.lOps}`); |
178 | this.lOps = this.ops; |
179 | process.stdout.write(parts.join(' ')); |
180 | this.last = curTs; |
181 | } |
182 | } |
183 | } |
184 | |
185 | const queue = new class { |
186 | /** @type {{element: Escapee, priority: number}[]} */ |
187 | elements = []; |
188 | |
189 | /** |
190 | * @param {Escapee} element |
191 | * @param {number} priority |
192 | */ |
193 | enqueue(element, priority) { |
194 | this.elements.push({ element, priority }); |
195 | this.elements.sort((a, b) => a.priority - b.priority); |
196 | } |
197 | |
198 | /** @returns {Escapee} */ |
199 | dequeue() { |
200 | return this.elements.shift().element; |
201 | } |
202 | |
203 | /** @returns {boolean} */ |
204 | isEmpty() { |
205 | return this.elements.length === 0; |
206 | } |
207 | } |
208 | |
209 | /** |
210 | * @param {boolean} stopOnFirst |
211 | * @returns {Escapee[]} |
212 | */ |
213 | const solveMaze = (stopOnFirst = false) => { |
214 | /** @type {Escapee[]} */ |
215 | const successful = []; |
216 | |
217 | queue.enqueue(ourEscapee, 0); |
218 | /** @type {Map<`${number},${number}.${keyof Escapee.DIRECTIONS}`, number>} */ |
219 | const minScores = new Map(); |
220 | |
221 | while (!queue.isEmpty()) { |
222 | const Escapee = queue.dequeue(); |
223 | logger.queued = queue.elements.length; |
224 | logger.ops += 1; |
225 | logger.write(Escapee); |
226 | |
227 | const stateKey = `${Escapee.pos.x},${Escapee.pos.y},${Escapee.direction}`; |
228 | if (minScores.has(stateKey) && minScores.get(stateKey) <= Escapee.score) { |
229 | continue; |
230 | } |
231 | minScores.set(stateKey, Escapee.score); |
232 | |
233 | const newEscapees = [ |
234 | Escapee.clone(), |
235 | Escapee.clone().rotate(), |
236 | Escapee.clone().rotate(true), |
237 | ]; |
238 | |
239 | for (const splitEscapee of newEscapees) { |
240 | const result = splitEscapee.move(); |
241 | if (result === true) { |
242 | logger.successes += 1; |
243 | successful.push(splitEscapee); |
244 | if (stopOnFirst) { |
245 | return successful; |
246 | } |
247 | continue; |
248 | } |
249 | |
250 | if (result === false) { |
251 | logger.failures += 1; |
252 | continue; |
253 | } |
254 | |
255 | queue.enqueue(splitEscapee, splitEscapee.score); |
256 | } |
257 | } |
258 | |
259 | return successful; |
260 | } |
261 | |
262 | const successfulEscapees = solveMaze(); |
263 | const answerOne = Math.min(...successfulEscapees.map(r => r.score)); |
264 | |
265 | process.stdout.clearLine(0); |
266 | process.stdout.cursorTo(0); |
267 | console.log( |
268 | `Part One: ${answerOne}` |
269 | ); |
270 | |
271 | for (let i = bytesReadPartOne; i <= input.split("\n").length; i += 1) { |
272 | logger.bytes = i; |
273 | const mapAtTime = simulateMapState( |
274 | input, |
275 | mazeSize, |
276 | i, |
277 | ); |
278 | ourEscapee.map = mapAtTime; |
279 | queue.enqueue(ourEscapee, 0); |
280 | const result = solveMaze(true); |
281 | if (result.length === 0) { |
282 | process.stdout.clearLine(0); |
283 | process.stdout.cursorTo(0); |
284 | console.log(`Part Two: ${input.split("\n")[i-1]}`); |
285 | break; |
286 | } |
287 | } |