advent of code 2025 in ts and nix
1const scriptDir = import.meta.dir;
2const file = await Bun.file(`${scriptDir}/../../shared/10/input.txt`).text();
3
4interface Machine {
5 target: boolean[];
6 buttons: number[][];
7 joltages: number[];
8}
9
10// Parse input
11const machines: Machine[] = file
12 .trim()
13 .split("\n")
14 .map((line) => {
15 const lightsMatch = line.match(/\[([.#]+)\]/);
16 const target = lightsMatch![1].split("").map((c) => c === "#");
17
18 const buttonsMatch = line.matchAll(/\(([0-9,]+)\)/g);
19 const buttons: number[][] = [];
20 for (const match of buttonsMatch) {
21 const indices = match[1].split(",").map(Number);
22 buttons.push(indices);
23 }
24
25 const joltagesMatch = line.match(/\{([0-9,]+)\}/);
26 const joltages = joltagesMatch ? joltagesMatch[1].split(",").map(Number) : [];
27
28 return { target, buttons, joltages };
29 });
30
31// Solve one machine
32function solveMachine(machine: Machine): { solution: number[]; steps: any[] } {
33 const n = machine.target.length;
34 const m = machine.buttons.length;
35
36 const matrix: number[][] = [];
37 for (let i = 0; i < n; i++) {
38 const row: number[] = [];
39 for (let j = 0; j < m; j++) {
40 row.push(machine.buttons[j].includes(i) ? 1 : 0);
41 }
42 row.push(machine.target[i] ? 1 : 0);
43 matrix.push(row);
44 }
45
46 const steps = [JSON.parse(JSON.stringify(matrix))];
47 const pivotCols: number[] = [];
48
49 for (let col = 0; col < m; col++) {
50 let pivotRow = -1;
51 for (let row = pivotCols.length; row < n; row++) {
52 if (matrix[row][col] === 1) {
53 pivotRow = row;
54 break;
55 }
56 }
57
58 if (pivotRow === -1) continue;
59
60 const targetRow = pivotCols.length;
61 if (pivotRow !== targetRow) {
62 [matrix[pivotRow], matrix[targetRow]] = [
63 matrix[targetRow],
64 matrix[pivotRow],
65 ];
66 steps.push(JSON.parse(JSON.stringify(matrix)));
67 }
68
69 pivotCols.push(col);
70
71 for (let row = 0; row < n; row++) {
72 if (row !== targetRow && matrix[row][col] === 1) {
73 for (let c = 0; c <= m; c++) {
74 matrix[row][c] ^= matrix[targetRow][c];
75 }
76 steps.push(JSON.parse(JSON.stringify(matrix)));
77 }
78 }
79 }
80
81 // Identify free variables
82 const isPivot = new Array(m).fill(false);
83 pivotCols.forEach((col) => (isPivot[col] = true));
84 const freeVars: number[] = [];
85 for (let j = 0; j < m; j++) {
86 if (!isPivot[j]) freeVars.push(j);
87 }
88
89 // Try all combinations of free variables to find minimum
90 let minPresses = Infinity;
91 let bestSolution: number[] = [];
92
93 const numCombinations = 1 << freeVars.length;
94 for (let combo = 0; combo < numCombinations; combo++) {
95 const solution: number[] = new Array(m).fill(0);
96
97 // Set free variables according to combo
98 for (let i = 0; i < freeVars.length; i++) {
99 solution[freeVars[i]] = (combo >> i) & 1;
100 }
101
102 // Back-substitution for pivot variables
103 for (let i = pivotCols.length - 1; i >= 0; i--) {
104 const col = pivotCols[i];
105 solution[col] = matrix[i][m];
106
107 for (let j = col + 1; j < m; j++) {
108 if (matrix[i][j] === 1) {
109 solution[col] ^= solution[j];
110 }
111 }
112 }
113
114 const presses = solution.reduce((sum, x) => sum + x, 0);
115 if (presses < minPresses) {
116 minPresses = presses;
117 bestSolution = solution;
118 }
119 }
120
121 return { solution: bestSolution, steps };
122}
123
124// Solve Part 2: joltage configuration
125function solveMachinePart2(machine: Machine): number[] {
126 const n = machine.joltages.length;
127 const m = machine.buttons.length;
128 const target = machine.joltages;
129
130 // Build coefficient matrix A
131 const A: number[][] = [];
132 for (let i = 0; i < n; i++) {
133 const row: number[] = [];
134 for (let j = 0; j < m; j++) {
135 row.push(machine.buttons[j].includes(i) ? 1 : 0);
136 }
137 A.push(row);
138 }
139
140 // Build augmented matrix [A | b]
141 const matrix: number[][] = [];
142 for (let i = 0; i < n; i++) {
143 matrix.push([...A[i], target[i]]);
144 }
145
146 // Gaussian elimination
147 const pivotCols: number[] = [];
148 for (let col = 0; col < m; col++) {
149 let pivotRow = -1;
150 for (let row = pivotCols.length; row < n; row++) {
151 if (matrix[row][col] !== 0) {
152 pivotRow = row;
153 break;
154 }
155 }
156
157 if (pivotRow === -1) continue;
158
159 const targetRow = pivotCols.length;
160 if (pivotRow !== targetRow) {
161 [matrix[pivotRow], matrix[targetRow]] = [
162 matrix[targetRow],
163 matrix[pivotRow],
164 ];
165 }
166
167 pivotCols.push(col);
168
169 // Scale row so pivot is 1
170 const pivot = matrix[targetRow][col];
171 for (let c = 0; c <= m; c++) {
172 matrix[targetRow][c] /= pivot;
173 }
174
175 // Eliminate column in other rows
176 for (let row = 0; row < n; row++) {
177 if (row !== targetRow && matrix[row][col] !== 0) {
178 const factor = matrix[row][col];
179 for (let c = 0; c <= m; c++) {
180 matrix[row][c] -= factor * matrix[targetRow][c];
181 }
182 }
183 }
184 }
185
186 // Identify free variables
187 const isPivot = new Array(m).fill(false);
188 pivotCols.forEach((col) => (isPivot[col] = true));
189 const freeVars: number[] = [];
190 for (let j = 0; j < m; j++) {
191 if (!isPivot[j]) freeVars.push(j);
192 }
193
194 if (freeVars.length > 15) {
195 return new Array(m).fill(0);
196 }
197
198 let minPresses = Infinity;
199 let bestSolution: number[] = [];
200
201 const maxTarget = Math.max(...target);
202 const maxFreeValue = Math.min(maxTarget * 2, 200);
203
204 function searchFreeVars(idx: number, currentSol: number[]) {
205 if (idx === freeVars.length) {
206 const sol = [...currentSol];
207 let valid = true;
208 for (let i = pivotCols.length - 1; i >= 0; i--) {
209 const col = pivotCols[i];
210 let val = matrix[i][m];
211 for (let j = col + 1; j < m; j++) {
212 val -= matrix[i][j] * sol[j];
213 }
214 sol[col] = val;
215
216 if (val < -1e-9 || Math.abs(val - Math.round(val)) > 1e-9) {
217 valid = false;
218 break;
219 }
220 }
221
222 if (valid) {
223 const intSol = sol.map((x) => Math.round(Math.max(0, x)));
224 const presses = intSol.reduce((sum, x) => sum + x, 0);
225 if (presses < minPresses) {
226 minPresses = presses;
227 bestSolution = intSol;
228 }
229 }
230 return;
231 }
232
233 for (let val = 0; val <= maxFreeValue; val++) {
234 currentSol[freeVars[idx]] = val;
235 searchFreeVars(idx + 1, currentSol);
236 }
237 }
238
239 searchFreeVars(0, new Array(m).fill(0));
240
241 return bestSolution;
242}
243
244const machinesData = JSON.stringify(machines);
245
246const html = `<!DOCTYPE html>
247<html lang="en">
248<head>
249 <meta charset="UTF-8">
250 <meta name="viewport" content="width=device-width, initial-scale=1.0">
251 <title>AoC 2025 Day 10 - Factory</title>
252 <style>
253 * {
254 box-sizing: border-box;
255 }
256 body {
257 background: #1e1e2e;
258 color: #cdd6f4;
259 font-family: "Source Code Pro", monospace;
260 font-size: 14pt;
261 font-weight: 300;
262 padding: 20px;
263 display: flex;
264 flex-direction: column;
265 align-items: center;
266 min-height: 100vh;
267 margin: 0;
268 }
269 h1 {
270 color: #a6e3a1;
271 text-shadow: 0 0 2px #a6e3a1, 0 0 5px #a6e3a1;
272 margin-bottom: 10px;
273 font-size: 1em;
274 font-weight: normal;
275 }
276 .controls {
277 background: #11111b;
278 border: 1px solid #313244;
279 padding: 15px;
280 margin: 15px 0;
281 max-width: 1200px;
282 border-radius: 4px;
283 width: 100%;
284 }
285 .control-row {
286 display: flex;
287 gap: 15px;
288 align-items: center;
289 margin-bottom: 15px;
290 flex-wrap: wrap;
291 justify-content: center;
292 }
293 .control-row:last-child {
294 margin-bottom: 0;
295 }
296 button {
297 background: #11111b;
298 color: #a6e3a1;
299 border: 1px solid #313244;
300 padding: 8px 16px;
301 cursor: pointer;
302 font-family: inherit;
303 font-size: 14px;
304 border-radius: 3px;
305 }
306 button:hover {
307 background: #181825;
308 }
309 button:disabled {
310 opacity: 0.5;
311 cursor: not-allowed;
312 }
313 .speed-control {
314 display: flex;
315 align-items: center;
316 gap: 8px;
317 font-size: 13px;
318 color: #a6adc8;
319 }
320 .speed-control input[type="range"] {
321 width: 120px;
322 height: 6px;
323 background: #313244;
324 outline: none;
325 -webkit-appearance: none;
326 border-radius: 3px;
327 }
328 .speed-control input[type="range"]::-webkit-slider-thumb {
329 -webkit-appearance: none;
330 appearance: none;
331 width: 14px;
332 height: 14px;
333 background: #a6e3a1;
334 cursor: pointer;
335 border-radius: 50%;
336 border: 1px solid #313244;
337 }
338 .speed-control input[type="range"]::-moz-range-thumb {
339 width: 14px;
340 height: 14px;
341 background: #a6e3a1;
342 cursor: pointer;
343 border-radius: 50%;
344 border: 1px solid #313244;
345 }
346 .machine-display {
347 background: #11111b;
348 border: 1px solid #313244;
349 padding: 20px;
350 margin: 20px 0;
351 max-width: 1200px;
352 border-radius: 4px;
353 width: 100%;
354 }
355 .lights {
356 display: flex;
357 gap: 10px;
358 justify-content: center;
359 margin: 20px 0;
360 flex-wrap: wrap;
361 padding: 10px;
362 }
363 .light {
364 width: 50px;
365 height: 50px;
366 border-radius: 50%;
367 border: 2px solid #313244;
368 display: flex;
369 flex-direction: column;
370 align-items: center;
371 justify-content: center;
372 font-size: 9px;
373 transition: all 0.3s ease;
374 overflow: hidden;
375 text-align: center;
376 padding: 3px;
377 line-height: 1.1;
378 position: relative;
379 background: #1e1e2e;
380 }
381 .light-inner {
382 position: absolute;
383 inset: 0;
384 border-radius: 50%;
385 overflow: hidden;
386 clip-path: circle(50% at 50% 50%);
387 }
388 .light::before {
389 content: '';
390 position: absolute;
391 bottom: 0;
392 left: -10%;
393 right: -10%;
394 width: 120%;
395 background: linear-gradient(to top, #a6e3a1, #a6e3a1cc);
396 height: var(--fill-height, 0%);
397 transition: height 0.3s ease;
398 z-index: 0;
399 border-radius: 0 0 50% 50%;
400 }
401 .light > div {
402 position: relative;
403 z-index: 1;
404 }
405 .light.off {
406 color: #6c7086;
407 box-shadow: none;
408 }
409 .light.off::before {
410 left: -10%;
411 right: -10%;
412 width: 120%;
413 border-radius: 0 0 50% 50%;
414 }
415 .light.on {
416 color: #1e1e2e;
417 box-shadow: 0 0 20px #a6e3a1, 0 0 30px #a6e3a180 !important;
418 overflow: visible;
419 }
420 .light.on::before {
421 left: 0;
422 right: 0;
423 width: 100%;
424 border-radius: 50%;
425 }
426 .light.target {
427 border-color: #f9e2af;
428 border-width: 3px;
429 }
430 .buttons-grid {
431 display: grid;
432 grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
433 gap: 10px;
434 margin: 20px 0;
435 }
436 .button-display {
437 background: #181825;
438 border: 1px solid #313244;
439 padding: 10px;
440 border-radius: 4px;
441 text-align: center;
442 cursor: pointer;
443 transition: all 0.2s ease;
444 }
445 .button-display:hover {
446 background: #313244;
447 }
448 .button-display.pressed {
449 background: #a6e3a1;
450 color: #1e1e2e;
451 border-color: #a6e3a1;
452 }
453 .button-label {
454 font-size: 12px;
455 margin-bottom: 5px;
456 color: #a6adc8;
457 }
458 .button-toggles {
459 font-size: 11px;
460 color: #6c7086;
461 }
462 .stats {
463 background: #11111b;
464 border: 1px solid #313244;
465 padding: 10px 15px;
466 margin: 10px 0;
467 max-width: 1200px;
468 border-radius: 4px;
469 text-align: center;
470 font-size: 13px;
471 color: #a6adc8;
472 width: 100%;
473 margin-top: auto;
474 }
475 .info {
476 margin: 10px 0;
477 text-align: center;
478 color: #f9e2af;
479 }
480 a {
481 text-decoration: none;
482 color: #a6e3a1;
483 outline: 0;
484 }
485 a:hover, a:focus {
486 text-decoration: underline;
487 }
488 </style>
489</head>
490<body>
491 <h1>AoC 2025 Day 10 - Factory Machines</h1>
492
493 <div class="controls">
494 <div class="control-row">
495 <button id="togglePart" style="color: #f9e2af; font-weight: bold;">Part 1</button>
496 <button id="prev">← Previous Machine</button>
497 <button id="play">▶ Play</button>
498 <button id="next">Next Machine →</button>
499 <button id="reset">↺ Reset</button>
500 <div class="speed-control">
501 <label for="speed">Speed:</label>
502 <input type="range" id="speed" min="1" max="25" value="5" step="1">
503 <span id="speedValue">5x</span>
504 </div>
505 </div>
506 </div>
507
508 <div class="info" id="machineInfo">Machine 1 / ${machines.length}</div>
509
510 <div class="machine-display">
511 <h2 id="displayTitle" style="text-align: center; color: #89b4fa; font-size: 18px; margin-bottom: 20px;">Indicator Lights</h2>
512 <div class="lights" id="lights"></div>
513
514 <h2 style="text-align: center; color: #89b4fa; font-size: 18px; margin: 30px 0 20px 0;">Buttons</h2>
515 <div class="buttons-grid" id="buttons"></div>
516 </div>
517
518 <div class="stats">
519 <div id="statsInfo">Buttons Pressed: 0 | Target: ? | Accumulated Total: 0</div>
520 <div style="margin-top: 5px; font-size: 11px;"><a href="../index.html">[Return to Index]</a></div>
521 </div>
522
523 <script type="module">
524 const machines = ${machinesData};
525
526 let currentMode = 1; // 1 or 2
527 let currentMachineIndex = 0;
528 let currentState = [];
529 let buttonStates = []; // Track which buttons are "on" (pressed odd number of times)
530 let isPlaying = false;
531 let showingSolution = false;
532 let solutionSteps = [];
533 let currentStep = 0;
534 let solvedMachines = new Set(); // Track which machines have been solved
535 let animationSpeed = 200; // ms between button presses (default 5x)
536
537
538 function renderMachine() {
539 const machine = machines[currentMachineIndex];
540
541 // Update title based on mode
542 const titleEl = document.getElementById('displayTitle');
543 if (currentMode === 1) {
544 titleEl.textContent = 'Indicator Lights';
545 titleEl.style.color = '#89b4fa';
546 } else {
547 titleEl.textContent = 'Joltage Counters';
548 titleEl.style.color = '#f9e2af';
549 }
550
551 // Render lights or counters
552 const lightsDiv = document.getElementById('lights');
553 lightsDiv.innerHTML = '';
554
555 if (currentMode === 1) {
556 // Part 1: Indicator lights
557 machine.target.forEach((target, i) => {
558 const light = document.createElement('div');
559 const isOn = currentState[i];
560 light.className = \`light \${isOn ? 'on' : 'off'} \${target ? 'target' : ''}\`;
561 light.style.setProperty('--fill-height', isOn ? '100%' : '0%');
562 const label = document.createElement('div');
563 label.textContent = i;
564 light.appendChild(label);
565 lightsDiv.appendChild(light);
566 });
567 } else {
568 // Part 2: Joltage counters with fill animation
569 machine.joltages.forEach((target, i) => {
570 const counter = document.createElement('div');
571 const current = currentState[i] || 0;
572 const isTarget = current >= target;
573 const fillPercent = target > 0 ? Math.min(100, (current / target) * 100) : 0;
574
575 counter.className = \`light \${isTarget ? 'on' : 'off'} \${true ? 'target' : ''}\`;
576 counter.style.setProperty('--fill-height', \`\${fillPercent}%\`);
577
578 const indexLabel = document.createElement('div');
579 indexLabel.style.fontSize = '7px';
580 indexLabel.style.opacity = '0.7';
581 indexLabel.textContent = \`[\${i}]\`;
582
583 const valueLabel = document.createElement('div');
584 valueLabel.style.fontSize = '10px';
585 valueLabel.style.fontWeight = 'bold';
586 valueLabel.innerHTML = \`\${current}/<span style="color: #f9e2af;">\${target}</span>\`;
587
588 counter.appendChild(indexLabel);
589 counter.appendChild(valueLabel);
590 lightsDiv.appendChild(counter);
591 });
592 }
593
594 // Render buttons
595 const buttonsDiv = document.getElementById('buttons');
596 buttonsDiv.innerHTML = '';
597 machine.buttons.forEach((toggles, i) => {
598 const btn = document.createElement('div');
599 const pressCount = buttonStates[i] || 0;
600 const isPressed = currentMode === 1 ? (pressCount % 2 === 1) : (pressCount > 0);
601 btn.className = \`button-display \${isPressed ? 'pressed' : ''}\`;
602 btn.innerHTML = \`
603 <div class="button-label">Button \${i}\${currentMode === 2 ? \` (\${pressCount})\` : ''}</div>
604 <div class="button-toggles">Affects: \${toggles.join(', ')}</div>
605 \`;
606 btn.addEventListener('click', () => toggleButton(i));
607 buttonsDiv.appendChild(btn);
608 });
609 }
610
611 function toggleButton(buttonIndex) {
612 const machine = machines[currentMachineIndex];
613
614 if (currentMode === 1) {
615 // Part 1: Toggle lights (XOR)
616 buttonStates[buttonIndex] = buttonStates[buttonIndex] ? 0 : 1;
617 machine.buttons[buttonIndex].forEach(lightIndex => {
618 currentState[lightIndex] = !currentState[lightIndex];
619 });
620 } else {
621 // Part 2: Increment counters
622 buttonStates[buttonIndex] = (buttonStates[buttonIndex] || 0) + 1;
623 machine.buttons[buttonIndex].forEach(counterIndex => {
624 currentState[counterIndex] = (currentState[counterIndex] || 0) + 1;
625 });
626 }
627
628 renderMachine();
629 updateStats();
630 }
631
632 function solveMachine(machine) {
633 const n = machine.target.length;
634 const m = machine.buttons.length;
635
636 const matrix = [];
637 for (let i = 0; i < n; i++) {
638 const row = [];
639 for (let j = 0; j < m; j++) {
640 row.push(machine.buttons[j].includes(i) ? 1 : 0);
641 }
642 row.push(machine.target[i] ? 1 : 0);
643 matrix.push(row);
644 }
645
646 const pivotCols = [];
647 for (let col = 0; col < m; col++) {
648 let pivotRow = -1;
649 for (let row = pivotCols.length; row < n; row++) {
650 if (matrix[row][col] === 1) {
651 pivotRow = row;
652 break;
653 }
654 }
655
656 if (pivotRow === -1) continue;
657
658 const targetRow = pivotCols.length;
659 if (pivotRow !== targetRow) {
660 [matrix[pivotRow], matrix[targetRow]] = [matrix[targetRow], matrix[pivotRow]];
661 }
662
663 pivotCols.push(col);
664
665 for (let row = 0; row < n; row++) {
666 if (row !== targetRow && matrix[row][col] === 1) {
667 for (let c = 0; c <= m; c++) {
668 matrix[row][c] ^= matrix[targetRow][c];
669 }
670 }
671 }
672 }
673
674 const solution = new Array(m).fill(0);
675 for (let i = pivotCols.length - 1; i >= 0; i--) {
676 const col = pivotCols[i];
677 solution[col] = matrix[i][m];
678 for (let j = col + 1; j < m; j++) {
679 if (matrix[i][j] === 1) {
680 solution[col] ^= solution[j];
681 }
682 }
683 }
684
685 return solution;
686 }
687
688 // Part 2 solver (copy of server-side logic)
689 function solveMachinePart2(machine) {
690 const n = machine.joltages.length;
691 const m = machine.buttons.length;
692 const target = machine.joltages;
693
694 const A = [];
695 for (let i = 0; i < n; i++) {
696 const row = [];
697 for (let j = 0; j < m; j++) {
698 row.push(machine.buttons[j].includes(i) ? 1 : 0);
699 }
700 A.push(row);
701 }
702
703 const matrix = [];
704 for (let i = 0; i < n; i++) {
705 matrix.push([...A[i], target[i]]);
706 }
707
708 const pivotCols = [];
709 for (let col = 0; col < m; col++) {
710 let pivotRow = -1;
711 for (let row = pivotCols.length; row < n; row++) {
712 if (matrix[row][col] !== 0) {
713 pivotRow = row;
714 break;
715 }
716 }
717
718 if (pivotRow === -1) continue;
719
720 const targetRow = pivotCols.length;
721 if (pivotRow !== targetRow) {
722 [matrix[pivotRow], matrix[targetRow]] = [matrix[targetRow], matrix[pivotRow]];
723 }
724
725 pivotCols.push(col);
726
727 const pivot = matrix[targetRow][col];
728 for (let c = 0; c <= m; c++) {
729 matrix[targetRow][c] /= pivot;
730 }
731
732 for (let row = 0; row < n; row++) {
733 if (row !== targetRow && matrix[row][col] !== 0) {
734 const factor = matrix[row][col];
735 for (let c = 0; c <= m; c++) {
736 matrix[row][c] -= factor * matrix[targetRow][c];
737 }
738 }
739 }
740 }
741
742 const isPivot = new Array(m).fill(false);
743 pivotCols.forEach(col => isPivot[col] = true);
744 const freeVars = [];
745 for (let j = 0; j < m; j++) {
746 if (!isPivot[j]) freeVars.push(j);
747 }
748
749 if (freeVars.length > 8) { // Reduced limit for browser
750 return new Array(m).fill(0);
751 }
752
753 let minPresses = Infinity;
754 let bestSolution = [];
755
756 const maxTarget = Math.max(...target);
757 const maxFreeValue = Math.min(maxTarget * 2, 100);
758
759 function searchFreeVars(idx, currentSol) {
760 if (idx === freeVars.length) {
761 const sol = [...currentSol];
762 let valid = true;
763 for (let i = pivotCols.length - 1; i >= 0; i--) {
764 const col = pivotCols[i];
765 let val = matrix[i][m];
766 for (let j = col + 1; j < m; j++) {
767 val -= matrix[i][j] * sol[j];
768 }
769 sol[col] = val;
770
771 if (val < -1e-9 || Math.abs(val - Math.round(val)) > 1e-9) {
772 valid = false;
773 break;
774 }
775 }
776
777 if (valid) {
778 const intSol = sol.map(x => Math.round(Math.max(0, x)));
779 const presses = intSol.reduce((sum, x) => sum + x, 0);
780 if (presses < minPresses) {
781 minPresses = presses;
782 bestSolution = intSol;
783 }
784 }
785 return;
786 }
787
788 for (let val = 0; val <= maxFreeValue; val++) {
789 currentSol[freeVars[idx]] = val;
790 searchFreeVars(idx + 1, currentSol);
791 }
792 }
793
794 searchFreeVars(0, new Array(m).fill(0));
795 return bestSolution;
796 }
797
798 function getCurrentSolution() {
799 const machine = machines[currentMachineIndex];
800 return currentMode === 1 ? solveMachine(machine) : solveMachinePart2(machine);
801 }
802
803 function showSolution() {
804 const machine = machines[currentMachineIndex];
805 const solution = getCurrentSolution();
806
807 if (currentMode === 1) {
808 currentState = new Array(machine.target.length).fill(false);
809 buttonStates = [...solution].map(v => v === 1);
810
811 solution.forEach((shouldPress, buttonIndex) => {
812 if (shouldPress === 1) {
813 machine.buttons[buttonIndex].forEach(lightIndex => {
814 currentState[lightIndex] = !currentState[lightIndex];
815 });
816 }
817 });
818 } else {
819 currentState = new Array(machine.joltages.length).fill(0);
820 buttonStates = [...solution];
821
822 solution.forEach((pressCount, buttonIndex) => {
823 for (let p = 0; p < pressCount; p++) {
824 machine.buttons[buttonIndex].forEach(counterIndex => {
825 currentState[counterIndex]++;
826 });
827 }
828 });
829 }
830
831 showingSolution = true;
832 renderMachine();
833 updateStats();
834 }
835
836 function updateStats() {
837 const machine = machines[currentMachineIndex];
838 const solution = getCurrentSolution();
839 const minPresses = solution.reduce((a, b) => a + b, 0);
840
841 let totalPressed;
842 if (currentMode === 1) {
843 totalPressed = buttonStates.filter(b => b).length;
844 } else {
845 totalPressed = buttonStates.reduce((sum, count) => sum + (count || 0), 0);
846 }
847
848 // Calculate accumulated total for solved machines
849 let accumulatedTotal = 0;
850 solvedMachines.forEach(idx => {
851 const m = machines[idx];
852 const sol = currentMode === 1 ? solveMachine(m) : solveMachinePart2(m);
853 accumulatedTotal += sol.reduce((a, b) => a + b, 0);
854 });
855
856 document.getElementById('statsInfo').textContent = \`Buttons Pressed: \${totalPressed} | Target: \${minPresses} | Accumulated Total: \${accumulatedTotal}\`;
857 document.getElementById('machineInfo').textContent = \`Machine \${currentMachineIndex + 1} / \${machines.length}\`;
858 }
859
860 document.getElementById('prev').addEventListener('click', () => {
861 if (currentMachineIndex > 0) {
862 isPlaying = false;
863 document.getElementById('play').textContent = '▶ Play';
864 currentMachineIndex--;
865 initMachine();
866 }
867 });
868
869 document.getElementById('next').addEventListener('click', () => {
870 if (currentMachineIndex < machines.length - 1) {
871 isPlaying = false;
872 document.getElementById('play').textContent = '▶ Play';
873 currentMachineIndex++;
874 initMachine();
875 }
876 });
877
878 document.getElementById('reset').addEventListener('click', initMachine);
879
880 document.getElementById('togglePart').addEventListener('click', () => {
881 currentMode = currentMode === 1 ? 2 : 1;
882 document.getElementById('togglePart').textContent = \`Part \${currentMode}\`;
883 solvedMachines.clear();
884 initMachine();
885 });
886
887 document.getElementById('play').addEventListener('click', () => {
888 isPlaying = !isPlaying;
889 document.getElementById('play').textContent = isPlaying ? '⏸ Pause' : '▶ Play';
890 if (isPlaying) {
891 animateSolution();
892 }
893 });
894
895 // Speed control
896 const speedSlider = document.getElementById('speed');
897 const speedValue = document.getElementById('speedValue');
898 speedSlider.addEventListener('input', (e) => {
899 const speed = parseInt(e.target.value);
900 speedValue.textContent = \`\${speed}x\`;
901 // Faster speed = shorter delay (inverse relationship)
902 animationSpeed = 1000 / speed;
903 });
904
905 function animateSolution() {
906 if (!isPlaying) return;
907
908 if (currentStep < solutionSteps.length) {
909 // Toggle the next button in the solution
910 const buttonIndex = solutionSteps[currentStep];
911 toggleButton(buttonIndex);
912 currentStep++;
913
914 // Use 10x faster speed for Part 2 (more button presses)
915 const delay = currentMode === 2 ? animationSpeed / 10 : animationSpeed;
916 setTimeout(animateSolution, delay);
917 } else {
918 // Mark this machine as solved
919 const machine = machines[currentMachineIndex];
920 let isCorrect;
921 if (currentMode === 1) {
922 isCorrect = currentState.every((state, i) => state === machine.target[i]);
923 } else {
924 isCorrect = currentState.every((state, i) => state === machine.joltages[i]);
925 }
926
927 if (isCorrect) {
928 solvedMachines.add(currentMachineIndex);
929 updateStats();
930 }
931
932 // Current machine done, move to next with a brief pause
933 if (currentMachineIndex < machines.length - 1) {
934 if (isPlaying) {
935 // Add delay between machines (3x the normal animation speed)
936 setTimeout(() => {
937 currentMachineIndex++;
938 initMachine();
939 setTimeout(animateSolution, animationSpeed);
940 }, animationSpeed * 3);
941 }
942 } else {
943 // All done
944 isPlaying = false;
945 document.getElementById('play').textContent = '▶ Play';
946 setTimeout(() => {
947 currentMachineIndex = 0;
948 initMachine();
949 }, animationSpeed * 4);
950 }
951 }
952 }
953
954 function initMachine() {
955 const machine = machines[currentMachineIndex];
956 showingSolution = false;
957 currentStep = 0;
958
959 if (currentMode === 1) {
960 // Part 1
961 currentState = new Array(machine.target.length).fill(false);
962 buttonStates = new Array(machine.buttons.length).fill(0);
963
964 const solution = solveMachine(machine);
965 solutionSteps = [];
966 solution.forEach((shouldPress, idx) => {
967 if (shouldPress === 1) {
968 solutionSteps.push(idx);
969 }
970 });
971 } else {
972 // Part 2
973 currentState = new Array(machine.joltages.length).fill(0);
974 buttonStates = new Array(machine.buttons.length).fill(0);
975
976 const solution = solveMachinePart2(machine);
977 solutionSteps = [];
978 solution.forEach((pressCount, idx) => {
979 for (let i = 0; i < pressCount; i++) {
980 solutionSteps.push(idx);
981 }
982 });
983 }
984
985 renderMachine();
986 updateStats();
987 }
988
989 // Initialize
990 initMachine();
991 </script>
992</body>
993</html>`;
994
995await Bun.write(`${scriptDir}/index.html`, html);