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