@hayro_o7/labyrinth 0.0.3 → 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.
- package/dist/algorithms/bidirectional-bfs.d.ts +6 -0
- package/dist/algorithms/bidirectional-bfs.js +153 -0
- package/dist/components/GraphVisualizer.svelte +449 -0
- package/dist/components/GraphVisualizer.svelte.d.ts +18 -0
- package/dist/graph-generator.d.ts +7 -0
- package/dist/graph-generator.js +57 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/types.d.ts +27 -0
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -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
|
|
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
|
+
}
|