diff --git a/2024/20/2024-20.run.xml b/2024/20/2024-20.run.xml new file mode 100644 index 0000000..1a5596a --- /dev/null +++ b/2024/20/2024-20.run.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/2024/20/index.test.ts b/2024/20/index.test.ts new file mode 100644 index 0000000..feb69c4 --- /dev/null +++ b/2024/20/index.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'bun:test'; +import { solveFirst, solveSecond } from './index.ts'; + +describe('2024-20', () => { + const testInput = `############### +#...#...#.....# +#.#.#.#.#.###.# +#S#...#.#.#...# +#######.#.#.### +#######.#.#...# +#######.#.###.# +###..E#...#...# +###.#######.### +#...###...#...# +#.#####.#.###.# +#.#...#.#.#...# +#.#.#.#.#.#.### +#...#...#...### +###############`; + it('first', () => { + expect(solveFirst(testInput, 1)).toBe(14 + 14 + 2 + 4 + 2 + 3 + 5); + }); + it('second', () => { + expect(solveSecond(testInput, 50)).toBe( + 32 + 31 + 29 + 39 + 25 + 23 + 20 + 19 + 12 + 14 + 12 + 22 + 4 + 3 + ); + }); +}); diff --git a/2024/20/index.ts b/2024/20/index.ts new file mode 100644 index 0000000..6a48361 --- /dev/null +++ b/2024/20/index.ts @@ -0,0 +1,120 @@ +import { Presets, SingleBar } from 'cli-progress'; +import { replaceChar } from '../../utils/array.ts'; +import { + type Coordinate, + type Coordinates, + findCoordsOfDigit, + getAdjacent, + inBounds, + isCoordinateEqual +} from '../../utils/coordinates.ts'; + +export function solveFirst(input: string, minSave = 100): number { + return solve(input, 2, minSave); +} +export function solveSecond(input: string, minSave = 100): number { + return solve(input, 20, minSave); +} + +function findPath( + rows: string[], + start: Coordinate, + end: Coordinate +): Coordinates { + let [x, y] = start; + const path: Coordinates = []; + while (x !== end[0] || y !== end[1]) { + const last = path.slice(-1)[0] || [-1, -1]; + path.push([x, y]); + + const { top, right, down, left } = getAdjacent(rows, x, y); + + if (top === '.' && last[1] !== y - 1) { + y--; + } else if (right === '.' && last[0] !== x + 1) { + x++; + } else if (down === '.' && last[1] !== y + 1) { + y++; + } else if (left === '.' && last[0] !== x - 1) { + x--; + } + } + return path; +} + +function getCheatDirections(maxDistance: number) { + const cheatDirections: Coordinates = []; + for (let x = -maxDistance; x <= maxDistance; x++) { + for (let y = -maxDistance; y <= maxDistance; y++) { + const d = Math.abs(x) + Math.abs(y); + if (d > 0 && d <= maxDistance) { + cheatDirections.push([x, y]); + } + } + } + return cheatDirections; +} + +function solve(input: string, maxDistance: number, minSave: number): number { + const rows = input.split('\n'); + + const start = findCoordsOfDigit(rows, 'S')[0]; + const end = findCoordsOfDigit(rows, 'E')[0]; + + replaceChar(rows, start[0], start[1], '.'); + replaceChar(rows, end[0], end[1], '.'); + + const path = findPath(rows, start, end); + const totalLength = path.length; + + const cheatDirections = getCheatDirections(maxDistance); + const isInBounds = inBounds(rows[0].length, rows.length); + const bar = + process.env.NODE_ENV === 'test' + ? null + : new SingleBar({}, Presets.shades_classic); + bar?.start(totalLength, 0); + + const cheats = path.flatMap((cell, i) => { + const [x, y] = cell; + const downgrade = path.slice(0, i + 3); + + const potentialSpots = cheatDirections + .map(([dx, dy]) => [x + dx, y + dy] as Coordinate) + .filter( + (c) => + isInBounds(c) && + rows[c[1]][c[0]] === '.' && + !downgrade.some((d) => isCoordinateEqual(c, d)) + ); + bar?.increment(); + + return potentialSpots + .map((newStart) => { + const end = path.findIndex((p) => isCoordinateEqual(p, newStart)); + return { + newStart, + cell, + length: + (end < 0 ? totalLength : end) - + (i + + (Math.abs(cell[0] - newStart[0]) + + Math.abs(cell[1] - newStart[1]))) + }; + }) + .filter((d) => d.length >= minSave); + }); + + bar?.start(cheats.length, 0); + const existing = new Set(); + for (let cheat of cheats) { + bar?.increment(); + const key = `${cheat.cell.join(',')}-${cheat.newStart.join(',')}`; + if (existing.has(key)) { + continue; + } + existing.add(key); + } + bar?.stop(); + return existing.size; +} diff --git a/2024/20/run.ts b/2024/20/run.ts new file mode 100644 index 0000000..88783cd --- /dev/null +++ b/2024/20/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/bun.lockb b/bun.lockb index 24b152f..171d588 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 4c76cca..f053467 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,10 @@ }, "devDependencies": { "@types/bun": "latest", + "@types/cli-progress": "^3.11.6", "@types/javascript-astar": "^0.0.35", "@types/prompts": "^2.4.9", + "cli-progress": "^3.12.0", "p-limit": "^6.1.0", "prettier": "^3.4.0", "prettier-plugin-organize-imports": "^4.1.0",