eyeling 1.16.2 → 1.16.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/HANDBOOK.md +4 -0
  2. package/README.md +0 -1
  3. package/examples/ershov-mixed-computation.n3 +106 -0
  4. package/examples/output/ershov-mixed-computation.n3 +15 -0
  5. package/eyeling.js +510 -263
  6. package/lib/cli.js +22 -12
  7. package/lib/engine.js +488 -251
  8. package/package.json +2 -3
  9. package/arctifacts/README.md +0 -59
  10. package/arctifacts/ackermann.html +0 -678
  11. package/arctifacts/auroracare.html +0 -1297
  12. package/arctifacts/bike-trip.html +0 -752
  13. package/arctifacts/binomial-theorem.html +0 -631
  14. package/arctifacts/bmi.html +0 -511
  15. package/arctifacts/building-performance.html +0 -750
  16. package/arctifacts/clinical-care.html +0 -726
  17. package/arctifacts/collatz.html +0 -403
  18. package/arctifacts/complex.html +0 -321
  19. package/arctifacts/control-system.html +0 -482
  20. package/arctifacts/delfour.html +0 -849
  21. package/arctifacts/earthquake-epicenter.html +0 -982
  22. package/arctifacts/eco-route.html +0 -662
  23. package/arctifacts/euclid-infinitude.html +0 -564
  24. package/arctifacts/euler-identity.html +0 -667
  25. package/arctifacts/exoplanet-transit.html +0 -1000
  26. package/arctifacts/faltings-theorem.html +0 -1046
  27. package/arctifacts/fibonacci.html +0 -299
  28. package/arctifacts/fundamental-theorem-arithmetic.html +0 -398
  29. package/arctifacts/godel-numbering.html +0 -743
  30. package/arctifacts/gps-bike.html +0 -759
  31. package/arctifacts/gps-clinical-bench.html +0 -792
  32. package/arctifacts/graph-french.html +0 -449
  33. package/arctifacts/grass-molecular.html +0 -592
  34. package/arctifacts/group-theory.html +0 -740
  35. package/arctifacts/health-info.html +0 -833
  36. package/arctifacts/kaprekar-constant.html +0 -576
  37. package/arctifacts/lee.html +0 -805
  38. package/arctifacts/linked-lists.html +0 -502
  39. package/arctifacts/lldm.html +0 -612
  40. package/arctifacts/matrix-multiplication.html +0 -502
  41. package/arctifacts/matrix.html +0 -651
  42. package/arctifacts/newton-raphson.html +0 -944
  43. package/arctifacts/peano-factorial.html +0 -456
  44. package/arctifacts/pi.html +0 -363
  45. package/arctifacts/polynomial.html +0 -646
  46. package/arctifacts/prime.html +0 -366
  47. package/arctifacts/pythagorean-theorem.html +0 -468
  48. package/arctifacts/rest-path.html +0 -469
  49. package/arctifacts/roots-of-unity.html +0 -363
  50. package/arctifacts/turing.html +0 -409
  51. package/arctifacts/wind-turbines.html +0 -726
