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