diff --git a/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.html b/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.html
index c001fd1..fecb592 100644
--- a/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.html
+++ b/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.html
@@ -6,11 +6,12 @@
-
-
+
+
-
+
+
diff --git a/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.ts b/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.ts
index 082d205..5e74869 100644
--- a/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.ts
+++ b/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.ts
@@ -1,4 +1,4 @@
-import {AfterViewInit, Component, inject, ViewChild} from '@angular/core';
+import {AfterViewInit, Component, inject, signal, ViewChild} from '@angular/core';
import {Information} from '../../information/information';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {TranslatePipe} from '@ngx-translate/core';
@@ -62,10 +62,13 @@ export class LabyrinthComponent implements AfterViewInit {
startNode: Node | null = null;
endNode: Node | null = null;
animationSpeed = 3;
+ mazeAnimationSpeed = 1;
pathLength = "0";
executionTime = 0;
private timeoutIds: number[] = [];
+ protected mazeNodesInOrder: Node[] = [];
+ readonly isAnimationRunning = signal(false);
ngAfterViewInit(): void {
if (this.genericGridComponent) {
@@ -85,14 +88,167 @@ export class LabyrinthComponent implements AfterViewInit {
initializeMazeGrid = (grid: Node[][]): void => {
this.grid = grid;
- this.createRandom();
+ this.createRandom(true);
};
- createRandom(): void {
+ createRandom(prim: boolean): void {
+ this.isAnimationRunning.set(true);
this.stopAnimations();
this.clearPath();
this.startNode = null;
this.endNode = null;
+ if (prim)
+ {
+ this.createPrimMaze();
+ }
+ else{
+ this.createKruskalMaze();
+ }
+
+ this.cleanupGrid();
+ this.genericGridComponent.drawGrid();
+ }
+// ------- Kuskal -------
+ private createKruskalMaze(): void {
+ this.initKuskal();
+ this.mazeNodesInOrder = [];
+ const walls = this.findWallsWithADistanceOfTwoRooms();
+ SharedFunctions.shuffleArray(walls);
+ for (const wallInfo of walls) {
+ const { row, col, roomA, roomB } = wallInfo;
+
+ if (roomA.nodeData !== roomB.nodeData) {
+ const wallNode = this.grid[row][col];
+ wallNode.isWall = true;
+ this.mazeNodesInOrder.push(wallNode);
+
+ const oldId = roomB.nodeData;
+ const newId = roomA.nodeData;
+
+ this.mergeSets(oldId, newId);
+ }
+ }
+ this.setRandomStartAndEnd();
+ this.animateMazeGeneration();
+ }
+
+ private initKuskal() {
+ let roomId = 0;
+
+ for (let row = 0; row < this.gridRows; row++) {
+ for (let col = 0; col < this.gridCols; col++) {
+ const node = this.grid[row][col];
+ node.isStart = false;
+ node.isEnd = false;
+
+ if (row % 2 === 0 && col % 2 === 0) {
+ node.isWall = false;
+ node.nodeData = roomId++;
+ } else {
+ node.isWall = true;
+ }
+ }
+ }
+ }
+
+ private mergeSets(oldId: number, newId: number): void {
+ for (let r = 0; r < this.gridRows; r += 2) {
+ for (let c = 0; c < this.gridCols; c += 2) {
+ if (this.grid[r][c].nodeData === oldId) {
+ this.grid[r][c].nodeData = newId;
+ }
+ }
+ }
+ }
+
+ private findWallsWithADistanceOfTwoRooms() {
+ const walls: { row: number, col: number, roomA: Node, roomB: Node }[] = [];
+ for (let row = 0; row < this.gridRows; row++) {
+ for (let col = 0; col < this.gridCols; col++) {
+ if (row % 2 === 0 && col % 2 !== 0 && col > 0 && col < this.gridCols - 1) {
+ walls.push({
+ row, col,
+ roomA: this.grid[row][col - 1],
+ roomB: this.grid[row][col + 1]
+ });
+ }
+ if (row % 2 !== 0 && col % 2 === 0 && row > 0 && row < this.gridRows - 1) {
+ walls.push({
+ row, col,
+ roomA: this.grid[row - 1][col],
+ roomB: this.grid[row + 1][col]
+ });
+ }
+ }
+ }
+ return walls;
+ }
+
+ private setRandomStartAndEnd(): void {
+ const lastRow = Math.floor((this.gridRows - 1) / 2) * 2;
+ const lastCol = Math.floor((this.gridCols - 1) / 2) * 2;
+
+ const corners = [
+ { r: 0, c: 0 },
+ { r: 0, c: lastCol },
+ { r: lastRow, c: 0 },
+ { r: lastRow, c: lastCol }
+ ];
+
+ const startIndex = Math.floor(Math.random() * corners.length);
+ let endIndex = Math.floor(Math.random() * corners.length);
+ while (endIndex === startIndex) {
+ endIndex = Math.floor(Math.random() * corners.length);
+ }
+
+ const start = corners[startIndex];
+ const end = corners[endIndex];
+
+ this.startNode = this.grid[start.r][start.c];
+ this.startNode.isStart = true;
+ this.startNode.isWall = false;
+
+ this.endNode =this.grid[end.r][end.c];
+ this.endNode.isEnd = true;
+ this.endNode.isWall = false;
+ }
+
+// ------- PRIM -------
+ private createPrimMaze(): void {
+ this.initPrim();
+ this.mazeNodesInOrder = [];
+ const frontier: Node[] = [];
+
+ const {startRow, startCol, startNode} = this.findStartNode();
+ this.mazeNodesInOrder.push(startNode);
+
+ this.getNeighborWalls(startRow, startCol, frontier);
+
+ while (frontier.length > 0) {
+ const randomIndex = SharedFunctions.randomIntFromInterval(0, frontier.length - 1);
+ const lastIndex = frontier.length - 1;
+ [frontier[randomIndex], frontier[lastIndex]] = [frontier[lastIndex], frontier[randomIndex]];
+
+ const wallNode = frontier.pop()!;
+ const target = wallNode.linkedNode;
+
+ if (!target || target.isVisited) {
+ continue;
+ }
+
+ wallNode.isVisited = true;
+ target.isVisited = true;
+
+ this.mazeNodesInOrder.push(wallNode, target);
+
+ this.getNeighborWalls(target.row, target.col, frontier);
+ }
+
+ this.setRandomStartAndEnd();
+ this.animateMazeGeneration();
+ }
+
+ private initPrim() {
for (let row = 0; row < this.grid.length; row++) {
for (let col = 0; col < this.grid[row].length; col++) {
this.grid[row][col].isWall = true;
@@ -100,34 +256,6 @@ export class LabyrinthComponent implements AfterViewInit {
this.grid[row][col].isEnd = false;
}
}
- const frontier: Node[] = [];
-
- const {startRow, startCol, startNode} = this.findStartNode();
- this.startNode = startNode;
- this.getNeighborWalls(startRow, startCol, frontier);
- while (frontier.length > 0) {
- const randomIndex = SharedFunctions.randomIntFromInterval(0, frontier.length - 1);
-
- //swap and pop from array
- const lastIndex = frontier.length - 1;
- [frontier[randomIndex], frontier[lastIndex]] = [frontier[lastIndex], frontier[randomIndex]];
- const wallFromFrontierList = frontier.pop()!;
- const target = wallFromFrontierList.linkedNode;
-
- if (!target || target.isVisited) {
- continue;
- }
- wallFromFrontierList.isWall = false;
- wallFromFrontierList.isVisited = true;
- target.isWall = false;
- target.isVisited = true;
- this.getNeighborWalls(target.row, target.col, frontier);
- }
-
- this.findEndNode(startNode);
- this.cleanupGrid();
-
- this.genericGridComponent.drawGrid();
}
private cleanupGrid() {
@@ -139,32 +267,12 @@ export class LabyrinthComponent implements AfterViewInit {
}
}
- private findEndNode(startNode: Node) {
- let endFound = false;
- while (!endFound) {
- const endRow: number = SharedFunctions.randomEventIntFromInterval(this.gridRows - 1);
- const endCol: number = SharedFunctions.randomEventIntFromInterval(this.gridCols - 1);
-
- const endNode = this.grid[endRow][endCol];
-
- if (endNode != startNode && !endNode.isWall) {
- endNode.isWall = false;
- endNode.isEnd = true;
- endNode.isVisited = true;
- this.endNode = endNode;
- endFound = true;
- }
- }
-
- }
-
private findStartNode() {
const startRow: number = SharedFunctions.randomEventIntFromInterval(this.gridRows - 1);
const startCol: number = SharedFunctions.randomEventIntFromInterval(this.gridCols - 1);
const startNode = this.grid[startRow][startCol];
startNode.isWall = false;
- startNode.isStart = true;
startNode.isVisited = true;
return {startRow, startCol, startNode};
}
@@ -220,7 +328,7 @@ export class LabyrinthComponent implements AfterViewInit {
isWall: false,
isVisited: false,
isPath: false,
- distance: Infinity,
+ nodeData: Infinity,
linkedNode: null,
hScore: 0,
fScore: Infinity,
@@ -255,7 +363,7 @@ export class LabyrinthComponent implements AfterViewInit {
const node = this.grid[row][col];
node.isVisited = false;
node.isPath = false;
- node.distance = Infinity;
+ node.nodeData = Infinity;
node.linkedNode = null;
}
}
@@ -274,7 +382,7 @@ export class LabyrinthComponent implements AfterViewInit {
const id = globalThis.setTimeout(() => {
if (!node.isStart && !node.isEnd) {
node.isVisited = true;
- this.genericGridComponent?.drawNode(node); // Redraw single node
+ this.genericGridComponent?.drawNode(node);
}
}, this.animationSpeed * i);
@@ -295,6 +403,31 @@ export class LabyrinthComponent implements AfterViewInit {
this.timeoutIds.push(id);
}
}
+ private animateMazeGeneration(): void {
+ for (let i = 0; i < this.mazeNodesInOrder.length; i++) {
+ const id = globalThis.setTimeout(() => {
+ const node = this.mazeNodesInOrder[i];
+ node.isWall = false;
+ this.genericGridComponent?.drawNode(node);
+ if (i === this.mazeNodesInOrder.length - 1) {
+ this.cleanupGrid();
+ if (this.startNode) {
+ this.genericGridComponent?.drawNode(this.startNode);
+ }
+
+ if (this.endNode) {
+ this.genericGridComponent?.drawNode(this.endNode);
+ }
+ }
+ if (i == this.mazeNodesInOrder.length - 1) {
+ this.isAnimationRunning.set(false);
+ }
+ }, this.mazeAnimationSpeed * i);
+
+ this.timeoutIds.push(id);
+ }
+ }
+
//utility
private getNeighborWalls(row: number, col: number, frontier: Node[]): void{
diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts
index 66a7fe4..4e224d3 100644
--- a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts
+++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts
@@ -119,7 +119,7 @@ export class PathfindingComponent implements AfterViewInit {
isWall: false,
isVisited: false,
isPath: false,
- distance: Infinity,
+ nodeData: Infinity,
linkedNode: null,
hScore: 0,
fScore: Infinity,
@@ -436,7 +436,7 @@ export class PathfindingComponent implements AfterViewInit {
const node = this.grid[row][col];
node.isVisited = false;
node.isPath = false;
- node.distance = Infinity;
+ node.nodeData = Infinity;
node.linkedNode = null;
}
}
diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.models.ts b/src/app/pages/algorithms/pathfinding/pathfinding.models.ts
index 8776882..bd1662d 100644
--- a/src/app/pages/algorithms/pathfinding/pathfinding.models.ts
+++ b/src/app/pages/algorithms/pathfinding/pathfinding.models.ts
@@ -6,7 +6,7 @@ export interface Node {
isWall: boolean;
isVisited: boolean;
isPath: boolean;
- distance: number;
+ nodeData: number; //can be used as distance or id or something
linkedNode: Node | null;
fScore: number;
hScore: number;
diff --git a/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts b/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts
index 063199a..4f7a183 100644
--- a/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts
+++ b/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts
@@ -33,7 +33,7 @@ export class PathfindingService {
// Dijkstra's Algorithm
dijkstra(grid: Node[][], startNode: Node, endNode: Node): { visitedNodesInOrder: Node[], nodesInShortestPathOrder: Node[] } {
const visitedNodesInOrder: Node[] = [];
- startNode.distance = 0;
+ startNode.nodeData = 0;
const unvisitedNodes: Node[] = this.getAllNodes(grid);
while (unvisitedNodes.length > 0) {
@@ -44,7 +44,7 @@ export class PathfindingService {
continue;
}
- const isTrapped = closestNode.distance === Infinity;
+ const isTrapped = closestNode.nodeData === Infinity;
if (isTrapped)
{
return { visitedNodesInOrder, nodesInShortestPathOrder: [] };
@@ -65,13 +65,13 @@ export class PathfindingService {
}
private sortNodesByDistance(unvisitedNodes: Node[]): void {
- unvisitedNodes.sort((nodeA, nodeB) => nodeA.distance - nodeB.distance);
+ unvisitedNodes.sort((nodeA, nodeB) => nodeA.nodeData - nodeB.nodeData);
}
private updateUnvisitedNeighbors(node: Node, grid: Node[][]): void {
const unvisitedNeighbors = this.getUnvisitedNeighbors(node, grid);
for (const neighbor of unvisitedNeighbors) {
- neighbor.distance = node.distance + 1;
+ neighbor.nodeData = node.nodeData + 1;
neighbor.linkedNode = node;
}
}
@@ -79,10 +79,10 @@ export class PathfindingService {
// A* Search Algorithm
aStar(grid: Node[][], startNode: Node, endNode: Node): { visitedNodesInOrder: Node[], nodesInShortestPathOrder: Node[] } {
const visitedNodesInOrder: Node[] = [];
- startNode.distance = 0;
+ startNode.nodeData = 0;
startNode['hScore'] = this.calculateHeuristic(startNode, endNode);
// fScore = gScore + hScore
- startNode['fScore'] = startNode.distance + startNode['hScore'];
+ startNode['fScore'] = startNode.nodeData + startNode['hScore'];
const openSet: Node[] = [startNode];
const allNodes = this.getAllNodes(grid);
@@ -97,7 +97,7 @@ export class PathfindingService {
continue;
}
- const isTrapped = currentNode.distance === Infinity;
+ const isTrapped = currentNode.nodeData === Infinity;
if (isTrapped)
{
return {visitedNodesInOrder, nodesInShortestPathOrder: []};
@@ -114,9 +114,9 @@ export class PathfindingService {
const neighbors = this.getUnvisitedNeighbors(currentNode, grid);
for (const neighbor of neighbors) {
- const tentativeGScore = currentNode.distance + 1; // Distance from start to neighbor
+ const tentativeGScore = currentNode.nodeData + 1; // Distance from start to neighbor
- if (tentativeGScore < neighbor.distance) {
+ if (tentativeGScore < neighbor.nodeData) {
this.updateNeighborNode(neighbor, currentNode, tentativeGScore, endNode, openSet);
}
}
@@ -137,10 +137,10 @@ export class PathfindingService {
private updateNeighborNode(neighbor: Node, currentNode: Node, tentativeGScore: number, endNode: Node, openSet: Node[]) {
neighbor.linkedNode = currentNode;
- neighbor.distance = tentativeGScore;
- neighbor['distance'] = this.calculateHeuristic(neighbor, endNode);
+ neighbor.nodeData = tentativeGScore;
+ neighbor['nodeData'] = this.calculateHeuristic(neighbor, endNode);
neighbor['hScore'] = this.calculateHeuristic(neighbor, endNode);
- neighbor['fScore'] = neighbor.distance + neighbor['hScore'];
+ neighbor['fScore'] = neighbor.nodeData + neighbor['hScore'];
if (!openSet.includes(neighbor)) {
openSet.push(neighbor);
@@ -151,7 +151,7 @@ export class PathfindingService {
for (const node of allNodes) {
if (node !== startNode) {
node['fScore'] = Infinity;
- node.distance = Infinity; // gScore
+ node.nodeData = Infinity; // gScore
}
}
}
diff --git a/src/app/shared/SharedFunctions.ts b/src/app/shared/SharedFunctions.ts
index 1a4ae7a..6920998 100644
--- a/src/app/shared/SharedFunctions.ts
+++ b/src/app/shared/SharedFunctions.ts
@@ -14,4 +14,12 @@ export class SharedFunctions {
static randomEventIntFromInterval(interval: number): number {
return Math.floor(Math.random() * (interval / 2)) * 2;
}
+
+ static shuffleArray(array: T[]): T[] {
+ for (let i = array.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [array[i], array[j]] = [array[j], array[i]];
+ }
+ return array;
+ }
}
diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json
index cc65eed..297d6dc 100644
--- a/src/assets/i18n/de.json
+++ b/src/assets/i18n/de.json
@@ -364,6 +364,8 @@
},
"LABYRINTH": {
"TITLE": "Labyrinth-Erzeugung",
+ "PRIM": "Erzeuge Prim's Labyrinth",
+ "KRUSKAL": "Erzeuge Kruskal's Labyrinth",
"EXPLANATION": {
"TITLE": "Algorithmen",
"PRIM_EXPLANATION": "startet an einem zufälligen Punkt und erweitert das Labyrinth, indem er immer eine zufällige benachbarte Wand zu einer bereits besuchten Zelle auswählt und diese öffnet. Vorteil: Erzeugt sehr gleichmäßige, natürlich wirkende Labyrinthe mit vielen kurzen Sackgassen. Visuell wirkt es wie ein organisches Wachstum von einem Zentrum aus.",
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 2597079..91bb333 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -363,6 +363,8 @@
},
"LABYRINTH": {
"TITLE": "Labyrinth Generation",
+ "PRIM": "Generate Prim's Labyrinth",
+ "KRUSKAL": "Generate Kruskal's Labyrinth",
"EXPLANATION": {
"TITLE": "Algorithms",
"PRIM_EXPLANATION": "starts at a random point and expands the labyrinth by always selecting a random neighboring wall of an already visited cell and opening it. Advantage: Produces very uniform, natural-looking labyrinths with many short dead ends. Visually, it appears like organic growth from a central point.",