@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.
@@ -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 = 650;
250
- const svgHeight = 650;
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>
@@ -5,9 +5,6 @@ interface Props {
5
5
  goalNodes?: string[];
6
6
  nodeRadius?: number;
7
7
  autoPlay?: boolean;
8
- buttons?: boolean;
9
- legend?: boolean;
10
- stepCount?: boolean;
11
8
  animationSpeed?: number;
12
9
  colors?: ColorScheme;
13
10
  showMultiGoal?: boolean;
@@ -1,7 +1,14 @@
1
1
  import type { GeneralGraph } from './types';
2
2
  export interface GraphGeneratorOptions {
3
3
  nodeCount: number;
4
- avgDegree: number;
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
+ };
@@ -6,17 +6,14 @@ function seededRandom(seed) {
6
6
  };
7
7
  }
8
8
  export function generateRandomGraph(options) {
9
- const { nodeCount, avgDegree, seed } = options;
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
- // Create nodes in a circular layout for better visualization
13
- const radius = 200;
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 angle = (i / nodeCount) * 2 * Math.PI;
18
- const x = centerX + radius * Math.cos(angle);
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 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)
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
- 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);
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++) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hayro_o7/labyrinth",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",