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.",