advent of code 2025 in ts and nix
at main 10 kB view raw
1const file = await Bun.file("../../shared/04/input.txt").text(); 2const paperMap: boolean[][] = file 3 .trim() 4 .split("\n") 5 .map((line) => 6 Array.from(line, (ch) => { 7 if (ch === ".") return false; 8 if (ch === "@") return true; 9 10 throw new Error(`Unexpected character '${ch}' in input.`); 11 }), 12 ); 13 14function accessiblePapers(map: boolean[][]): { 15 map: boolean[][]; 16 accessible: number; 17} { 18 let accessible = 0; 19 const nextMap: boolean[][] = map.map((row) => row.slice()); 20 map.forEach((rows, row) => { 21 rows.forEach((cell, col) => { 22 if (cell) { 23 let fullAdj = 0; 24 25 const offsets: { row: number; col: number }[] = [ 26 // cardinal 27 { row: -1, col: 0 }, 28 { row: 1, col: 0 }, 29 { row: 0, col: 1 }, 30 { row: 0, col: -1 }, 31 // diagonals 32 { row: -1, col: 1 }, 33 { row: 1, col: 1 }, 34 { row: -1, col: -1 }, 35 { row: 1, col: -1 }, 36 ]; 37 38 for (const off of offsets) { 39 const rowIdx = row + off.row; 40 const colIdx = col + off.col; 41 42 if (rowIdx < 0 || colIdx < 0) continue; 43 if (rowIdx > paperMap.length - 1 || colIdx > rows.length - 1) 44 continue; 45 46 if (map.at(rowIdx)?.at(colIdx)) fullAdj++; 47 48 if (fullAdj >= 4) break; 49 } 50 51 if (fullAdj < 4) { 52 accessible++; 53 (nextMap[row] as boolean[])[col] = false; 54 } 55 } 56 }); 57 }); 58 59 return { map: nextMap, accessible }; 60} 61 62// Collect all stages 63const stages: { map: boolean[][]; accessible: number; iteration: number }[] = 64 []; 65let map = paperMap; 66let iteration = 0; 67 68stages.push({ map: JSON.parse(JSON.stringify(map)), accessible: 0, iteration }); 69 70while (true) { 71 const res = accessiblePapers(map); 72 iteration++; 73 74 stages.push({ 75 map: JSON.parse(JSON.stringify(res.map)), 76 accessible: res.accessible, 77 iteration, 78 }); 79 80 map = res.map; 81 if (res.accessible === 0) break; 82} 83 84// Generate HTML 85const html = `<!DOCTYPE html> 86<html lang="en"> 87<head> 88 <meta charset="UTF-8"> 89 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 90 <title>AoC 2025 Day 4 - Paper Removal Visualization</title> 91 <style> 92 body { 93 background: #1e1e2e; 94 color: #cdd6f4; 95 font-family: "Source Code Pro", monospace; 96 font-size: 14pt; 97 font-weight: 300; 98 padding: 20px; 99 display: flex; 100 flex-direction: column; 101 align-items: center; 102 min-height: 100vh; 103 } 104 a { 105 text-decoration: none; 106 color: #a6e3a1; 107 outline: 0; 108 } 109 a:hover, a:focus { 110 background-color: #181825 !important; 111 } 112 h1 { 113 color: #a6e3a1; 114 text-shadow: 0 0 2px #a6e3a1, 0 0 5px #a6e3a1; 115 margin-bottom: 10px; 116 font-size: 1em; 117 font-weight: normal; 118 } 119 .controls { 120 margin: 20px 0; 121 display: flex; 122 gap: 10px; 123 align-items: center; 124 flex-wrap: wrap; 125 justify-content: center; 126 } 127 button { 128 background: #11111b; 129 color: #a6e3a1; 130 border: 1px solid #313244; 131 padding: 8px 16px; 132 cursor: pointer; 133 font-family: inherit; 134 font-size: 14px; 135 } 136 button:hover { 137 background: #181825; 138 } 139 button:disabled { 140 opacity: 0.5; 141 cursor: not-allowed; 142 } 143 .info { 144 color: #f9e2af; 145 font-size: 14px; 146 margin: 10px 0; 147 } 148 .speed-control { 149 display: flex; 150 align-items: center; 151 gap: 8px; 152 } 153 .speed-control input[type="range"] { 154 -webkit-appearance: none; 155 appearance: none; 156 width: 120px; 157 height: 6px; 158 background: #313244; 159 outline: none; 160 border: 1px solid #313244; 161 } 162 .speed-control input[type="range"]::-webkit-slider-thumb { 163 -webkit-appearance: none; 164 appearance: none; 165 width: 16px; 166 height: 16px; 167 background: #a6e3a1; 168 cursor: pointer; 169 border: 1px solid #313244; 170 } 171 .speed-control input[type="range"]::-moz-range-thumb { 172 width: 16px; 173 height: 16px; 174 background: #a6e3a1; 175 cursor: pointer; 176 border: 1px solid #313244; 177 } 178 .speed-control input[type="range"]::-webkit-slider-thumb:hover { 179 background: #b4e7b9; 180 } 181 .speed-control input[type="range"]::-moz-range-thumb:hover { 182 background: #b4e7b9; 183 } 184 .grid-container { 185 background: #11111b; 186 padding: 10px; 187 border: 2px solid #313244; 188 border-radius: 4px; 189 flex: 1; 190 display: flex; 191 align-items: center; 192 justify-content: center; 193 width: 100%; 194 max-width: 95vw; 195 overflow: hidden; 196 } 197 .grid { 198 display: grid; 199 gap: 1px; 200 image-rendering: pixelated; 201 } 202 .cell { 203 background: #181825; 204 display: flex; 205 align-items: center; 206 justify-content: center; 207 font-size: 14px; 208 } 209 .cell.paper { 210 background: #a6e3a1; 211 color: #1e1e2e; 212 } 213 .stats { 214 margin-top: 20px; 215 color: #a6adc8; 216 text-align: center; 217 font-size: 13px; 218 } 219 .footer { 220 margin-top: 20px; 221 color: #a6adc8; 222 text-align: center; 223 font-size: 12px; 224 } 225 </style> 226</head> 227<body> 228 <h1>AoC 2025 Day 4 - Paper Removal Visualization</h1> 229 230 <div class="controls"> 231 <button id="prev">← Previous</button> 232 <button id="play" data-playing="false">▶ Play</button> 233 <button id="next">Next →</button> 234 <button id="reset">↺ Reset</button> 235 <span class="speed-control"> 236 <label for="speed">Speed:</label> 237 <input type="range" id="speed" min="100" max="1000" value="500" step="50"> 238 </span> 239 </div> 240 241 <div class="info"> 242 Stage: <span id="stage">0</span> / <span id="total">${stages.length - 1}</span> 243 | Accessible: <span id="accessible">0</span> 244 | Total Removed: <span id="totalRemoved">0</span> 245 </div> 246 247 <div class="grid-container"> 248 <div id="grid" class="grid"></div> 249 </div> 250 251 <div class="stats"> 252 Grid size: ${paperMap.length} × ${paperMap[0].length} 253 | Total iterations: ${stages.length - 1} 254 | Final answer: ${stages.slice(1).reduce((sum, s) => sum + s.accessible, 0)} 255 </div> 256 257 <div class="footer"> 258 <a href="../index.html">[Return to Index]</a> 259 </div> 260 261 <script> 262 const stages = ${JSON.stringify(stages)}; 263 let currentStage = 0; 264 let playInterval = null; 265 266 const grid = document.getElementById('grid'); 267 const stageEl = document.getElementById('stage'); 268 const accessibleEl = document.getElementById('accessible'); 269 const totalRemovedEl = document.getElementById('totalRemoved'); 270 const prevBtn = document.getElementById('prev'); 271 const nextBtn = document.getElementById('next'); 272 const playBtn = document.getElementById('play'); 273 const resetBtn = document.getElementById('reset'); 274 const speedSlider = document.getElementById('speed'); 275 276 function renderGrid() { 277 const stage = stages[currentStage]; 278 const numRows = stage.map.length; 279 const numCols = stage.map[0].length; 280 281 // Only rebuild grid if it doesn't exist 282 if (grid.children.length === 0) { 283 grid.style.gridTemplateColumns = \`repeat(\${numCols}, 1fr)\`; 284 const totalCells = numRows * numCols; 285 for (let i = 0; i < totalCells; i++) { 286 const cell = document.createElement('div'); 287 cell.className = 'cell'; 288 grid.appendChild(cell); 289 } 290 } 291 292 // Update cell states 293 const cells = grid.children; 294 for (let row = 0; row < numRows; row++) { 295 for (let col = 0; col < numCols; col++) { 296 const idx = row * numCols + col; 297 const cell = cells[idx]; 298 if (stage.map[row][col]) { 299 cell.classList.add('paper'); 300 cell.textContent = '@'; 301 } else { 302 cell.classList.remove('paper'); 303 cell.textContent = ''; 304 } 305 } 306 } 307 308 scaleGrid(); 309 310 stageEl.textContent = currentStage; 311 accessibleEl.textContent = stage.accessible; 312 313 const totalRemoved = stages.slice(1, currentStage + 1).reduce((sum, s) => sum + s.accessible, 0); 314 totalRemovedEl.textContent = totalRemoved; 315 316 prevBtn.disabled = currentStage === 0; 317 nextBtn.disabled = currentStage === stages.length - 1; 318 } 319 320 function scaleGrid() { 321 const container = document.querySelector('.grid-container'); 322 const stage = stages[currentStage]; 323 const numRows = stage.map.length; 324 const numCols = stage.map[0].length; 325 326 const containerWidth = container.clientWidth; 327 const containerHeight = container.clientHeight; 328 329 const cellWidth = Math.floor((containerWidth - numCols) / numCols); 330 const cellHeight = Math.floor((containerHeight - numRows) / numRows); 331 332 const cellSize = Math.max(1, Math.min(cellWidth, cellHeight, 20)); 333 334 const cells = grid.children; 335 for (let i = 0; i < cells.length; i++) { 336 cells[i].style.width = cellSize + 'px'; 337 cells[i].style.height = cellSize + 'px'; 338 cells[i].style.fontSize = Math.max(10, cellSize * 0.5) + 'px'; 339 } 340 } 341 342 function goToStage(index) { 343 currentStage = Math.max(0, Math.min(stages.length - 1, index)); 344 renderGrid(); 345 } 346 347 function resetAnimation() { 348 goToStage(0); 349 } 350 351 prevBtn.addEventListener('click', () => goToStage(currentStage - 1)); 352 nextBtn.addEventListener('click', () => goToStage(currentStage + 1)); 353 resetBtn.addEventListener('click', resetAnimation); 354 355 playBtn.addEventListener('click', () => { 356 if (playInterval) { 357 clearInterval(playInterval); 358 playInterval = null; 359 playBtn.textContent = '▶ Play'; 360 } else { 361 if (currentStage === stages.length - 1) { 362 resetAnimation(); 363 } 364 playBtn.textContent = '⏸ Pause'; 365 const speed = 1100 - parseInt(speedSlider.value); 366 playInterval = setInterval(() => { 367 if (currentStage < stages.length - 1) { 368 goToStage(currentStage + 1); 369 } else { 370 clearInterval(playInterval); 371 playInterval = null; 372 playBtn.textContent = '▶ Play'; 373 } 374 }, speed); 375 } 376 }); 377 378 speedSlider.addEventListener('input', () => { 379 if (playInterval) { 380 clearInterval(playInterval); 381 const speed = 1100 - parseInt(speedSlider.value); 382 playInterval = setInterval(() => { 383 if (currentStage < stages.length - 1) { 384 goToStage(currentStage + 1); 385 } else { 386 clearInterval(playInterval); 387 playInterval = null; 388 playBtn.textContent = '▶ Play'; 389 } 390 }, speed); 391 } 392 }); 393 394 // Keyboard controls 395 document.addEventListener('keydown', (e) => { 396 if (e.key === 'ArrowLeft') prevBtn.click(); 397 if (e.key === 'ArrowRight') nextBtn.click(); 398 if (e.key === ' ') { 399 e.preventDefault(); 400 playBtn.click(); 401 } 402 if (e.key === 'r' || e.key === 'R') resetBtn.click(); 403 }); 404 405 // Rescale on window resize 406 window.addEventListener('resize', scaleGrid); 407 408 renderGrid(); 409 </script> 410</body> 411</html>`; 412 413await Bun.write("index.html", html); 414console.log("Generated index.html with", stages.length, "stages");