@hayro_o7/labyrinth 0.0.2 → 0.0.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.
@@ -0,0 +1,6 @@
1
+ import type { GeneralGraph, BFSResult } from '../types';
2
+ export declare function bidirectionalBFS(graph: GeneralGraph, startId: string, endId: string): BFSResult;
3
+ export declare function findOptimalMultiGoalPath(graph: GeneralGraph, startId: string, goalIds: string[]): {
4
+ optimalPath: string[];
5
+ allPaths: Map<string, Map<string, string[]>>;
6
+ };
@@ -0,0 +1,153 @@
1
+ export function bidirectionalBFS(graph, startId, endId) {
2
+ const steps = [];
3
+ // Forward search from start
4
+ const forwardQueue = [[startId]];
5
+ const forwardVisited = new Map();
6
+ forwardVisited.set(startId, [startId]);
7
+ // Backward search from end
8
+ const backwardQueue = [[endId]];
9
+ const backwardVisited = new Map();
10
+ backwardVisited.set(endId, [endId]);
11
+ steps.push({ nodeId: startId, type: 'start-forward', side: 'forward', level: 0 });
12
+ steps.push({ nodeId: endId, type: 'start-backward', side: 'backward', level: 0 });
13
+ let level = 0;
14
+ while (forwardQueue.length > 0 && backwardQueue.length > 0) {
15
+ level++;
16
+ // Process entire forward level
17
+ const forwardLevelSize = forwardQueue.length;
18
+ const forwardNewLevel = [];
19
+ for (let i = 0; i < forwardLevelSize; i++) {
20
+ const currentPath = forwardQueue.shift();
21
+ const currentId = currentPath[currentPath.length - 1];
22
+ steps.push({ nodeId: currentId, type: 'current-forward', side: 'forward', level });
23
+ const currentNode = graph.nodes.get(currentId);
24
+ if (!currentNode)
25
+ continue;
26
+ for (const neighborId of currentNode.neighbors) {
27
+ // Check for intersection with backward search
28
+ if (backwardVisited.has(neighborId)) {
29
+ const backwardPath = backwardVisited.get(neighborId);
30
+ const fullPath = [...currentPath, ...backwardPath.slice().reverse()];
31
+ steps.push({ nodeId: neighborId, type: 'intersection', level });
32
+ // Mark final path
33
+ for (const nodeId of fullPath) {
34
+ steps.push({ nodeId, type: 'path' });
35
+ }
36
+ return {
37
+ path: fullPath,
38
+ steps,
39
+ found: true,
40
+ intersectionNode: neighborId
41
+ };
42
+ }
43
+ if (!forwardVisited.has(neighborId)) {
44
+ const newPath = [...currentPath, neighborId];
45
+ forwardVisited.set(neighborId, newPath);
46
+ forwardNewLevel.push(newPath);
47
+ steps.push({ nodeId: neighborId, type: 'goal-forward', side: 'forward', level });
48
+ }
49
+ }
50
+ steps.push({ nodeId: currentId, type: 'visited-forward', side: 'forward', level });
51
+ }
52
+ forwardQueue.push(...forwardNewLevel);
53
+ // Process entire backward level
54
+ const backwardLevelSize = backwardQueue.length;
55
+ const backwardNewLevel = [];
56
+ for (let i = 0; i < backwardLevelSize; i++) {
57
+ const currentPath = backwardQueue.shift();
58
+ const currentId = currentPath[currentPath.length - 1];
59
+ steps.push({ nodeId: currentId, type: 'current-backward', side: 'backward', level });
60
+ const currentNode = graph.nodes.get(currentId);
61
+ if (!currentNode)
62
+ continue;
63
+ for (const neighborId of currentNode.neighbors) {
64
+ // Check for intersection with forward search
65
+ if (forwardVisited.has(neighborId)) {
66
+ const forwardPath = forwardVisited.get(neighborId);
67
+ const fullPath = [...forwardPath, ...currentPath.slice().reverse()];
68
+ steps.push({ nodeId: neighborId, type: 'intersection', level });
69
+ // Mark final path
70
+ for (const nodeId of fullPath) {
71
+ steps.push({ nodeId, type: 'path' });
72
+ }
73
+ return {
74
+ path: fullPath,
75
+ steps,
76
+ found: true,
77
+ intersectionNode: neighborId
78
+ };
79
+ }
80
+ if (!backwardVisited.has(neighborId)) {
81
+ const newPath = [...currentPath, neighborId];
82
+ backwardVisited.set(neighborId, newPath);
83
+ backwardNewLevel.push(newPath);
84
+ steps.push({ nodeId: neighborId, type: 'goal-backward', side: 'backward', level });
85
+ }
86
+ }
87
+ steps.push({ nodeId: currentId, type: 'visited-backward', side: 'backward', level });
88
+ }
89
+ backwardQueue.push(...backwardNewLevel);
90
+ }
91
+ return {
92
+ path: [],
93
+ steps,
94
+ found: false
95
+ };
96
+ }
97
+ function permutations(arr) {
98
+ if (arr.length <= 1)
99
+ return [arr];
100
+ const result = [];
101
+ for (let i = 0; i < arr.length; i++) {
102
+ const rest = [...arr.slice(0, i), ...arr.slice(i + 1)];
103
+ const perms = permutations(rest);
104
+ for (const perm of perms) {
105
+ result.push([arr[i], ...perm]);
106
+ }
107
+ }
108
+ return result;
109
+ }
110
+ export function findOptimalMultiGoalPath(graph, startId, goalIds) {
111
+ const allPaths = new Map();
112
+ // Compute paths between all pairs
113
+ const allNodes = [startId, ...goalIds];
114
+ for (const from of allNodes) {
115
+ const pathsFrom = new Map();
116
+ for (const to of allNodes) {
117
+ if (from !== to) {
118
+ const result = bidirectionalBFS(graph, from, to);
119
+ if (result.found) {
120
+ pathsFrom.set(to, result.path);
121
+ }
122
+ }
123
+ }
124
+ allPaths.set(from, pathsFrom);
125
+ }
126
+ // Try all permutations to find shortest tour
127
+ let shortestPath = [];
128
+ let shortestLength = Infinity;
129
+ const goalPermutations = permutations(goalIds);
130
+ for (const perm of goalPermutations) {
131
+ const tour = [startId, ...perm];
132
+ let totalPath = [];
133
+ let valid = true;
134
+ for (let i = 0; i < tour.length - 1; i++) {
135
+ const segment = allPaths.get(tour[i])?.get(tour[i + 1]);
136
+ if (!segment) {
137
+ valid = false;
138
+ break;
139
+ }
140
+ if (i === 0) {
141
+ totalPath = [...segment];
142
+ }
143
+ else {
144
+ totalPath = [...totalPath.slice(0, -1), ...segment];
145
+ }
146
+ }
147
+ if (valid && totalPath.length < shortestLength) {
148
+ shortestPath = totalPath;
149
+ shortestLength = totalPath.length;
150
+ }
151
+ }
152
+ return { optimalPath: shortestPath, allPaths };
153
+ }
@@ -0,0 +1,449 @@
1
+ <script lang="ts">
2
+ import type { GeneralGraph, BFSStep, ColorScheme, LabyrinthControls } from '../types';
3
+ import { bidirectionalBFS, findOptimalMultiGoalPath } from '../algorithms/bidirectional-bfs';
4
+
5
+ interface Props {
6
+ graph: GeneralGraph;
7
+ startNode?: string;
8
+ goalNodes?: string[];
9
+ nodeRadius?: number;
10
+ autoPlay?: boolean;
11
+ buttons?: boolean;
12
+ legend?: boolean;
13
+ stepCount?: boolean;
14
+ animationSpeed?: number;
15
+ colors?: ColorScheme;
16
+ showMultiGoal?: boolean;
17
+ onControls?: (controls: LabyrinthControls) => void;
18
+ }
19
+
20
+ let {
21
+ graph,
22
+ startNode,
23
+ goalNodes = [],
24
+ nodeRadius = 20,
25
+ autoPlay = false,
26
+ buttons = true,
27
+ legend = true,
28
+ stepCount = true,
29
+ animationSpeed = 100,
30
+ colors,
31
+ showMultiGoal = false,
32
+ onControls
33
+ }: Props = $props();
34
+
35
+ const defaultColors: Required<ColorScheme> = {
36
+ start: '#22c55e',
37
+ end: '#ef4444',
38
+ current: '#f59e0b',
39
+ visiting: '#fef3c7',
40
+ visited: '#e5e7eb',
41
+ path: '#3b82f6',
42
+ background: '#ffffff',
43
+ wall: '#1f2937',
44
+ grid: '#e5e7eb',
45
+ legend: '#f9fafb',
46
+ legendtext: '#1f2937',
47
+ buttons: '#3b82f6',
48
+ buttonshover: '#2563eb',
49
+ buttonsdisabled: '#9ca3af',
50
+ buttonstext: '#ffffff'
51
+ };
52
+
53
+ const colorScheme = $derived({ ...defaultColors, ...colors });
54
+
55
+ const nodeIds = $derived(Array.from(graph.nodes.keys()));
56
+ const computedStartNode = $derived(startNode ?? nodeIds[0]);
57
+ const computedGoalNodes = $derived(
58
+ goalNodes.length > 0 ? goalNodes : [nodeIds[Math.floor(nodeIds.length / 2)]]
59
+ );
60
+
61
+ const cssVars = $derived(
62
+ `--graph-buttons:${colorScheme.buttons};
63
+ --graph-buttonstext:${colorScheme.buttonstext};
64
+ --graph-buttonshover:${colorScheme.buttonshover};
65
+ --graph-buttonsdisabled:${colorScheme.buttonsdisabled};
66
+ --graph-legend:${colorScheme.legend};
67
+ --graph-legend-text:${colorScheme.legendtext};`
68
+ );
69
+
70
+ let currentStepIndex = $state(0);
71
+ let isPlaying = $state(false);
72
+ let steps: BFSStep[] = $state([]);
73
+ let intervalId: number | null = null;
74
+ let optimalPath: string[] = $state([]);
75
+
76
+ const forwardVisitedNodes = $derived(
77
+ new Set(
78
+ steps
79
+ .slice(0, currentStepIndex)
80
+ .filter((s) => s.type === 'visited-forward')
81
+ .map((s) => s.nodeId)
82
+ )
83
+ );
84
+
85
+ const backwardVisitedNodes = $derived(
86
+ new Set(
87
+ steps
88
+ .slice(0, currentStepIndex)
89
+ .filter((s) => s.type === 'visited-backward')
90
+ .map((s) => s.nodeId)
91
+ )
92
+ );
93
+
94
+ const forwardFrontier = $derived(
95
+ new Set(
96
+ steps
97
+ .slice(0, currentStepIndex)
98
+ .filter((s) => s.type === 'goal-forward')
99
+ .map((s) => s.nodeId)
100
+ )
101
+ );
102
+
103
+ const backwardFrontier = $derived(
104
+ new Set(
105
+ steps
106
+ .slice(0, currentStepIndex)
107
+ .filter((s) => s.type === 'goal-backward')
108
+ .map((s) => s.nodeId)
109
+ )
110
+ );
111
+
112
+ const currentForwardNode = $derived(
113
+ steps[currentStepIndex - 1]?.type === 'current-forward'
114
+ ? steps[currentStepIndex - 1]?.nodeId
115
+ : null
116
+ );
117
+
118
+ const currentBackwardNode = $derived(
119
+ steps[currentStepIndex - 1]?.type === 'current-backward'
120
+ ? steps[currentStepIndex - 1]?.nodeId
121
+ : null
122
+ );
123
+
124
+ const intersectionNode = $derived(
125
+ steps.slice(0, currentStepIndex).find((s) => s.type === 'intersection')?.nodeId
126
+ );
127
+
128
+ const pathNodes = $derived(
129
+ new Set(steps.slice(0, currentStepIndex).filter((s) => s.type === 'path').map((s) => s.nodeId))
130
+ );
131
+
132
+ function runAlgorithm() {
133
+ if (showMultiGoal && computedGoalNodes.length > 1) {
134
+ const result = findOptimalMultiGoalPath(graph, computedStartNode, computedGoalNodes);
135
+ optimalPath = result.optimalPath;
136
+
137
+ // Generate visualization steps for the optimal path
138
+ steps = [];
139
+ for (const nodeId of optimalPath) {
140
+ steps.push({ nodeId, type: 'path' });
141
+ }
142
+ } else {
143
+ const result = bidirectionalBFS(graph, computedStartNode, computedGoalNodes[0]);
144
+ steps = result.steps;
145
+ optimalPath = result.path;
146
+ }
147
+ currentStepIndex = 0;
148
+ }
149
+
150
+ $effect(() => {
151
+ computedStartNode;
152
+ computedGoalNodes;
153
+ reset();
154
+ });
155
+
156
+ function play() {
157
+ if (steps.length === 0) {
158
+ runAlgorithm();
159
+ }
160
+
161
+ isPlaying = true;
162
+ intervalId = window.setInterval(() => {
163
+ if (currentStepIndex < steps.length) {
164
+ currentStepIndex++;
165
+ } else {
166
+ pause();
167
+ }
168
+ }, animationSpeed);
169
+ }
170
+
171
+ function pause() {
172
+ isPlaying = false;
173
+ if (intervalId !== null) {
174
+ clearInterval(intervalId);
175
+ intervalId = null;
176
+ }
177
+ }
178
+
179
+ function reset() {
180
+ pause();
181
+ currentStepIndex = 0;
182
+ steps = [];
183
+ optimalPath = [];
184
+ }
185
+
186
+ function stepForward() {
187
+ if (steps.length === 0) {
188
+ runAlgorithm();
189
+ }
190
+ if (currentStepIndex < steps.length) {
191
+ currentStepIndex++;
192
+ }
193
+ }
194
+
195
+ function stepBackward() {
196
+ if (currentStepIndex > 0) {
197
+ currentStepIndex--;
198
+ }
199
+ }
200
+
201
+ function getNodeColor(nodeId: string): string {
202
+ if (nodeId === computedStartNode) return colorScheme.start;
203
+ if (computedGoalNodes.includes(nodeId)) return colorScheme.end;
204
+ if (pathNodes.has(nodeId)) return colorScheme.path;
205
+ if (nodeId === intersectionNode) return '#a855f7';
206
+ if (nodeId === currentForwardNode) return colorScheme.current;
207
+ if (nodeId === currentBackwardNode) return '#f97316';
208
+ if (forwardVisitedNodes.has(nodeId)) return colorScheme.visited;
209
+ if (backwardVisitedNodes.has(nodeId)) return '#fecaca';
210
+ if (forwardFrontier.has(nodeId)) return colorScheme.visiting;
211
+ if (backwardFrontier.has(nodeId)) return '#fed7aa';
212
+ return colorScheme.background;
213
+ }
214
+
215
+ function getNodeStroke(nodeId: string): string {
216
+ if (nodeId === computedStartNode || computedGoalNodes.includes(nodeId)) {
217
+ return colorScheme.wall;
218
+ }
219
+ if (pathNodes.has(nodeId)) return colorScheme.path;
220
+ return colorScheme.grid;
221
+ }
222
+
223
+ $effect(() => {
224
+ if (autoPlay && steps.length === 0) {
225
+ play();
226
+ }
227
+ });
228
+
229
+ $effect(() => {
230
+ return () => {
231
+ if (intervalId !== null) {
232
+ clearInterval(intervalId);
233
+ }
234
+ };
235
+ });
236
+
237
+ const controlApi: LabyrinthControls = {
238
+ play,
239
+ pause,
240
+ reset,
241
+ stepForward,
242
+ stepBackward
243
+ };
244
+
245
+ $effect(() => {
246
+ onControls?.(controlApi);
247
+ });
248
+
249
+ const svgWidth = 650;
250
+ const svgHeight = 650;
251
+ </script>
252
+
253
+ <div class="graph-container" style={cssVars}>
254
+ <svg width={svgWidth} height={svgHeight} class="graph-svg">
255
+ <defs>
256
+ <marker
257
+ id="arrowhead"
258
+ markerWidth="10"
259
+ markerHeight="7"
260
+ refX="9"
261
+ refY="3.5"
262
+ orient="auto"
263
+ >
264
+ <polygon points="0 0, 10 3.5, 0 7" fill={colorScheme.grid} />
265
+ </marker>
266
+ </defs>
267
+
268
+ {#each Array.from(graph.nodes.values()) as node}
269
+ {#each node.neighbors as neighborId}
270
+ {@const neighbor = graph.nodes.get(neighborId)}
271
+ {#if neighbor && node.id < neighborId}
272
+ <line
273
+ x1={node.x}
274
+ y1={node.y}
275
+ x2={neighbor.x}
276
+ y2={neighbor.y}
277
+ stroke={colorScheme.grid}
278
+ stroke-width="2"
279
+ />
280
+ {/if}
281
+ {/each}
282
+ {/each}
283
+
284
+ {#each Array.from(graph.nodes.values()) as node}
285
+ {@const color = getNodeColor(node.id)}
286
+ {@const stroke = getNodeStroke(node.id)}
287
+ <circle
288
+ cx={node.x}
289
+ cy={node.y}
290
+ r={nodeRadius}
291
+ fill={color}
292
+ stroke={stroke}
293
+ stroke-width="3"
294
+ />
295
+ <text
296
+ x={node.x}
297
+ y={node.y}
298
+ text-anchor="middle"
299
+ dominant-baseline="middle"
300
+ font-size="10"
301
+ font-weight="600"
302
+ fill={colorScheme.wall}
303
+ >
304
+ {node.id.replace('n', '')}
305
+ </text>
306
+ {/each}
307
+ </svg>
308
+
309
+ <div class="controls">
310
+ {#if buttons}
311
+ <button onclick={play} disabled={isPlaying}>Play</button>
312
+ <button onclick={pause} disabled={!isPlaying}>Pause</button>
313
+ <button onclick={reset}>Reset</button>
314
+ <button onclick={stepBackward} disabled={isPlaying || currentStepIndex === 0}>
315
+ Step Back
316
+ </button>
317
+ <button onclick={stepForward} disabled={isPlaying || currentStepIndex === steps.length}>
318
+ Step Forward
319
+ </button>
320
+ {/if}
321
+ {#if stepCount}
322
+ <span class="step-counter">
323
+ Step: {currentStepIndex} / {steps.length}
324
+ </span>
325
+ {/if}
326
+ </div>
327
+
328
+ {#if legend}
329
+ <div class="legend">
330
+ <div class="legend-item">
331
+ <div class="legend-color" style="background-color: {colorScheme.start};"></div>
332
+ <span>Start</span>
333
+ </div>
334
+ <div class="legend-item">
335
+ <div class="legend-color" style="background-color: {colorScheme.end};"></div>
336
+ <span>Goal{computedGoalNodes.length > 1 ? 's' : ''}</span>
337
+ </div>
338
+ <div class="legend-item">
339
+ <div class="legend-color" style="background-color: {colorScheme.current};"></div>
340
+ <span>Forward</span>
341
+ </div>
342
+ <div class="legend-item">
343
+ <div class="legend-color" style="background-color: #f97316;"></div>
344
+ <span>Backward</span>
345
+ </div>
346
+ <div class="legend-item">
347
+ <div class="legend-color" style="background-color: {colorScheme.visiting};"></div>
348
+ <span>Forward Frontier</span>
349
+ </div>
350
+ <div class="legend-item">
351
+ <div class="legend-color" style="background-color: #fed7aa;"></div>
352
+ <span>Backward Frontier</span>
353
+ </div>
354
+ <div class="legend-item">
355
+ <div class="legend-color" style="background-color: {colorScheme.visited};"></div>
356
+ <span>Forward Visited</span>
357
+ </div>
358
+ <div class="legend-item">
359
+ <div class="legend-color" style="background-color: #fecaca;"></div>
360
+ <span>Backward Visited</span>
361
+ </div>
362
+ <div class="legend-item">
363
+ <div class="legend-color" style="background-color: #a855f7;"></div>
364
+ <span>Intersection</span>
365
+ </div>
366
+ <div class="legend-item">
367
+ <div class="legend-color" style="background-color: {colorScheme.path};"></div>
368
+ <span>Path</span>
369
+ </div>
370
+ </div>
371
+ {/if}
372
+ </div>
373
+
374
+ <style>
375
+ .graph-container {
376
+ display: flex;
377
+ flex-direction: column;
378
+ gap: 1rem;
379
+ align-items: center;
380
+ padding: 1rem;
381
+ }
382
+
383
+ .graph-svg {
384
+ background-color: white;
385
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
386
+ border: 2px solid #e5e7eb;
387
+ border-radius: 0.5rem;
388
+ }
389
+
390
+ .controls {
391
+ display: flex;
392
+ gap: 0.5rem;
393
+ align-items: center;
394
+ flex-wrap: wrap;
395
+ }
396
+
397
+ .controls button {
398
+ padding: 0.5rem 1rem;
399
+ border: none;
400
+ border-radius: 0.375rem;
401
+ cursor: pointer;
402
+ font-weight: 500;
403
+ transition: background-color 0.2s;
404
+ background-color: var(--graph-buttons);
405
+ color: var(--graph-buttonstext);
406
+ }
407
+
408
+ .controls button:hover:not(:disabled) {
409
+ background-color: var(--graph-buttonshover);
410
+ }
411
+
412
+ .controls button:disabled {
413
+ background-color: var(--graph-buttonsdisabled);
414
+ cursor: not-allowed;
415
+ }
416
+
417
+ .step-counter {
418
+ padding: 0.5rem 1rem;
419
+ border-radius: 0.375rem;
420
+ font-weight: 500;
421
+ background-color: var(--graph-legend);
422
+ color: var(--graph-legend-text);
423
+ }
424
+
425
+ .legend {
426
+ display: flex;
427
+ gap: 1rem;
428
+ flex-wrap: wrap;
429
+ padding: 0.75rem;
430
+ border-radius: 0.5rem;
431
+ background-color: var(--graph-legend);
432
+ color: var(--graph-legend-text);
433
+ max-width: 650px;
434
+ }
435
+
436
+ .legend-item {
437
+ display: flex;
438
+ align-items: center;
439
+ gap: 0.5rem;
440
+ font-size: 0.875rem;
441
+ }
442
+
443
+ .legend-color {
444
+ width: 1.5rem;
445
+ height: 1.5rem;
446
+ border-radius: 0.25rem;
447
+ border: 1px solid var(--graph-legend-text);
448
+ }
449
+ </style>
@@ -0,0 +1,18 @@
1
+ import type { GeneralGraph, ColorScheme, LabyrinthControls } from '../types';
2
+ interface Props {
3
+ graph: GeneralGraph;
4
+ startNode?: string;
5
+ goalNodes?: string[];
6
+ nodeRadius?: number;
7
+ autoPlay?: boolean;
8
+ buttons?: boolean;
9
+ legend?: boolean;
10
+ stepCount?: boolean;
11
+ animationSpeed?: number;
12
+ colors?: ColorScheme;
13
+ showMultiGoal?: boolean;
14
+ onControls?: (controls: LabyrinthControls) => void;
15
+ }
16
+ declare const GraphVisualizer: import("svelte").Component<Props, {}, "">;
17
+ type GraphVisualizer = ReturnType<typeof GraphVisualizer>;
18
+ export default GraphVisualizer;
@@ -13,6 +13,7 @@
13
13
  startNode?: string;
14
14
  endNode?: string;
15
15
  autoPlay?: boolean;
16
+ buttons?: boolean;
16
17
  legend?: boolean;
17
18
  stepCount?: boolean;
18
19
  animationSpeed?: number;
@@ -29,6 +30,7 @@
29
30
  startNode = '0,0',
30
31
  endNode,
31
32
  autoPlay = false,
33
+ buttons = true,
32
34
  legend = true,
33
35
  stepCount = true,
34
36
  animationSpeed = 50,
@@ -267,11 +269,13 @@
267
269
  </svg>
268
270
 
269
271
  <div class="controls">
270
- <button onclick={play} disabled={isPlaying}>Play</button>
271
- <button onclick={pause} disabled={!isPlaying}>Pause</button>
272
- <button onclick={reset}>Reset</button>
273
- <button onclick={stepBackward} disabled={isPlaying || currentStepIndex === 0}>Step Back</button>
274
- <button onclick={stepForward} disabled={isPlaying || currentStepIndex === steps.length}>Step Forward</button>
272
+ {#if buttons}
273
+ <button onclick={play} disabled={isPlaying}>Play</button>
274
+ <button onclick={pause} disabled={!isPlaying}>Pause</button>
275
+ <button onclick={reset}>Reset</button>
276
+ <button onclick={stepBackward} disabled={isPlaying || currentStepIndex === 0}>Step Back</button>
277
+ <button onclick={stepForward} disabled={isPlaying || currentStepIndex === steps.length}>Step Forward</button>
278
+ {/if}
275
279
  {#if stepCount}
276
280
  <span class="step-counter">
277
281
  Step: {currentStepIndex} / {steps.length}
@@ -8,6 +8,7 @@ interface Props {
8
8
  startNode?: string;
9
9
  endNode?: string;
10
10
  autoPlay?: boolean;
11
+ buttons?: boolean;
11
12
  legend?: boolean;
12
13
  stepCount?: boolean;
13
14
  animationSpeed?: number;
@@ -0,0 +1,7 @@
1
+ import type { GeneralGraph } from './types';
2
+ export interface GraphGeneratorOptions {
3
+ nodeCount: number;
4
+ avgDegree: number;
5
+ seed?: number;
6
+ }
7
+ export declare function generateRandomGraph(options: GraphGeneratorOptions): GeneralGraph;
@@ -0,0 +1,57 @@
1
+ function seededRandom(seed) {
2
+ let state = seed;
3
+ return () => {
4
+ state = (state * 1664525 + 1013904223) % 4294967296;
5
+ return state / 4294967296;
6
+ };
7
+ }
8
+ export function generateRandomGraph(options) {
9
+ const { nodeCount, avgDegree, seed } = options;
10
+ const random = seed !== undefined ? seededRandom(seed) : Math.random;
11
+ const nodes = new Map();
12
+ // Create nodes in a circular layout for better visualization
13
+ const radius = 200;
14
+ const centerX = 300;
15
+ const centerY = 300;
16
+ for (let i = 0; i < nodeCount; i++) {
17
+ const angle = (i / nodeCount) * 2 * Math.PI;
18
+ const x = centerX + radius * Math.cos(angle);
19
+ const y = centerY + radius * Math.sin(angle);
20
+ nodes.set(`n${i}`, {
21
+ id: `n${i}`,
22
+ x,
23
+ y,
24
+ neighbors: []
25
+ });
26
+ }
27
+ // Connect nodes to create a connected graph
28
+ // First, create a spanning tree to ensure connectivity
29
+ const nodeIds = Array.from(nodes.keys());
30
+ const connected = new Set([nodeIds[0]]);
31
+ const unconnected = new Set(nodeIds.slice(1));
32
+ while (unconnected.size > 0) {
33
+ const connectedNode = Array.from(connected)[Math.floor(random() * connected.size)];
34
+ const unconnectedNode = Array.from(unconnected)[Math.floor(random() * unconnected.size)];
35
+ nodes.get(connectedNode).neighbors.push(unconnectedNode);
36
+ nodes.get(unconnectedNode).neighbors.push(connectedNode);
37
+ connected.add(unconnectedNode);
38
+ unconnected.delete(unconnectedNode);
39
+ }
40
+ // Add additional edges to reach target average degree
41
+ const targetEdges = Math.floor((nodeCount * avgDegree) / 2);
42
+ const currentEdges = nodeCount - 1; // from spanning tree
43
+ const edgesToAdd = targetEdges - currentEdges;
44
+ for (let i = 0; i < edgesToAdd; i++) {
45
+ const node1Id = nodeIds[Math.floor(random() * nodeIds.length)];
46
+ const node2Id = nodeIds[Math.floor(random() * nodeIds.length)];
47
+ if (node1Id === node2Id)
48
+ continue;
49
+ const node1 = nodes.get(node1Id);
50
+ const node2 = nodes.get(node2Id);
51
+ if (!node1.neighbors.includes(node2Id)) {
52
+ node1.neighbors.push(node2Id);
53
+ node2.neighbors.push(node1Id);
54
+ }
55
+ }
56
+ return { nodes };
57
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  export { generateLabyrinth } from './labyrinth-generator';
2
+ export { generateRandomGraph } from './graph-generator';
2
3
  export { dijkstra } from './algorithms/dijkstra';
3
4
  export { astar } from './algorithms/astar';
5
+ export { bidirectionalBFS, findOptimalMultiGoalPath } from './algorithms/bidirectional-bfs';
4
6
  export { default as Labyrinth } from './components/Labyrinth.svelte';
5
- export type { Graph, GraphNode, Point, Cell, PathStep, AlgorithmResult, AlgorithmType, ColorScheme, LabyrinthControls } from './types';
7
+ export { default as GraphVisualizer } from './components/GraphVisualizer.svelte';
8
+ export type { Graph, GraphNode, Point, Cell, PathStep, AlgorithmResult, AlgorithmType, ColorScheme, LabyrinthControls, GeneralGraph, GeneralGraphNode, BFSStep, BFSResult, MultiGoalResult } from './types';
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  export { generateLabyrinth } from './labyrinth-generator';
2
+ export { generateRandomGraph } from './graph-generator';
2
3
  export { dijkstra } from './algorithms/dijkstra';
3
4
  export { astar } from './algorithms/astar';
5
+ export { bidirectionalBFS, findOptimalMultiGoalPath } from './algorithms/bidirectional-bfs';
4
6
  export { default as Labyrinth } from './components/Labyrinth.svelte';
7
+ export { default as GraphVisualizer } from './components/GraphVisualizer.svelte';
package/dist/types.d.ts CHANGED
@@ -61,3 +61,30 @@ export interface LabyrinthControls {
61
61
  stepForward: () => void;
62
62
  stepBackward: () => void;
63
63
  }
64
+ export interface GeneralGraphNode {
65
+ id: string;
66
+ x: number;
67
+ y: number;
68
+ neighbors: string[];
69
+ }
70
+ export interface GeneralGraph {
71
+ nodes: Map<string, GeneralGraphNode>;
72
+ }
73
+ export interface BFSStep {
74
+ nodeId: string;
75
+ type: 'start-forward' | 'start-backward' | 'goal-forward' | 'goal-backward' | 'visited-forward' | 'visited-backward' | 'path' | 'current-forward' | 'current-backward' | 'intersection';
76
+ side?: 'forward' | 'backward';
77
+ level?: number;
78
+ }
79
+ export interface BFSResult {
80
+ path: string[];
81
+ steps: BFSStep[];
82
+ found: boolean;
83
+ intersectionNode?: string;
84
+ }
85
+ export interface MultiGoalResult {
86
+ goals: string[];
87
+ paths: Map<string, string[]>;
88
+ optimalTour: string[];
89
+ steps: BFSStep[];
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hayro_o7/labyrinth",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",