@hayro_o7/labyrinth 0.0.4 → 0.0.6
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/components/GraphVisualizer.svelte +6 -132
- package/dist/components/GraphVisualizer.svelte.d.ts +0 -3
- package/dist/graph-generator.d.ts +8 -1
- package/dist/graph-generator.js +85 -34
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/labyrinth-generator.d.ts +1 -1
- package/dist/labyrinth-generator.js +49 -1
- package/package.json +1 -1
|
@@ -8,9 +8,6 @@
|
|
|
8
8
|
goalNodes?: string[];
|
|
9
9
|
nodeRadius?: number;
|
|
10
10
|
autoPlay?: boolean;
|
|
11
|
-
buttons?: boolean;
|
|
12
|
-
legend?: boolean;
|
|
13
|
-
stepCount?: boolean;
|
|
14
11
|
animationSpeed?: number;
|
|
15
12
|
colors?: ColorScheme;
|
|
16
13
|
showMultiGoal?: boolean;
|
|
@@ -23,9 +20,6 @@
|
|
|
23
20
|
goalNodes = [],
|
|
24
21
|
nodeRadius = 20,
|
|
25
22
|
autoPlay = false,
|
|
26
|
-
buttons = true,
|
|
27
|
-
legend = true,
|
|
28
|
-
stepCount = true,
|
|
29
23
|
animationSpeed = 100,
|
|
30
24
|
colors,
|
|
31
25
|
showMultiGoal = false,
|
|
@@ -246,8 +240,12 @@
|
|
|
246
240
|
onControls?.(controlApi);
|
|
247
241
|
});
|
|
248
242
|
|
|
249
|
-
const svgWidth =
|
|
250
|
-
|
|
243
|
+
const svgWidth = $derived(
|
|
244
|
+
Math.max(...Array.from(graph.nodes.values()).map(n => n.x)) + 100
|
|
245
|
+
);
|
|
246
|
+
const svgHeight = $derived(
|
|
247
|
+
Math.max(...Array.from(graph.nodes.values()).map(n => n.y)) + 100
|
|
248
|
+
);
|
|
251
249
|
</script>
|
|
252
250
|
|
|
253
251
|
<div class="graph-container" style={cssVars}>
|
|
@@ -306,69 +304,6 @@
|
|
|
306
304
|
{/each}
|
|
307
305
|
</svg>
|
|
308
306
|
|
|
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
307
|
</div>
|
|
373
308
|
|
|
374
309
|
<style>
|
|
@@ -377,7 +312,6 @@
|
|
|
377
312
|
flex-direction: column;
|
|
378
313
|
gap: 1rem;
|
|
379
314
|
align-items: center;
|
|
380
|
-
padding: 1rem;
|
|
381
315
|
}
|
|
382
316
|
|
|
383
317
|
.graph-svg {
|
|
@@ -386,64 +320,4 @@
|
|
|
386
320
|
border: 2px solid #e5e7eb;
|
|
387
321
|
border-radius: 0.5rem;
|
|
388
322
|
}
|
|
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
323
|
</style>
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { GeneralGraph } from './types';
|
|
2
2
|
export interface GraphGeneratorOptions {
|
|
3
3
|
nodeCount: number;
|
|
4
|
-
|
|
4
|
+
width?: number;
|
|
5
|
+
height?: number;
|
|
6
|
+
maxConnectionsPerNode?: number;
|
|
7
|
+
connectionRadius?: number;
|
|
5
8
|
seed?: number;
|
|
6
9
|
}
|
|
7
10
|
export declare function generateRandomGraph(options: GraphGeneratorOptions): GeneralGraph;
|
|
11
|
+
export declare function findOppositeCornerNodes(graph: GeneralGraph): {
|
|
12
|
+
topLeft: string;
|
|
13
|
+
bottomRight: string;
|
|
14
|
+
};
|
package/dist/graph-generator.js
CHANGED
|
@@ -6,17 +6,14 @@ function seededRandom(seed) {
|
|
|
6
6
|
};
|
|
7
7
|
}
|
|
8
8
|
export function generateRandomGraph(options) {
|
|
9
|
-
const { nodeCount,
|
|
9
|
+
const { nodeCount, width = 600, height = 600, maxConnectionsPerNode = 5, connectionRadius = 150, seed } = options;
|
|
10
10
|
const random = seed !== undefined ? seededRandom(seed) : Math.random;
|
|
11
11
|
const nodes = new Map();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const centerX = 300;
|
|
15
|
-
const centerY = 300;
|
|
12
|
+
const padding = 50;
|
|
13
|
+
// Generate node positions in a rectangular cloud
|
|
16
14
|
for (let i = 0; i < nodeCount; i++) {
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const y = centerY + radius * Math.sin(angle);
|
|
15
|
+
const x = padding + random() * (width - 2 * padding);
|
|
16
|
+
const y = padding + random() * (height - 2 * padding);
|
|
20
17
|
nodes.set(`n${i}`, {
|
|
21
18
|
id: `n${i}`,
|
|
22
19
|
x,
|
|
@@ -24,34 +21,88 @@ export function generateRandomGraph(options) {
|
|
|
24
21
|
neighbors: []
|
|
25
22
|
});
|
|
26
23
|
}
|
|
27
|
-
// Connect nodes
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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)
|
|
24
|
+
// Connect nodes based on proximity
|
|
25
|
+
const nodeArray = Array.from(nodes.values());
|
|
26
|
+
for (let i = 0; i < nodeArray.length; i++) {
|
|
27
|
+
const node = nodeArray[i];
|
|
28
|
+
// Skip if already at max connections
|
|
29
|
+
if (node.neighbors.length >= maxConnectionsPerNode)
|
|
48
30
|
continue;
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
31
|
+
// Find nearby nodes
|
|
32
|
+
const distances = [];
|
|
33
|
+
for (let j = 0; j < nodeArray.length; j++) {
|
|
34
|
+
if (i === j)
|
|
35
|
+
continue;
|
|
36
|
+
const other = nodeArray[j];
|
|
37
|
+
// Skip if other node is at max connections
|
|
38
|
+
if (other.neighbors.length >= maxConnectionsPerNode)
|
|
39
|
+
continue;
|
|
40
|
+
// Skip if already connected
|
|
41
|
+
if (node.neighbors.includes(other.id))
|
|
42
|
+
continue;
|
|
43
|
+
const dx = node.x - other.x;
|
|
44
|
+
const dy = node.y - other.y;
|
|
45
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
46
|
+
if (distance <= connectionRadius) {
|
|
47
|
+
distances.push({ nodeId: other.id, distance });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Sort by distance and connect to closest nodes
|
|
51
|
+
distances.sort((a, b) => a.distance - b.distance);
|
|
52
|
+
const connectionsNeeded = Math.min(maxConnectionsPerNode - node.neighbors.length, distances.length);
|
|
53
|
+
for (let k = 0; k < connectionsNeeded; k++) {
|
|
54
|
+
const targetId = distances[k].nodeId;
|
|
55
|
+
const targetNode = nodes.get(targetId);
|
|
56
|
+
// Only connect if target isn't at max
|
|
57
|
+
if (targetNode.neighbors.length < maxConnectionsPerNode) {
|
|
58
|
+
node.neighbors.push(targetId);
|
|
59
|
+
targetNode.neighbors.push(node.id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Ensure all nodes have at least one connection
|
|
64
|
+
for (const node of nodeArray) {
|
|
65
|
+
if (node.neighbors.length === 0) {
|
|
66
|
+
// Find closest node that can accept a connection
|
|
67
|
+
let closestNode = null;
|
|
68
|
+
let closestDistance = Infinity;
|
|
69
|
+
for (const other of nodeArray) {
|
|
70
|
+
if (other.id === node.id)
|
|
71
|
+
continue;
|
|
72
|
+
if (other.neighbors.length >= maxConnectionsPerNode)
|
|
73
|
+
continue;
|
|
74
|
+
const dx = node.x - other.x;
|
|
75
|
+
const dy = node.y - other.y;
|
|
76
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
77
|
+
if (distance < closestDistance) {
|
|
78
|
+
closestDistance = distance;
|
|
79
|
+
closestNode = other;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (closestNode) {
|
|
83
|
+
node.neighbors.push(closestNode.id);
|
|
84
|
+
closestNode.neighbors.push(node.id);
|
|
85
|
+
}
|
|
54
86
|
}
|
|
55
87
|
}
|
|
56
88
|
return { nodes };
|
|
57
89
|
}
|
|
90
|
+
export function findOppositeCornerNodes(graph) {
|
|
91
|
+
const nodeArray = Array.from(graph.nodes.values());
|
|
92
|
+
let topLeftNode = nodeArray[0];
|
|
93
|
+
let bottomRightNode = nodeArray[0];
|
|
94
|
+
for (const node of nodeArray) {
|
|
95
|
+
// Top-left: minimize x + y
|
|
96
|
+
if (node.x + node.y < topLeftNode.x + topLeftNode.y) {
|
|
97
|
+
topLeftNode = node;
|
|
98
|
+
}
|
|
99
|
+
// Bottom-right: maximize x + y
|
|
100
|
+
if (node.x + node.y > bottomRightNode.x + bottomRightNode.y) {
|
|
101
|
+
bottomRightNode = node;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
topLeft: topLeftNode.id,
|
|
106
|
+
bottomRight: bottomRightNode.id
|
|
107
|
+
};
|
|
108
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { generateLabyrinth } from './labyrinth-generator';
|
|
2
|
-
export { generateRandomGraph } from './graph-generator';
|
|
2
|
+
export { generateRandomGraph, findOppositeCornerNodes } from './graph-generator';
|
|
3
3
|
export { dijkstra } from './algorithms/dijkstra';
|
|
4
4
|
export { astar } from './algorithms/astar';
|
|
5
5
|
export { bidirectionalBFS, findOptimalMultiGoalPath } from './algorithms/bidirectional-bfs';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { generateLabyrinth } from './labyrinth-generator';
|
|
2
|
-
export { generateRandomGraph } from './graph-generator';
|
|
2
|
+
export { generateRandomGraph, findOppositeCornerNodes } from './graph-generator';
|
|
3
3
|
export { dijkstra } from './algorithms/dijkstra';
|
|
4
4
|
export { astar } from './algorithms/astar';
|
|
5
5
|
export { bidirectionalBFS, findOptimalMultiGoalPath } from './algorithms/bidirectional-bfs';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { Graph } from './types';
|
|
2
|
-
export declare function generateLabyrinth(width: number, height: number): Graph;
|
|
2
|
+
export declare function generateLabyrinth(width: number, height: number, wallRemovalPercent?: number): Graph;
|
|
@@ -5,7 +5,7 @@ function parseKey(key) {
|
|
|
5
5
|
const [x, y] = key.split(',').map(Number);
|
|
6
6
|
return { x, y };
|
|
7
7
|
}
|
|
8
|
-
export function generateLabyrinth(width, height) {
|
|
8
|
+
export function generateLabyrinth(width, height, wallRemovalPercent = 25) {
|
|
9
9
|
const cells = [];
|
|
10
10
|
for (let y = 0; y < height; y++) {
|
|
11
11
|
cells[y] = [];
|
|
@@ -40,6 +40,10 @@ export function generateLabyrinth(width, height) {
|
|
|
40
40
|
stack.pop();
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
// Remove additional walls randomly to create multiple paths
|
|
44
|
+
if (wallRemovalPercent > 0) {
|
|
45
|
+
removeRandomWalls(cells, width, height, wallRemovalPercent);
|
|
46
|
+
}
|
|
43
47
|
return cellsToGraph(cells, width, height);
|
|
44
48
|
}
|
|
45
49
|
function getUnvisitedNeighbors(cell, cells, width, height) {
|
|
@@ -75,6 +79,50 @@ function removeWall(cell1, cell2) {
|
|
|
75
79
|
cell2.walls.bottom = false;
|
|
76
80
|
}
|
|
77
81
|
}
|
|
82
|
+
function removeRandomWalls(cells, width, height, removalPercent) {
|
|
83
|
+
const allWalls = [];
|
|
84
|
+
// Collect all existing walls
|
|
85
|
+
for (let y = 0; y < height; y++) {
|
|
86
|
+
for (let x = 0; x < width; x++) {
|
|
87
|
+
const cell = cells[y][x];
|
|
88
|
+
if (cell.walls.top && y > 0)
|
|
89
|
+
allWalls.push({ cell, direction: 'top' });
|
|
90
|
+
if (cell.walls.right && x < width - 1)
|
|
91
|
+
allWalls.push({ cell, direction: 'right' });
|
|
92
|
+
if (cell.walls.bottom && y < height - 1)
|
|
93
|
+
allWalls.push({ cell, direction: 'bottom' });
|
|
94
|
+
if (cell.walls.left && x > 0)
|
|
95
|
+
allWalls.push({ cell, direction: 'left' });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Calculate how many walls to remove
|
|
99
|
+
const wallsToRemove = Math.floor(allWalls.length * (removalPercent / 100));
|
|
100
|
+
// Shuffle and remove random walls
|
|
101
|
+
for (let i = 0; i < wallsToRemove; i++) {
|
|
102
|
+
const randomIndex = Math.floor(Math.random() * allWalls.length);
|
|
103
|
+
const { cell, direction } = allWalls[randomIndex];
|
|
104
|
+
// Remove wall from both sides
|
|
105
|
+
const { x, y } = cell;
|
|
106
|
+
if (direction === 'top' && y > 0) {
|
|
107
|
+
cell.walls.top = false;
|
|
108
|
+
cells[y - 1][x].walls.bottom = false;
|
|
109
|
+
}
|
|
110
|
+
else if (direction === 'right' && x < width - 1) {
|
|
111
|
+
cell.walls.right = false;
|
|
112
|
+
cells[y][x + 1].walls.left = false;
|
|
113
|
+
}
|
|
114
|
+
else if (direction === 'bottom' && y < height - 1) {
|
|
115
|
+
cell.walls.bottom = false;
|
|
116
|
+
cells[y + 1][x].walls.top = false;
|
|
117
|
+
}
|
|
118
|
+
else if (direction === 'left' && x > 0) {
|
|
119
|
+
cell.walls.left = false;
|
|
120
|
+
cells[y][x - 1].walls.right = false;
|
|
121
|
+
}
|
|
122
|
+
// Remove this wall from the array to avoid removing it twice
|
|
123
|
+
allWalls.splice(randomIndex, 1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
78
126
|
function cellsToGraph(cells, width, height) {
|
|
79
127
|
const nodes = new Map();
|
|
80
128
|
for (let y = 0; y < height; y++) {
|