advent of code 2025 in ts and nix
at main 21 kB view raw
1const scriptDir = import.meta.dir; 2const file = await Bun.file(`${scriptDir}/../../shared/06/input.txt`).text(); 3 4const problemsArray: (string | ("*" | "+"))[][] = (() => { 5 const rows = file.trimEnd().split("\n"); 6 const dataRows = rows.slice(0, rows.length - 1); 7 8 const maxLen = Math.max(...dataRows.map((r) => r.length)); 9 const splitCols: number[] = []; 10 for (let i = 0; i < maxLen; i++) { 11 let allWS = true; 12 for (const row of dataRows) { 13 const ch = i < row.length ? row[i] : " "; 14 if (ch !== " " && ch !== "\t") { 15 allWS = false; 16 break; 17 } 18 } 19 if (allWS) splitCols.push(i); 20 } 21 22 const cuts = Array.from(new Set(splitCols)).sort((a, b) => a - b); 23 const segmentedRows: string[][] = rows.map((row) => { 24 const segs: string[] = []; 25 let start = 0; 26 for (const cut of cuts) { 27 const end = Math.min(cut + 1, row.length); 28 segs.push(row.slice(start, end)); 29 start = end; 30 } 31 segs.push(row.slice(start)); 32 return segs; 33 }); 34 35 return segmentedRows.reduce<(string | ("*" | "+"))[][]>((cols, row) => { 36 row.forEach((cell, i) => { 37 (cols[i] ??= []).push(cell as string | ("*" | "+")); 38 }); 39 return cols; 40 }, []); 41})(); 42 43// Generate HTML with visualization 44const html = `<!DOCTYPE html> 45<html lang="en"> 46<head> 47 <meta charset="UTF-8"> 48 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 49 <title>AoC 2025 Day 6 - Cephalopod Math</title> 50 <style> 51 * { 52 box-sizing: border-box; 53 } 54 body { 55 background: #1e1e2e; 56 color: #cdd6f4; 57 font-family: "Source Code Pro", monospace; 58 font-size: 14pt; 59 font-weight: 300; 60 padding: 20px; 61 display: flex; 62 flex-direction: column; 63 align-items: center; 64 min-height: 100vh; 65 margin: 0; 66 } 67 a { 68 text-decoration: none; 69 color: #a6e3a1; 70 outline: 0; 71 } 72 a:hover, a:focus { 73 background-color: #181825 !important; 74 } 75 h1 { 76 color: #a6e3a1; 77 text-shadow: 0 0 2px #a6e3a1, 0 0 5px #a6e3a1; 78 margin-bottom: 10px; 79 font-size: 1em; 80 font-weight: normal; 81 } 82 .mode-toggle { 83 display: flex; 84 align-items: center; 85 gap: 0; 86 margin: 10px 0; 87 border: 1px solid #313244; 88 background: #11111b; 89 } 90 .mode-toggle label { 91 cursor: pointer; 92 padding: 8px 16px; 93 font-size: 14px; 94 transition: all 0.2s ease; 95 border-right: 1px solid #313244; 96 } 97 .mode-toggle label:last-child { 98 border-right: none; 99 } 100 .mode-toggle label.active { 101 background: #313244; 102 color: #a6e3a1; 103 } 104 .description { 105 color: #a6adc8; 106 font-size: 13px; 107 margin: 10px 0; 108 text-align: center; 109 max-width: 600px; 110 } 111 .controls { 112 margin: 15px 0; 113 display: flex; 114 gap: 10px; 115 align-items: center; 116 flex-wrap: wrap; 117 justify-content: center; 118 } 119 button { 120 background: #11111b; 121 color: #a6e3a1; 122 border: 1px solid #313244; 123 padding: 8px 16px; 124 cursor: pointer; 125 font-family: inherit; 126 font-size: 14px; 127 } 128 button:hover { 129 background: #181825; 130 } 131 button:disabled { 132 opacity: 0.5; 133 cursor: not-allowed; 134 } 135 .speed-control { 136 display: flex; 137 align-items: center; 138 gap: 8px; 139 font-size: 13px; 140 color: #a6adc8; 141 } 142 .speed-control input[type="range"] { 143 width: 120px; 144 height: 6px; 145 background: #313244; 146 outline: none; 147 -webkit-appearance: none; 148 } 149 .speed-control input[type="range"]::-webkit-slider-thumb { 150 -webkit-appearance: none; 151 appearance: none; 152 width: 14px; 153 height: 14px; 154 background: #a6e3a1; 155 cursor: pointer; 156 border: 1px solid #313244; 157 } 158 .speed-control input[type="range"]::-moz-range-thumb { 159 width: 14px; 160 height: 14px; 161 background: #a6e3a1; 162 cursor: pointer; 163 border: 1px solid #313244; 164 } 165 .info { 166 color: #f9e2af; 167 font-size: 14px; 168 margin: 10px 0; 169 } 170 .problem-container { 171 background: #11111b; 172 padding: 20px; 173 border: 2px solid #313244; 174 border-radius: 4px; 175 margin: 20px 0; 176 max-width: 95vw; 177 overflow-x: auto; 178 display: grid; 179 grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); 180 gap: 15px; 181 align-items: start; 182 } 183 .problem-item { 184 background: #181825; 185 padding: 15px; 186 border: 1px solid #313244; 187 border-radius: 4px; 188 position: relative; 189 min-height: 120px; 190 } 191 .problem-item.animating { 192 background: #a6e3a1; 193 color: #1e1e2e; 194 } 195 .problem-grid { 196 font-family: "Source Code Pro", monospace; 197 font-size: 14px; 198 line-height: 1.6; 199 white-space: pre; 200 } 201 .problem-grid .row { 202 display: block; 203 } 204 .problem-grid .digit { 205 display: inline-block; 206 transition: all 0.3s ease; 207 min-width: 0.6em; 208 text-align: center; 209 } 210 .problem-grid .digit.highlight:not(.space) { 211 background: #a6e3a1; 212 color: #1e1e2e; 213 font-weight: bold; 214 } 215 .problem-grid .digit.fade { 216 opacity: 0.3; 217 } 218 .problem-grid .number { 219 display: block; 220 transition: all 0.3s ease; 221 } 222 .problem-grid .number.highlight { 223 background: #a6e3a1; 224 color: #1e1e2e; 225 font-weight: bold; 226 } 227 .problem-grid .number.fade { 228 opacity: 0.3; 229 } 230 .problem-grid .operator { 231 color: #fab387; 232 font-weight: bold; 233 } 234 .accumulator { 235 margin-top: 10px; 236 padding-top: 10px; 237 border-top: 2px solid #313244; 238 font-weight: bold; 239 color: #f9e2af; 240 text-align: center; 241 font-size: 16px; 242 } 243 .stats { 244 margin-top: 20px; 245 color: #a6adc8; 246 text-align: center; 247 font-size: 13px; 248 } 249 .footer { 250 margin-top: 20px; 251 color: #a6adc8; 252 text-align: center; 253 font-size: 12px; 254 } 255 .calculation { 256 margin-bottom: 10px; 257 color: #cdd6f4; 258 font-size: 14px; 259 text-align: center; 260 } 261 .calculation .nums { 262 color: #a6e3a1; 263 font-weight: bold; 264 } 265 .calculation .op { 266 color: #fab387; 267 font-weight: bold; 268 } 269 .calculation .result { 270 color: #f9e2af; 271 font-weight: bold; 272 } 273 </style> 274</head> 275<body> 276 <h1>AoC 2025 Day 6 - Cephalopod Math</h1> 277 278 <div class="mode-toggle"> 279 <label id="part1Label">Part 1: Human Reading</label> 280 <label id="part2Label">Part 2: Cephalopod Reading</label> 281 </div> 282 283 <div class="description" id="description"> 284 Read numbers vertically down each column 285 </div> 286 287 <div class="controls"> 288 <button id="prev">← Previous</button> 289 <button id="play">▶ Play</button> 290 <button id="next">Next →</button> 291 <button id="reset">↺ Reset</button> 292 <div class="speed-control"> 293 <label for="speed">Speed:</label> 294 <input type="range" id="speed" min="1" max="25" value="5" step="1"> 295 <span id="speedValue">5x</span> 296 </div> 297 </div> 298 299 <div class="info" id="infoBar"> 300 Group: <span id="groupNum">1</span> / <span id="totalGroups">100</span> 301 | Grand Total: <span id="grandTotal">0</span> 302 </div> 303 304 <div class="problem-container" id="problemContainer"></div> 305 306 <div class="calculation" id="calculation"></div> 307 308 <div class="stats" id="statsBar"> 309 Total problems: ${problemsArray.length} 310 </div> 311 312 <div class="footer"> 313 <a href="../index.html">[Return to Index]</a> 314 </div> 315 316 <script> 317 const problems = ${JSON.stringify(problemsArray)}; 318 319 // Calculate group size based on how many cards fit in a row 320 function calculateGroupSize() { 321 const containerWidth = window.innerWidth * 0.95; // 95vw max 322 const cardMinWidth = 150; // minmax(150px, 1fr) 323 const gap = 15; 324 const containerPadding = 40; // 20px on each side 325 326 const availableWidth = containerWidth - containerPadding; 327 const cardsPerRow = Math.floor((availableWidth + gap) / (cardMinWidth + gap)); 328 329 // Calculate rows that fit on screen (approximate) 330 const viewportHeight = window.innerHeight; 331 const headerHeight = 300; // Approximate space for header, controls, info 332 const availableHeight = viewportHeight - headerHeight; 333 const cardHeight = 150; // Approximate card height 334 const rowsPerScreen = Math.max(1, Math.floor((availableHeight + gap) / (cardHeight + gap))); 335 336 return Math.max(cardsPerRow, cardsPerRow * rowsPerScreen); 337 } 338 339 let GROUP_SIZE = calculateGroupSize(); 340 let totalGroups = Math.ceil(problems.length / GROUP_SIZE); 341 342 let currentGroup = 0; 343 let isPart2 = false; 344 let isPlaying = false; 345 let shouldStop = false; 346 let runningTotal = 0; 347 let speed = 5; 348 349 // Step state for fine-grained navigation 350 let currentProblemIdx = 0; // Which problem in the group (0-9) 351 let currentStepIdx = 0; // Which number/column within that problem 352 let problemAccumulators = []; // Track accumulator for each problem 353 let problemData = []; // Calculated data for each problem in group 354 355 const groupNumEl = document.getElementById('groupNum'); 356 const totalGroupsEl = document.getElementById('totalGroups'); 357 const grandTotalEl = document.getElementById('grandTotal'); 358 const part1Label = document.getElementById('part1Label'); 359 const part2Label = document.getElementById('part2Label'); 360 const description = document.getElementById('description'); 361 const prevBtn = document.getElementById('prev'); 362 const nextBtn = document.getElementById('next'); 363 const playBtn = document.getElementById('play'); 364 const resetBtn = document.getElementById('reset'); 365 const problemContainer = document.getElementById('problemContainer'); 366 const calculation = document.getElementById('calculation'); 367 const speedSlider = document.getElementById('speed'); 368 const speedValue = document.getElementById('speedValue'); 369 370 function updateModeLabels() { 371 part1Label.classList.toggle('active', !isPart2); 372 part2Label.classList.toggle('active', isPart2); 373 if (isPart2) { 374 description.textContent = 'Read digits column by column from right to left'; 375 } else { 376 description.textContent = 'Read numbers vertically down each column'; 377 } 378 } 379 380 function calculateProblemData(problem) { 381 const localProblem = [...problem]; 382 const operator = localProblem.pop()?.trim(); 383 const maxWidth = localProblem.reduce((m, s) => Math.max(m, s.length), 0); 384 385 let nums; 386 if (isPart2) { 387 const cephNums = []; 388 for (let colR = 0; colR < maxWidth; colR++) { 389 let digits = ""; 390 for (let r = 0; r < localProblem.length; r++) { 391 const s = localProblem[r]; 392 const idx = maxWidth - 1 - colR; 393 if (idx >= 0 && idx < s.length) { 394 const ch = s[idx]; 395 if (ch !== " ") digits += ch; 396 } 397 } 398 if (digits.length > 0) { 399 cephNums.push(parseInt(digits, 10)); 400 } 401 } 402 nums = cephNums; 403 } else { 404 nums = localProblem.map((val) => parseInt(val.trim(), 10)); 405 } 406 return { nums, operator }; 407 } 408 409 function renderGroup() { 410 const startIdx = currentGroup * GROUP_SIZE; 411 const endIdx = Math.min(startIdx + GROUP_SIZE, problems.length); 412 const groupProblems = problems.slice(startIdx, endIdx); 413 414 problemContainer.innerHTML = ''; 415 calculation.innerHTML = '<span class="nums">Group Total: <span class="result">0</span></span>'; 416 417 // Reset step state 418 currentProblemIdx = 0; 419 currentStepIdx = 0; 420 problemAccumulators = []; 421 problemData = []; 422 423 groupProblems.forEach((problem, i) => { 424 const item = document.createElement('div'); 425 item.className = 'problem-item'; 426 item.id = \`problem-\${i}\`; 427 428 const localProblem = [...problem]; 429 const operator = localProblem.pop()?.trim(); 430 const maxWidth = localProblem.reduce((m, s) => Math.max(m, s.length), 0); 431 432 // Calculate and store problem data 433 const data = calculateProblemData(problem); 434 problemData.push(data); 435 problemAccumulators.push(operator === '*' ? 1 : 0); 436 437 let gridHtml = '<div class="problem-grid" id="grid-' + i + '">'; 438 439 if (isPart2) { 440 // For Part 2, render character by character for column highlighting 441 for (let row = 0; row < localProblem.length; row++) { 442 const str = localProblem[row]; 443 gridHtml += '<span class="row">'; 444 for (let col = 0; col < str.length; col++) { 445 const char = str[col]; 446 const isSpace = char === ' '; 447 gridHtml += '<span class="digit' + (isSpace ? ' space' : '') + '" data-row="' + row + '" data-col="' + col + '">' + char + '</span>'; 448 } 449 gridHtml += '</span>'; 450 } 451 } else { 452 // For Part 1, render rows as whole numbers 453 for (let row = 0; row < localProblem.length; row++) { 454 gridHtml += '<span class="row number" data-row="' + row + '">' + localProblem[row] + '</span>'; 455 } 456 } 457 458 gridHtml += '<span class="operator">' + operator + '</span>'; 459 gridHtml += '</div>'; 460 gridHtml += '<div class="accumulator" id="acc-' + i + '"></div>'; 461 462 item.innerHTML = gridHtml; 463 problemContainer.appendChild(item); 464 }); 465 466 groupNumEl.textContent = currentGroup + 1; 467 totalGroupsEl.textContent = totalGroups; 468 updateButtons(); 469 } 470 471 function updateButtons() { 472 const atStart = currentGroup === 0 && currentProblemIdx === 0 && currentStepIdx === 0; 473 const atEnd = currentGroup === totalGroups - 1 && 474 currentProblemIdx === problemData.length - 1 && 475 currentStepIdx === problemData[currentProblemIdx]?.nums.length; 476 477 prevBtn.disabled = isPlaying || atStart; 478 nextBtn.disabled = isPlaying || atEnd; 479 } 480 481 function performStep(problemIdx, stepIdx) { 482 const data = problemData[problemIdx]; 483 const { nums, operator } = data; 484 const grid = document.getElementById(\`grid-\${problemIdx}\`); 485 const acc = document.getElementById(\`acc-\${problemIdx}\`); 486 487 if (!grid || !acc || stepIdx >= nums.length) return; 488 489 const startIdx = currentGroup * GROUP_SIZE; 490 const problem = problems[startIdx + problemIdx]; 491 492 if (isPart2) { 493 // Part 2: Highlight columns 494 const localProblem = [...problem]; 495 localProblem.pop(); // Remove operator 496 const maxWidth = localProblem.reduce((m, s) => Math.max(m, s.length), 0); 497 const colIdx = maxWidth - 2 - stepIdx; 498 499 // Highlight all digits in this column 500 const digitsInCol = grid.querySelectorAll(\`[data-col="\${colIdx}"]\`); 501 digitsInCol.forEach(d => d.classList.add('highlight')); 502 } else { 503 // Part 1: Highlight rows 504 const numberElements = grid.querySelectorAll('.number'); 505 if (numberElements[stepIdx]) { 506 numberElements[stepIdx].classList.add('highlight'); 507 } 508 } 509 510 // Perform operation 511 if (operator === '*') { 512 problemAccumulators[problemIdx] *= nums[stepIdx]; 513 } else { 514 problemAccumulators[problemIdx] += nums[stepIdx]; 515 } 516 acc.textContent = problemAccumulators[problemIdx].toLocaleString(); 517 } 518 519 function fadeStep(problemIdx, stepIdx) { 520 const grid = document.getElementById(\`grid-\${problemIdx}\`); 521 if (!grid) return; 522 523 const startIdx = currentGroup * GROUP_SIZE; 524 const problem = problems[startIdx + problemIdx]; 525 526 if (isPart2) { 527 const localProblem = [...problem]; 528 localProblem.pop(); 529 const maxWidth = localProblem.reduce((m, s) => Math.max(m, s.length), 0); 530 const colIdx = maxWidth - 2 - stepIdx; 531 532 const digitsInCol = grid.querySelectorAll(\`[data-col="\${colIdx}"]\`); 533 digitsInCol.forEach(d => { 534 d.classList.remove('highlight'); 535 d.classList.add('fade'); 536 }); 537 } else { 538 const numberElements = grid.querySelectorAll('.number'); 539 if (numberElements[stepIdx]) { 540 numberElements[stepIdx].classList.remove('highlight'); 541 numberElements[stepIdx].classList.add('fade'); 542 } 543 } 544 } 545 546 function stepForward(fromPlayback = false) { 547 if (isPlaying && !fromPlayback) return; 548 549 // Perform current step 550 performStep(currentProblemIdx, currentStepIdx); 551 552 // Advance step 553 currentStepIdx++; 554 555 // Check if we've finished this problem 556 if (currentStepIdx >= problemData[currentProblemIdx].nums.length) { 557 // Fade the last step 558 fadeStep(currentProblemIdx, currentStepIdx - 1); 559 560 // Update grand total 561 runningTotal += problemAccumulators[currentProblemIdx]; 562 grandTotalEl.textContent = runningTotal.toLocaleString(); 563 564 // Move to next problem 565 currentProblemIdx++; 566 currentStepIdx = 0; 567 568 // Check if we've finished the group 569 if (currentProblemIdx >= problemData.length) { 570 // Update group total 571 const groupTotal = problemAccumulators.reduce((sum, val) => sum + val, 0); 572 calculation.innerHTML = \`<span class="nums">Group Total: <span class="result">\${groupTotal.toLocaleString()}</span></span>\`; 573 574 // Move to next group 575 if (currentGroup < totalGroups - 1) { 576 currentGroup++; 577 renderGroup(); 578 } 579 } 580 } else { 581 // Fade previous step 582 if (currentStepIdx > 0) { 583 fadeStep(currentProblemIdx, currentStepIdx - 1); 584 } 585 } 586 587 if (!fromPlayback) updateButtons(); 588 } 589 590 function stepBackward() { 591 if (isPlaying) return; 592 593 // Move back one step 594 currentStepIdx--; 595 596 // If we're before the start of this problem, go to previous problem 597 if (currentStepIdx < 0) { 598 currentProblemIdx--; 599 600 // If we're before the start of the group, go to previous group 601 if (currentProblemIdx < 0) { 602 if (currentGroup > 0) { 603 currentGroup--; 604 renderGroup(); 605 // Set to end of this group 606 currentProblemIdx = problemData.length - 1; 607 currentStepIdx = problemData[currentProblemIdx].nums.length - 1; 608 } else { 609 // Already at the very start 610 currentProblemIdx = 0; 611 currentStepIdx = 0; 612 } 613 } else { 614 // Go to end of previous problem 615 currentStepIdx = problemData[currentProblemIdx].nums.length - 1; 616 617 // Revert the grand total 618 runningTotal -= problemAccumulators[currentProblemIdx + 1]; 619 grandTotalEl.textContent = runningTotal.toLocaleString(); 620 } 621 } 622 623 // Clear current state and rebuild up to current step 624 renderGroupState(); 625 updateButtons(); 626 } 627 628 function renderGroupState() { 629 // Re-render the group with current state 630 const startIdx = currentGroup * GROUP_SIZE; 631 const endIdx = Math.min(startIdx + GROUP_SIZE, problems.length); 632 const groupProblems = problems.slice(startIdx, endIdx); 633 634 // Reset accumulators 635 for (let i = 0; i < problemAccumulators.length; i++) { 636 const data = problemData[i]; 637 problemAccumulators[i] = data.operator === '*' ? 1 : 0; 638 const acc = document.getElementById(\`acc-\${i}\`); 639 if (acc) acc.textContent = ''; 640 } 641 642 // Clear all highlights and fades 643 document.querySelectorAll('.highlight, .fade').forEach(el => { 644 el.classList.remove('highlight', 'fade'); 645 }); 646 647 // Replay all steps up to current position 648 for (let p = 0; p <= currentProblemIdx; p++) { 649 const maxStep = p === currentProblemIdx ? currentStepIdx : problemData[p].nums.length; 650 for (let s = 0; s < maxStep; s++) { 651 performStep(p, s); 652 fadeStep(p, s); 653 } 654 } 655 } 656 657 async function playAll() { 658 isPlaying = true; 659 shouldStop = false; 660 playBtn.textContent = '⏸ Pause'; 661 updateButtons(); 662 663 while (!shouldStop) { 664 // Check if we're at the end 665 const atEnd = currentGroup === totalGroups - 1 && 666 currentProblemIdx === problemData.length - 1 && 667 currentStepIdx >= problemData[currentProblemIdx].nums.length; 668 669 if (atEnd) break; 670 671 stepForward(true); 672 // Speed: 1 = 1000ms, 5 = 600ms, 10 = 200ms, 25 = 20ms 673 const delay = Math.max(20, 1050 - (speed * 50)); 674 await new Promise(resolve => setTimeout(resolve, delay)); 675 } 676 677 isPlaying = false; 678 shouldStop = false; 679 playBtn.textContent = '▶ Play'; 680 updateButtons(); 681 } 682 683 function stopPlaying() { 684 shouldStop = true; 685 isPlaying = false; 686 playBtn.textContent = '▶ Play'; 687 updateButtons(); 688 } 689 690 function resetAnimation() { 691 if (isPlaying) stopPlaying(); 692 currentGroup = 0; 693 runningTotal = 0; 694 grandTotalEl.textContent = '0'; 695 calculation.innerHTML = ''; 696 renderGroup(); 697 } 698 699 function toggleMode() { 700 if (isPlaying) return; 701 isPart2 = !isPart2; 702 updateModeLabels(); 703 resetAnimation(); 704 } 705 706 part1Label.addEventListener('click', () => { 707 if (isPart2 && !isPlaying) toggleMode(); 708 }); 709 710 part2Label.addEventListener('click', () => { 711 if (!isPart2 && !isPlaying) toggleMode(); 712 }); 713 714 prevBtn.addEventListener('click', () => { 715 stepBackward(); 716 }); 717 718 nextBtn.addEventListener('click', () => { 719 stepForward(); 720 }); 721 722 resetBtn.addEventListener('click', resetAnimation); 723 724 playBtn.addEventListener('click', () => { 725 if (isPlaying) { 726 stopPlaying(); 727 } else { 728 playAll(); 729 } 730 }); 731 732 speedSlider.addEventListener('input', (e) => { 733 speed = parseInt(e.target.value); 734 speedValue.textContent = speed + 'x'; 735 }); 736 737 document.addEventListener('keydown', (e) => { 738 if (e.key === 'ArrowLeft') prevBtn.click(); 739 if (e.key === 'ArrowRight') nextBtn.click(); 740 if (e.key === ' ') { 741 e.preventDefault(); 742 playBtn.click(); 743 } 744 if (e.key === 'r' || e.key === 'R') { 745 if (!isPlaying) resetBtn.click(); 746 } 747 if (e.key === 't' || e.key === 'T') { 748 if (!isPlaying) toggleMode(); 749 } 750 }); 751 752 // Handle window resize 753 window.addEventListener('resize', () => { 754 const newGroupSize = calculateGroupSize(); 755 if (newGroupSize !== GROUP_SIZE && !isPlaying) { 756 GROUP_SIZE = newGroupSize; 757 totalGroups = Math.ceil(problems.length / GROUP_SIZE); 758 // Adjust current group to maintain position 759 const currentProblemGlobal = currentGroup * GROUP_SIZE + currentProblemIdx; 760 currentGroup = Math.floor(currentProblemGlobal / GROUP_SIZE); 761 currentProblemIdx = currentProblemGlobal % GROUP_SIZE; 762 totalGroupsEl.textContent = totalGroups; 763 renderGroup(); 764 } 765 }); 766 767 updateModeLabels(); 768 renderGroup(); 769 </script> 770</body> 771</html>`; 772 773await Bun.write(`${scriptDir}/index.html`, html); 774console.log("Generated index.html with", problemsArray.length, "problems");