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: 16px;
146 }
147 .grid-container {
148 background: #11111b;
149 padding: 10px;
150 border: 2px solid #313244;
151 border-radius: 4px;
152 flex: 1;
153 display: flex;
154 align-items: center;
155 justify-content: center;
156 width: 100%;
157 max-width: 95vw;
158 overflow: hidden;
159 }
160 .grid {
161 display: grid;
162 gap: 1px;
163 image-rendering: pixelated;
164 }
165 .cell {
166 background: #181825;
167 }
168 .cell.paper {
169 background: #a6e3a1;
170 }
171 .stats {
172 margin-top: 20px;
173 color: #a6adc8;
174 text-align: center;
175 }
176 .footer {
177 margin-top: 20px;
178 color: #a6adc8;
179 text-align: center;
180 font-size: 12px;
181 }
182 </style>
183</head>
184<body>
185 <h1>AOC 2025 Day 4 - Paper Removal Visualization</h1>
186
187 <div class="controls">
188 <button id="prev">← Previous</button>
189 <button id="play" data-playing="false">▶ Play</button>
190 <button id="next">Next →</button>
191 <span class="info">
192 Stage: <span id="stage">0</span> / <span id="total">${stages.length - 1}</span>
193 | Accessible: <span id="accessible">0</span>
194 | Total Removed: <span id="totalRemoved">0</span>
195 </span>
196 </div>
197
198 <div class="grid-container">
199 <div id="grid" class="grid"></div>
200 </div>
201
202 <div class="stats">
203 Grid size: ${paperMap.length} × ${paperMap[0].length}
204 | Total iterations: ${stages.length - 1}
205 | Final answer: ${stages.slice(1).reduce((sum, s) => sum + s.accessible, 0)}
206 </div>
207
208 <div class="footer">
209 <a href="../index.html">[Return to Index]</a>
210 </div>
211
212 <script>
213 const stages = ${JSON.stringify(stages)};
214 let currentStage = 0;
215 let playInterval = null;
216
217 const grid = document.getElementById('grid');
218 const stageEl = document.getElementById('stage');
219 const accessibleEl = document.getElementById('accessible');
220 const totalRemovedEl = document.getElementById('totalRemoved');
221 const prevBtn = document.getElementById('prev');
222 const nextBtn = document.getElementById('next');
223 const playBtn = document.getElementById('play');
224
225 function renderGrid() {
226 const stage = stages[currentStage];
227 const numRows = stage.map.length;
228 const numCols = stage.map[0].length;
229
230 // Only rebuild grid if it doesn't exist
231 if (grid.children.length === 0) {
232 grid.style.gridTemplateColumns = \`repeat(\${numCols}, 1fr)\`;
233 const totalCells = numRows * numCols;
234 for (let i = 0; i < totalCells; i++) {
235 const cell = document.createElement('div');
236 cell.className = 'cell';
237 grid.appendChild(cell);
238 }
239 }
240
241 // Update cell states
242 const cells = grid.children;
243 for (let row = 0; row < numRows; row++) {
244 for (let col = 0; col < numCols; col++) {
245 const idx = row * numCols + col;
246 if (stage.map[row][col]) {
247 cells[idx].classList.add('paper');
248 } else {
249 cells[idx].classList.remove('paper');
250 }
251 }
252 }
253
254 scaleGrid();
255
256 stageEl.textContent = currentStage;
257 accessibleEl.textContent = stage.accessible;
258
259 const totalRemoved = stages.slice(1, currentStage + 1).reduce((sum, s) => sum + s.accessible, 0);
260 totalRemovedEl.textContent = totalRemoved;
261
262 prevBtn.disabled = currentStage === 0;
263 nextBtn.disabled = currentStage === stages.length - 1;
264 }
265
266 function scaleGrid() {
267 const container = document.querySelector('.grid-container');
268 const stage = stages[currentStage];
269 const numRows = stage.map.length;
270 const numCols = stage.map[0].length;
271
272 const containerWidth = container.clientWidth;
273 const containerHeight = container.clientHeight;
274
275 const cellWidth = Math.floor((containerWidth - numCols) / numCols);
276 const cellHeight = Math.floor((containerHeight - numRows) / numRows);
277
278 const cellSize = Math.max(1, Math.min(cellWidth, cellHeight, 20));
279
280 const cells = grid.children;
281 for (let i = 0; i < cells.length; i++) {
282 cells[i].style.width = cellSize + 'px';
283 cells[i].style.height = cellSize + 'px';
284 }
285 }
286
287 function goToStage(index) {
288 currentStage = Math.max(0, Math.min(stages.length - 1, index));
289 renderGrid();
290 }
291
292 prevBtn.addEventListener('click', () => {
293 goToStage(currentStage - 1);
294 });
295
296 nextBtn.addEventListener('click', () => {
297 goToStage(currentStage + 1);
298 });
299
300 playBtn.addEventListener('click', () => {
301 if (playInterval) {
302 clearInterval(playInterval);
303 playInterval = null;
304 playBtn.textContent = '▶ Play';
305 } else {
306 playBtn.textContent = '⏸ Pause';
307 playInterval = setInterval(() => {
308 if (currentStage < stages.length - 1) {
309 goToStage(currentStage + 1);
310 } else {
311 clearInterval(playInterval);
312 playInterval = null;
313 playBtn.textContent = '▶ Play';
314 }
315 }, 500);
316 }
317 });
318
319 // Keyboard controls
320 document.addEventListener('keydown', (e) => {
321 if (e.key === 'ArrowLeft') prevBtn.click();
322 if (e.key === 'ArrowRight') nextBtn.click();
323 if (e.key === ' ') {
324 e.preventDefault();
325 playBtn.click();
326 }
327 });
328
329 // Rescale on window resize
330 window.addEventListener('resize', scaleGrid);
331
332 renderGrid();
333 </script>
334</body>
335</html>`;
336
337await Bun.write("index.html", html);
338console.log("Generated index.html with", stages.length, "stages");