diff --git a/2024/21/2024-21.run.xml b/2024/21/2024-21.run.xml
new file mode 100644
index 0000000..3adaa76
--- /dev/null
+++ b/2024/21/2024-21.run.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/2024/21/index.test.ts b/2024/21/index.test.ts
new file mode 100644
index 0000000..5086329
--- /dev/null
+++ b/2024/21/index.test.ts
@@ -0,0 +1,13 @@
+import { describe, expect, it } from 'bun:test';
+import { solveFirst } from './index.ts';
+
+describe('2024-21', () => {
+ it('first', () => {
+ const testInput = `029A
+980A
+179A
+456A
+379A`;
+ expect(solveFirst(testInput)).toBe(126384);
+ });
+});
diff --git a/2024/21/index.ts b/2024/21/index.ts
new file mode 100644
index 0000000..1a712ea
--- /dev/null
+++ b/2024/21/index.ts
@@ -0,0 +1,120 @@
+import { sumBy } from '../../utils/array.ts';
+import {
+ type Coordinate,
+ type Coordinates,
+ findCoordsOfDigit
+} from '../../utils/coordinates.ts';
+
+const DIRS = [
+ // up
+ [0, -1],
+ // right
+ [1, 0],
+ // down
+ [0, 1],
+ // left
+ [-1, 0]
+] as Coordinates;
+
+const DIRS_LIT = ['^', '>', 'v', '<'];
+const numPad = ['789', '456', '123', '#0A'];
+const dirPad = ['#^A', ''];
+
+const findShortestPaths = (pad: string[], startVal: string, endVal: string) => {
+ const start = findCoordsOfDigit(pad, startVal === '^' ? /\^/g : startVal)[0];
+ const stack: {
+ p: Coordinate;
+ path: string[];
+ cost: number;
+ dirId?: number;
+ }[] = [{ p: start, path: [], cost: 0 }];
+ const seen: Map = new Map();
+ const paths: string[][] = [];
+ let minCost = Infinity;
+
+ while (stack.length) {
+ const {
+ cost,
+ dirId,
+ p: [x, y],
+ path
+ } = stack.shift()!;
+ if (dirId !== undefined) {
+ path.push(DIRS_LIT[dirId]);
+ }
+ if (pad[y]?.[x] === endVal) {
+ if (cost < minCost) {
+ paths.length = 0;
+ minCost = cost;
+ }
+ if (cost === minCost) paths.push(path);
+ continue;
+ }
+
+ const k = `${x}_${y}`;
+ if ((seen.get(k) || Infinity) < cost) {
+ continue;
+ }
+ seen.set(k, cost);
+ if (cost > minCost) {
+ continue;
+ }
+
+ DIRS.forEach(([dx, dy], dirId) => {
+ const [px, py] = [x + dx, y + dy];
+ if (pad[py]?.[px] === '#') {
+ return;
+ }
+ stack.push({
+ path: [...path],
+ p: [px, py],
+ dirId,
+ cost: cost + 1
+ });
+ });
+ }
+
+ return paths.map((p) => p.join('') + 'A');
+};
+
+const execute = (
+ pad: string[],
+ code: string,
+ depth: number,
+ memo: Map = new Map()
+) => {
+ const k = `${code}_${depth}`;
+ const saved = memo.get(k);
+ if (saved !== undefined) return saved;
+
+ let curPos = 'A';
+ let length = 0;
+
+ for (const nextPos of code.split('')) {
+ const paths = findShortestPaths(pad, curPos, nextPos);
+ if (depth === 0) {
+ length += paths[0].length;
+ } else {
+ length += Math.min(
+ ...paths.map((path) => execute(dirPad, path, depth - 1, memo))
+ );
+ }
+ curPos = nextPos;
+ }
+
+ memo.set(k, length);
+ return length;
+};
+export function solveFirst(input: string): number {
+ return sumBy(
+ input.split('\n'),
+ (code) => parseInt(code) * execute(numPad, code, 2)
+ );
+}
+
+export function solveSecond(input: string): number {
+ return sumBy(
+ input.split('\n'),
+ (code) => parseInt(code) * execute(numPad, code, 25)
+ );
+}
diff --git a/2024/21/run.ts b/2024/21/run.ts
new file mode 100644
index 0000000..88783cd
--- /dev/null
+++ b/2024/21/run.ts
@@ -0,0 +1,12 @@
+import { join } from 'path';
+import { solveFirst, solveSecond } from './index.ts';
+
+const input = await Bun.file(join(__dirname, 'input.txt')).text();
+
+const firstAnswer = solveFirst(input);
+
+console.log(firstAnswer);
+
+const secondAnswer = solveSecond(input);
+
+console.log(secondAnswer);
diff --git a/utils/multi-map.ts b/utils/multi-map.ts
index c356d3c..5d85e59 100644
--- a/utils/multi-map.ts
+++ b/utils/multi-map.ts
@@ -24,5 +24,6 @@ export class MultiMap extends Map {
} else {
this.set(firstKey, value);
}
+ return value;
}
}