advent of code 2025 in ts and nix
1const file = await Bun.file("../../shared/04/input.txt").text();
2const paperMap: boolean[][] = file
3 .trim()
4 .split("\n")
5 .map((line) =>
6 Array.from(line, (ch) => {
7 if (ch === ".") return false;
8 if (ch === "@") return true;
9
10 throw new Error(`Unexpected character '${ch}' in input.`);
11 }),
12 );
13
14function accessiblePapers(map: boolean[][]): {
15 map: boolean[][];
16 accessible: number;
17} {
18 let accessible = 0;
19 const nextMap: boolean[][] = map.map((row) => row.slice());
20 map.forEach((rows, row) => {
21 rows.forEach((cell, col) => {
22 if (cell) {
23 let fullAdj = 0;
24
25 const offsets: { row: number; col: number }[] = [
26 // cardinal
27 { row: -1, col: 0 },
28 { row: 1, col: 0 },
29 { row: 0, col: 1 },
30 { row: 0, col: -1 },
31 // diagonals
32 { row: -1, col: 1 },
33 { row: 1, col: 1 },
34 { row: -1, col: -1 },
35 { row: 1, col: -1 },
36 ];
37
38 for (const off of offsets) {
39 const rowIdx = row + off.row;
40 const colIdx = col + off.col;
41
42 if (rowIdx < 0 || colIdx < 0) continue;
43 if (rowIdx > paperMap.length - 1 || colIdx > rows.length - 1)
44 continue;
45
46 if (map.at(rowIdx)?.at(colIdx)) fullAdj++;
47
48 if (fullAdj >= 4) break;
49 }
50
51 if (fullAdj < 4) {
52 accessible++;
53 (nextMap[row] as boolean[])[col] = false;
54 }
55 }
56 });
57 });
58
59 return { map: nextMap, accessible };
60}
61
62// Collect all stages
63const stages: { map: boolean[][]; accessible: number; iteration: number }[] =
64 [];
65let map = paperMap;
66let iteration = 0;
67
68stages.push({ map: JSON.parse(JSON.stringify(map)), accessible: 0, iteration });
69
70while (true) {
71 const res = accessiblePapers(map);
72 iteration++;
73
74 stages.push({
75 map: JSON.parse(JSON.stringify(res.map)),
76 accessible: res.accessible,
77 iteration,
78 });
79
80 map = res.map;
81 if (res.accessible === 0) break;
82}
83
84// Generate HTML
85const html = `<!DOCTYPE html>
86<html lang="en">
87<head>
88 <meta charset="UTF-8">
89 <meta name="viewport" content="width=device-width, initial-scale=1.0">
90 <title>AoC 2025 Day 4 - Paper Removal Visualization</title>
91 <style>
92 body {
93 background: #1e1e2e;
94 color: #cdd6f4;
95 font-family: "Source Code Pro", monospace;
96 font-size: 14pt;
97 font-weight: 300;
98 padding: 20px;
99 display: flex;
100 flex-direction: column;
101 align-items: center;
102 min-height: 100vh;
103 }
104 a {
105 text-decoration: none;
106 color: #a6e3a1;
107 outline: 0;
108 }
109 a:hover, a:focus {
110 background-color: #181825 !important;
111 }
112 h1 {
113 color: #a6e3a1;
114 text-shadow: 0 0 2px #a6e3a1, 0 0 5px #a6e3a1;
115 margin-bottom: 10px;
116 font-size: 1em;
117 font-weight: normal;
118 }
119 .controls {
120 margin: 20px 0;
121 display: flex;
122 gap: 10px;
123 align-items: center;
124 flex-wrap: wrap;
125 justify-content: center;
126 }
127 button {
128 background: #11111b;
129 color: #a6e3a1;
130 border: 1px solid #313244;
131 padding: 8px 16px;
132 cursor: pointer;
133 font-family: inherit;
134 font-size: 14px;
135 }
136 button:hover {
137 background: #181825;
138 }
139 button:disabled {
140 opacity: 0.5;
141 cursor: not-allowed;
142 }
143 .info {
144 color: #f9e2af;
145 font-size: 14px;
146 margin: 10px 0;
147 }
148 .speed-control {
149 display: flex;
150 align-items: center;
151 gap: 8px;
152 }
153 .speed-control input[type="range"] {
154 -webkit-appearance: none;
155 appearance: none;
156 width: 120px;
157 height: 6px;
158 background: #313244;
159 outline: none;
160 border: 1px solid #313244;
161 }
162 .speed-control input[type="range"]::-webkit-slider-thumb {
163 -webkit-appearance: none;
164 appearance: none;
165 width: 16px;
166 height: 16px;
167 background: #a6e3a1;
168 cursor: pointer;
169 border: 1px solid #313244;
170 }
171 .speed-control input[type="range"]::-moz-range-thumb {
172 width: 16px;
173 height: 16px;
174 background: #a6e3a1;
175 cursor: pointer;
176 border: 1px solid #313244;
177 }
178 .speed-control input[type="range"]::-webkit-slider-thumb:hover {
179 background: #b4e7b9;
180 }
181 .speed-control input[type="range"]::-moz-range-thumb:hover {
182 background: #b4e7b9;
183 }
184 .grid-container {
185 background: #11111b;
186 padding: 10px;
187 border: 2px solid #313244;
188 border-radius: 4px;
189 flex: 1;
190 display: flex;
191 align-items: center;
192 justify-content: center;
193 width: 100%;
194 max-width: 95vw;
195 overflow: hidden;
196 }
197 .grid {
198 display: grid;
199 gap: 1px;
200 image-rendering: pixelated;
201 }
202 .cell {
203 background: #181825;
204 display: flex;
205 align-items: center;
206 justify-content: center;
207 font-size: 14px;
208 }
209 .cell.paper {
210 background: #a6e3a1;
211 color: #1e1e2e;
212 }
213 .stats {
214 margin-top: 20px;
215 color: #a6adc8;
216 text-align: center;
217 font-size: 13px;
218 }
219 .footer {
220 margin-top: 20px;
221 color: #a6adc8;
222 text-align: center;
223 font-size: 12px;
224 }
225 </style>
226</head>
227<body>
228 <h1>AoC 2025 Day 4 - Paper Removal Visualization</h1>
229
230 <div class="controls">
231 <button id="prev">← Previous</button>
232 <button id="play" data-playing="false">▶ Play</button>
233 <button id="next">Next →</button>
234 <button id="reset">↺ Reset</button>
235 <span class="speed-control">
236 <label for="speed">Speed:</label>
237 <input type="range" id="speed" min="100" max="1000" value="500" step="50">
238 </span>
239 </div>
240
241 <div class="info">
242 Stage: <span id="stage">0</span> / <span id="total">${stages.length - 1}</span>
243 | Accessible: <span id="accessible">0</span>
244 | Total Removed: <span id="totalRemoved">0</span>
245 </div>
246
247 <div class="grid-container">
248 <div id="grid" class="grid"></div>
249 </div>
250
251 <div class="stats">
252 Grid size: ${paperMap.length} × ${paperMap[0].length}
253 | Total iterations: ${stages.length - 1}
254 | Final answer: ${stages.slice(1).reduce((sum, s) => sum + s.accessible, 0)}
255 </div>
256
257 <div class="footer">
258 <a href="../index.html">[Return to Index]</a>
259 </div>
260
261 <script>
262 const stages = ${JSON.stringify(stages)};
263 let currentStage = 0;
264 let playInterval = null;
265
266 const grid = document.getElementById('grid');
267 const stageEl = document.getElementById('stage');
268 const accessibleEl = document.getElementById('accessible');
269 const totalRemovedEl = document.getElementById('totalRemoved');
270 const prevBtn = document.getElementById('prev');
271 const nextBtn = document.getElementById('next');
272 const playBtn = document.getElementById('play');
273 const resetBtn = document.getElementById('reset');
274 const speedSlider = document.getElementById('speed');
275
276 function renderGrid() {
277 const stage = stages[currentStage];
278 const numRows = stage.map.length;
279 const numCols = stage.map[0].length;
280
281 // Only rebuild grid if it doesn't exist
282 if (grid.children.length === 0) {
283 grid.style.gridTemplateColumns = \`repeat(\${numCols}, 1fr)\`;
284 const totalCells = numRows * numCols;
285 for (let i = 0; i < totalCells; i++) {
286 const cell = document.createElement('div');
287 cell.className = 'cell';
288 grid.appendChild(cell);
289 }
290 }
291
292 // Update cell states
293 const cells = grid.children;
294 for (let row = 0; row < numRows; row++) {
295 for (let col = 0; col < numCols; col++) {
296 const idx = row * numCols + col;
297 const cell = cells[idx];
298 if (stage.map[row][col]) {
299 cell.classList.add('paper');
300 cell.textContent = '@';
301 } else {
302 cell.classList.remove('paper');
303 cell.textContent = '';
304 }
305 }
306 }
307
308 scaleGrid();
309
310 stageEl.textContent = currentStage;
311 accessibleEl.textContent = stage.accessible;
312
313 const totalRemoved = stages.slice(1, currentStage + 1).reduce((sum, s) => sum + s.accessible, 0);
314 totalRemovedEl.textContent = totalRemoved;
315
316 prevBtn.disabled = currentStage === 0;
317 nextBtn.disabled = currentStage === stages.length - 1;
318 }
319
320 function scaleGrid() {
321 const container = document.querySelector('.grid-container');
322 const stage = stages[currentStage];
323 const numRows = stage.map.length;
324 const numCols = stage.map[0].length;
325
326 const containerWidth = container.clientWidth;
327 const containerHeight = container.clientHeight;
328
329 const cellWidth = Math.floor((containerWidth - numCols) / numCols);
330 const cellHeight = Math.floor((containerHeight - numRows) / numRows);
331
332 const cellSize = Math.max(1, Math.min(cellWidth, cellHeight, 20));
333
334 const cells = grid.children;
335 for (let i = 0; i < cells.length; i++) {
336 cells[i].style.width = cellSize + 'px';
337 cells[i].style.height = cellSize + 'px';
338 cells[i].style.fontSize = Math.max(10, cellSize * 0.5) + 'px';
339 }
340 }
341
342 function goToStage(index) {
343 currentStage = Math.max(0, Math.min(stages.length - 1, index));
344 renderGrid();
345 }
346
347 function resetAnimation() {
348 goToStage(0);
349 }
350
351 prevBtn.addEventListener('click', () => goToStage(currentStage - 1));
352 nextBtn.addEventListener('click', () => goToStage(currentStage + 1));
353 resetBtn.addEventListener('click', resetAnimation);
354
355 playBtn.addEventListener('click', () => {
356 if (playInterval) {
357 clearInterval(playInterval);
358 playInterval = null;
359 playBtn.textContent = '▶ Play';
360 } else {
361 if (currentStage === stages.length - 1) {
362 resetAnimation();
363 }
364 playBtn.textContent = '⏸ Pause';
365 const speed = 1100 - parseInt(speedSlider.value);
366 playInterval = setInterval(() => {
367 if (currentStage < stages.length - 1) {
368 goToStage(currentStage + 1);
369 } else {
370 clearInterval(playInterval);
371 playInterval = null;
372 playBtn.textContent = '▶ Play';
373 }
374 }, speed);
375 }
376 });
377
378 speedSlider.addEventListener('input', () => {
379 if (playInterval) {
380 clearInterval(playInterval);
381 const speed = 1100 - parseInt(speedSlider.value);
382 playInterval = setInterval(() => {
383 if (currentStage < stages.length - 1) {
384 goToStage(currentStage + 1);
385 } else {
386 clearInterval(playInterval);
387 playInterval = null;
388 playBtn.textContent = '▶ Play';
389 }
390 }, speed);
391 }
392 });
393
394 // Keyboard controls
395 document.addEventListener('keydown', (e) => {
396 if (e.key === 'ArrowLeft') prevBtn.click();
397 if (e.key === 'ArrowRight') nextBtn.click();
398 if (e.key === ' ') {
399 e.preventDefault();
400 playBtn.click();
401 }
402 if (e.key === 'r' || e.key === 'R') resetBtn.click();
403 });
404
405 // Rescale on window resize
406 window.addEventListener('resize', scaleGrid);
407
408 renderGrid();
409 </script>
410</body>
411</html>`;
412
413await Bun.write("index.html", html);
414console.log("Generated index.html with", stages.length, "stages");