advent of code 2025 in ts and nix
at main 20 kB view raw
1const scriptDir = import.meta.dir; 2const file = await Bun.file(`${scriptDir}/../../shared/07/input.txt`).text(); 3const grid: string[][] = file 4 .trim() 5 .split("\n") 6 .map((line) => Array.from(line)); 7 8const ROWS = grid.length; 9const COLS = grid[0].length; 10 11// Find starting position 12let startRow = -1; 13let startCol = -1; 14for (let r = 0; r < ROWS; r++) { 15 const c = grid[r].indexOf("S"); 16 if (c !== -1) { 17 startRow = r; 18 startCol = c; 19 break; 20 } 21} 22 23function inBounds(r: number, c: number): boolean { 24 return r >= 0 && r < ROWS && c >= 0 && c < COLS; 25} 26 27// ============ PART 1: Track unique beam positions ============ 28type BeamPosition = [number, number]; 29type Stage1 = { 30 beams: BeamPosition[]; 31 splitCount: number; 32 totalSplits: number; 33 step: number; 34}; 35 36const stages1: Stage1[] = []; 37let active1: BeamPosition[] = [[startRow, startCol]]; 38let totalSplits1 = 0; 39 40stages1.push({ 41 beams: [...active1], 42 splitCount: 0, 43 totalSplits: 0, 44 step: 0, 45}); 46 47while (active1.length > 0) { 48 const nextActive: BeamPosition[] = []; 49 let splitThisStep = 0; 50 51 for (const [r, c] of active1) { 52 const nr = r + 1; 53 const nc = c; 54 55 if (!inBounds(nr, nc)) continue; 56 57 const cell = grid[nr][nc]; 58 59 if (cell === ".") { 60 nextActive.push([nr, nc]); 61 } else if (cell === "^") { 62 splitThisStep++; 63 totalSplits1++; 64 65 const leftC = nc - 1; 66 const rightC = nc + 1; 67 68 if (inBounds(nr, leftC)) nextActive.push([nr, leftC]); 69 if (inBounds(nr, rightC)) nextActive.push([nr, rightC]); 70 } else { 71 nextActive.push([nr, nc]); 72 } 73 } 74 75 const uniqueBeams = [...new Set(nextActive.map(([r, c]) => `${r},${c}`))].map( 76 (s) => s.split(",").map(Number) as BeamPosition, 77 ); 78 79 if (uniqueBeams.length > 0) { 80 stages1.push({ 81 beams: uniqueBeams, 82 splitCount: splitThisStep, 83 totalSplits: totalSplits1, 84 step: stages1.length, 85 }); 86 } 87 88 active1 = uniqueBeams; 89} 90 91// ============ PART 2: Track timeline counts per position ============ 92type Stage2 = { 93 positions: Record<string, number>; // "r,c" -> count 94 totalTimelines: number; 95 step: number; 96}; 97 98const stages2: Stage2[] = []; 99let currentStates: Record<string, number> = { [`${startRow},${startCol}`]: 1 }; 100 101stages2.push({ 102 positions: { ...currentStates }, 103 totalTimelines: 1, 104 step: 0, 105}); 106 107for (let step = 0; step < ROWS; step++) { 108 const nextStates: Record<string, number> = {}; 109 110 for (const [key, count] of Object.entries(currentStates)) { 111 const [r, c] = key.split(",").map(Number); 112 const nr = r + 1; 113 114 if (nr >= ROWS) continue; 115 if (!inBounds(nr, c)) continue; 116 117 const cell = grid[nr][c]; 118 119 if (cell === ".") { 120 const nkey = `${nr},${c}`; 121 nextStates[nkey] = (nextStates[nkey] || 0) + count; 122 } else if (cell === "^") { 123 const lc = c - 1; 124 const rc = c + 1; 125 if (inBounds(nr, lc)) { 126 const lkey = `${nr},${lc}`; 127 nextStates[lkey] = (nextStates[lkey] || 0) + count; 128 } 129 if (inBounds(nr, rc)) { 130 const rkey = `${nr},${rc}`; 131 nextStates[rkey] = (nextStates[rkey] || 0) + count; 132 } 133 } else { 134 const nkey = `${nr},${c}`; 135 nextStates[nkey] = (nextStates[nkey] || 0) + count; 136 } 137 } 138 139 if (Object.keys(nextStates).length === 0) break; 140 141 const totalTimelines = Object.values(nextStates).reduce((a, b) => a + b, 0); 142 stages2.push({ 143 positions: { ...nextStates }, 144 totalTimelines, 145 step: stages2.length, 146 }); 147 148 currentStates = nextStates; 149} 150 151const finalTimelines = stages2[stages2.length - 1]?.totalTimelines ?? 1; 152 153// Generate HTML 154const html = `<!DOCTYPE html> 155<html lang="en"> 156<head> 157 <meta charset="UTF-8"> 158 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 159 <title>AoC 2025 Day 7 - Timeline Beam Splitting</title> 160 <style> 161 * { 162 box-sizing: border-box; 163 } 164 body { 165 background: #1e1e2e; 166 color: #cdd6f4; 167 font-family: "Source Code Pro", monospace; 168 font-size: 14pt; 169 font-weight: 300; 170 padding: 20px; 171 display: flex; 172 flex-direction: column; 173 align-items: center; 174 margin: 0; 175 } 176 a { 177 text-decoration: none; 178 color: #a6e3a1; 179 outline: 0; 180 } 181 a:hover, a:focus { 182 background-color: #181825 !important; 183 } 184 h1 { 185 color: #a6e3a1; 186 text-shadow: 0 0 2px #a6e3a1, 0 0 5px #a6e3a1; 187 margin-bottom: 10px; 188 font-size: 1em; 189 font-weight: normal; 190 } 191 .controls { 192 margin: 15px 0; 193 display: flex; 194 gap: 10px; 195 align-items: center; 196 flex-wrap: wrap; 197 justify-content: center; 198 } 199 button { 200 background: #11111b; 201 color: #a6e3a1; 202 border: 1px solid #313244; 203 padding: 8px 16px; 204 cursor: pointer; 205 font-family: inherit; 206 font-size: 14px; 207 } 208 button:hover { 209 background: #181825; 210 } 211 button:disabled { 212 opacity: 0.5; 213 cursor: not-allowed; 214 } 215 .info { 216 color: #f9e2af; 217 font-size: 14px; 218 margin: 10px 0; 219 } 220 .mode-toggle { 221 display: flex; 222 align-items: center; 223 gap: 0; 224 margin: 10px 0; 225 border: 1px solid #313244; 226 background: #11111b; 227 } 228 .mode-toggle label { 229 cursor: pointer; 230 padding: 8px 16px; 231 font-size: 14px; 232 transition: all 0.2s ease; 233 border-right: 1px solid #313244; 234 } 235 .mode-toggle label:last-child { 236 border-right: none; 237 } 238 .mode-toggle label.active { 239 background: #313244; 240 color: #a6e3a1; 241 } 242 .mode-toggle input[type="checkbox"] { 243 display: none; 244 } 245 .grid-container { 246 background: #11111b; 247 padding: 10px; 248 border: 2px solid #313244; 249 border-radius: 4px; 250 display: flex; 251 align-items: center; 252 justify-content: center; 253 width: 100%; 254 max-width: 100%; 255 overflow: auto; 256 } 257 .grid { 258 display: grid; 259 gap: 1px; 260 image-rendering: pixelated; 261 } 262 .cell { 263 background: #181825; 264 display: flex; 265 align-items: center; 266 justify-content: center; 267 font-size: 14px; 268 transition: background 0.1s ease; 269 position: relative; 270 color: #1e1e2e; 271 } 272 .cell.splitter { 273 background: #fab387; 274 color: #1e1e2e; 275 } 276 .cell.start { 277 background: #f9e2af; 278 color: #1e1e2e; 279 font-weight: bold; 280 } 281 .cell.beam { 282 background: #a6e3a1; 283 color: #1e1e2e; 284 font-weight: bold; 285 font-size: 10px; 286 } 287 .cell.beam-trail { 288 background: rgba(166, 227, 161, 0.25); 289 } 290 @keyframes pulse { 291 from { box-shadow: 0 0 8px #a6e3a1, 0 0 16px #a6e3a1; } 292 to { box-shadow: 0 0 12px #a6e3a1, 0 0 24px #a6e3a1; } 293 } 294 .stats { 295 margin-top: 20px; 296 color: #a6adc8; 297 text-align: center; 298 font-size: 13px; 299 } 300 .legend { 301 display: flex; 302 gap: 20px; 303 margin-top: 15px; 304 flex-wrap: wrap; 305 justify-content: center; 306 } 307 .legend-item { 308 display: flex; 309 align-items: center; 310 gap: 6px; 311 font-size: 12px; 312 color: #a6adc8; 313 } 314 .legend-box { 315 width: 16px; 316 height: 16px; 317 } 318 .legend-box.start { background: #f9e2af; } 319 .legend-box.splitter { background: #fab387; } 320 .legend-box.beam { background: #a6e3a1; box-shadow: 0 0 4px #a6e3a1; } 321 .legend-box.empty { background: #181825; border: 1px solid #313244; } 322 .footer { 323 margin-top: 20px; 324 color: #a6adc8; 325 text-align: center; 326 font-size: 12px; 327 } 328 .speed-control { 329 display: flex; 330 align-items: center; 331 gap: 8px; 332 } 333 .speed-control input[type="range"] { 334 -webkit-appearance: none; 335 appearance: none; 336 width: 120px; 337 height: 6px; 338 background: #313244; 339 outline: none; 340 border: 1px solid #313244; 341 } 342 .speed-control input[type="range"]::-webkit-slider-thumb { 343 -webkit-appearance: none; 344 appearance: none; 345 width: 16px; 346 height: 16px; 347 background: #a6e3a1; 348 cursor: pointer; 349 border: 1px solid #313244; 350 } 351 .speed-control input[type="range"]::-moz-range-thumb { 352 width: 16px; 353 height: 16px; 354 background: #a6e3a1; 355 cursor: pointer; 356 border: 1px solid #313244; 357 } 358 .speed-control input[type="range"]::-webkit-slider-thumb:hover { 359 background: #b4e7b9; 360 } 361 .speed-control input[type="range"]::-moz-range-thumb:hover { 362 background: #b4e7b9; 363 } 364 </style> 365</head> 366<body> 367 <h1>AoC 2025 Day 7 - Timeline Beam Splitting</h1> 368 369 <div class="mode-toggle"> 370 <label id="part1Label">Part 1</label> 371 <label id="part2Label">Part 2</label> 372 </div> 373 374 <div style="color: #a6adc8; font-size: 12px; margin-bottom: 5px;">Contribution by <a href="https://jaspermayone.com">@jsp</a> ♥</div> 375 376 <div class="controls"> 377 <button id="prev">← Previous</button> 378 <button id="play" data-playing="false">▶ Play</button> 379 <button id="next">Next →</button> 380 <button id="reset">↺ Reset</button> 381 <span class="speed-control"> 382 <label for="speed">Speed:</label> 383 <input type="range" id="speed" min="100" max="1000" value="400" step="50"> 384 </span> 385 </div> 386 387 <div class="info" id="infoBar"> 388 Step: <span id="step">0</span> / <span id="total">${stages1.length - 1}</span> 389 | <span id="metricLabel">Active Beams</span>: <span id="metricValue">1</span> 390 | <span id="secondaryLabel">Total Splits</span>: <span id="secondaryValue">0</span> 391 </div> 392 393 <div class="grid-container"> 394 <div id="grid" class="grid"></div> 395 </div> 396 397 <div class="legend"> 398 <div class="legend-item"><div class="legend-box start"></div> Start (S)</div> 399 <div class="legend-item"><div class="legend-box splitter"></div> Splitter (^)</div> 400 <div class="legend-item"><div class="legend-box beam"></div> Active Beam</div> 401 <div class="legend-item"><div class="legend-box empty"></div> Empty (.)</div> 402 </div> 403 404 <div class="stats" id="statsBar"> 405 Grid: ${ROWS} × ${COLS} 406 | Total steps: ${stages1.length - 1} 407 | Final beams: ${stages1[stages1.length - 1]?.beams.length ?? 1} 408 | Total splits: ${totalSplits1} 409 </div> 410 411 <div class="footer"> 412 <a href="../index.html">[Return to Index]</a> 413 </div> 414 415 <script> 416 const grid = ${JSON.stringify(grid)}; 417 const stages1 = ${JSON.stringify(stages1)}; 418 const stages2 = ${JSON.stringify(stages2)}; 419 const ROWS = ${ROWS}; 420 const COLS = ${COLS}; 421 const totalSplits1 = ${totalSplits1}; 422 const finalTimelines = ${finalTimelines}; 423 424 let currentStage = 0; 425 let playInterval = null; 426 let beamTrail = new Set(); 427 let isPart2 = false; 428 let cellSize = 5; 429 let cellElements = []; 430 431 const gridEl = document.getElementById('grid'); 432 const stepEl = document.getElementById('step'); 433 const totalEl = document.getElementById('total'); 434 const metricLabelEl = document.getElementById('metricLabel'); 435 const metricValueEl = document.getElementById('metricValue'); 436 const secondaryLabelEl = document.getElementById('secondaryLabel'); 437 const secondaryValueEl = document.getElementById('secondaryValue'); 438 const prevBtn = document.getElementById('prev'); 439 const nextBtn = document.getElementById('next'); 440 const playBtn = document.getElementById('play'); 441 const resetBtn = document.getElementById('reset'); 442 const speedSlider = document.getElementById('speed'); 443 const part1Label = document.getElementById('part1Label'); 444 const part2Label = document.getElementById('part2Label'); 445 const statsBar = document.getElementById('statsBar'); 446 447 function getStages() { 448 return isPart2 ? stages2 : stages1; 449 } 450 451 function updateModeLabels() { 452 part1Label.classList.toggle('active', !isPart2); 453 part2Label.classList.toggle('active', isPart2); 454 455 if (isPart2) { 456 metricLabelEl.textContent = 'Total Timelines'; 457 secondaryLabelEl.textContent = 'Positions'; 458 statsBar.innerHTML = \`Grid: ${ROWS} × ${COLS} | Total steps: \${stages2.length - 1} | Final timelines: \${finalTimelines}\`; 459 } else { 460 metricLabelEl.textContent = 'Active Beams'; 461 secondaryLabelEl.textContent = 'Total Splits'; 462 statsBar.innerHTML = \`Grid: ${ROWS} × ${COLS} | Total steps: \${stages1.length - 1} | Final beams: \${stages1[stages1.length - 1]?.beams.length ?? 1} | Total splits: \${totalSplits1}\`; 463 } 464 465 totalEl.textContent = getStages().length - 1; 466 } 467 468 function initGrid() { 469 gridEl.style.gridTemplateColumns = \`repeat(\${COLS}, 1fr)\`; 470 cellElements = new Array(ROWS * COLS); 471 472 for (let r = 0; r < ROWS; r++) { 473 for (let c = 0; c < COLS; c++) { 474 const cell = document.createElement('div'); 475 const char = grid[r][c]; 476 const idx = r * COLS + c; 477 478 cell.className = 'cell'; 479 cell.dataset.row = r; 480 cell.dataset.col = c; 481 cell.dataset.char = char; 482 483 // Set static content once 484 if (char === 'S') { 485 cell.classList.add('start'); 486 } else if (char === '^') { 487 cell.classList.add('splitter'); 488 } 489 490 gridEl.appendChild(cell); 491 cellElements[idx] = cell; 492 } 493 } 494 495 scaleGrid(); 496 } 497 498 function renderGrid() { 499 const stages = getStages(); 500 const stage = stages[currentStage]; 501 502 let beamPositions = new Map(); 503 504 if (isPart2) { 505 for (const [key, count] of Object.entries(stage.positions)) { 506 beamPositions.set(key, count); 507 } 508 } else { 509 stage.beams.forEach(([r, c]) => beamPositions.set(r + ',' + c, 1)); 510 } 511 512 // Add current beams to trail 513 for (const key of beamPositions.keys()) { 514 beamTrail.add(key); 515 } 516 517 // Only update changed cells 518 const prevBeamKeys = new Set(); 519 if (currentStage > 0) { 520 const prevStage = stages[currentStage - 1]; 521 if (isPart2) { 522 Object.keys(prevStage.positions).forEach(k => prevBeamKeys.add(k)); 523 } else { 524 prevStage.beams.forEach(([r, c]) => prevBeamKeys.add(r + ',' + c)); 525 } 526 } 527 528 // Find cells that need updating 529 const toUpdate = new Set(); 530 beamPositions.forEach((_, key) => toUpdate.add(key)); 531 prevBeamKeys.forEach(key => toUpdate.add(key)); 532 533 // Update only changed cells 534 for (const key of toUpdate) { 535 const [r, c] = key.split(',').map(Number); 536 const idx = r * COLS + c; 537 const cell = cellElements[idx]; 538 const char = cell.dataset.char; 539 const count = beamPositions.get(key); 540 541 // Reset dynamic classes 542 cell.classList.remove('beam', 'beam-trail'); 543 544 if (count !== undefined) { 545 cell.classList.add('beam'); 546 547 // For Part 2, adjust opacity/intensity based on timeline count 548 if (isPart2 && count > 1) { 549 // Logarithmic scale for better visual range 550 const intensity = Math.min(1, Math.log10(count + 1) / 4); 551 cell.style.opacity = 0.3 + (intensity * 0.7); 552 553 // Only show small counts in upper portion to reduce clutter 554 if (count <= 99 && r < ROWS * 0.6) { 555 cell.textContent = count; 556 } else { 557 cell.textContent = ''; 558 } 559 } else { 560 cell.style.opacity = '1'; 561 cell.textContent = ''; 562 } 563 } else if (beamTrail.has(key) && currentStage > 0) { 564 cell.classList.add('beam-trail'); 565 cell.style.opacity = '1'; 566 cell.textContent = ''; 567 } else { 568 // Show static char 569 cell.style.opacity = '1'; 570 cell.textContent = (char === 'S' || char === '^') ? char : ''; 571 } 572 } 573 574 stepEl.textContent = currentStage; 575 576 if (isPart2) { 577 metricValueEl.textContent = stage.totalTimelines; 578 secondaryValueEl.textContent = Object.keys(stage.positions).length; 579 } else { 580 metricValueEl.textContent = stage.beams.length; 581 secondaryValueEl.textContent = stage.totalSplits; 582 } 583 584 prevBtn.disabled = currentStage === 0; 585 nextBtn.disabled = currentStage === stages.length - 1; 586 } 587 588 function scaleGrid() { 589 const container = document.querySelector('.grid-container'); 590 const containerWidth = container.clientWidth * 0.9; // Account for padding (10px + 10px) + border (2px + 2px) 591 592 // Calculate cell size based on width to fill the container 593 const cellWidth = Math.floor(containerWidth / COLS); 594 595 // Set a reasonable max size for readability 596 cellSize = Math.max(2, Math.min(cellWidth, 15)); 597 598 const fontSize = Math.max(8, Math.min(cellSize * 0.7, 12)); 599 600 for (let i = 0; i < cellElements.length; i++) { 601 cellElements[i].style.width = cellSize + 'px'; 602 cellElements[i].style.height = cellSize + 'px'; 603 cellElements[i].style.fontSize = fontSize + 'px'; 604 } 605 } 606 607 function goToStage(index) { 608 const stages = getStages(); 609 // If going backwards, rebuild trail 610 if (index < currentStage) { 611 beamTrail = new Set(); 612 for (let i = 0; i <= index; i++) { 613 if (isPart2) { 614 Object.keys(stages[i].positions).forEach(key => beamTrail.add(key)); 615 } else { 616 stages[i].beams.forEach(([r, c]) => beamTrail.add(r + ',' + c)); 617 } 618 } 619 } 620 currentStage = Math.max(0, Math.min(stages.length - 1, index)); 621 renderGrid(); 622 } 623 624 function resetAnimation() { 625 beamTrail = new Set(); 626 goToStage(0); 627 } 628 629 function toggleMode() { 630 if (playInterval) { 631 playInterval = null; 632 if (animationFrameId) { 633 cancelAnimationFrame(animationFrameId); 634 animationFrameId = null; 635 } 636 playBtn.textContent = '▶ Play'; 637 } 638 639 isPart2 = !isPart2; 640 updateModeLabels(); 641 642 // Clamp stage to new max 643 const stages = getStages(); 644 if (currentStage >= stages.length) { 645 currentStage = stages.length - 1; 646 } 647 648 // Rebuild trail for current stage 649 beamTrail = new Set(); 650 for (let i = 0; i <= currentStage; i++) { 651 if (isPart2) { 652 Object.keys(stages[i].positions).forEach(key => beamTrail.add(key)); 653 } else { 654 stages[i].beams.forEach(([r, c]) => beamTrail.add(r + ',' + c)); 655 } 656 } 657 658 // Full re-render on mode change - reset all cells 659 for (let i = 0; i < cellElements.length; i++) { 660 const cell = cellElements[i]; 661 const char = cell.dataset.char; 662 cell.classList.remove('beam', 'beam-trail'); 663 cell.style.opacity = '1'; 664 cell.textContent = (char === 'S' || char === '^') ? char : ''; 665 } 666 667 renderGrid(); 668 } 669 670 part1Label.addEventListener('click', () => { 671 if (isPart2) toggleMode(); 672 }); 673 674 part2Label.addEventListener('click', () => { 675 if (!isPart2) toggleMode(); 676 }); 677 678 prevBtn.addEventListener('click', () => goToStage(currentStage - 1)); 679 nextBtn.addEventListener('click', () => goToStage(currentStage + 1)); 680 resetBtn.addEventListener('click', resetAnimation); 681 682 let animationFrameId = null; 683 let lastFrameTime = 0; 684 685 function animate(timestamp) { 686 const stages = getStages(); 687 const speed = 1100 - parseInt(speedSlider.value); 688 689 if (timestamp - lastFrameTime >= speed) { 690 if (currentStage < stages.length - 1) { 691 goToStage(currentStage + 1); 692 lastFrameTime = timestamp; 693 } else { 694 // Animation complete 695 playInterval = null; 696 playBtn.textContent = '▶ Play'; 697 animationFrameId = null; 698 return; 699 } 700 } 701 702 if (playInterval) { 703 animationFrameId = requestAnimationFrame(animate); 704 } 705 } 706 707 playBtn.addEventListener('click', () => { 708 if (playInterval) { 709 playInterval = null; 710 if (animationFrameId) { 711 cancelAnimationFrame(animationFrameId); 712 animationFrameId = null; 713 } 714 playBtn.textContent = '▶ Play'; 715 } else { 716 const stages = getStages(); 717 if (currentStage === stages.length - 1) { 718 resetAnimation(); 719 } 720 playBtn.textContent = '⏸ Pause'; 721 playInterval = true; 722 lastFrameTime = 0; 723 animationFrameId = requestAnimationFrame(animate); 724 } 725 }); 726 727 speedSlider.addEventListener('input', () => { 728 // Speed change is handled in the animate loop 729 }); 730 731 document.addEventListener('keydown', (e) => { 732 if (e.key === 'ArrowLeft') prevBtn.click(); 733 if (e.key === 'ArrowRight') nextBtn.click(); 734 if (e.key === ' ') { 735 e.preventDefault(); 736 playBtn.click(); 737 } 738 if (e.key === 'r' || e.key === 'R') resetBtn.click(); 739 if (e.key === 't' || e.key === 'T') { 740 toggleMode(); 741 } 742 }); 743 744 let resizeTimeout; 745 window.addEventListener('resize', () => { 746 clearTimeout(resizeTimeout); 747 resizeTimeout = setTimeout(scaleGrid, 100); 748 }); 749 750 updateModeLabels(); 751 initGrid(); 752 renderGrid(); 753 </script> 754</body> 755</html>`; 756 757await Bun.write(`${scriptDir}/index.html`, html); 758console.log("Generated index.html"); 759console.log( 760 "Part 1:", 761 stages1.length, 762 "stages,", 763 totalSplits1, 764 "splits,", 765 stages1[stages1.length - 1]?.beams.length ?? 0, 766 "final beams", 767); 768console.log( 769 "Part 2:", 770 stages2.length, 771 "stages,", 772 finalTimelines, 773 "final timelines", 774);