diff --git a/eslint.config.js b/eslint.config.js index c6c231f..7c67f09 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -17,6 +17,7 @@ module.exports = defineConfig([ rules: { "@typescript-eslint/no-inferrable-types": "off", "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/prefer-for-of": "off", "@angular-eslint/directive-selector": [ "error", { diff --git a/package.json b/package.json index 2d81c4a..984652d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "playground-frontend", - "version": "0.2.0", + "version": "1.0.0", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 33423b1..d739666 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -10,6 +10,7 @@ export const routes: Routes = [ { path: RouterConstants.PATHFINDING.PATH, component: RouterConstants.PATHFINDING.COMPONENT}, { path: RouterConstants.SORTING.PATH, component: RouterConstants.SORTING.COMPONENT}, { path: RouterConstants.IMPRINT.PATH, component: RouterConstants.IMPRINT.COMPONENT}, - { path: RouterConstants.GOL.PATH, component: RouterConstants.GOL.COMPONENT} + { path: RouterConstants.GOL.PATH, component: RouterConstants.GOL.COMPONENT}, + { path: RouterConstants.LABYRINTH.PATH, component: RouterConstants.LABYRINTH.COMPONENT} ]; diff --git a/src/app/constants/RouterConstants.ts b/src/app/constants/RouterConstants.ts index 30122d5..05f9d04 100644 --- a/src/app/constants/RouterConstants.ts +++ b/src/app/constants/RouterConstants.ts @@ -4,7 +4,8 @@ import {ImprintComponent} from '../pages/imprint/imprint.component'; import {AlgorithmsComponent} from '../pages/algorithms/algorithms.component'; import {PathfindingComponent} from '../pages/algorithms/pathfinding/pathfinding.component'; import {SortingComponent} from '../pages/algorithms/sorting/sorting.component'; -import {ConwayGol} from '../pages/algorithms/conway-gol/conway-gol'; +import {ConwayGolComponent} from '../pages/algorithms/conway-gol/conway-gol.component'; +import {LabyrinthComponent} from '../pages/algorithms/pathfinding/labyrinth/labyrinth.component'; export class RouterConstants { @@ -41,7 +42,13 @@ export class RouterConstants { static readonly GOL = { PATH: 'algorithms/gol', LINK: '/algorithms/gol', - COMPONENT: ConwayGol + COMPONENT: ConwayGolComponent + }; + + static readonly LABYRINTH = { + PATH: 'algorithms/labyrinth', + LINK: '/algorithms/labyrinth', + COMPONENT: LabyrinthComponent }; static readonly IMPRINT = { diff --git a/src/app/constants/UrlConstants.ts b/src/app/constants/UrlConstants.ts index b71c228..87164ee 100644 --- a/src/app/constants/UrlConstants.ts +++ b/src/app/constants/UrlConstants.ts @@ -8,4 +8,6 @@ static readonly HEAP_SORT_WIKI = 'https://de.wikipedia.org/wiki/Heapsort' static readonly SHAKE_SORT_WIKI = 'https://de.wikipedia.org/wiki/Shakersort' static readonly CONWAYS_WIKI = 'https://de.wikipedia.org/wiki/Conways_Spiel_des_Lebens' + static readonly PRIMS_WIKI = 'https://de.wikipedia.org/wiki/Algorithmus_von_Prim' + static readonly KRUSKAL_WIKI = 'https://de.wikipedia.org/wiki/Algorithmus_von_Kruskal' } diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.html b/src/app/pages/algorithms/conway-gol/conway-gol.component.html similarity index 100% rename from src/app/pages/algorithms/conway-gol/conway-gol.html rename to src/app/pages/algorithms/conway-gol/conway-gol.component.html diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.ts b/src/app/pages/algorithms/conway-gol/conway-gol.component.ts similarity index 98% rename from src/app/pages/algorithms/conway-gol/conway-gol.ts rename to src/app/pages/algorithms/conway-gol/conway-gol.component.ts index 743965b..ea54f8a 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.ts +++ b/src/app/pages/algorithms/conway-gol/conway-gol.component.ts @@ -29,9 +29,9 @@ import {GenericGridComponent, GridPos} from '../../../shared/components/generic- FormsModule, GenericGridComponent ], - templateUrl: './conway-gol.html', + templateUrl: './conway-gol.component.html', }) -export class ConwayGol implements AfterViewInit { +export class ConwayGolComponent implements AfterViewInit { algoInformation: AlgorithmInformation = { title: 'GOL.EXPLANATION.TITLE', diff --git a/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.html b/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.html new file mode 100644 index 0000000..fecb592 --- /dev/null +++ b/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.html @@ -0,0 +1,42 @@ + + + {{ 'LABYRINTH.TITLE' | translate }} + + + +
+
+ + +
+
+ + +
+ +
+ {{ 'PATHFINDING.START_NODE' | translate }} + {{ 'PATHFINDING.END_NODE' | translate }} + {{ 'PATHFINDING.WALL' | translate }} + {{ 'PATHFINDING.VISITED' | translate }} + {{ 'PATHFINDING.PATH' | translate }} +
+
+

{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}

+

{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms

+
+
+ +
+
diff --git a/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.scss b/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.ts b/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.ts new file mode 100644 index 0000000..5e74869 --- /dev/null +++ b/src/app/pages/algorithms/pathfinding/labyrinth/labyrinth.component.ts @@ -0,0 +1,458 @@ +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'; +import {GenericGridComponent, GridPos} from '../../../../shared/components/generic-grid/generic-grid'; +import {AlgorithmInformation} from '../../information/information.models'; +import {UrlConstants} from '../../../../constants/UrlConstants'; +import {Node} from '../pathfinding.models'; +import {SharedFunctions} from '../../../../shared/SharedFunctions'; +import {MatButton} from '@angular/material/button'; +import {DecimalPipe} from '@angular/common'; +import {PathfindingService} from '../service/pathfinding.service'; + +@Component({ + selector: 'app-labyrinth', + imports: [ + Information, + MatCard, + MatCardContent, + MatCardHeader, + MatCardTitle, + TranslatePipe, + GenericGridComponent, + MatButton, + DecimalPipe + ], + templateUrl: './labyrinth.component.html', + styleUrl: './labyrinth.component.scss', +}) +export class LabyrinthComponent implements AfterViewInit { + + protected readonly gridRows = 101; + protected readonly gridCols = 101; + protected readonly MAX_GRID_SIZE = 101; + protected readonly MAX_GRID_PX = 1000; + protected readonly MIN_GRID_SIZE = 101; + private readonly pathfindingService = inject(PathfindingService); + + + algoInformation: AlgorithmInformation = { + title: 'LABYRINTH.EXPLANATION.TITLE', + entries: [ + { + name: 'Prim’s', + description: 'LABYRINTH.EXPLANATION.PRIM_EXPLANATION', + link: UrlConstants.PRIMS_WIKI + }, + { + name: 'Kruskal’s', + description: 'LABYRINTH.EXPLANATION.KRUSKAL_EXPLANATION', + link: UrlConstants.KRUSKAL_WIKI + } + ], + disclaimer: 'LABYRINTH.EXPLANATION.DISCLAIMER', + disclaimerBottom: '', + disclaimerListEntry: ['LABYRINTH.EXPLANATION.DISCLAIMER_1', 'LABYRINTH.EXPLANATION.DISCLAIMER_2', 'LABYRINTH.EXPLANATION.DISCLAIMER_3', 'LABYRINTH.EXPLANATION.DISCLAIMER_4'] + }; + + @ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent; + + grid: Node[][] = []; + 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) { + this.genericGridComponent.initializationFn = this.initializeMazeGrid; + this.genericGridComponent.createNodeFn = this.createMazeNode; + this.genericGridComponent.getNodeColorFn = this.getMazeColor; + this.genericGridComponent.applySelectionFn = this.applyNoSelection; + this.genericGridComponent.gridRows = this.gridRows; + this.genericGridComponent.gridCols = this.gridCols; + this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE; + this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE; + this.genericGridComponent.maxGridPx = 1000; + this.genericGridComponent.applyGridSize(); + this.genericGridComponent.initializeGrid(); + } + } + + initializeMazeGrid = (grid: Node[][]): void => { + this.grid = grid; + this.createRandom(true); + }; + + 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; + this.grid[row][col].isStart = false; + this.grid[row][col].isEnd = false; + } + } + } + + private cleanupGrid() { + for (let row = 0; row < this.grid.length; row++) { + for (let col = 0; col < this.grid[row].length; col++) { + this.grid[row][col].isVisited = false; + this.grid[row][col].linkedNode = null; + } + } + } + + 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.isVisited = true; + return {startRow, startCol, startNode}; + } + + visualize(algorithm: string): void { + this.stopAnimations(); + this.clearPath(); + + const startTime = performance.now(); + let result; + + switch (algorithm) { + case 'dijkstra': result = this.pathfindingService.dijkstra( + this.grid, + this.grid[this.startNode!.row][this.startNode!.col], + this.grid[this.endNode!.row][this.endNode!.col] + ); + break; + case 'astar': result = this.pathfindingService.aStar( + this.grid, + this.grid[this.startNode!.row][this.startNode!.col], + this.grid[this.endNode!.row][this.endNode!.col] + ); + break; + } + + if (!result) + { + return; + } + + const endTime = performance.now(); + const lengthOfShortestPath = result.nodesInShortestPathOrder.length; + if (lengthOfShortestPath === 0) + { + this.pathLength = "∞" + } + else + { + this.pathLength = result.nodesInShortestPathOrder.length + ""; + } + this.executionTime = endTime - startTime; + + this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder); + } + + createMazeNode = (row: number, col: number): Node => { + return { + row, + col, + isStart: false, + isEnd: false, + isWall: false, + isVisited: false, + isPath: false, + nodeData: Infinity, + linkedNode: null, + hScore: 0, + fScore: Infinity, + }; + }; + + getMazeColor = (node: Node): string => { + if (node.isStart) return 'green'; + if (node.isEnd) return 'red'; + if (node.isPath) return 'gold'; + if (node.isVisited) return 'skyblue'; + if (node.isWall) return 'black'; + return 'lightgray'; + }; + + applyNoSelection = (pos: GridPos, grid: Node[][]): void => { + this.grid = grid; + //dont need a selection for the maze case + } + + // --- Animation (adapted to use genericGridComponent for redraw) --- + private stopAnimations(): void { + for (const id of this.timeoutIds) { + clearTimeout(id); + } + this.timeoutIds = []; + } + + private clearPath(): void { + for (let row = 0; row < this.gridRows; row++) { + for (let col = 0; col < this.gridCols; col++) { + const node = this.grid[row][col]; + node.isVisited = false; + node.isPath = false; + node.nodeData = Infinity; + node.linkedNode = null; + } + } + this.genericGridComponent?.drawGrid(); // Redraw the grid via generic grid component + } + + private animateAlgorithm(visited: Node[], path: Node[]): void { + for (let i = 0; i <= visited.length; i++) { + if (i === visited.length) { + const id = globalThis.setTimeout(() => this.animateShortestPath(path), this.animationSpeed * i); + this.timeoutIds.push(id); + return; + } + + const node = visited[i]; + const id = globalThis.setTimeout(() => { + if (!node.isStart && !node.isEnd) { + node.isVisited = true; + this.genericGridComponent?.drawNode(node); + } + }, this.animationSpeed * i); + + this.timeoutIds.push(id); + } + } + + private animateShortestPath(path: Node[]): void { + for (let i = 0; i < path.length; i++) { + const node = path[i]; + const id = globalThis.setTimeout(() => { + if (!node.isStart && !node.isEnd) { + node.isPath = true; + this.genericGridComponent?.drawNode(node); // Redraw single node + } + }, this.animationSpeed * i); + + 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{ + + const directions = [ + [0, 2], [0, -2], [2, 0], [-2, 0] + ]; + + for (const [dr, dc] of directions) { + const nextRow = row + dr; + const nextCol = col + dc; + + + if (this.isValid(nextRow, nextCol) && this.grid[nextRow][nextCol].isWall && !this.grid[nextRow][nextCol].isVisited) { + const wallRow = row + dr / 2; + const wallCol = col + dc / 2; + + const node = this.grid[wallRow][wallCol]; + node.linkedNode = this.grid[nextRow][nextCol]; + frontier.push(node); + } + } + } + + isValid = (row: number, col: number): boolean => { + return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols; + }; +} diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts index 98e99ca..4e224d3 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts @@ -16,6 +16,7 @@ import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/mat import {Information} from '../information/information'; import {AlgorithmInformation} from '../information/information.models'; import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid'; +import {SharedFunctions} from '../../../shared/SharedFunctions'; enum NodeType { Start = 'start', @@ -118,8 +119,8 @@ export class PathfindingComponent implements AfterViewInit { isWall: false, isVisited: false, isPath: false, - distance: Infinity, - previousNode: null, + nodeData: Infinity, + linkedNode: null, hScore: 0, fScore: Infinity, }; @@ -326,16 +327,16 @@ export class PathfindingComponent implements AfterViewInit { private createRandomStartEndPosition(): { start: GridPos; end: GridPos } { const midCol = Math.floor(this.gridCols / 2); - const startRow: number = this.randomIntFromInterval(0, this.gridRows - 1); - const startCol: number = this.randomIntFromInterval(0, this.gridCols - 1); + const startRow: number = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1); + const startCol: number = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1); - const endRow: number = this.randomIntFromInterval(0, this.gridRows - 1); + const endRow: number = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1); let endCol: number; if (startCol <= midCol) { - endCol = this.randomIntFromInterval(midCol + 1, this.gridCols - 1); + endCol = SharedFunctions.randomIntFromInterval(midCol + 1, this.gridCols - 1); } else { - endCol = this.randomIntFromInterval(0, midCol); + endCol = SharedFunctions.randomIntFromInterval(0, midCol); } return { @@ -359,8 +360,8 @@ export class PathfindingComponent implements AfterViewInit { for (let wall = 0; wall < maxNumberOfWalls; wall++) { - const row: number = this.randomIntFromInterval(0, this.gridRows - 1); - const col: number = this.randomIntFromInterval(0, this.gridCols - 1); + const row: number = SharedFunctions.randomIntFromInterval(0, this.gridRows - 1); + const col: number = SharedFunctions.randomIntFromInterval(0, this.gridCols - 1); if (!this.grid[row][col]) { // Use the grid passed from GenericGrid wall--; @@ -435,8 +436,8 @@ export class PathfindingComponent implements AfterViewInit { const node = this.grid[row][col]; node.isVisited = false; node.isPath = false; - node.distance = Infinity; - node.previousNode = null; + node.nodeData = Infinity; + node.linkedNode = null; } } this.genericGridComponent?.drawGrid(); // Redraw the grid via generic grid component @@ -486,8 +487,4 @@ export class PathfindingComponent implements AfterViewInit { return false; } - // --- Utility --- - private randomIntFromInterval(min: number, max: number): number { - return Math.floor(Math.random() * (max - min + 1) + min); - } } diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.models.ts b/src/app/pages/algorithms/pathfinding/pathfinding.models.ts index 7d34f94..bd1662d 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.models.ts +++ b/src/app/pages/algorithms/pathfinding/pathfinding.models.ts @@ -6,8 +6,8 @@ export interface Node { isWall: boolean; isVisited: boolean; isPath: boolean; - distance: number; - previousNode: Node | null; + 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 9519c84..4f7a183 100644 --- a/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts +++ b/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts @@ -25,7 +25,7 @@ export class PathfindingService { let currentNode: Node | null = endNode; while (currentNode !== null) { shortestPathNodes.unshift(currentNode); - currentNode = currentNode.previousNode; + currentNode = currentNode.linkedNode; } return shortestPathNodes; } @@ -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,24 +65,24 @@ 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.previousNode = node; + neighbor.nodeData = node.nodeData + 1; + neighbor.linkedNode = node; } } // 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); } } @@ -136,11 +136,11 @@ export class PathfindingService { } private updateNeighborNode(neighbor: Node, currentNode: Node, tentativeGScore: number, endNode: Node, openSet: Node[]) { - neighbor.previousNode = currentNode; - neighbor.distance = tentativeGScore; - neighbor['distance'] = this.calculateHeuristic(neighbor, endNode); + neighbor.linkedNode = currentNode; + 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/pages/algorithms/service/algorithms.service.ts b/src/app/pages/algorithms/service/algorithms.service.ts index f834c16..8643b1b 100644 --- a/src/app/pages/algorithms/service/algorithms.service.ts +++ b/src/app/pages/algorithms/service/algorithms.service.ts @@ -26,6 +26,12 @@ export class AlgorithmsService { title: 'ALGORITHM.GOL.TITLE', description: 'ALGORITHM.GOL.DESCRIPTION', routerLink: RouterConstants.GOL.LINK + }, + { + id: 'labyrinth', + title: 'ALGORITHM.LABYRINTH.TITLE', + description: 'ALGORITHM.LABYRINTH.DESCRIPTION', + routerLink: RouterConstants.LABYRINTH.LINK } ]; diff --git a/src/app/shared/SharedFunctions.ts b/src/app/shared/SharedFunctions.ts index c54b1e5..6920998 100644 --- a/src/app/shared/SharedFunctions.ts +++ b/src/app/shared/SharedFunctions.ts @@ -7,4 +7,19 @@ export class SharedFunctions { globalThis.location.href = `mailto:${user}@${domain}`; } + static randomIntFromInterval(min: number, max: number): number { + return Math.floor(Math.random() * (max - min + 1) + min); + } + + 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 1a69b8f..297d6dc 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -362,6 +362,21 @@ "DISCLAIMER_4": " Eine tote Zelle bleibt tot, wenn sie nicht genau drei lebende Nachbarn hat." } }, + "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.", + "KRUSKAL_EXPLANATION": "betrachtet alle Wände des Gitters als potenzielle Wege. Er wählt zufällig Wände aus und öffnet sie nur dann, wenn die beiden angrenzenden Zellen noch nicht miteinander verbunden sind (verhindert Kreise). Vorteil: Erzeugt ein sehr komplexes Labyrinth mit vielen langen, verwinkelten Pfaden. Visuell ist es spannend, da das Labyrinth an vielen Stellen gleichzeitig entsteht und am Ende zu einem Ganzen verschmilzt.", + "DISCLAIMER": "Beide Algorithmen basieren auf dem Prinzip des 'Minimal Spanning Tree' (Minimaler Spannbaum). Das bedeutet für dein Labyrinth:", + "DISCLAIMER_1": "Perfektes Labyrinth: Es gibt keine geschlossenen Kreise (Loops) – jeder Punkt ist erreichbar, aber es gibt immer nur genau einen Weg zwischen zwei Punkten.", + "DISCLAIMER_2": "Erreichbarkeit: Da es ein Spannbaum ist, wird garantiert jede Zelle des Gitters Teil des Labyrinths, es gibt keine isolierten Bereiche.", + "DISCLAIMER_3": "Zufälligkeit: Durch die Gewichtung der Kanten mit Zufallswerten entstehen bei jedem Durchlauf völlig neue, einzigartige Strukturen.", + "DISCLAIMER_4": "Anwendung: Solche Labyrinthe sind die perfekte Testumgebung für Pfadfindungsalgorithmen wie Dijkstra oder A*." + } + }, "ALGORITHM": { "TITLE": "Algorithmen", "PATHFINDING": { @@ -376,6 +391,10 @@ "TITLE": "Conway's Game of Life", "DESCRIPTION": "Das 'Spiel des Lebens' ist ein vom Mathematiker John Horton Conway 1970 entworfenes Spiel." }, + "LABYRINTH": { + "TITLE": "Labyrinth-Erzeugung", + "DESCRIPTION": "Visualisierung verschiedener Laybrinth-Erzeugungs-Algorithmen." + }, "NOTE": "HINWEIS", "GRID_HEIGHT": "Höhe", "GRID_WIDTH": "Beite" diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 8de833b..91bb333 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -361,6 +361,21 @@ "DISCLAIMER_4": "A dead cell remains dead if it does not have exactly three living neighbors." } }, + "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.", + "KRUSKAL_EXPLANATION": "considers all walls of the grid as potential paths. It randomly selects walls and opens them only if the two adjacent cells are not yet connected (preventing cycles). Advantage: Produces a very complex labyrinth with many long, winding paths. Visually, it is engaging because the labyrinth emerges simultaneously in many places and eventually merges into a whole.", + "DISCLAIMER": "Both algorithms are based on the principle of the 'Minimum Spanning Tree'. This means for your labyrinth:", + "DISCLAIMER_1": "Perfect labyrinth: There are no closed loops – every point is reachable, but there is always exactly one path between any two points.", + "DISCLAIMER_2": "Reachability: Since it is a spanning tree, every cell in the grid is guaranteed to be part of the labyrinth; there are no isolated areas.", + "DISCLAIMER_3": "Randomness: By weighting the edges with random values, each run produces completely new, unique structures.", + "DISCLAIMER_4": "Application: Such labyrinths are the perfect test environment for pathfinding algorithms such as Dijkstra or A*." + } + }, "ALGORITHM": { "TITLE": "Algorithms", "PATHFINDING": { @@ -375,6 +390,10 @@ "TITLE": "Conway's Game of Life", "DESCRIPTION": "The Game of Life is a cellular automaton devised by the British mathematician John Horton Conway in 1970." }, + "LABYRINTH": { + "TITLE": "Maze Generation", + "DESCRIPTION": "Visualizing various maze generation algorithms." + }, "NOTE": "Note", "GRID_HEIGHT": "Height", "GRID_WIDTH": "Width"