Add labyrinth maze generator and integrate routes
Introduce a new Labyrinth feature: add LabyrinthComponent (TS/HTML/SCSS) implementing maze generation (Prim's/Kruskal) and visualization using the existing generic grid. Wire the component into RouterConstants and app.routes, and add the algorithm entry to AlgorithmsService. Refactor pathfinding internals: rename Node.previousNode -> Node.linkedNode and update PathfindingService and PathfindingComponent accordingly. Add SharedFunctions.random helpers and replace local random utilities. Rename Conway component files/class to ConwayGolComponent and update template path. Add i18n entries for labyrinth (en/de). Minor housekeeping: bump package version to 1.0.0 and disable @typescript-eslint/prefer-for-of in ESLint config.
This commit is contained in:
@@ -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',
|
||||
@@ -0,0 +1,41 @@
|
||||
<mat-card class="container">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'LABYRINTH.TITLE' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<app-information [algorithmInformation]="algoInformation"/>
|
||||
<div class="controls-container">
|
||||
<div class="controls-panel">
|
||||
<button matButton="filled" (click)="visualize('dijkstra')">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
|
||||
<button matButton="filled" (click)="visualize('astar')">{{ 'PATHFINDING.ASTAR' | translate }}</button>
|
||||
</div>
|
||||
<div class="controls-panel">
|
||||
<button matButton="filled" (click)="createRandom()">{{ 'PATHFINDING.CLEAR_BOARD' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<span><span class="legend-color start"></span> {{ 'PATHFINDING.START_NODE' | translate }}</span>
|
||||
<span><span class="legend-color end"></span> {{ 'PATHFINDING.END_NODE' | translate }}</span>
|
||||
<span><span class="legend-color wall"></span> {{ 'PATHFINDING.WALL' | translate }}</span>
|
||||
<span><span class="legend-color visited"></span> {{ 'PATHFINDING.VISITED' | translate }}</span>
|
||||
<span><span class="legend-color path"></span> {{ 'PATHFINDING.PATH' | translate }}</span>
|
||||
</div>
|
||||
<div class="controls-panel">
|
||||
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}</p>
|
||||
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms</p>
|
||||
</div>
|
||||
</div>
|
||||
<app-generic-grid
|
||||
[gridRows]="gridRows"
|
||||
[gridCols]="gridCols"
|
||||
[minGridSize]="MIN_GRID_SIZE"
|
||||
[maxGridSize]="MAX_GRID_SIZE"
|
||||
[maxGridPx]="MAX_GRID_PX"
|
||||
[createNodeFn]="createMazeNode"
|
||||
[getNodeColorFn]="getMazeColor"
|
||||
[applySelectionFn]="applyNoSelection"
|
||||
[backgroundColor]="'lightgray'"
|
||||
(gridChange)="grid = $event"
|
||||
></app-generic-grid>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -0,0 +1,325 @@
|
||||
import {AfterViewInit, Component, inject, 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;
|
||||
pathLength = "0";
|
||||
executionTime = 0;
|
||||
|
||||
private timeoutIds: number[] = [];
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
createRandom(): void {
|
||||
this.stopAnimations();
|
||||
this.clearPath();
|
||||
this.startNode = null;
|
||||
this.endNode = null;
|
||||
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;
|
||||
}
|
||||
}
|
||||
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() {
|
||||
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 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};
|
||||
}
|
||||
|
||||
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,
|
||||
distance: 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.distance = 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); // Redraw single 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);
|
||||
}
|
||||
}
|
||||
|
||||
//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;
|
||||
};
|
||||
}
|
||||
@@ -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',
|
||||
@@ -119,7 +120,7 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
isVisited: false,
|
||||
isPath: false,
|
||||
distance: Infinity,
|
||||
previousNode: null,
|
||||
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--;
|
||||
@@ -436,7 +437,7 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
node.isVisited = false;
|
||||
node.isPath = false;
|
||||
node.distance = Infinity;
|
||||
node.previousNode = null;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface Node {
|
||||
isVisited: boolean;
|
||||
isPath: boolean;
|
||||
distance: number;
|
||||
previousNode: Node | null;
|
||||
linkedNode: Node | null;
|
||||
fScore: number;
|
||||
hScore: number;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -72,7 +72,7 @@ export class PathfindingService {
|
||||
const unvisitedNeighbors = this.getUnvisitedNeighbors(node, grid);
|
||||
for (const neighbor of unvisitedNeighbors) {
|
||||
neighbor.distance = node.distance + 1;
|
||||
neighbor.previousNode = node;
|
||||
neighbor.linkedNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ export class PathfindingService {
|
||||
}
|
||||
|
||||
private updateNeighborNode(neighbor: Node, currentNode: Node, tentativeGScore: number, endNode: Node, openSet: Node[]) {
|
||||
neighbor.previousNode = currentNode;
|
||||
neighbor.linkedNode = currentNode;
|
||||
neighbor.distance = tentativeGScore;
|
||||
neighbor['distance'] = this.calculateHeuristic(neighbor, endNode);
|
||||
neighbor['hScore'] = this.calculateHeuristic(neighbor, endNode);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user