advent of code 2025 in ts and nix
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");