@@ -1,805 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Lee (Grid BFS shortest path)</title>
7
- <style>
8
- :root {
9
- --fg: #101014;
10
- --bg: #ffffff;
11
- --muted: #666;
12
- --accent: #2563eb;
13
- --chip: #eef2ff;
14
- --ok: #16a34a;
15
- --bad: #dc2626;
16
- --warn: #ca8a04;
17
- --card: color-mix(in srgb, var(--accent) 4%, transparent);
18
- }
19
- @media (prefers-color-scheme: dark) {
20
- :root {
21
- --fg: #eaeaf0;
22
- --bg: #0b0b10;
23
- --muted: #a0a0b0;
24
- --accent: #60a5fa;
25
- --chip: #0e1a32;
26
- --card: color-mix(in srgb, var(--accent) 6%, transparent);
27
- }
28
- }
29
- html,
30
- body {
31
- margin: 0;
32
- padding: 0;
33
- background: var(--bg);
34
- color: var(--fg);
35
- font:
36
- 15px/1.6 ui-sans-serif,
37
- system-ui,
38
- -apple-system,
39
- Segoe UI,
40
- Roboto,
41
- Helvetica,
42
- Arial;
43
- }
44
- main {
45
- max-width: 1200px;
46
- margin: 0 auto;
47
- padding: 28px 16px 96px;
48
- }
49
- h1 {
50
- font-size: clamp(1.6rem, 2.6vw + 1rem, 2.2rem);
51
- margin: 0 0 6px;
52
- }
53
- header p {
54
- margin: 0;
55
- color: var(--muted);
56
- }
57
- section {
58
- margin: 18px 0 22px;
59
- padding: 14px 14px 16px;
60
- border: 1px solid color-mix(in srgb, var(--accent) 18%, transparent);
61
- border-radius: 14px;
62
- background: var(--card);
63
- }
64
- section h2 {
65
- margin: 0 0 10px;
66
- font-size: 1.15rem;
67
- }
68
- .row {
69
- display: flex;
70
- gap: 12px;
71
- align-items: center;
72
- flex-wrap: wrap;
73
- }
74
- .col {
75
- display: flex;
76
- flex-direction: column;
77
- gap: 10px;
78
- }
79
- .btn {
80
- appearance: none;
81
- border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
82
- background: color-mix(in srgb, var(--accent) 8%, transparent);
83
- color: var(--fg);
84
- border-radius: 10px;
85
- padding: 8px 12px;
86
- font-weight: 700;
87
- cursor: pointer;
88
- }
89
- .mono {
90
- font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace;
91
- }
92
- .muted {
93
- color: var(--muted);
94
- }
95
- .small {
96
- font-size: 0.92em;
97
- }
98
- .chip {
99
- display: inline-block;
100
- padding: 2px 8px;
101
- border-radius: 999px;
102
- background: var(--chip);
103
- font-weight: 600;
104
- }
105
- input[type='number'],
106
- input[type='range'] {
107
- border-radius: 10px;
108
- border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
109
- padding: 6px 8px;
110
- }
111
- label {
112
- user-select: none;
113
- }
114
- /* Grid */
115
- #boardWrap {
116
- position: relative;
117
- }
118
- .board {
119
- display: grid;
120
- gap: 2px;
121
- background: color-mix(in srgb, var(--accent) 15%, transparent);
122
- padding: 4px;
123
- border-radius: 10px;
124
- }
125
- .cell {
126
- width: 22px;
127
- height: 22px;
128
- border-radius: 4px;
129
- background: color-mix(in srgb, var(--accent) 8%, transparent);
130
- border: 1px solid color-mix(in srgb, var(--accent) 24%, transparent);
131
- }
132
- .cell.wall {
133
- background: color-mix(in srgb, #000 50%, transparent);
134
- border-color: color-mix(in srgb, #000 60%, transparent);
135
- }
136
- .cell.start {
137
- background: #16a34a;
138
- border-color: #15803d;
139
- }
140
- .cell.goal {
141
- background: #dc2626;
142
- border-color: #991b1b;
143
- }
144
- .cell.path {
145
- background: #f59e0b;
146
- border-color: #b45309;
147
- }
148
- .toolbar {
149
- display: flex;
150
- gap: 8px;
151
- flex-wrap: wrap;
152
- align-items: center;
153
- }
154
- .tool {
155
- padding: 6px 10px;
156
- border-radius: 10px;
157
- border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
158
- cursor: pointer;
159
- background: color-mix(in srgb, var(--accent) 8%, transparent);
160
- }
161
- .tool.active {
162
- outline: 2px solid color-mix(in srgb, var(--accent) 50%, transparent);
163
- }
164
- #answer {
165
- overflow-x: auto;
166
- }
167
- #answer pre {
168
- white-space: pre !important;
169
- overflow-x: auto;
170
- overflow-y: auto;
171
- max-width: 100%;
172
- }
173
- .ok {
174
- color: var(--ok);
175
- }
176
- .bad {
177
- color: var(--bad);
178
- }
179
- .warn {
180
- color: var(--warn);
181
- }
182
- </style>
183
- </head>
184
- <body>
185
- <main>
186
- <header class="row">
187
- <div>
188
- <h1>Lee (Grid BFS shortest path)</h1>
189
- <p>Self‑contained grid pathfinding with the Lee algorithm (breadth‑first search on a 4‑neighborhood).</p>
190
- </div>
191
- <div class="row" style="margin-left: auto">
192
- <label class="muted small">Rows: <input id="rows" type="number" min="5" max="80" value="25" /></label>
193
- <label class="muted small">Cols: <input id="cols" type="number" min="5" max="80" value="25" /></label>
194
- <button id="resize" class="btn">Resize</button>
195
- <button id="clear" class="btn">Clear</button>
196
- <button id="maze" class="btn">Random maze</button>
197
- <label class="muted small"><input id="ensure" type="checkbox" checked /> ensure solvable</label>
198
- <label class="muted small"
199
- >density <input id="density" type="range" min="0" max="0.6" step="0.02" value="0.28"
200
- /></label>
201
- <button id="solve" class="btn">Solve</button>
202
- <button id="checkBtn" class="btn">Check</button>
203
- </div>
204
- </header>
205
-
206
- <section>
207
- <h2>What this is?</h2>
208
- <p>
209
- The Lee algorithm finds the shortest path in a grid with uniform step cost. It’s a classic breadth‑first
210
- search: we expand from the start, layer by layer, until we reach the goal. Then we backtrack by following
211
- decreasing distance values to reconstruct a shortest path. Works with walls (blocked cells) and 4‑neighborhood
212
- moves.
213
- </p>
214
- </section>
215
-
216
- <section class="row" style="align-items: flex-start">
217
- <div class="col">
218
- <div class="toolbar">
219
- <span class="muted small">Tool:</span>
220
- <button class="tool active" id="tool-wall" data-tool="wall">Draw walls</button>
221
- <button class="tool" id="tool-erase" data-tool="erase">Erase</button>
222
- <button class="tool" id="tool-start" data-tool="start">Set start</button>
223
- <button class="tool" id="tool-goal" data-tool="goal">Set goal</button>
224
- </div>
225
- <div id="boardWrap">
226
- <div id="board" class="board"></div>
227
- </div>
228
- </div>
229
- <div class="col" style="flex: 1">
230
- <h2>Text</h2>
231
- <p class="small muted">
232
- Paste or copy your grid here. Use <code>#</code> for walls, <code>.</code> for empty, <code>S</code> start,
233
- <code>G</code> goal.
234
- </p>
235
- <textarea id="txt" class="mono" spellcheck="false" style="width: 100%; min-height: 180px"></textarea>
236
- <div class="row">
237
- <button id="loadDemo" class="btn">Load demo</button>
238
- <button id="syncFromText" class="btn">Apply from text</button>
239
- <button id="syncToText" class="btn">Copy from grid</button>
240
- </div>
241
- </div>
242
- </section>
243
-
244
- <section id="answer">
245
- <h2>Answer</h2>
246
- <div id="chips" class="row small" style="gap: 8px"></div>
247
- <div id="summary" class="mono small"></div>
248
- </section>
249
-
250
- <section id="reason">
251
- <h2>Reason why</h2>
252
- <div class="small">
253
- <p>
254
- On grids with unit weights, BFS explores in concentric “rings”. The first time it dequeues the goal, the
255
- discovered distance is optimal. The backtracking step simply walks to any neighbor with distance −1 until it
256
- reaches the start. Obstacles are simply never enqueued.
257
- </p>
258
- </div>
259
- </section>
260
-
261
- <section id="check">
262
- <h2>Check (harness)</h2>
263
- <div id="check-body"></div>
264
- </section>
265
- </main>
266
-
267
- <script>
268
- (function () {
269
- 'use strict';
270
- const $ = (id) => document.getElementById(id);
271
- const setHTML = (id, html) => {
272
- const el = $(id);
273
- if (el) el.innerHTML = html;
274
- };
275
- const now = () => performance.now();
276
-
277
- // State
278
- let R = 25,
279
- C = 25;
280
- let grid = []; // 0 empty, 1 wall
281
- let start = null; // [r,c]
282
- let goal = null; // [r,c]
283
- let mouseDown = false,
284
- currentTool = 'wall';
285
-
286
- // Helpers
287
- const inside = (r, c) => r >= 0 && r < R && c >= 0 && c < C;
288
- const idx = (r, c) => r * C + c;
289
- const dirs = [
290
- [1, 0],
291
- [-1, 0],
292
- [0, 1],
293
- [0, -1],
294
- ];
295
-
296
- // Build board UI
297
- function buildBoard() {
298
- const board = $('board');
299
- board.style.gridTemplateColumns = `repeat(${C}, 22px)`;
300
- board.innerHTML = '';
301
- for (let r = 0; r < R; r++) {
302
- for (let c = 0; c < C; c++) {
303
- const div = document.createElement('div');
304
- div.className = 'cell';
305
- div.dataset.r = r;
306
- div.dataset.c = c;
307
- board.appendChild(div);
308
- }
309
- }
310
- paintAll();
311
- }
312
-
313
- function paintAll(pathSet = null) {
314
- const board = $('board');
315
- for (let r = 0; r < R; r++) {
316
- for (let c = 0; c < C; c++) {
317
- const d = board.children[idx(r, c)];
318
- d.className = 'cell';
319
- if (grid[idx(r, c)] === 1) d.classList.add('wall');
320
- if (pathSet && pathSet.has(idx(r, c))) d.classList.add('path');
321
- if (start && start[0] === r && start[1] === c) d.classList.add('start');
322
- if (goal && goal[0] === r && goal[1] === c) d.classList.add('goal');
323
- }
324
- }
325
- }
326
-
327
- function setTool(tool) {
328
- currentTool = tool;
329
- for (const b of document.querySelectorAll('.tool')) b.classList.remove('active');
330
- const el = document.querySelector('.tool[data-tool="' + tool + '"]');
331
- if (el) el.classList.add('active');
332
- }
333
-
334
- function attachBoardEvents() {
335
- const board = $('board');
336
- board.addEventListener('mousedown', (e) => {
337
- mouseDown = true;
338
- onBoardInteract(e);
339
- });
340
- board.addEventListener('mousemove', (e) => {
341
- if (mouseDown) onBoardInteract(e);
342
- });
343
- window.addEventListener('mouseup', () => (mouseDown = false));
344
- board.addEventListener(
345
- 'touchstart',
346
- (e) => {
347
- onBoardInteract(e.touches[0]);
348
- e.preventDefault();
349
- },
350
- { passive: false },
351
- );
352
- board.addEventListener(
353
- 'touchmove',
354
- (e) => {
355
- onBoardInteract(e.touches[0]);
356
- e.preventDefault();
357
- },
358
- { passive: false },
359
- );
360
- }
361
-
362
- function onBoardInteract(e) {
363
- const target = document.elementFromPoint(e.clientX, e.clientY);
364
- if (!target || !target.classList.contains('cell')) return;
365
- const r = Number(target.dataset.r),
366
- c = Number(target.dataset.c);
367
- if (currentTool === 'wall') {
368
- if (start && start[0] === r && start[1] === c) start = null;
369
- if (goal && goal[0] === r && goal[1] === c) goal = null;
370
- grid[idx(r, c)] = 1;
371
- } else if (currentTool === 'erase') {
372
- grid[idx(r, c)] = 0;
373
- } else if (currentTool === 'start') {
374
- if (start) grid[idx(start[0], start[1])] = 0;
375
- start = [r, c];
376
- grid[idx(r, c)] = 0;
377
- } else if (currentTool === 'goal') {
378
- if (goal) grid[idx(goal[0], goal[1])] = 0;
379
- goal = [r, c];
380
- grid[idx(r, c)] = 0;
381
- }
382
- paintAll();
383
- syncTextFromGrid();
384
- }
385
-
386
- // Text <-> grid
387
- function gridToText() {
388
- const lines = [];
389
- for (let r = 0; r < R; r++) {
390
- let s = '';
391
- for (let c = 0; c < C; c++) {
392
- if (start && start[0] === r && start[1] === c) s += 'S';
393
- else if (goal && goal[0] === r && goal[1] === c) s += 'G';
394
- else s += grid[idx(r, c)] === 1 ? '#' : '.';
395
- }
396
- lines.push(s);
397
- }
398
- return lines.join('\n');
399
- }
400
-
401
- function textToGrid(txt) {
402
- const lines = (txt || '')
403
- .trim()
404
- .split(/\\r?\\n/)
405
- .filter(Boolean);
406
- if (lines.length === 0) return false;
407
- const rr = lines.length,
408
- cc = Math.max(...lines.map((s) => s.length));
409
- R = rr;
410
- C = cc;
411
- grid = Array(R * C).fill(0);
412
- start = null;
413
- goal = null;
414
- for (let r = 0; r < R; r++) {
415
- const row = lines[r];
416
- for (let c = 0; c < C; c++) {
417
- const ch = row[c] || '.';
418
- if (ch === '#') grid[idx(r, c)] = 1;
419
- else if (ch === 'S') {
420
- start = [r, c];
421
- grid[idx(r, c)] = 0;
422
- } else if (ch === 'G') {
423
- goal = [r, c];
424
- grid[idx(r, c)] = 0;
425
- } else grid[idx(r, c)] = 0;
426
- }
427
- }
428
- buildBoard();
429
- syncToText();
430
- return true;
431
- }
432
-
433
- function syncFromText() {
434
- textToGrid($('txt').value);
435
- }
436
- function syncToText() {
437
- $('txt').value = gridToText();
438
- }
439
- function syncTextFromGrid() {
440
- $('txt').value = gridToText();
441
- }
442
-
443
- // Resize / clear / maze
444
- function doResize() {
445
- const r = Math.max(5, Math.min(80, Math.floor(Number($('rows').value) || 25)));
446
- const c = Math.max(5, Math.min(80, Math.floor(Number($('cols').value) || 25)));
447
- R = r;
448
- C = c;
449
- grid = Array(R * C).fill(0);
450
- start = null;
451
- goal = null;
452
- buildBoard();
453
- syncToText();
454
- }
455
- function doClear() {
456
- grid.fill(0);
457
- start = null;
458
- goal = null;
459
- paintAll();
460
- syncToText();
461
- }
462
-
463
- function baselineReachable(a, rows, cols, S, G) {
464
- const inside = (r, c) => r >= 0 && r < rows && c >= 0 && c < cols;
465
- const id = (r, c) => r * cols + c;
466
- const dist = new Int32Array(rows * cols).fill(-1);
467
- const s = id(S[0], S[1]),
468
- g = id(G[0], G[1]);
469
- const q = [s];
470
- dist[s] = 0;
471
- let qi = 0;
472
- while (qi < q.length) {
473
- const cur = q[qi++],
474
- r = (cur / cols) | 0,
475
- c = cur % cols;
476
- if (cur === g) return true;
477
- for (const [dr, dc] of [
478
- [1, 0],
479
- [-1, 0],
480
- [0, 1],
481
- [0, -1],
482
- ]) {
483
- const nr = r + dr,
484
- nc = c + dc;
485
- if (!inside(nr, nc)) continue;
486
- const ni = id(nr, nc);
487
- if (a[ni] === 1 || dist[ni] !== -1) continue;
488
- dist[ni] = dist[cur] + 1;
489
- q.push(ni);
490
- }
491
- }
492
- return false;
493
- }
494
-
495
- function doMaze() {
496
- const p = Number($('density').value || 0.28);
497
- const needSolvable = $('ensure') && $('ensure').checked;
498
- const maxTries = 200;
499
- let tries = 0,
500
- ok = false;
501
-
502
- function randomFill() {
503
- for (let i = 0; i < R * C; i++) grid[i] = Math.random() < p ? 1 : 0;
504
- start = [0, 0];
505
- goal = [R - 1, C - 1];
506
- grid[idx(start[0], start[1])] = 0;
507
- grid[idx(goal[0], goal[1])] = 0;
508
- }
509
-
510
- if (!needSolvable) {
511
- randomFill();
512
- } else {
513
- for (tries = 0; tries < maxTries; tries++) {
514
- randomFill();
515
- if (baselineReachable(grid, R, C, start, goal)) {
516
- ok = true;
517
- break;
518
- }
519
- }
520
- if (!ok) {
521
- // As a fallback, thin the maze a bit and try again.
522
- for (tries = 0; tries < maxTries; tries++) {
523
- for (let i = 0; i < R * C; i++) grid[i] = Math.random() < p * 0.6 ? 1 : 0;
524
- start = [0, 0];
525
- goal = [R - 1, C - 1];
526
- grid[idx(start[0], start[1])] = 0;
527
- grid[idx(goal[0], goal[1])] = 0;
528
- if (baselineReachable(grid, R, C, start, goal)) {
529
- ok = true;
530
- break;
531
- }
532
- }
533
- }
534
- }
535
-
536
- paintAll();
537
- syncToText();
538
- if (needSolvable) {
539
- const msg = ok ? '(found solvable maze)' : '(could not ensure solvable after attempts; try lower density)';
540
- setHTML('summary', '<span class="small muted">Random maze ' + msg + '</span>');
541
- }
542
- }
543
- // Lee (BFS) — returns {found, dist, path: [idx...], expansions}
544
- function lee() {
545
- const t0 = now();
546
- if (!start || !goal)
547
- return { found: false, dist: -1, path: [], expansions: 0, ms: 0, reason: 'Start or goal missing.' };
548
- const q = [];
549
- const dist = new Int32Array(R * C).fill(-1);
550
- const sidx = idx(start[0], start[1]);
551
- const gidx = idx(goal[0], goal[1]);
552
- if (grid[sidx] === 1 || grid[gidx] === 1)
553
- return { found: false, dist: -1, path: [], expansions: 0, ms: 0, reason: 'Start or goal on a wall.' };
554
-
555
- q.push(sidx);
556
- dist[sidx] = 0;
557
- let qi = 0,
558
- expansions = 0,
559
- reached = false;
560
- while (qi < q.length) {
561
- const cur = q[qi++];
562
- const r = Math.floor(cur / C),
563
- c = cur % C;
564
- if (cur === gidx) {
565
- reached = true;
566
- break;
567
- }
568
- for (const [dr, dc] of dirs) {
569
- const nr = r + dr,
570
- nc = c + dc;
571
- if (!inside(nr, nc)) continue;
572
- const ni = idx(nr, nc);
573
- if (grid[ni] === 1) continue;
574
- if (dist[ni] !== -1) continue;
575
- dist[ni] = dist[cur] + 1;
576
- q.push(ni);
577
- }
578
- expansions++;
579
- }
580
- const t1 = now();
581
- if (!reached) return { found: false, dist: -1, path: [], expansions, ms: t1 - t0, reason: 'No path.' };
582
-
583
- // Backtrack
584
- const path = [];
585
- let cur = gidx;
586
- path.push(cur);
587
- while (cur !== sidx) {
588
- const r = Math.floor(cur / C),
589
- c = cur % C;
590
- let moved = false;
591
- for (const [dr, dc] of dirs) {
592
- const nr = r + dr,
593
- nc = c + dc;
594
- if (!inside(nr, nc)) continue;
595
- const ni = idx(nr, nc);
596
- if (dist[ni] === dist[cur] - 1) {
597
- path.push(ni);
598
- cur = ni;
599
- moved = true;
600
- break;
601
- }
602
- }
603
- if (!moved) break; // shouldn't happen
604
- }
605
- path.reverse();
606
- return { found: true, dist: dist[gidx], path, expansions, ms: t1 - t0 };
607
- }
608
-
609
- // Solve + render
610
- function doSolve() {
611
- const res = lee();
612
- if (!res.found) {
613
- setHTML('summary', `<div class="bad">No solution: ${res.reason || ''}</div>`);
614
- $('chips').innerHTML = '';
615
- paintAll(null);
616
- return;
617
- }
618
- const pset = new Set(res.path);
619
- paintAll(pset);
620
- const chips = [
621
- ['rows', R],
622
- ['cols', C],
623
- ['walls', grid.reduce((a, b) => a + (b === 1), 0)],
624
- ['expansions', res.expansions],
625
- ['dist', res.dist],
626
- ['time', res.ms.toFixed(2) + ' ms'],
627
- ]
628
- .map(([k, v]) => `<span class="chip">${k}: ${v}</span>`)
629
- .join(' ');
630
- $('chips').innerHTML = chips;
631
- setHTML(
632
- 'summary',
633
- `<pre class="mono">Path length (steps): ${res.dist}
634
- Nodes expanded: ${res.expansions}
635
- </pre>`,
636
- );
637
- }
638
-
639
- // Check (harness)
640
- function runChecks() {
641
- const lines = [];
642
- const ok = (b) => (b ? '✓' : '✗');
643
-
644
- // 1) If path found, verify monotone decrease along path and that it starts at S and ends at G.
645
- const res = lee();
646
- if (res.found) {
647
- const distOk = (function () {
648
- const dist = new Int32Array(R * C).fill(-1);
649
- const q = [idx(start[0], start[1])];
650
- dist[q[0]] = 0;
651
- let qi = 0;
652
- while (qi < q.length) {
653
- const cur = q[qi++];
654
- const r = Math.floor(cur / C),
655
- c = cur % C;
656
- for (const [dr, dc] of dirs) {
657
- const nr = r + dr,
658
- nc = c + dc;
659
- if (!inside(nr, nc)) continue;
660
- const ni = idx(nr, nc);
661
- if (grid[ni] === 1 || dist[ni] !== -1) continue;
662
- dist[ni] = dist[cur] + 1;
663
- q.push(ni);
664
- }
665
- }
666
- // Check endpoints
667
- const sOK = res.path.length > 0 && res.path[0] === idx(start[0], start[1]);
668
- const gOK = res.path.length > 0 && res.path[res.path.length - 1] === idx(goal[0], goal[1]);
669
- let mono = true;
670
- for (let i = 1; i < res.path.length; i++) {
671
- if (dist[res.path[i]] !== dist[res.path[i - 1]] + 1) {
672
- mono = false;
673
- break;
674
- }
675
- }
676
- const lenOK = res.path.length - 1 === dist[idx(goal[0], goal[1])];
677
- return sOK && gOK && mono && lenOK;
678
- })();
679
- lines.push(`Path correctness (start→goal, monotone distances, optimal length): ${ok(distOk)}`);
680
- } else {
681
- lines.push('No path found — skipping path checks.');
682
- }
683
-
684
- // 2) Random mazes quick sanity: generate 5 grids and ensure BFS agrees with itself on distance (trivial) and respects walls
685
- let rndOK = true;
686
- for (let t = 0; t < 5; t++) {
687
- const r = 20,
688
- c = 20,
689
- p = 0.25;
690
- const arr = Array(r * c)
691
- .fill(0)
692
- .map(() => (Math.random() < p ? 1 : 0));
693
- const s = [0, 0],
694
- g = [r - 1, c - 1];
695
- arr[0] = 0;
696
- arr[r * c - 1] = 0;
697
- // single-run BFS
698
- function bfs(arrR, arrC, a, sx, sy, gx, gy) {
699
- const inside = (rr, cc) => rr >= 0 && rr < arrR && cc >= 0 && cc < arrC;
700
- const id = (rr, cc) => rr * arrC + cc;
701
- const dist = new Int32Array(arrR * arrC).fill(-1);
702
- const q = [id(sx, sy)];
703
- dist[q[0]] = 0;
704
- let qi = 0;
705
- let reached = false;
706
- while (qi < q.length) {
707
- const cur = q[qi++];
708
- const rr = Math.floor(cur / arrC),
709
- cc = cur % arrC;
710
- if (rr === gx && cc === gy) {
711
- reached = true;
712
- break;
713
- }
714
- for (const [dr, dc] of [
715
- [1, 0],
716
- [-1, 0],
717
- [0, 1],
718
- [0, -1],
719
- ]) {
720
- const nr = rr + dr,
721
- nc = cc + dc;
722
- if (!inside(nr, nc)) continue;
723
- const ni = id(nr, nc);
724
- if (a[ni] === 1 || dist[ni] !== -1) continue;
725
- dist[ni] = dist[cur] + 1;
726
- q.push(ni);
727
- }
728
- }
729
- return { reached, dist };
730
- }
731
- const r1 = bfs(r, c, arr, s[0], s[1], g[0], g[1]);
732
- if (!r1.reached && r1.dist[r * c - 1] !== -1) {
733
- rndOK = false;
734
- break;
735
- }
736
- }
737
- lines.push(`Randomized quick checks (5 samples): ${ok(rndOK)}`);
738
-
739
- const pre = document.createElement('pre');
740
- pre.className = 'mono';
741
- pre.style.whiteSpace = 'pre-wrap';
742
- pre.textContent = lines.join('\n');
743
- $('check-body').replaceChildren(pre);
744
- }
745
-
746
- // Demo grid
747
- const DEMO = [
748
- 'S....#.................',
749
- '#####.#.#############..',
750
- '.....#.#...........#..G',
751
- '.###.#.#.#######.#.#.##',
752
- '.#...#.#.....#...#.#..#',
753
- '.#.###.#####.#.###.#..#',
754
- '.#...#.....#.#...#.#..#',
755
- '.#.#.#####.#.###.#.####',
756
- '.#.#.....#.#.....#....#',
757
- '.#.#.###.#.#######.##.#',
758
- '.#.#.#...#.........#..#',
759
- '.#.#.#.###########.#..#',
760
- '.#.#.#.............#..#',
761
- '.#.#.###############..#',
762
- '.#.#.................#.',
763
- ].join('\n');
764
-
765
- function loadDemo() {
766
- $('txt').value = DEMO;
767
- syncFromText();
768
- }
769
-
770
- // Wire up
771
- $('resize').addEventListener('click', doResize);
772
- $('clear').addEventListener('click', doClear);
773
- $('maze').addEventListener('click', doMaze);
774
- $('solve').addEventListener('click', () => {
775
- doSolve();
776
- runChecks();
777
- });
778
- $('checkBtn').addEventListener('click', runChecks);
779
- $('loadDemo').addEventListener('click', loadDemo);
780
- $('syncFromText').addEventListener('click', syncFromText);
781
- $('syncToText').addEventListener('click', syncToText);
782
- for (const el of document.querySelectorAll('.tool')) {
783
- el.addEventListener('click', () => setTool(el.dataset.tool));
784
- }
785
-
786
- // Init
787
- (function init() {
788
- R = 25;
789
- C = 25;
790
- grid = Array(R * C).fill(0);
791
- start = null;
792
- goal = null;
793
- buildBoard();
794
- attachBoardEvents();
795
- doMaze(); // start with a random maze
796
- setTool('wall');
797
- setHTML(
798
- 'summary',
799
- '<span class="muted small">Click Solve to find a path, or draw your own maze. Tools: walls, erase, set start/goal.</span>',
800
- );
801
- })();
802
- })();
803
- </script>
804
- </body>
805
- </html>