├── 1 └── index.ts ├── 2 ├── example.txt └── index.ts ├── 3 ├── example.txt └── index.ts ├── 4 ├── example.txt └── index.ts ├── 5 ├── example.txt └── index.ts ├── 6 ├── example.txt └── index.ts ├── 7 ├── example.txt └── index.ts ├── 8 ├── example.txt └── index.ts ├── 9 ├── example.txt └── index.ts ├── 10 ├── example.txt └── index.ts ├── 11 ├── example.txt ├── initial-solution.ts └── index.ts ├── 12 ├── example.txt └── index.ts ├── 13 ├── example.txt └── index.ts ├── 14 ├── example.txt └── index.ts ├── 16 ├── example.txt └── index.ts ├── 20 ├── example.txt └── index.ts ├── 25 ├── example.txt └── index.ts ├── template ├── example.txt └── index.ts ├── index.ts ├── bun.lockb ├── README.md ├── package.json ├── tsconfig.json ├── fetcher.ts └── .gitignore /11/example.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 2 3 | 3 -------------------------------------------------------------------------------- /9/example.txt: -------------------------------------------------------------------------------- 1 | 2333133121414131402 -------------------------------------------------------------------------------- /template/example.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 2 3 | 3 -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | console.log("Hello via Bun!"); -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/t3dotgg/aoc-2024/HEAD/bun.lockb -------------------------------------------------------------------------------- /3/example.txt: -------------------------------------------------------------------------------- 1 | xmul(2,4)&mul[3,7]!^don't()_mul(5,5)+mul(32,64](mul(11,8)undo()?mul(8,5)) -------------------------------------------------------------------------------- /2/example.txt: -------------------------------------------------------------------------------- 1 | 7 6 4 2 1 2 | 1 2 7 8 9 3 | 9 7 6 2 1 4 | 1 3 2 4 5 5 | 8 6 4 4 1 6 | 1 3 6 7 9 -------------------------------------------------------------------------------- /10/example.txt: -------------------------------------------------------------------------------- 1 | 89010123 2 | 78121874 3 | 87430965 4 | 96549874 5 | 45678903 6 | 32019012 7 | 01329801 8 | 10456732 -------------------------------------------------------------------------------- /4/example.txt: -------------------------------------------------------------------------------- 1 | MMMSXXMASM 2 | MSAMXMSMSA 3 | AMXSXMAAMM 4 | MSAMASMSMX 5 | XMASAMXAMM 6 | XXAMMXXAMA 7 | SMSMSASXSS 8 | SAXAMASAAA 9 | MAMMMXMMMM 10 | MXMXAXMASX -------------------------------------------------------------------------------- /6/example.txt: -------------------------------------------------------------------------------- 1 | ....#..... 2 | .........# 3 | .......... 4 | ..#....... 5 | .......#.. 6 | .......... 7 | .#..^..... 8 | ........#. 9 | #......... 10 | ......#... -------------------------------------------------------------------------------- /7/example.txt: -------------------------------------------------------------------------------- 1 | 190: 10 19 2 | 3267: 81 40 27 3 | 83: 17 5 4 | 156: 15 6 5 | 7290: 6 8 6 15 6 | 161011: 16 10 13 7 | 192: 17 8 14 8 | 21037: 9 7 18 13 9 | 292: 11 6 16 20 -------------------------------------------------------------------------------- /12/example.txt: -------------------------------------------------------------------------------- 1 | RRRRIICCFF 2 | RRRRIICCCF 3 | VVRRRCCFFF 4 | VVRCCCJFFF 5 | VVVVCJJCFE 6 | VVIVCCJJEE 7 | VVIIICJJEE 8 | MIIIIIJJEE 9 | MIIISIJEEE 10 | MMMISSJEEE -------------------------------------------------------------------------------- /8/example.txt: -------------------------------------------------------------------------------- 1 | ............ 2 | ........0... 3 | .....0...... 4 | .......0.... 5 | ....0....... 6 | ......A..... 7 | ............ 8 | ............ 9 | ........A... 10 | .........A.. 11 | ............ 12 | ............ -------------------------------------------------------------------------------- /14/example.txt: -------------------------------------------------------------------------------- 1 | p=0,4 v=3,-3 2 | p=6,3 v=-1,-3 3 | p=10,3 v=-1,2 4 | p=2,0 v=2,-1 5 | p=0,0 v=1,3 6 | p=3,0 v=-2,-2 7 | p=7,6 v=-1,-3 8 | p=3,0 v=-1,-2 9 | p=9,3 v=2,3 10 | p=7,3 v=-1,2 11 | p=2,4 v=2,-3 12 | p=9,5 v=-3,-3 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aoc-2024 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run index.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v1.1.27. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aoc-2024", 3 | "module": "index.ts", 4 | "type": "module", 5 | "devDependencies": { 6 | "@types/bun": "latest" 7 | }, 8 | "scripts": { 9 | "fetch": "bun run fetcher.ts" 10 | }, 11 | "peerDependencies": { 12 | "typescript": "^5.0.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /16/example.txt: -------------------------------------------------------------------------------- 1 | ############### 2 | #.......#....E# 3 | #.#.###.#.###.# 4 | #.....#.#...#.# 5 | #.###.#####.#.# 6 | #.#.#.......#.# 7 | #.#.#####.###.# 8 | #...........#.# 9 | ###.#.#####.#.# 10 | #...#.....#.#.# 11 | #.#.#.###.#.#.# 12 | #.....#...#.#.# 13 | #.###.#.#.#.#.# 14 | #S..#.....#...# 15 | ############### -------------------------------------------------------------------------------- /20/example.txt: -------------------------------------------------------------------------------- 1 | ############### 2 | #...#...#.....# 3 | #.#.#.#.#.###.# 4 | #S#...#.#.#...# 5 | #######.#.#.### 6 | #######.#.#...# 7 | #######.#.###.# 8 | ###..E#...#...# 9 | ###.#######.### 10 | #...###...#...# 11 | #.#####.#.###.# 12 | #.#...#.#.#...# 13 | #.#.#.#.#.#.### 14 | #...#...#...### 15 | ############### -------------------------------------------------------------------------------- /13/example.txt: -------------------------------------------------------------------------------- 1 | Button A: X+94, Y+34 2 | Button B: X+22, Y+67 3 | Prize: X=8400, Y=5400 4 | 5 | Button A: X+26, Y+66 6 | Button B: X+67, Y+21 7 | Prize: X=12748, Y=12176 8 | 9 | Button A: X+17, Y+86 10 | Button B: X+84, Y+37 11 | Prize: X=7870, Y=6450 12 | 13 | Button A: X+69, Y+23 14 | Button B: X+27, Y+71 15 | Prize: X=18641, Y=10279 -------------------------------------------------------------------------------- /5/example.txt: -------------------------------------------------------------------------------- 1 | 47|53 2 | 97|13 3 | 97|61 4 | 97|47 5 | 75|29 6 | 61|13 7 | 75|53 8 | 29|13 9 | 97|29 10 | 53|29 11 | 61|53 12 | 97|53 13 | 61|29 14 | 47|13 15 | 75|47 16 | 97|75 17 | 47|61 18 | 75|61 19 | 47|29 20 | 75|13 21 | 53|13 22 | 23 | 75,47,61,53,29 24 | 97,61,53,29,13 25 | 75,29,13 26 | 75,97,47,61,53 27 | 61,13,29 28 | 97,13,75,29,47 -------------------------------------------------------------------------------- /template/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | let lines = fs.readFileSync("input.txt", "utf8").split("\n"); 4 | // lines = fs.readFileSync("example.txt", "utf8").split("\n"); 5 | 6 | function part1() { 7 | console.log(lines.length); 8 | } 9 | function part2() { 10 | console.log(lines.length); 11 | } 12 | 13 | part1(); 14 | // part2(); 15 | -------------------------------------------------------------------------------- /25/example.txt: -------------------------------------------------------------------------------- 1 | ##### 2 | .#### 3 | .#### 4 | .#### 5 | .#.#. 6 | .#... 7 | ..... 8 | 9 | ##### 10 | ##.## 11 | .#.## 12 | ...## 13 | ...#. 14 | ...#. 15 | ..... 16 | 17 | ..... 18 | #.... 19 | #.... 20 | #...# 21 | #.#.# 22 | #.### 23 | ##### 24 | 25 | ..... 26 | ..... 27 | #.#.. 28 | ###.. 29 | ###.# 30 | ###.# 31 | ##### 32 | 33 | ..... 34 | ..... 35 | ..... 36 | #.... 37 | #.#.. 38 | #.#.# 39 | ##### -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /1/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const lines = fs 4 | .readFileSync("input.txt", "utf8") 5 | .split("\n") 6 | .map((line) => line.split(" ")); 7 | 8 | const leftList = lines.map(([left, _]) => parseInt(left, 10)).sort(); 9 | const rightList = lines.map(([_, right]) => parseInt(right, 10)).sort(); 10 | 11 | function sum(list: number[]) { 12 | return list.reduce((acc, curr) => acc + curr, 0); 13 | } 14 | 15 | const sumDifferences = sum( 16 | leftList.map((leftNum, index) => Math.abs(leftNum - rightList[index])) 17 | ); 18 | 19 | const sumOccurences = sum( 20 | leftList.map( 21 | (leftNum) => 22 | leftNum * rightList.filter((rightNum) => rightNum === leftNum).length 23 | ) 24 | ); 25 | 26 | console.log("part 1:", sumDifferences); 27 | console.log("part 2:", sumOccurences); 28 | -------------------------------------------------------------------------------- /2/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const lines = fs.readFileSync("input.txt", "utf8").split("\n"); 4 | 5 | // Check that all numbers increase or decrease by 1-3 6 | function safeLine(numlist: number[]) { 7 | const diffs = numlist.map((n, i) => n - numlist[i - 1]); 8 | diffs.shift(); 9 | return ( 10 | diffs.every((d) => d >= -3 && d < 0) || diffs.every((d) => d <= 3 && d > 0) 11 | ); 12 | } 13 | 14 | function a() { 15 | const nums = lines.map((r) => r.split(" ").map((n) => parseInt(n, 10))); 16 | return nums.filter((numlist) => safeLine(numlist)); 17 | } 18 | 19 | function b() { 20 | const nums = lines.map((r) => r.split(" ").map((n) => parseInt(n, 10))); 21 | 22 | return nums.filter((numlist) => { 23 | if (safeLine(numlist)) return true; 24 | 25 | for (let i = 0; i < numlist.length; i++) { 26 | if (safeLine(numlist.toSpliced(i, 1))) return true; 27 | } 28 | }); 29 | } 30 | 31 | console.log(a().length); 32 | console.log(b().length); 33 | -------------------------------------------------------------------------------- /11/initial-solution.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | // Parse input numbers 4 | const stones = fs 5 | .readFileSync("input.txt", "utf8") 6 | .trim() 7 | .split(/\s+/) 8 | .map(Number); 9 | 10 | function transformStone(n: number): number[] { 11 | // Rule 1: If stone is 0, replace with 1 12 | if (n === 0) return [1]; 13 | 14 | // Rule 2: If even number of digits, split into two stones 15 | const numStr = n.toString(); 16 | if (numStr.length % 2 === 0) { 17 | const mid = Math.floor(numStr.length / 2); 18 | const left = parseInt(numStr.slice(0, mid)); 19 | const right = parseInt(numStr.slice(mid)); 20 | return [left, right]; 21 | } 22 | 23 | // Rule 3: Multiply by 2024 24 | return [n * 2024]; 25 | } 26 | 27 | function simulateBlink(stones: number[]): number[] { 28 | const result: number[] = []; 29 | 30 | for (const stone of stones) { 31 | result.push(...transformStone(stone)); 32 | } 33 | 34 | return result; 35 | } 36 | 37 | function part1() { 38 | let currentStones = stones; 39 | for (let i = 0; i < 25; i++) { 40 | currentStones = simulateBlink(currentStones); 41 | } 42 | 43 | console.log("Part 1:", currentStones.length); 44 | } 45 | 46 | part1(); 47 | -------------------------------------------------------------------------------- /25/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const chunks = fs 4 | .readFileSync("input.txt", "utf8") 5 | .split("\n\n") 6 | .map((c) => c.split("\n")); 7 | 8 | function parseHeights(schematic: string[]) { 9 | const heights = [0, 0, 0, 0, 0]; 10 | for (let col = 0; col < 5; col++) { 11 | for (let row = 1; row <= 5; row++) { 12 | if (schematic[row][col] === "#") heights[col]++; 13 | } 14 | } 15 | return heights; 16 | } 17 | 18 | const locks: number[][] = []; 19 | const keys: number[][] = []; 20 | 21 | chunks.forEach((chunk) => { 22 | const top = chunk[0]; 23 | const bottom = chunk[6]; 24 | 25 | if (top === "#####") { 26 | locks.push(parseHeights(chunk)); 27 | } else if (bottom === "#####") { 28 | keys.push(parseHeights(chunk)); 29 | } else { 30 | throw new Error("Unknown schematic type"); 31 | } 32 | }); 33 | 34 | let validPairs = 0; 35 | 36 | locks.forEach((lockHeights) => { 37 | keys.forEach((keyHeights) => { 38 | let fits = true; 39 | for (let c = 0; c < 5; c++) { 40 | if (lockHeights[c] + keyHeights[c] > 5) { 41 | fits = false; 42 | break; 43 | } 44 | } 45 | if (fits) validPairs++; 46 | }); 47 | }); 48 | 49 | console.log(validPairs); 50 | -------------------------------------------------------------------------------- /fetcher.ts: -------------------------------------------------------------------------------- 1 | async function getInput(year: number, day: number) { 2 | const url = `https://adventofcode.com/${year}/day/${day}/input`; 3 | const response = await fetch(url, { 4 | headers: { 5 | Cookie: `session=${process.env.AOC_SESSION_ID}`, 6 | }, 7 | }); 8 | return response.text(); 9 | } 10 | 11 | import fs from "fs"; 12 | 13 | async function getOrWriteInput(day: number) { 14 | const input = await getInput(2024, day); 15 | fs.writeFileSync(`${day}/input.txt`, input.trim()); 16 | } 17 | 18 | function getDay() { 19 | return new Date().getDate() + 1; 20 | } 21 | 22 | function waitUntilTime( 23 | hour: number, 24 | minute: number, 25 | second: number 26 | ): Promise { 27 | return new Promise((resolve) => { 28 | const check = () => { 29 | const now = new Date(); 30 | const target = new Date(); 31 | target.setHours(hour, minute, second, 0); 32 | 33 | if (now >= target) { 34 | resolve(); 35 | } else { 36 | setTimeout(check, 100); 37 | } 38 | }; 39 | check(); 40 | }); 41 | } 42 | 43 | async function main() { 44 | await waitUntilTime(21, 0, 1); 45 | const day = getDay(); 46 | console.log(`Fetching input for day ${day}`); 47 | await getOrWriteInput(day); 48 | } 49 | 50 | main(); 51 | -------------------------------------------------------------------------------- /11/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | let line = fs.readFileSync("input.txt", "utf8").trim(); 4 | // line = fs.readFileSync("example.txt", "utf8").trim(); 5 | 6 | const startingStones: Record = {}; 7 | line.split(" ").forEach((stone) => { 8 | const num = Number(stone); 9 | startingStones[num] = (startingStones[num] || 0) + 1; 10 | }); 11 | 12 | function transformStone(stone: number): number[] { 13 | if (stone === 0) return [1]; 14 | 15 | const stoneStr = stone.toString(); 16 | if (stoneStr.length % 2 === 0) { 17 | const mid = stoneStr.length / 2; 18 | return [Number(stoneStr.slice(0, mid)), Number(stoneStr.slice(mid))]; 19 | } 20 | 21 | return [stone * 2024]; 22 | } 23 | 24 | function blink(stones: Record) { 25 | const nextStones: Record = {}; 26 | Object.entries(stones).forEach(([stone, count]) => { 27 | transformStone(+stone).forEach((newStone) => { 28 | nextStones[newStone] = (nextStones[newStone] || 0) + count; 29 | }); 30 | }); 31 | return nextStones; 32 | } 33 | 34 | function countStonesAfterBlinks(numBlinks: number) { 35 | let stones = { ...startingStones }; 36 | for (let i = 0; i < numBlinks; i++) { 37 | stones = blink(stones); 38 | } 39 | return Object.values(stones).reduce((sum, count) => sum + count, 0); 40 | } 41 | 42 | function part1() { 43 | console.log("Part 1:", countStonesAfterBlinks(25)); 44 | } 45 | 46 | function part2() { 47 | console.log("Part 2:", countStonesAfterBlinks(75)); 48 | } 49 | 50 | part1(); 51 | part2(); 52 | -------------------------------------------------------------------------------- /3/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | let lines = fs.readFileSync("input.txt", "utf8").split("\n"); 4 | // lines = fs.readFileSync("example.txt", "utf8").split("\n"); 5 | 6 | // Regex that grabs all instances of mul(x,y) where x and y are numbers 7 | const mulRegex = /mul\((\d+),(\d+)\)/g; 8 | 9 | function a() { 10 | const allLinesCombined = lines.join(""); 11 | 12 | const matches = allLinesCombined.match(mulRegex)!; 13 | 14 | const pairs = matches.map((m) => m.match(/\d+/g)); 15 | 16 | const products = pairs.map((p) => p!.map((n) => parseInt(n, 10))); 17 | 18 | const sum = products.reduce((acc, p) => acc + p[0] * p[1], 0); 19 | 20 | console.log(sum); 21 | } 22 | 23 | // Match do(), don't(), and mul(x,y) with a single regex 24 | const instructionRegex = /do\(\)|don't\(\)|mul\((\d+),(\d+)\)/g; 25 | 26 | function b() { 27 | const allLinesCombined = lines.join(""); 28 | 29 | // Get all matches with a single regex 30 | const matches = [...allLinesCombined.matchAll(instructionRegex)]; 31 | 32 | let doo = true; 33 | let sum = 0; 34 | 35 | for (const match of matches) { 36 | const instruction = match[0]; 37 | if (instruction === "do()") { 38 | doo = true; 39 | } else if (instruction === "don't()") { 40 | doo = false; 41 | } else if (instruction.startsWith("mul")) { 42 | if (doo) { 43 | const x = parseInt(match[1], 10); 44 | const y = parseInt(match[2], 10); 45 | sum += x * y; 46 | } 47 | } 48 | } 49 | 50 | console.log(sum); 51 | } 52 | 53 | a(); 54 | b(); 55 | -------------------------------------------------------------------------------- /4/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | let lines = fs.readFileSync("input.txt", "utf8").split("\n"); 4 | lines = fs.readFileSync("example.txt", "utf8").split("\n"); 5 | 6 | const grid = lines.map((line) => line.split("")); 7 | 8 | function gp(x: number, y: number) { 9 | return grid[y]?.[x]; 10 | } 11 | 12 | function a() { 13 | function generateAllDirsForPoint(x: number, y: number) { 14 | return [ 15 | gp(x, y) + gp(x + 1, y) + gp(x + 2, y) + gp(x + 3, y), 16 | gp(x, y) + gp(x - 1, y) + gp(x - 2, y) + gp(x - 3, y), 17 | gp(x, y) + gp(x, y + 1) + gp(x, y + 2) + gp(x, y + 3), 18 | gp(x, y) + gp(x, y - 1) + gp(x, y - 2) + gp(x, y - 3), 19 | gp(x, y) + gp(x + 1, y + 1) + gp(x + 2, y + 2) + gp(x + 3, y + 3), 20 | gp(x, y) + gp(x - 1, y + 1) + gp(x - 2, y + 2) + gp(x - 3, y + 3), 21 | gp(x, y) + gp(x + 1, y - 1) + gp(x + 2, y - 2) + gp(x + 3, y - 3), 22 | gp(x, y) + gp(x - 1, y - 1) + gp(x - 2, y - 2) + gp(x - 3, y - 3), 23 | ]; 24 | } 25 | 26 | function checkXmasFromCoordinate(x: number, y: number): number { 27 | if (gp(x, y) !== "X") return 0; 28 | 29 | return generateAllDirsForPoint(x, y).filter((dir) => dir === "XMAS").length; 30 | } 31 | 32 | let total = 0; 33 | for (let y = 0; y < grid.length; y++) { 34 | for (let x = 0; x < grid[0].length; x++) { 35 | total += checkXmasFromCoordinate(x, y); 36 | } 37 | } 38 | console.log("Part 1:", total); 39 | } 40 | 41 | function b() { 42 | function generateDiagonalsForPoint(x: number, y: number) { 43 | return [ 44 | gp(x - 1, y - 1) + gp(x, y) + gp(x + 1, y + 1), 45 | gp(x - 1, y + 1) + gp(x, y) + gp(x + 1, y - 1), 46 | ]; 47 | } 48 | 49 | function checkMasPattern(x: number, y: number): boolean { 50 | return generateDiagonalsForPoint(x, y).every( 51 | (dir) => dir === "MAS" || dir === "SAM" 52 | ); 53 | } 54 | 55 | let total = 0; 56 | for (let y = 1; y < grid.length - 1; y++) { 57 | for (let x = 1; x < grid[0].length - 1; x++) { 58 | if (checkMasPattern(x, y)) { 59 | total++; 60 | } 61 | } 62 | } 63 | console.log("Part 2:", total); 64 | } 65 | 66 | a(); 67 | b(); 68 | -------------------------------------------------------------------------------- /7/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | let lines = fs.readFileSync("input.txt", "utf8").split("\n"); 4 | // lines = fs.readFileSync("example.txt", "utf8").split("\n"); 5 | 6 | const pairings = lines.map((line) => { 7 | const [left, right] = line.split(": "); 8 | return { target: parseInt(left), values: right.split(" ").map(Number) }; 9 | }); 10 | 11 | function evaluateExpression(values: number[], operators: string[]): number { 12 | let result = values[0]; 13 | for (let i = 0; i < operators.length; i++) { 14 | if (operators[i] === "+") { 15 | result += values[i + 1]; 16 | } else if (operators[i] === "*") { 17 | result *= values[i + 1]; 18 | } else if (operators[i] === "||") { 19 | result = parseInt(result.toString() + values[i + 1].toString()); 20 | } 21 | } 22 | return result; 23 | } 24 | 25 | function findValidOperators( 26 | target: number, 27 | values: number[], 28 | possibleOperators: string[] 29 | ): boolean { 30 | // Generate all possible combinations of operators 31 | const operatorCount = values.length - 1; 32 | 33 | const allowedOperators = possibleOperators.length; 34 | 35 | // Try all possible combinations of operators 36 | for (let i = 0; i < Math.pow(allowedOperators, operatorCount); i++) { 37 | const operators: string[] = []; 38 | let n = i; 39 | for (let j = 0; j < operatorCount; j++) { 40 | operators.push(possibleOperators[n % allowedOperators]); 41 | n = Math.floor(n / allowedOperators); 42 | } 43 | 44 | const result = evaluateExpression(values, operators); 45 | if (result === target) { 46 | return true; 47 | } 48 | } 49 | 50 | return false; 51 | } 52 | 53 | function part1() { 54 | const validPairings = pairings.filter((pairing) => { 55 | return findValidOperators(pairing.target, pairing.values, ["+", "*"]); 56 | }); 57 | 58 | const sum = validPairings.reduce((acc, curr) => acc + curr.target, 0); 59 | console.log("Part 1:", sum); 60 | } 61 | 62 | function part2() { 63 | const validPairings = pairings.filter((pairing) => { 64 | return findValidOperators(pairing.target, pairing.values, ["+", "*", "||"]); 65 | }); 66 | 67 | const sum = validPairings.reduce((acc, curr) => acc + curr.target, 0); 68 | console.log("Part 2:", sum); 69 | } 70 | 71 | part1(); 72 | part2(); 73 | -------------------------------------------------------------------------------- /10/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const lines = fs.readFileSync("input.txt", "utf8").trim().split("\n"); 4 | // const lines = fs.readFileSync("example.txt", "utf8").trim().split("\n"); 5 | 6 | const grid = lines.map((line) => line.split("").map(Number)); 7 | const rows = grid.length; 8 | const cols = grid[0].length; 9 | 10 | const directions = [ 11 | [-1, 0], 12 | [1, 0], 13 | [0, -1], 14 | [0, 1], 15 | ]; 16 | 17 | function getTrailheads(): [number, number][] { 18 | const trailheads: [number, number][] = []; 19 | for (let r = 0; r < rows; r++) { 20 | for (let c = 0; c < cols; c++) { 21 | if (grid[r][c] === 0) { 22 | trailheads.push([r, c]); 23 | } 24 | } 25 | } 26 | return trailheads; 27 | } 28 | 29 | function part1() { 30 | function countReachableNines(r0: number, c0: number): number { 31 | const visited = new Set(); 32 | const queue: [number, number][] = [[r0, c0]]; 33 | visited.add(`${r0},${c0}`); 34 | const nines = new Set(); 35 | 36 | while (queue.length > 0) { 37 | const [r, c] = queue.shift()!; 38 | const h = grid[r][c]; 39 | if (h === 9) { 40 | nines.add(`${r},${c}`); 41 | continue; 42 | } 43 | for (const [dr, dc] of directions) { 44 | const nr = r + dr, 45 | nc = c + dc; 46 | if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue; 47 | if (grid[nr][nc] === h + 1 && !visited.has(`${nr},${nc}`)) { 48 | visited.add(`${nr},${nc}`); 49 | queue.push([nr, nc]); 50 | } 51 | } 52 | } 53 | 54 | return nines.size; 55 | } 56 | 57 | const trailheads = getTrailheads(); 58 | let sum = 0; 59 | for (const [r, c] of trailheads) sum += countReachableNines(r, c); 60 | console.log("Part 1:", sum); 61 | } 62 | 63 | function part2() { 64 | const memo = new Map(); 65 | 66 | function key(r: number, c: number): string { 67 | return `${r},${c}`; 68 | } 69 | 70 | function ways(r: number, c: number): number { 71 | const k = key(r, c); 72 | if (memo.has(k)) return memo.get(k)!; 73 | const h = grid[r][c]; 74 | if (h === 9) { 75 | memo.set(k, 1); 76 | return 1; 77 | } 78 | let total = 0; 79 | for (const [dr, dc] of directions) { 80 | const nr = r + dr, 81 | nc = c + dc; 82 | if (nr < 0 || nr >= rows || nc < 0 || nc >= cols) continue; 83 | if (grid[nr][nc] === h + 1) total += ways(nr, nc); 84 | } 85 | memo.set(k, total); 86 | return total; 87 | } 88 | 89 | const trailheads = getTrailheads(); 90 | let sum = 0; 91 | for (const [r, c] of trailheads) sum += ways(r, c); 92 | console.log("Part 2:", sum); 93 | } 94 | 95 | part1(); 96 | part2(); 97 | -------------------------------------------------------------------------------- /13/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const chunks = fs.readFileSync("input.txt", "utf8").trim().split("\n\n"); 4 | // const chunks = fs.readFileSync("example.txt", "utf8").trim().split("\n\n"); 5 | 6 | const groups = chunks.map((chunk) => { 7 | const [buttonA, buttonB, prize] = chunk.split("\n"); 8 | const [x1, y1] = buttonA.split("X+")[1].split(", Y+"); 9 | const [x2, y2] = buttonB.split("X+")[1].split(", Y+"); 10 | const [px, py] = prize.split("X=")[1].split(", Y="); 11 | return { 12 | buttonA: [Number(x1), Number(y1)], 13 | buttonB: [Number(x2), Number(y2)], 14 | prize: [Number(px), Number(py)], 15 | }; 16 | }); 17 | 18 | // Cursor helped me write this comment and then wrote the function 19 | // We want to find a combination of A and B presses (A_count, B_count) for each machine 20 | // such that: 21 | // A_count * ax + B_count * bx = px 22 | // A_count * ay + B_count * by = py 23 | // 24 | // This forms a system of linear equations that we can solve algebraically: 25 | // ax*A + bx*B = px ... (1) 26 | // ay*A + by*B = py ... (2) 27 | // 28 | // Multiply (1) by by and (2) by bx: 29 | // ax*by*A + bx*by*B = px*by 30 | // ay*bx*A + by*bx*B = py*bx 31 | // 32 | // Subtract to eliminate B: 33 | // ax*by*A - ay*bx*A = px*by - py*bx 34 | // A*(ax*by - ay*bx) = px*by - py*bx 35 | // A = (px*by - py*bx)/(ax*by - ay*bx) 36 | // 37 | // Similarly for B: 38 | // B = (px*ay - py*ax)/(bx*ay - by*ax) 39 | function solveMachine(group: (typeof groups)[number], prizeBonus: number = 0) { 40 | // Calculate determinant to check if system is solvable 41 | const det = 42 | group.buttonA[0] * group.buttonB[1] - group.buttonA[1] * group.buttonB[0]; 43 | if (det === 0) return null; 44 | 45 | const px = group.prize[0] + prizeBonus; 46 | const py = group.prize[1] + prizeBonus; 47 | 48 | // Solve for A and B 49 | const A_count = (px * group.buttonB[1] - py * group.buttonB[0]) / det; 50 | const B_count = (px * group.buttonA[1] - py * group.buttonA[0]) / -det; 51 | 52 | // Check if solution is valid (non-negative integers) 53 | if ( 54 | A_count < 0 || 55 | B_count < 0 || 56 | !Number.isInteger(A_count) || 57 | !Number.isInteger(B_count) 58 | ) { 59 | return null; 60 | } 61 | 62 | return 3 * A_count + B_count; 63 | } 64 | 65 | function part1() { 66 | const costs = groups.map(solveMachine).filter((c) => c !== null) as number[]; 67 | 68 | console.log( 69 | "Part 1:", 70 | costs.reduce((a, b) => a + b, 0) 71 | ); 72 | } 73 | 74 | function part2() { 75 | const costs = groups 76 | .map((group) => solveMachine(group, 10000000000000)) 77 | .filter((c) => c !== null) as number[]; 78 | 79 | console.log( 80 | "Part 2:", 81 | costs.reduce((a, b) => a + b, 0) 82 | ); 83 | } 84 | 85 | part1(); 86 | part2(); 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | 177 | input.txt -------------------------------------------------------------------------------- /20/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | let lines = fs.readFileSync("input.txt", "utf8").split("\n"); 4 | // lines = fs.readFileSync("example.txt", "utf8").split("\n"); 5 | 6 | const grid = lines.map((line) => line.split("")); 7 | 8 | // Find start and end points 9 | let start: [number, number] = [-1, -1]; 10 | let end: [number, number] = [-1, -1]; 11 | 12 | grid.forEach((row, y) => { 13 | row.forEach((cell, x) => { 14 | if (cell === "S") start = [x, y]; 15 | if (cell === "E") end = [x, y]; 16 | }); 17 | }); 18 | 19 | // Returns distances from a starting point to all other points 20 | function getDistances(startPos: [number, number]) { 21 | const distances = Array.from({ length: grid.length }, () => 22 | Array(grid[0].length).fill(Infinity) 23 | ); 24 | 25 | distances[startPos[1]][startPos[0]] = 0; 26 | const queue: [number, number][] = [[startPos[0], startPos[1]]]; 27 | 28 | while (queue.length > 0) { 29 | const [x, y] = queue.shift()!; 30 | const currentDist = distances[y][x]; 31 | 32 | // Check all adjacent cells 33 | for (const [dx, dy] of [ 34 | [1, 0], 35 | [-1, 0], 36 | [0, 1], 37 | [0, -1], 38 | ]) { 39 | const newX = x + dx; 40 | const newY = y + dy; 41 | 42 | if ( 43 | newX >= 0 && 44 | newX < grid[0].length && 45 | newY >= 0 && 46 | newY < grid.length && 47 | grid[newY][newX] !== "#" && 48 | distances[newY][newX] === Infinity 49 | ) { 50 | distances[newY][newX] = currentDist + 1; 51 | queue.push([newX, newY]); 52 | } 53 | } 54 | } 55 | 56 | return distances; 57 | } 58 | 59 | // Find shortcuts with savings >= 100 for a given max portal distance 60 | function findShortcuts(maxPortalDist: number) { 61 | const startDistances = getDistances(start); 62 | const endDistances = getDistances(end); 63 | const normalDist = startDistances[end[1]][end[0]]; 64 | 65 | let shortcuts = 0; 66 | 67 | for (let y1 = 0; y1 < grid.length; y1++) { 68 | for (let x1 = 0; x1 < grid[0].length; x1++) { 69 | if (grid[y1][x1] === "#" || startDistances[y1][x1] === Infinity) continue; 70 | 71 | const minY = Math.max(0, y1 - maxPortalDist); 72 | const maxY = Math.min(grid.length - 1, y1 + maxPortalDist); 73 | 74 | for (let y2 = minY; y2 <= maxY; y2++) { 75 | const xRange = maxPortalDist - Math.abs(y2 - y1); 76 | if (xRange < 0) continue; 77 | 78 | const minX = Math.max(0, x1 - xRange); 79 | const maxX = Math.min(grid[0].length - 1, x1 + xRange); 80 | 81 | for (let x2 = minX; x2 <= maxX; x2++) { 82 | if (grid[y2][x2] === "#" || endDistances[y2][x2] === Infinity) 83 | continue; 84 | 85 | const portalSteps = Math.abs(x2 - x1) + Math.abs(y2 - y1); 86 | const totalDist = 87 | startDistances[y1][x1] + portalSteps + endDistances[y2][x2]; 88 | 89 | if (normalDist - totalDist >= 100) { 90 | shortcuts++; 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | return shortcuts; 98 | } 99 | 100 | console.log("Part 1:", findShortcuts(2)); 101 | console.log("Part 2:", findShortcuts(20)); 102 | -------------------------------------------------------------------------------- /9/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const input = fs.readFileSync("input.txt", "utf8").split("\n"); 4 | // input = fs.readFileSync("example.txt", "utf8").split("\n"); 5 | 6 | const diskMap = input[0]; 7 | 8 | function parseDiskMap(diskMapString: string) { 9 | let fileId = 0; 10 | const disk: number[] = []; 11 | for (let i = 0; i < diskMapString.length; i++) { 12 | const length = parseInt(diskMapString[i], 10); 13 | if (i % 2 === 0) { 14 | for (let j = 0; j < length; j++) disk.push(fileId); 15 | fileId++; 16 | } else { 17 | for (let j = 0; j < length; j++) disk.push(-1); 18 | } 19 | } 20 | return { disk, fileCount: fileId }; 21 | } 22 | 23 | function compactBlockByBlock(disk: number[]) { 24 | while (true) { 25 | const leftFreeIndex = disk.indexOf(-1); 26 | if (leftFreeIndex < 0) break; 27 | let fileFoundRight = false; 28 | for (let i = leftFreeIndex + 1; i < disk.length; i++) { 29 | if (disk[i] !== -1) { 30 | fileFoundRight = true; 31 | break; 32 | } 33 | } 34 | if (!fileFoundRight) break; 35 | let rightFileIndex = -1; 36 | for (let i = disk.length - 1; i >= 0; i--) { 37 | if (disk[i] !== -1) { 38 | rightFileIndex = i; 39 | break; 40 | } 41 | } 42 | disk[leftFreeIndex] = disk[rightFileIndex]; 43 | disk[rightFileIndex] = -1; 44 | } 45 | } 46 | 47 | function findFreeSpanLeft( 48 | disk: number[], 49 | maxEnd: number, 50 | lengthNeeded: number 51 | ) { 52 | const limit = maxEnd - 1; 53 | if (limit < 0) return null; 54 | let spanStart = -1, 55 | currentSpanLength = 0; 56 | for (let i = 0; i <= limit; i++) { 57 | if (disk[i] === -1) { 58 | if (spanStart < 0) spanStart = i; 59 | currentSpanLength++; 60 | if (currentSpanLength >= lengthNeeded) return spanStart; 61 | } else { 62 | spanStart = -1; 63 | currentSpanLength = 0; 64 | } 65 | } 66 | return null; 67 | } 68 | 69 | function compactWholeFiles(disk: number[], fileCount: number) { 70 | for (let currentFileId = fileCount - 1; currentFileId >= 0; currentFileId--) { 71 | let start = -1, 72 | end = -1; 73 | for (let i = 0; i < disk.length; i++) { 74 | if (disk[i] === currentFileId) { 75 | if (start < 0) start = i; 76 | end = i; 77 | } 78 | } 79 | if (start < 0) continue; 80 | const fileLength = end - start + 1; 81 | const freeSpanStart = findFreeSpanLeft(disk, start, fileLength); 82 | if (freeSpanStart == null) continue; 83 | for (let i = 0; i < fileLength; i++) 84 | disk[freeSpanStart + i] = currentFileId; 85 | for (let i = start; i <= end; i++) disk[i] = -1; 86 | } 87 | } 88 | 89 | function checksum(disk: number[]) { 90 | let sum = 0; 91 | for (let i = 0; i < disk.length; i++) if (disk[i] !== -1) sum += i * disk[i]; 92 | return sum; 93 | } 94 | 95 | function part1() { 96 | const { disk } = parseDiskMap(diskMap); 97 | compactBlockByBlock(disk); 98 | return checksum(disk); 99 | } 100 | 101 | function part2() { 102 | const { disk, fileCount } = parseDiskMap(diskMap); 103 | compactWholeFiles(disk, fileCount); 104 | return checksum(disk); 105 | } 106 | 107 | console.log(part1()); 108 | console.log(part2()); 109 | -------------------------------------------------------------------------------- /14/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | // Real input 4 | const lines = fs.readFileSync("input.txt", "utf8").split("\n"); 5 | const WIDTH = 101; 6 | const HEIGHT = 103; 7 | 8 | // Example input 9 | // const lines = fs.readFileSync('example.txt', 'utf8').split('\n'); 10 | // const WIDTH = 11; 11 | // const HEIGHT = 7; 12 | 13 | const robots = lines.map((line) => { 14 | const [pos, vel] = line.split(" "); 15 | const [px, py] = pos.replace("p=", "").split(",").map(Number); 16 | const [vx, vy] = vel.replace("v=", "").split(",").map(Number); 17 | return { 18 | pos: [px, py], 19 | vel: [vx, vy], 20 | }; 21 | }); 22 | 23 | type Robot = (typeof robots)[number]; 24 | 25 | function moveRobots(robots: Robot[], steps: number) { 26 | return robots.map((robot) => { 27 | const x = robot.pos[0] + robot.vel[0] * steps; 28 | const y = robot.pos[1] + robot.vel[1] * steps; 29 | return { 30 | pos: [((x % WIDTH) + WIDTH) % WIDTH, ((y % HEIGHT) + HEIGHT) % HEIGHT], 31 | vel: robot.vel, 32 | }; 33 | }); 34 | } 35 | 36 | function part1() { 37 | const SIMULATION_TIME = 100; 38 | function getQuadrantCounts(robots: Robot[]) { 39 | const quadrants = [0, 0, 0, 0]; 40 | const midX = Math.floor(WIDTH / 2); 41 | const midY = Math.floor(HEIGHT / 2); 42 | 43 | robots.forEach((robot) => { 44 | const [x, y] = robot.pos; 45 | // Skip robots on center lines 46 | if (x === midX || y === midY) return; 47 | 48 | if (x < midX) { 49 | if (y < midY) quadrants[0]++; 50 | else quadrants[2]++; 51 | } else { 52 | if (y < midY) quadrants[1]++; 53 | else quadrants[3]++; 54 | } 55 | }); 56 | 57 | return quadrants; 58 | } 59 | 60 | const finalPositions = moveRobots(robots, SIMULATION_TIME); 61 | const quadrantCounts = getQuadrantCounts(finalPositions); 62 | 63 | return quadrantCounts.reduce((acc, count) => acc * count); 64 | } 65 | 66 | function part2() { 67 | function visualizeGrid(positions: Robot[]) { 68 | const grid = Array.from({ length: HEIGHT }, () => 69 | Array.from({ length: WIDTH }, () => ".") 70 | ); 71 | 72 | positions.forEach((robot) => { 73 | const [x, y] = robot.pos; 74 | grid[y][x] = "#"; 75 | }); 76 | 77 | return grid.map((row) => row.join("")).join("\n"); 78 | } 79 | 80 | let seconds = 0; 81 | 82 | while (true) { 83 | const positions = moveRobots(robots, seconds); 84 | 85 | // Count adjacent robots (robots that have neighbors) 86 | const robotsWithNeighbors = positions.filter((robot) => { 87 | const [x, y] = robot.pos; 88 | return positions.some((other) => { 89 | if (other === robot) return false; 90 | const [ox, oy] = other.pos; 91 | const dx = Math.abs(x - ox); 92 | const dy = Math.abs(y - oy); 93 | return dx <= 1 && dy <= 1; // Adjacent including diagonals 94 | }); 95 | }).length; 96 | 97 | // If most robots have neighbors, they might be forming a pattern 98 | if (robotsWithNeighbors > positions.length * 0.7) { 99 | console.log( 100 | `\nPotential message found at second ${seconds} (${robotsWithNeighbors}/${positions.length} robots clustered):` 101 | ); 102 | console.log(visualizeGrid(positions)); 103 | return seconds; 104 | } 105 | 106 | seconds++; 107 | } 108 | } 109 | 110 | console.log("part 1:", part1()); 111 | console.log("part 2:", part2()); 112 | -------------------------------------------------------------------------------- /5/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | let chunks = fs.readFileSync("input.txt", "utf8").split("\n\n"); 4 | // chunks = fs.readFileSync("example.txt", "utf8").split("\n\n"); 5 | 6 | // Pairs of ints. Left number must always become before right number in a valid page 7 | const rules = chunks[0].split("\n").map((line) => line.split("|").map(Number)); 8 | 9 | const pages = chunks[1].split("\n").map((line) => line.split(",").map(Number)); 10 | 11 | function a() { 12 | const validPages = pages.filter((page) => { 13 | // Check if page follows all rules 14 | return rules.every(([left, right]) => { 15 | // Find indices of both numbers in the page 16 | const leftIndex = page.indexOf(left); 17 | const rightIndex = page.indexOf(right); 18 | 19 | // Rule is satisfied if: 20 | // - If both numbers exist, left must come before right 21 | // - If only one or neither number exists, that's fine 22 | return leftIndex === -1 || rightIndex === -1 || leftIndex < rightIndex; 23 | }); 24 | }); 25 | 26 | // Take the middle values from each page and sum them 27 | const sum = validPages 28 | .map((page) => page[Math.floor(page.length / 2)]) 29 | .reduce((a, b) => a + b, 0); 30 | 31 | console.log("part 1:", sum); 32 | } 33 | 34 | function b() { 35 | const invalidPages = pages.filter((page) => { 36 | // Check if page follows all rules 37 | return rules.some(([left, right]) => { 38 | // Find indices of both numbers in the page 39 | const leftIndex = page.indexOf(left); 40 | const rightIndex = page.indexOf(right); 41 | 42 | // Rule is satisfied if: 43 | // - If both numbers exist, left must come before right 44 | // - If only one or neither number exists, that's fine 45 | return !(leftIndex === -1 || rightIndex === -1 || leftIndex < rightIndex); 46 | }); 47 | }); 48 | 49 | // Reorder the invalid pages so they follow the rules 50 | const reorderedPages = invalidPages.map((page) => { 51 | // Keep track of numbers that must come before each number 52 | const mustComeBefore: Record> = {}; 53 | 54 | // Initialize sets for each number in the page 55 | for (const num of page) { 56 | mustComeBefore[num] = new Set(); 57 | } 58 | 59 | // Build dependencies based on rules 60 | for (const [left, right] of rules) { 61 | if (page.includes(left) && page.includes(right)) { 62 | mustComeBefore[right].add(left); 63 | } 64 | } 65 | 66 | // Repeatedly find numbers that can come next (have no dependencies) 67 | const result: number[] = []; 68 | const remaining = new Set(page); 69 | 70 | while (remaining.size > 0) { 71 | const available = Array.from(remaining).filter((num) => 72 | Array.from(mustComeBefore[num]).every((dep) => !remaining.has(dep)) 73 | ); 74 | 75 | if (available.length === 0) { 76 | // If we get stuck, there must be a cycle - just take any remaining number 77 | const next = Array.from(remaining)[0]; 78 | result.push(next); 79 | remaining.delete(next); 80 | } else { 81 | // Take the first available number 82 | const next = available[0]; 83 | result.push(next); 84 | remaining.delete(next); 85 | } 86 | } 87 | 88 | return result; 89 | }); 90 | 91 | // Take the middle values from each page and sum them 92 | const sum = reorderedPages 93 | .map((page) => page[Math.floor(page.length / 2)]) 94 | .reduce((a, b) => a + b, 0); 95 | 96 | console.log("part 2:", sum); 97 | } 98 | 99 | a(); 100 | b(); 101 | -------------------------------------------------------------------------------- /6/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | let lines = fs.readFileSync("input.txt", "utf8").split("\n"); 4 | // lines = fs.readFileSync("example.txt", "utf8").split("\n"); 5 | 6 | const grid = lines.map((line) => line.split("")); 7 | 8 | const hashpoints = new Set(); 9 | 10 | let start: [number, number] = [0, 0]; 11 | 12 | grid.forEach((row, y) => { 13 | row.forEach((cell, x) => { 14 | if (cell === "#") hashpoints.add(`${x},${y}`); 15 | if (cell === "^") start = [x, y]; 16 | }); 17 | }); 18 | 19 | const directions = { 20 | N: [0, -1], 21 | E: [1, 0], 22 | S: [0, 1], 23 | W: [-1, 0], 24 | } as const; 25 | 26 | const turns = { 27 | N: "E", 28 | E: "S", 29 | S: "W", 30 | W: "N", 31 | } as const; 32 | 33 | type Direction = keyof typeof directions; 34 | 35 | // Move in the current direction if nothing is in the way (edge or hashpoint). Otherwise turn right. 36 | function getNextLocation( 37 | currentLocation: [number, number], 38 | currentDirection: Direction, 39 | extraPoint?: string 40 | ): 41 | | { outOfBounds: true } 42 | | { 43 | nextLocation: [number, number]; 44 | nextDirection: Direction; 45 | outOfBounds: false; 46 | } { 47 | const nextX = currentLocation[0] + directions[currentDirection][0]; 48 | const nextY = currentLocation[1] + directions[currentDirection][1]; 49 | const nextPoint = `${nextX},${nextY}`; 50 | 51 | // Check if next position is in bounds 52 | const inBounds = 53 | nextX >= 0 && nextX < grid[0].length && nextY >= 0 && nextY < grid.length; 54 | 55 | if (!inBounds) { 56 | return { 57 | outOfBounds: true, 58 | }; 59 | } 60 | 61 | // Check if next position hits a hashpoint 62 | if (hashpoints.has(`${nextX},${nextY}`) || extraPoint === nextPoint) { 63 | return { 64 | nextLocation: currentLocation, 65 | nextDirection: turns[currentDirection], 66 | outOfBounds: false, 67 | }; 68 | } 69 | 70 | // Valid move 71 | return { 72 | nextLocation: [nextX, nextY], 73 | nextDirection: currentDirection, 74 | outOfBounds: false, 75 | }; 76 | } 77 | 78 | function a() { 79 | // Set of visited points, include direction (N, S, E, W) 80 | const visitedPoints = new Set(); 81 | 82 | let currentLocation = start; 83 | let currentDirection = "N" as Direction; 84 | 85 | while (true) { 86 | visitedPoints.add( 87 | `${currentLocation[0]},${currentLocation[1]},${currentDirection}` 88 | ); 89 | 90 | const next = getNextLocation(currentLocation, currentDirection); 91 | 92 | if (next.outOfBounds) break; 93 | 94 | currentLocation = next.nextLocation; 95 | currentDirection = next.nextDirection; 96 | } 97 | 98 | const pointsNoDirection = new Set(); 99 | 100 | visitedPoints.forEach((point) => { 101 | pointsNoDirection.add(point.split(",")[0] + "," + point.split(",")[1]); 102 | }); 103 | 104 | console.log("Part 1:", pointsNoDirection.size); 105 | } 106 | 107 | function gridHasLoop(newHashPoint: [number, number]) { 108 | const turnPoints = new Set(); 109 | 110 | if (hashpoints.has(`${newHashPoint[0]},${newHashPoint[1]}`)) { 111 | return false; 112 | } 113 | 114 | const nhp = `${newHashPoint[0]},${newHashPoint[1]}`; 115 | 116 | let currentLocation = start; 117 | let currentDirection = "N" as Direction; 118 | 119 | while (true) { 120 | const next = getNextLocation(currentLocation, currentDirection, nhp); 121 | if (next.outOfBounds) return false; 122 | 123 | // Only track points where we turn 124 | if (next.nextDirection !== currentDirection) { 125 | const key = `${currentLocation[0]},${currentLocation[1]},${currentDirection}`; 126 | if (turnPoints.has(key)) { 127 | return true; 128 | } 129 | turnPoints.add(key); 130 | } 131 | 132 | currentLocation = next.nextLocation; 133 | currentDirection = next.nextDirection; 134 | } 135 | } 136 | 137 | function b() { 138 | let loopCount = 0; 139 | for (let y = 0; y < grid.length; y++) { 140 | for (let x = 0; x < grid[y].length; x++) { 141 | if (gridHasLoop([x, y])) { 142 | loopCount++; 143 | } 144 | } 145 | } 146 | 147 | console.log("Part 2:", loopCount); 148 | } 149 | 150 | a(); 151 | b(); 152 | -------------------------------------------------------------------------------- /16/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | let lines = fs.readFileSync("input.txt", "utf8").split("\n"); 4 | // lines = fs.readFileSync("example.txt", "utf8").split("\n"); 5 | 6 | const grid = lines.map((line) => line.split("")); 7 | 8 | let start = [-1, -1]; 9 | let end = [-1, -1]; 10 | 11 | grid.forEach((row, y) => { 12 | row.forEach((cell, x) => { 13 | if (cell === "S") start = [x, y]; 14 | if (cell === "E") end = [x, y]; 15 | }); 16 | }); 17 | 18 | const directions = [ 19 | { dx: 1, dy: 0, name: "E" }, 20 | { dx: 0, dy: 1, name: "S" }, 21 | { dx: -1, dy: 0, name: "W" }, 22 | { dx: 0, dy: -1, name: "N" }, 23 | ] as const; 24 | 25 | function getTurnCost(from: number, to: number) { 26 | const diff = Math.abs(from - to); 27 | const turns = Math.min(diff, 4 - diff); 28 | return turns * 1000; 29 | } 30 | 31 | type StateKey = `${number},${number},${number}`; 32 | type Position = `${number},${number}`; 33 | function findLowestScore() { 34 | const queue: { cost: number; x: number; y: number; dir: number }[] = []; 35 | const visited = new Set(); 36 | 37 | queue.push({ cost: 0, x: start[0], y: start[1], dir: 0 }); 38 | 39 | while (queue.length > 0) { 40 | const { cost, x, y, dir } = queue.shift()!; 41 | const key: StateKey = `${x},${y},${dir}`; 42 | 43 | if (x === end[0] && y === end[1]) { 44 | return cost; 45 | } 46 | 47 | if (visited.has(key)) continue; 48 | visited.add(key); 49 | 50 | directions.forEach((newDir, newDirIndex) => { 51 | const newX = x + newDir.dx; 52 | const newY = y + newDir.dy; 53 | 54 | if (newX < 0 || newY < 0 || newX >= grid[0].length || newY >= grid.length) 55 | return; 56 | if (grid[newY][newX] === "#") return; 57 | 58 | const moveCost = 1; 59 | const turnCost = getTurnCost(dir, newDirIndex); 60 | const totalCost = cost + moveCost + turnCost; 61 | 62 | queue.push({ cost: totalCost, x: newX, y: newY, dir: newDirIndex }); 63 | queue.sort((a, b) => a.cost - b.cost); 64 | }); 65 | } 66 | 67 | return Infinity; 68 | } 69 | 70 | function part1() { 71 | console.log("Part 1:", findLowestScore()); 72 | } 73 | 74 | function part2() { 75 | const minEndCost = findLowestScore(); 76 | 77 | const costs = new Map(); 78 | const optimalPaths = new Set(); 79 | const paths = new Map(); 80 | 81 | const queue: { 82 | cost: number; 83 | x: number; 84 | y: number; 85 | dir: number; 86 | path: Position[]; 87 | }[] = []; 88 | 89 | queue.push({ 90 | cost: 0, 91 | x: start[0], 92 | y: start[1], 93 | dir: 0, 94 | path: [`${start[0]},${start[1]}`], 95 | }); 96 | 97 | while (queue.length > 0) { 98 | const { cost, x, y, dir, path } = queue.shift()!; 99 | const key: StateKey = `${x},${y},${dir}`; 100 | 101 | if (cost > minEndCost) continue; 102 | if (costs.has(key) && costs.get(key)! < cost) continue; 103 | 104 | costs.set(key, cost); 105 | paths.set(key, path); 106 | 107 | if (x === end[0] && y === end[1]) { 108 | path.forEach((pos) => optimalPaths.add(pos)); 109 | continue; 110 | } 111 | 112 | directions.forEach((newDir, newDirIndex) => { 113 | const newX = x + newDir.dx; 114 | const newY = y + newDir.dy; 115 | 116 | if (newX < 0 || newY < 0 || newX >= grid[0].length || newY >= grid.length) 117 | return; 118 | if (grid[newY][newX] === "#") return; 119 | 120 | const moveCost = 1; 121 | const turnCost = getTurnCost(dir, newDirIndex); 122 | const totalCost = cost + moveCost + turnCost; 123 | const newPos: Position = `${newX},${newY}`; 124 | 125 | const newPath = [...path, newPos]; 126 | const insertIndex = queue.findIndex(({ cost }) => cost > totalCost); 127 | if (insertIndex === -1) { 128 | queue.push({ 129 | cost: totalCost, 130 | x: newX, 131 | y: newY, 132 | dir: newDirIndex, 133 | path: newPath, 134 | }); 135 | } else { 136 | queue.splice(insertIndex, 0, { 137 | cost: totalCost, 138 | x: newX, 139 | y: newY, 140 | dir: newDirIndex, 141 | path: newPath, 142 | }); 143 | } 144 | }); 145 | } 146 | 147 | console.log("Part 2:", optimalPaths.size); 148 | } 149 | 150 | part1(); 151 | part2(); 152 | -------------------------------------------------------------------------------- /8/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const input = fs.readFileSync("input.txt", "utf8").split("\n"); 4 | // input = fs.readFileSync("example.txt", "utf8").split("\n"); 5 | 6 | const rows = input.length; 7 | const cols = input[0].length; 8 | 9 | // Collect antennas by frequency 10 | const antennae: Record = {}; 11 | for (let r = 0; r < rows; r++) { 12 | for (let c = 0; c < cols; c++) { 13 | const ch = input[r][c]; 14 | if (ch !== ".") { 15 | if (!antennae[ch]) antennae[ch] = []; 16 | antennae[ch].push({ r, c }); 17 | } 18 | } 19 | } 20 | 21 | function gcd(a: number, b: number): number { 22 | if (b === 0) return Math.abs(a); 23 | return gcd(b, a % b); 24 | } 25 | 26 | function a() { 27 | const antinodes = new Set(); 28 | 29 | for (const freq in antennae) { 30 | const coords = antennae[freq]; 31 | 32 | for (let i = 0; i < coords.length; i++) { 33 | for (let j = i + 1; j < coords.length; j++) { 34 | const { r: r1, c: c1 } = coords[i]; 35 | const { r: r2, c: c2 } = coords[j]; 36 | // Compute antinode positions for part 1: 37 | // antinode1 = (2*r2 - r1, 2*c2 - c1) 38 | // antinode2 = (2*r1 - r2, 2*c1 - c2) 39 | const a1r = 2 * r2 - r1; 40 | const a1c = 2 * c2 - c1; 41 | const a2r = 2 * r1 - r2; 42 | const a2c = 2 * c1 - c2; 43 | 44 | if (a1r >= 0 && a1r < rows && a1c >= 0 && a1c < cols) { 45 | antinodes.add(`${a1r},${a1c}`); 46 | } 47 | if (a2r >= 0 && a2r < rows && a2c >= 0 && a2c < cols) { 48 | antinodes.add(`${a2r},${a2c}`); 49 | } 50 | } 51 | } 52 | } 53 | 54 | console.log("part 1:", antinodes.size); 55 | } 56 | 57 | function b() { 58 | const antinodes = new Set(); 59 | 60 | for (const freq in antennae) { 61 | const coords = antennae[freq]; 62 | if (coords.length < 2) { 63 | // No lines can be formed if only one antenna of that frequency 64 | continue; 65 | } 66 | 67 | // Lines: represented by direction (dr, dc) and offset K (from normal form) 68 | const lines = new Map(); 69 | 70 | for (let i = 0; i < coords.length; i++) { 71 | for (let j = i + 1; j < coords.length; j++) { 72 | const { r: r1, c: c1 } = coords[i]; 73 | const { r: r2, c: c2 } = coords[j]; 74 | 75 | let dr = r2 - r1; 76 | let dc = c2 - c1; 77 | const g = gcd(dr, dc); 78 | dr /= g; 79 | dc /= g; 80 | 81 | // Fix direction for uniqueness 82 | if (dr < 0 || (dr === 0 && dc < 0)) { 83 | dr = -dr; 84 | dc = -dc; 85 | } 86 | 87 | // Line normal form: dc*r - dr*c = K 88 | const K = dc * r1 - dr * c1; 89 | const lineKey = `${dr},${dc},${K}`; 90 | // Store a representative point for this line 91 | if (!lines.has(lineKey)) { 92 | lines.set(lineKey, { r: r1, c: c1 }); 93 | } 94 | } 95 | } 96 | 97 | // For each line, generate all points within the grid 98 | for (const [lineKey, point] of lines) { 99 | const [drStr, dcStr, KStr] = lineKey.split(","); 100 | const dr = parseInt(drStr, 10); 101 | const dc = parseInt(dcStr, 10); 102 | const { r: r0, c: c0 } = point; 103 | 104 | // Find valid m range: (r, c) = (r0 + m*dr, c0 + m*dc) 105 | function rangeForOneDimension( 106 | pos: number, 107 | d: number, 108 | limit: number 109 | ): [number, number] { 110 | if (d === 0) { 111 | // Must remain pos in range for all m 112 | if (pos < 0 || pos >= limit) return [1, -1]; // empty range 113 | return [-Infinity, Infinity]; 114 | } else if (d > 0) { 115 | const minM = Math.ceil(-pos / d); 116 | const maxM = Math.floor((limit - 1 - pos) / d); 117 | return [minM, maxM]; 118 | } else { 119 | // d < 0 120 | const minM = Math.ceil((limit - 1 - pos) / d); 121 | const maxM = Math.floor(-pos / d); 122 | return [minM, maxM]; 123 | } 124 | } 125 | 126 | const [minMr, maxMr] = rangeForOneDimension(r0, dr, rows); 127 | const [minMc, maxMc] = rangeForOneDimension(c0, dc, cols); 128 | const minM = Math.max(minMr, minMc); 129 | const maxM = Math.min(maxMr, maxMc); 130 | 131 | if (minM <= maxM) { 132 | for (let m = minM; m <= maxM; m++) { 133 | const rr = r0 + m * dr; 134 | const cc = c0 + m * dc; 135 | antinodes.add(`${rr},${cc}`); 136 | } 137 | } 138 | } 139 | } 140 | 141 | console.log("part 2:", antinodes.size); 142 | } 143 | 144 | a(); 145 | b(); 146 | -------------------------------------------------------------------------------- /12/index.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | const lines = fs.readFileSync("input.txt", "utf8").trim().split("\n"); 4 | // const lines = fs.readFileSync("example.txt", "utf8").trim().split("\n"); 5 | const grid = lines.map((line) => line.split("")); 6 | 7 | const height = grid.length; 8 | const width = grid[0].length; 9 | 10 | function inBounds(x: number, y: number) { 11 | return x >= 0 && x < width && y >= 0 && y < height; 12 | } 13 | 14 | const directions = [ 15 | [0, -1], 16 | [1, 0], 17 | [0, 1], 18 | [-1, 0], 19 | ]; 20 | 21 | const visited = Array.from({ length: height }, () => Array(width).fill(false)); 22 | const regions: { cells: { x: number; y: number }[]; char: string }[] = []; 23 | 24 | for (let y = 0; y < height; y++) { 25 | for (let x = 0; x < width; x++) { 26 | if (!visited[y][x]) { 27 | visited[y][x] = true; 28 | const character = grid[y][x]; 29 | const cells = [{ x, y }]; 30 | const queue = [{ x, y }]; 31 | 32 | while (queue.length) { 33 | const { x: cx, y: cy } = queue.shift()!; 34 | directions.forEach(([dx, dy]) => { 35 | const nx = cx + dx; 36 | const ny = cy + dy; 37 | if ( 38 | inBounds(nx, ny) && 39 | !visited[ny][nx] && 40 | grid[ny][nx] === character 41 | ) { 42 | visited[ny][nx] = true; 43 | cells.push({ x: nx, y: ny }); 44 | queue.push({ x: nx, y: ny }); 45 | } 46 | }); 47 | } 48 | 49 | regions.push({ cells, char: character }); 50 | } 51 | } 52 | } 53 | 54 | function computePerimeter(region: { cells: { x: number; y: number }[] }) { 55 | const cellSet = new Set(region.cells.map((c) => `${c.x},${c.y}`)); 56 | let perimeter = 0; 57 | region.cells.forEach(({ x, y }) => { 58 | directions.forEach(([dx, dy]) => { 59 | const nx = x + dx; 60 | const ny = y + dy; 61 | if (!inBounds(nx, ny) || !cellSet.has(`${nx},${ny}`)) perimeter++; 62 | }); 63 | }); 64 | return perimeter; 65 | } 66 | 67 | function createUnionFind(elements: string[]) { 68 | const parent: Record = {}; 69 | const size: Record = {}; 70 | elements.forEach((e) => { 71 | parent[e] = e; 72 | size[e] = 1; 73 | }); 74 | 75 | function find(element: string) { 76 | let p = element; 77 | while (p !== parent[p]) { 78 | parent[p] = parent[parent[p]]; 79 | p = parent[p]; 80 | } 81 | return p; 82 | } 83 | 84 | function union(a: string, b: string) { 85 | const rootA = find(a); 86 | const rootB = find(b); 87 | if (rootA !== rootB) { 88 | if (size[rootA] < size[rootB]) { 89 | parent[rootA] = rootB; 90 | size[rootB] += size[rootA]; 91 | } else { 92 | parent[rootB] = rootA; 93 | size[rootA] += size[rootB]; 94 | } 95 | } 96 | } 97 | 98 | function countComponents() { 99 | const roots = elements.map(find); 100 | return new Set(roots).size; 101 | } 102 | 103 | return { union, countComponents }; 104 | } 105 | 106 | function computeSides(region: { cells: { x: number; y: number }[] }) { 107 | const cellSet = new Set(region.cells.map((c) => `${c.x},${c.y}`)); 108 | 109 | const leftEdges: string[] = []; 110 | const rightEdges: string[] = []; 111 | const topEdges: string[] = []; 112 | const bottomEdges: string[] = []; 113 | 114 | region.cells.forEach(({ x, y }) => { 115 | const key = `${x},${y}`; 116 | if (!cellSet.has(`${x - 1},${y}`)) leftEdges.push(key); 117 | if (!cellSet.has(`${x + 1},${y}`)) rightEdges.push(key); 118 | if (!cellSet.has(`${x},${y - 1}`)) topEdges.push(key); 119 | if (!cellSet.has(`${x},${y + 1}`)) bottomEdges.push(key); 120 | }); 121 | 122 | function parseKey(k: string) { 123 | const [X, Y] = k.split(",").map(Number); 124 | return [X, Y] as [number, number]; 125 | } 126 | 127 | function unionLineSegments(elements: string[], vertical: boolean) { 128 | if (!elements.length) return 0; 129 | const uf = createUnionFind(elements); 130 | const positions = new Set(elements); 131 | elements.forEach((element) => { 132 | const [X, Y] = parseKey(element); 133 | if (vertical) { 134 | const up = `${X},${Y - 1}`; 135 | const down = `${X},${Y + 1}`; 136 | if (positions.has(up)) uf.union(element, up); 137 | if (positions.has(down)) uf.union(element, down); 138 | } else { 139 | const left = `${X - 1},${Y}`; 140 | const right = `${X + 1},${Y}`; 141 | if (positions.has(left)) uf.union(element, left); 142 | if (positions.has(right)) uf.union(element, right); 143 | } 144 | }); 145 | return uf.countComponents(); 146 | } 147 | 148 | return ( 149 | unionLineSegments(leftEdges, true) + 150 | unionLineSegments(rightEdges, true) + 151 | unionLineSegments(topEdges, false) + 152 | unionLineSegments(bottomEdges, false) 153 | ); 154 | } 155 | 156 | function part1() { 157 | let total = 0; 158 | regions.forEach((region) => { 159 | const area = region.cells.length; 160 | total += area * computePerimeter(region); 161 | }); 162 | console.log("Part 1:", total); 163 | } 164 | 165 | function part2() { 166 | let total = 0; 167 | regions.forEach((region) => { 168 | const area = region.cells.length; 169 | total += area * computeSides(region); 170 | }); 171 | console.log("Part 2:", total); 172 | } 173 | 174 | part1(); 175 | part2(); 176 | --------------------------------------------------------------------------------