@hayro_o7/labyrinth 0.0.1

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/README.md ADDED
@@ -0,0 +1,243 @@
1
+ # Labyrinth
2
+
3
+ A Svelte package for visualizing graph pathfinding algorithms with interactive maze generation and solving.
4
+
5
+ ## Features
6
+
7
+ - 🎯 **Algorithm Visualization**: Step-by-step visualization of Dijkstra's and A* pathfinding algorithms
8
+ - 🌀 **Maze Generation**: Generates perfect mazes using Recursive Backtracking algorithm
9
+ - 🎨 **Interactive Component**: Fully customizable Svelte component with play/pause controls
10
+ - 📊 **Graph-based**: Uses proper graph data structures for efficient pathfinding
11
+ - 🔧 **TypeScript**: Fully typed for excellent developer experience
12
+ - âš¡ **Configurable**: Adjustable maze size, cell size, and animation speed
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install labyrinth
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### Basic Example
23
+
24
+ ```svelte
25
+ <script>
26
+ import { generateLabyrinth, Labyrinth } from 'labyrinth';
27
+
28
+ const graph = generateLabyrinth(20, 20);
29
+ </script>
30
+
31
+ <Labyrinth {graph} algorithm="astar" />
32
+ ```
33
+
34
+ ### Advanced Example
35
+
36
+ ```svelte
37
+ <script>
38
+ import { generateLabyrinth, Labyrinth } from 'labyrinth';
39
+ import type { Graph, AlgorithmType, ColorScheme } from 'labyrinth';
40
+
41
+ let width = 25;
42
+ let height = 25;
43
+ let algorithm: AlgorithmType = 'dijkstra';
44
+
45
+ const graph = generateLabyrinth(width, height);
46
+
47
+ const customColors: ColorScheme = {
48
+ start: '#10b981',
49
+ end: '#dc2626',
50
+ path: '#8b5cf6',
51
+ wall: '#374151'
52
+ };
53
+ </script>
54
+
55
+ <Labyrinth
56
+ {graph}
57
+ {algorithm}
58
+ cellSize={30}
59
+ animationSpeed={50}
60
+ autoPlay={false}
61
+ showGrid={true}
62
+ colors={customColors}
63
+ />
64
+ ```
65
+
66
+ ## API Reference
67
+
68
+ ### `generateLabyrinth(width: number, height: number): Graph`
69
+
70
+ Generates a perfect maze (exactly one path between any two points) using the Recursive Backtracking algorithm.
71
+
72
+ **Parameters:**
73
+ - `width`: Number of cells horizontally
74
+ - `height`: Number of cells vertically
75
+
76
+ **Returns:** A `Graph` object representing the maze
77
+
78
+ ### `Labyrinth` Component Props
79
+
80
+ | Prop | Type | Default | Description |
81
+ |------|------|---------|-------------|
82
+ | `graph` | `Graph` | **required** | The maze graph to visualize |
83
+ | `algorithm` | `'dijkstra' \| 'astar'` | `'astar'` | Pathfinding algorithm to use |
84
+ | `cellSize` | `number` | `30` | Size of each cell in pixels |
85
+ | `wallThickness` | `number` | `2` | Thickness of walls in pixels |
86
+ | `startNode` | `string` | `'0,0'` | Starting node ID (format: "x,y") |
87
+ | `endNode` | `string` | `'{width-1},{height-1}'` | Ending node ID (format: "x,y"), auto-computed from graph size |
88
+ | `autoPlay` | `boolean` | `false` | Start animation automatically |
89
+ | `animationSpeed` | `number` | `50` | Delay between steps in milliseconds |
90
+ | `showGrid` | `boolean` | `true` | Show grid lines |
91
+ | `legend` | `boolean` | `true` | Toggle the legend panel |
92
+ | `stepCount` | `boolean` | `true` | Toggle the step counter |
93
+ | `colors` | `ColorScheme` | default theme | Custom color scheme (see below) |
94
+
95
+ ### Algorithms
96
+
97
+ #### Dijkstra's Algorithm
98
+ - Explores all nodes systematically
99
+ - Guarantees shortest path
100
+ - Works well for unweighted graphs
101
+ - Time complexity: O((V+E) log V)
102
+
103
+ #### A* Algorithm
104
+ - Uses Manhattan distance heuristic
105
+ - More efficient than Dijkstra for pathfinding
106
+ - Explores fewer nodes by prioritizing promising paths
107
+ - Guarantees shortest path with admissible heuristic
108
+
109
+ ### Functions
110
+
111
+ ```typescript
112
+ // Generate a maze
113
+ import { generateLabyrinth } from 'labyrinth';
114
+ const graph = generateLabyrinth(20, 20);
115
+
116
+ // Run algorithms programmatically
117
+ import { dijkstra, astar } from 'labyrinth';
118
+ const result = dijkstra(graph, '0,0', '19,19');
119
+ const result2 = astar(graph, '0,0', '19,19');
120
+ ```
121
+
122
+ ### Color Customization
123
+
124
+ You can customize all colors used in the visualization by passing a `colors` object:
125
+
126
+ ```typescript
127
+ import type { ColorScheme } from 'labyrinth';
128
+
129
+ const customColors: ColorScheme = {
130
+ start: '#10b981', // Start node color
131
+ end: '#dc2626', // End node color
132
+ current: '#f59e0b', // Current node being explored
133
+ visiting: '#fef3c7', // Nodes in the frontier
134
+ visited: '#e5e7eb', // Already visited nodes
135
+ path: '#8b5cf6', // Final path color
136
+ background: '#ffffff', // Cell background
137
+ wall: '#374151', // Wall color
138
+ grid: '#e5e7eb', // Grid line color
139
+ legend: '#f3f4f6', // Legend background
140
+ legendtext: '#1f2937', // Legend text color
141
+ buttons: '#0ea5e9', // Button background
142
+ buttonshover: '#0284c7', // Button hover background
143
+ buttonsdisabled: '#94a3b8', // Disabled button background
144
+ buttonstext: '#ffffff' // Button text color
145
+ };
146
+ ```
147
+
148
+ All color properties are optional - only specify the ones you want to override.
149
+
150
+ ### Types
151
+
152
+ ```typescript
153
+ interface Graph {
154
+ nodes: Map<string, GraphNode>;
155
+ width: number;
156
+ height: number;
157
+ }
158
+
159
+ interface GraphNode {
160
+ id: string;
161
+ x: number;
162
+ y: number;
163
+ neighbors: string[];
164
+ }
165
+
166
+ interface AlgorithmResult {
167
+ path: string[];
168
+ steps: PathStep[];
169
+ found: boolean;
170
+ }
171
+
172
+ interface PathStep {
173
+ nodeId: string;
174
+ type: 'visiting' | 'visited' | 'path' | 'current';
175
+ distance?: number;
176
+ heuristic?: number;
177
+ fScore?: number;
178
+ }
179
+
180
+ interface ColorScheme {
181
+ start?: string;
182
+ end?: string;
183
+ current?: string;
184
+ visiting?: string;
185
+ visited?: string;
186
+ path?: string;
187
+ background?: string;
188
+ wall?: string;
189
+ grid?: string;
190
+ legend?: string;
191
+ legendtext?: string;
192
+ buttons?: string;
193
+ buttonshover?: string;
194
+ buttonsdisabled?: string;
195
+ buttonstext?: string;
196
+ }
197
+ ```
198
+
199
+ ## Development
200
+
201
+ ```bash
202
+ # Install dependencies
203
+ npm install
204
+
205
+ # Start dev server with demo
206
+ npm run dev
207
+
208
+ # Build the package
209
+ npm run package
210
+
211
+ # Type check
212
+ npm run check
213
+ ```
214
+
215
+ ## How It Works
216
+
217
+ ### Maze Generation
218
+ The package uses **Recursive Backtracking** (a DFS-based algorithm) to generate perfect mazes:
219
+ 1. Start at a random cell and mark it as visited
220
+ 2. While there are unvisited neighbors:
221
+ - Choose a random unvisited neighbor
222
+ - Remove the wall between current cell and chosen neighbor
223
+ - Recursively visit the neighbor
224
+ 3. Backtrack when stuck
225
+
226
+ This guarantees a maze with exactly one path between any two points.
227
+
228
+ ### Pathfinding Visualization
229
+ The component visualizes the algorithm's execution step-by-step:
230
+ - **Green**: Start node
231
+ - **Red**: End node
232
+ - **Orange**: Current node being explored
233
+ - **Yellow**: Nodes in the frontier (being considered)
234
+ - **Gray**: Visited nodes
235
+ - **Blue**: Final shortest path
236
+
237
+ ## License
238
+
239
+ MIT
240
+
241
+ ## Contributing
242
+
243
+ Contributions are welcome! Please feel free to submit a Pull Request.
@@ -0,0 +1,2 @@
1
+ import type { Graph, AlgorithmResult } from '../types';
2
+ export declare function astar(graph: Graph, startId: string, endId: string): AlgorithmResult;
@@ -0,0 +1,105 @@
1
+ import { PriorityQueue } from '../utils/priority-queue';
2
+ function parseKey(key) {
3
+ const [x, y] = key.split(',').map(Number);
4
+ return { x, y };
5
+ }
6
+ function manhattanDistance(id1, id2) {
7
+ const pos1 = parseKey(id1);
8
+ const pos2 = parseKey(id2);
9
+ return Math.abs(pos1.x - pos2.x) + Math.abs(pos1.y - pos2.y);
10
+ }
11
+ export function astar(graph, startId, endId) {
12
+ const gScore = new Map();
13
+ const fScore = new Map();
14
+ const previous = new Map();
15
+ const visited = new Set();
16
+ const steps = [];
17
+ const pq = new PriorityQueue();
18
+ for (const nodeId of graph.nodes.keys()) {
19
+ gScore.set(nodeId, Infinity);
20
+ fScore.set(nodeId, Infinity);
21
+ previous.set(nodeId, null);
22
+ }
23
+ const h = manhattanDistance(startId, endId);
24
+ gScore.set(startId, 0);
25
+ fScore.set(startId, h);
26
+ pq.enqueue(startId, h);
27
+ while (!pq.isEmpty()) {
28
+ const currentId = pq.dequeue();
29
+ if (!currentId)
30
+ break;
31
+ if (visited.has(currentId))
32
+ continue;
33
+ visited.add(currentId);
34
+ const currentG = gScore.get(currentId) ?? Infinity;
35
+ const currentF = fScore.get(currentId) ?? Infinity;
36
+ const currentH = manhattanDistance(currentId, endId);
37
+ steps.push({
38
+ nodeId: currentId,
39
+ type: 'current',
40
+ distance: currentG,
41
+ heuristic: currentH,
42
+ fScore: currentF
43
+ });
44
+ if (currentId === endId) {
45
+ break;
46
+ }
47
+ const currentNode = graph.nodes.get(currentId);
48
+ if (!currentNode)
49
+ continue;
50
+ for (const neighborId of currentNode.neighbors) {
51
+ if (visited.has(neighborId))
52
+ continue;
53
+ const tentativeGScore = currentG + 1;
54
+ const oldGScore = gScore.get(neighborId) ?? Infinity;
55
+ if (tentativeGScore < oldGScore) {
56
+ previous.set(neighborId, currentId);
57
+ gScore.set(neighborId, tentativeGScore);
58
+ const heuristic = manhattanDistance(neighborId, endId);
59
+ const newFScore = tentativeGScore + heuristic;
60
+ fScore.set(neighborId, newFScore);
61
+ pq.enqueue(neighborId, newFScore);
62
+ steps.push({
63
+ nodeId: neighborId,
64
+ type: 'visiting',
65
+ distance: tentativeGScore,
66
+ heuristic,
67
+ fScore: newFScore
68
+ });
69
+ }
70
+ }
71
+ steps.push({
72
+ nodeId: currentId,
73
+ type: 'visited',
74
+ distance: currentG,
75
+ heuristic: currentH,
76
+ fScore: currentF
77
+ });
78
+ }
79
+ const path = reconstructPath(previous, startId, endId);
80
+ for (const nodeId of path) {
81
+ steps.push({
82
+ nodeId,
83
+ type: 'path'
84
+ });
85
+ }
86
+ return {
87
+ path,
88
+ steps,
89
+ found: path.length > 0 && path[path.length - 1] === endId
90
+ };
91
+ }
92
+ function reconstructPath(previous, startId, endId) {
93
+ const path = [];
94
+ let current = endId;
95
+ while (current !== null) {
96
+ path.unshift(current);
97
+ if (current === startId)
98
+ break;
99
+ current = previous.get(current) ?? null;
100
+ }
101
+ if (path[0] !== startId) {
102
+ return [];
103
+ }
104
+ return path;
105
+ }
@@ -0,0 +1,2 @@
1
+ import type { Graph, AlgorithmResult } from '../types';
2
+ export declare function dijkstra(graph: Graph, startId: string, endId: string): AlgorithmResult;
@@ -0,0 +1,81 @@
1
+ import { PriorityQueue } from '../utils/priority-queue';
2
+ export function dijkstra(graph, startId, endId) {
3
+ const distances = new Map();
4
+ const previous = new Map();
5
+ const visited = new Set();
6
+ const steps = [];
7
+ const pq = new PriorityQueue();
8
+ for (const nodeId of graph.nodes.keys()) {
9
+ distances.set(nodeId, Infinity);
10
+ previous.set(nodeId, null);
11
+ }
12
+ distances.set(startId, 0);
13
+ pq.enqueue(startId, 0);
14
+ while (!pq.isEmpty()) {
15
+ const currentId = pq.dequeue();
16
+ if (!currentId)
17
+ break;
18
+ if (visited.has(currentId))
19
+ continue;
20
+ visited.add(currentId);
21
+ steps.push({
22
+ nodeId: currentId,
23
+ type: 'current',
24
+ distance: distances.get(currentId)
25
+ });
26
+ if (currentId === endId) {
27
+ break;
28
+ }
29
+ const currentNode = graph.nodes.get(currentId);
30
+ if (!currentNode)
31
+ continue;
32
+ const currentDistance = distances.get(currentId) ?? Infinity;
33
+ for (const neighborId of currentNode.neighbors) {
34
+ if (visited.has(neighborId))
35
+ continue;
36
+ const newDistance = currentDistance + 1;
37
+ const oldDistance = distances.get(neighborId) ?? Infinity;
38
+ if (newDistance < oldDistance) {
39
+ distances.set(neighborId, newDistance);
40
+ previous.set(neighborId, currentId);
41
+ pq.enqueue(neighborId, newDistance);
42
+ steps.push({
43
+ nodeId: neighborId,
44
+ type: 'visiting',
45
+ distance: newDistance
46
+ });
47
+ }
48
+ }
49
+ steps.push({
50
+ nodeId: currentId,
51
+ type: 'visited',
52
+ distance: distances.get(currentId)
53
+ });
54
+ }
55
+ const path = reconstructPath(previous, startId, endId);
56
+ for (const nodeId of path) {
57
+ steps.push({
58
+ nodeId,
59
+ type: 'path'
60
+ });
61
+ }
62
+ return {
63
+ path,
64
+ steps,
65
+ found: path.length > 0 && path[path.length - 1] === endId
66
+ };
67
+ }
68
+ function reconstructPath(previous, startId, endId) {
69
+ const path = [];
70
+ let current = endId;
71
+ while (current !== null) {
72
+ path.unshift(current);
73
+ if (current === startId)
74
+ break;
75
+ current = previous.get(current) ?? null;
76
+ }
77
+ if (path[0] !== startId) {
78
+ return [];
79
+ }
80
+ return path;
81
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
@@ -0,0 +1,370 @@
1
+ <script lang="ts">
2
+ import type { Graph, AlgorithmType, PathStep } from '../types';
3
+ import { dijkstra } from '../algorithms/dijkstra';
4
+ import { astar } from '../algorithms/astar';
5
+
6
+ import type { ColorScheme } from '../types';
7
+
8
+ interface Props {
9
+ graph: Graph;
10
+ cellSize?: number;
11
+ wallThickness?: number;
12
+ algorithm?: AlgorithmType;
13
+ startNode?: string;
14
+ endNode?: string;
15
+ autoPlay?: boolean;
16
+ legend?: boolean;
17
+ stepCount?: boolean;
18
+ animationSpeed?: number;
19
+ showGrid?: boolean;
20
+ colors?: ColorScheme;
21
+ }
22
+
23
+ let {
24
+ graph,
25
+ cellSize = 30,
26
+ wallThickness = 2,
27
+ algorithm = 'astar',
28
+ startNode = '0,0',
29
+ endNode,
30
+ autoPlay = false,
31
+ legend = true,
32
+ stepCount = true,
33
+ animationSpeed = 50,
34
+ showGrid = true,
35
+ colors
36
+ }: Props = $props();
37
+
38
+ const defaultColors: Required<ColorScheme> = {
39
+ start: '#22c55e',
40
+ end: '#ef4444',
41
+ current: '#f59e0b',
42
+ visiting: '#fef3c7',
43
+ visited: '#e5e7eb',
44
+ path: '#3b82f6',
45
+ background: '#ffffff',
46
+ wall: '#1f2937',
47
+ grid: '#e5e7eb',
48
+ legend: '#f9fafb',
49
+ legendtext: '#1f2937',
50
+ buttons: '#3b82f6',
51
+ buttonshover: '#2563eb',
52
+ buttonsdisabled: '#9ca3af',
53
+ buttonstext: '#ffffff'
54
+ };
55
+
56
+ const colorScheme = $derived({ ...defaultColors, ...colors });
57
+ const computedEndNode = $derived(endNode ?? `${graph.width - 1},${graph.height - 1}`);
58
+ const cssVars = $derived(
59
+ `--lab-buttons:${colorScheme.buttons};
60
+ --lab-buttonstext:${colorScheme.buttonstext};
61
+ --lab-buttonshover:${colorScheme.buttonshover};
62
+ --lab-buttonsdisabled:${colorScheme.buttonsdisabled};
63
+ --lab-legend:${colorScheme.legend};
64
+ --lab-legend-text:${colorScheme.legendtext};`
65
+ );
66
+
67
+ let currentStepIndex = $state(0);
68
+ let isPlaying = $state(false);
69
+ let steps: PathStep[] = $state([]);
70
+ let intervalId: number | null = null;
71
+
72
+ const visitedNodes = $derived(new Set(
73
+ steps.slice(0, currentStepIndex).filter(s => s.type === 'visited').map(s => s.nodeId)
74
+ ));
75
+
76
+ const visitingNodes = $derived(new Set(
77
+ steps.slice(0, currentStepIndex).filter(s => s.type === 'visiting').map(s => s.nodeId)
78
+ ));
79
+
80
+ const currentNode = $derived(
81
+ steps[currentStepIndex - 1]?.type === 'current' ? steps[currentStepIndex - 1]?.nodeId : null
82
+ );
83
+
84
+ const pathNodes = $derived(new Set(
85
+ steps.slice(0, currentStepIndex).filter(s => s.type === 'path').map(s => s.nodeId)
86
+ ));
87
+
88
+ function runAlgorithm() {
89
+ const result = algorithm === 'dijkstra'
90
+ ? dijkstra(graph, startNode, computedEndNode)
91
+ : astar(graph, startNode, computedEndNode);
92
+
93
+ steps = result.steps;
94
+ currentStepIndex = 0;
95
+ }
96
+
97
+ $effect(() => {
98
+ algorithm;
99
+ reset();
100
+ });
101
+
102
+ function play() {
103
+ if (steps.length === 0) {
104
+ runAlgorithm();
105
+ }
106
+
107
+ isPlaying = true;
108
+ intervalId = window.setInterval(() => {
109
+ if (currentStepIndex < steps.length) {
110
+ currentStepIndex++;
111
+ } else {
112
+ pause();
113
+ }
114
+ }, animationSpeed);
115
+ }
116
+
117
+ function pause() {
118
+ isPlaying = false;
119
+ if (intervalId !== null) {
120
+ clearInterval(intervalId);
121
+ intervalId = null;
122
+ }
123
+ }
124
+
125
+ function reset() {
126
+ pause();
127
+ currentStepIndex = 0;
128
+ steps = [];
129
+ }
130
+
131
+ function stepForward() {
132
+ if (steps.length === 0) {
133
+ runAlgorithm();
134
+ }
135
+ if (currentStepIndex < steps.length) {
136
+ currentStepIndex++;
137
+ }
138
+ }
139
+
140
+ function stepBackward() {
141
+ if (currentStepIndex > 0) {
142
+ currentStepIndex--;
143
+ }
144
+ }
145
+
146
+ function getCellColor(nodeId: string): string {
147
+ if (nodeId === startNode) return colorScheme.start;
148
+ if (nodeId === computedEndNode) return colorScheme.end;
149
+ if (pathNodes.has(nodeId)) return colorScheme.path;
150
+ if (nodeId === currentNode) return colorScheme.current;
151
+ if (visitedNodes.has(nodeId)) return colorScheme.visited;
152
+ if (visitingNodes.has(nodeId)) return colorScheme.visiting;
153
+ return colorScheme.background;
154
+ }
155
+
156
+ function getWalls(nodeId: string): { top: boolean; right: boolean; bottom: boolean; left: boolean } {
157
+ const node = graph.nodes.get(nodeId);
158
+ if (!node) return { top: true, right: true, bottom: true, left: true };
159
+
160
+ const { x, y } = node;
161
+ const hasTop = !node.neighbors.includes(`${x},${y - 1}`);
162
+ const hasRight = !node.neighbors.includes(`${x + 1},${y}`);
163
+ const hasBottom = !node.neighbors.includes(`${x},${y + 1}`);
164
+ const hasLeft = !node.neighbors.includes(`${x - 1},${y}`);
165
+
166
+ return { top: hasTop, right: hasRight, bottom: hasBottom, left: hasLeft };
167
+ }
168
+
169
+ $effect(() => {
170
+ if (autoPlay && steps.length === 0) {
171
+ play();
172
+ }
173
+ });
174
+
175
+ $effect(() => {
176
+ return () => {
177
+ if (intervalId !== null) {
178
+ clearInterval(intervalId);
179
+ }
180
+ };
181
+ });
182
+
183
+ const width = $derived(graph.width * cellSize);
184
+ const height = $derived(graph.height * cellSize);
185
+ </script>
186
+
187
+ <div class="labyrinth-container" style={cssVars}>
188
+ <svg
189
+ {width}
190
+ {height}
191
+ class="labyrinth-svg"
192
+ style="border: {wallThickness}px solid {colorScheme.wall};"
193
+ >
194
+ {#each Array.from(graph.nodes.values()) as node}
195
+ {@const walls = getWalls(node.id)}
196
+ {@const x = node.x * cellSize}
197
+ {@const y = node.y * cellSize}
198
+ {@const color = getCellColor(node.id)}
199
+
200
+ <rect
201
+ {x}
202
+ {y}
203
+ width={cellSize}
204
+ height={cellSize}
205
+ fill={color}
206
+ stroke={showGrid ? colorScheme.grid : 'none'}
207
+ stroke-width="0.5"
208
+ />
209
+
210
+ {#if walls.top}
211
+ <line
212
+ x1={x}
213
+ y1={y}
214
+ x2={x + cellSize}
215
+ y2={y}
216
+ stroke={colorScheme.wall}
217
+ stroke-width={wallThickness}
218
+ />
219
+ {/if}
220
+
221
+ {#if walls.right}
222
+ <line
223
+ x1={x + cellSize}
224
+ y1={y}
225
+ x2={x + cellSize}
226
+ y2={y + cellSize}
227
+ stroke={colorScheme.wall}
228
+ stroke-width={wallThickness}
229
+ />
230
+ {/if}
231
+
232
+ {#if walls.bottom}
233
+ <line
234
+ x1={x}
235
+ y1={y + cellSize}
236
+ x2={x + cellSize}
237
+ y2={y + cellSize}
238
+ stroke={colorScheme.wall}
239
+ stroke-width={wallThickness}
240
+ />
241
+ {/if}
242
+
243
+ {#if walls.left}
244
+ <line
245
+ x1={x}
246
+ y1={y}
247
+ x2={x}
248
+ y2={y + cellSize}
249
+ stroke={colorScheme.wall}
250
+ stroke-width={wallThickness}
251
+ />
252
+ {/if}
253
+ {/each}
254
+ </svg>
255
+
256
+ <div class="controls">
257
+ <button onclick={play} disabled={isPlaying}>Play</button>
258
+ <button onclick={pause} disabled={!isPlaying}>Pause</button>
259
+ <button onclick={reset}>Reset</button>
260
+ <button onclick={stepBackward} disabled={isPlaying || currentStepIndex === 0}>Step Back</button>
261
+ <button onclick={stepForward} disabled={isPlaying || currentStepIndex === steps.length}>Step Forward</button>
262
+ {#if stepCount}
263
+ <span class="step-counter">
264
+ Step: {currentStepIndex} / {steps.length}
265
+ </span>
266
+ {/if}
267
+ </div>
268
+
269
+ {#if legend}
270
+ <div class="legend">
271
+ <div class="legend-item">
272
+ <div class="legend-color" style="background-color: {colorScheme.start};"></div>
273
+ <span>Start</span>
274
+ </div>
275
+ <div class="legend-item">
276
+ <div class="legend-color" style="background-color: {colorScheme.end};"></div>
277
+ <span>End</span>
278
+ </div>
279
+ <div class="legend-item">
280
+ <div class="legend-color" style="background-color: {colorScheme.current};"></div>
281
+ <span>Current</span>
282
+ </div>
283
+ <div class="legend-item">
284
+ <div class="legend-color" style="background-color: {colorScheme.visiting};"></div>
285
+ <span>Visiting</span>
286
+ </div>
287
+ <div class="legend-item">
288
+ <div class="legend-color" style="background-color: {colorScheme.visited};"></div>
289
+ <span>Visited</span>
290
+ </div>
291
+ <div class="legend-item">
292
+ <div class="legend-color" style="background-color: {colorScheme.path};"></div>
293
+ <span>Path</span>
294
+ </div>
295
+ </div>
296
+ {/if}
297
+ </div>
298
+
299
+ <style>
300
+ .labyrinth-container {
301
+ display: flex;
302
+ flex-direction: column;
303
+ gap: 1rem;
304
+ align-items: center;
305
+ padding: 1rem;
306
+ }
307
+
308
+ .labyrinth-svg {
309
+ background-color: white;
310
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
311
+ }
312
+
313
+ .controls {
314
+ display: flex;
315
+ gap: 0.5rem;
316
+ align-items: center;
317
+ flex-wrap: wrap;
318
+ }
319
+
320
+ .controls button {
321
+ padding: 0.5rem 1rem;
322
+ border: none;
323
+ border-radius: 0.375rem;
324
+ cursor: pointer;
325
+ font-weight: 500;
326
+ transition: background-color 0.2s;
327
+ background-color: var(--lab-buttons);
328
+ color: var(--lab-buttonstext);
329
+ }
330
+
331
+ .controls button:hover:not(:disabled) {
332
+ background-color: var(--lab-buttonshover);
333
+ }
334
+
335
+ .controls button:disabled {
336
+ background-color: var(--lab-buttonsdisabled);
337
+ cursor: not-allowed;
338
+ }
339
+
340
+ .step-counter {
341
+ padding: 0.5rem 1rem;
342
+ border-radius: 0.375rem;
343
+ font-weight: 500;
344
+ background-color: var(--lab-legend);
345
+ color: var(--lab-legend-text);
346
+ }
347
+
348
+ .legend {
349
+ display: flex;
350
+ gap: 1rem;
351
+ flex-wrap: wrap;
352
+ padding: 0.75rem;
353
+ border-radius: 0.5rem;
354
+ background-color: var(--lab-legend);
355
+ color: var(--lab-legend-text);
356
+ }
357
+
358
+ .legend-item {
359
+ display: flex;
360
+ align-items: center;
361
+ gap: 0.5rem;
362
+ }
363
+
364
+ .legend-color {
365
+ width: 1.5rem;
366
+ height: 1.5rem;
367
+ border-radius: 0.25rem;
368
+ border: 1px solid var(--lab-legend-text);
369
+ }
370
+ </style>
@@ -0,0 +1,19 @@
1
+ import type { Graph, AlgorithmType } from '../types';
2
+ import type { ColorScheme } from '../types';
3
+ interface Props {
4
+ graph: Graph;
5
+ cellSize?: number;
6
+ wallThickness?: number;
7
+ algorithm?: AlgorithmType;
8
+ startNode?: string;
9
+ endNode?: string;
10
+ autoPlay?: boolean;
11
+ legend?: boolean;
12
+ stepCount?: boolean;
13
+ animationSpeed?: number;
14
+ showGrid?: boolean;
15
+ colors?: ColorScheme;
16
+ }
17
+ declare const Labyrinth: import("svelte").Component<Props, {}, "">;
18
+ type Labyrinth = ReturnType<typeof Labyrinth>;
19
+ export default Labyrinth;
@@ -0,0 +1,5 @@
1
+ export { generateLabyrinth } from './labyrinth-generator';
2
+ export { dijkstra } from './algorithms/dijkstra';
3
+ export { astar } from './algorithms/astar';
4
+ export { default as Labyrinth } from './components/Labyrinth.svelte';
5
+ export type { Graph, GraphNode, Point, Cell, PathStep, AlgorithmResult, AlgorithmType, ColorScheme } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { generateLabyrinth } from './labyrinth-generator';
2
+ export { dijkstra } from './algorithms/dijkstra';
3
+ export { astar } from './algorithms/astar';
4
+ export { default as Labyrinth } from './components/Labyrinth.svelte';
@@ -0,0 +1,2 @@
1
+ import type { Graph } from './types';
2
+ export declare function generateLabyrinth(width: number, height: number): Graph;
@@ -0,0 +1,106 @@
1
+ function createKey(x, y) {
2
+ return `${x},${y}`;
3
+ }
4
+ function parseKey(key) {
5
+ const [x, y] = key.split(',').map(Number);
6
+ return { x, y };
7
+ }
8
+ export function generateLabyrinth(width, height) {
9
+ const cells = [];
10
+ for (let y = 0; y < height; y++) {
11
+ cells[y] = [];
12
+ for (let x = 0; x < width; x++) {
13
+ cells[y][x] = {
14
+ x,
15
+ y,
16
+ walls: {
17
+ top: true,
18
+ right: true,
19
+ bottom: true,
20
+ left: true
21
+ },
22
+ visited: false
23
+ };
24
+ }
25
+ }
26
+ const stack = [];
27
+ const startCell = cells[0][0];
28
+ startCell.visited = true;
29
+ stack.push(startCell);
30
+ while (stack.length > 0) {
31
+ const current = stack[stack.length - 1];
32
+ const neighbors = getUnvisitedNeighbors(current, cells, width, height);
33
+ if (neighbors.length > 0) {
34
+ const next = neighbors[Math.floor(Math.random() * neighbors.length)];
35
+ removeWall(current, next);
36
+ next.visited = true;
37
+ stack.push(next);
38
+ }
39
+ else {
40
+ stack.pop();
41
+ }
42
+ }
43
+ return cellsToGraph(cells, width, height);
44
+ }
45
+ function getUnvisitedNeighbors(cell, cells, width, height) {
46
+ const neighbors = [];
47
+ const { x, y } = cell;
48
+ if (y > 0 && !cells[y - 1][x].visited)
49
+ neighbors.push(cells[y - 1][x]);
50
+ if (x < width - 1 && !cells[y][x + 1].visited)
51
+ neighbors.push(cells[y][x + 1]);
52
+ if (y < height - 1 && !cells[y + 1][x].visited)
53
+ neighbors.push(cells[y + 1][x]);
54
+ if (x > 0 && !cells[y][x - 1].visited)
55
+ neighbors.push(cells[y][x - 1]);
56
+ return neighbors;
57
+ }
58
+ function removeWall(cell1, cell2) {
59
+ const dx = cell2.x - cell1.x;
60
+ const dy = cell2.y - cell1.y;
61
+ if (dx === 1) {
62
+ cell1.walls.right = false;
63
+ cell2.walls.left = false;
64
+ }
65
+ else if (dx === -1) {
66
+ cell1.walls.left = false;
67
+ cell2.walls.right = false;
68
+ }
69
+ else if (dy === 1) {
70
+ cell1.walls.bottom = false;
71
+ cell2.walls.top = false;
72
+ }
73
+ else if (dy === -1) {
74
+ cell1.walls.top = false;
75
+ cell2.walls.bottom = false;
76
+ }
77
+ }
78
+ function cellsToGraph(cells, width, height) {
79
+ const nodes = new Map();
80
+ for (let y = 0; y < height; y++) {
81
+ for (let x = 0; x < width; x++) {
82
+ const cell = cells[y][x];
83
+ const nodeId = createKey(x, y);
84
+ const neighbors = [];
85
+ if (!cell.walls.top && y > 0) {
86
+ neighbors.push(createKey(x, y - 1));
87
+ }
88
+ if (!cell.walls.right && x < width - 1) {
89
+ neighbors.push(createKey(x + 1, y));
90
+ }
91
+ if (!cell.walls.bottom && y < height - 1) {
92
+ neighbors.push(createKey(x, y + 1));
93
+ }
94
+ if (!cell.walls.left && x > 0) {
95
+ neighbors.push(createKey(x - 1, y));
96
+ }
97
+ nodes.set(nodeId, {
98
+ id: nodeId,
99
+ x,
100
+ y,
101
+ neighbors
102
+ });
103
+ }
104
+ }
105
+ return { nodes, width, height };
106
+ }
@@ -0,0 +1,56 @@
1
+ export interface Point {
2
+ x: number;
3
+ y: number;
4
+ }
5
+ export interface Cell {
6
+ x: number;
7
+ y: number;
8
+ walls: {
9
+ top: boolean;
10
+ right: boolean;
11
+ bottom: boolean;
12
+ left: boolean;
13
+ };
14
+ visited: boolean;
15
+ }
16
+ export interface GraphNode {
17
+ id: string;
18
+ x: number;
19
+ y: number;
20
+ neighbors: string[];
21
+ }
22
+ export interface Graph {
23
+ nodes: Map<string, GraphNode>;
24
+ width: number;
25
+ height: number;
26
+ }
27
+ export interface PathStep {
28
+ nodeId: string;
29
+ type: 'visiting' | 'visited' | 'path' | 'current';
30
+ distance?: number;
31
+ heuristic?: number;
32
+ fScore?: number;
33
+ }
34
+ export interface AlgorithmResult {
35
+ path: string[];
36
+ steps: PathStep[];
37
+ found: boolean;
38
+ }
39
+ export type AlgorithmType = 'dijkstra' | 'astar';
40
+ export interface ColorScheme {
41
+ start?: string;
42
+ end?: string;
43
+ current?: string;
44
+ visiting?: string;
45
+ visited?: string;
46
+ path?: string;
47
+ background?: string;
48
+ wall?: string;
49
+ grid?: string;
50
+ legend?: string;
51
+ legendtext?: string;
52
+ buttons?: string;
53
+ buttonshover?: string;
54
+ buttonsdisabled?: string;
55
+ buttonstext?: string;
56
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ export declare class PriorityQueue<T> {
2
+ private items;
3
+ enqueue(element: T, priority: number): void;
4
+ dequeue(): T | undefined;
5
+ isEmpty(): boolean;
6
+ size(): number;
7
+ }
@@ -0,0 +1,26 @@
1
+ export class PriorityQueue {
2
+ items = [];
3
+ enqueue(element, priority) {
4
+ const item = { element, priority };
5
+ let added = false;
6
+ for (let i = 0; i < this.items.length; i++) {
7
+ if (item.priority < this.items[i].priority) {
8
+ this.items.splice(i, 0, item);
9
+ added = true;
10
+ break;
11
+ }
12
+ }
13
+ if (!added) {
14
+ this.items.push(item);
15
+ }
16
+ }
17
+ dequeue() {
18
+ return this.items.shift()?.element;
19
+ }
20
+ isEmpty() {
21
+ return this.items.length === 0;
22
+ }
23
+ size() {
24
+ return this.items.length;
25
+ }
26
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@hayro_o7/labyrinth",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite dev",
7
+ "build": "vite build && npm run package",
8
+ "preview": "vite preview",
9
+ "package": "svelte-kit sync && svelte-package && publint",
10
+ "prepublishOnly": "npm run package",
11
+ "prepare": "svelte-kit sync || echo ''",
12
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
13
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
14
+ },
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "svelte": "./dist/index.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "!dist/**/*.test.*",
24
+ "!dist/**/*.spec.*"
25
+ ],
26
+ "peerDependencies": {
27
+ "svelte": "^5.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@sveltejs/adapter-auto": "^7.0.0",
31
+ "@sveltejs/kit": "^2.49.1",
32
+ "@sveltejs/package": "^2.3.7",
33
+ "@sveltejs/vite-plugin-svelte": "^6.2.1",
34
+ "@tailwindcss/vite": "^4.1.17",
35
+ "publint": "^0.2.12",
36
+ "svelte": "^5.45.6",
37
+ "svelte-check": "^4.3.4",
38
+ "tailwindcss": "^4.1.17",
39
+ "typescript": "^5.9.3",
40
+ "vite": "^7.2.6"
41
+ },
42
+ "svelte": "./dist/index.js",
43
+ "types": "./dist/index.d.ts"
44
+ }