fesature/maze-gen #16
@@ -6,11 +6,12 @@
|
||||
<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>
|
||||
<button matButton="filled" [disabled]="isAnimationRunning()" (click)="visualize('dijkstra')">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
|
||||
<button matButton="filled" [disabled]="isAnimationRunning()" (click)="visualize('astar')">{{ 'PATHFINDING.ASTAR' | translate }}</button>
|
||||
</div>
|
||||
<div class="controls-panel">
|
||||
<button matButton="filled" (click)="createRandom()">{{ 'PATHFINDING.CLEAR_BOARD' | translate }}</button>
|
||||
<button matButton="filled" [disabled]="isAnimationRunning()" (click)="createRandom(true)">{{ 'LABYRINTH.PRIM' | translate }}</button>
|
||||
<button matButton="filled" [disabled]="isAnimationRunning()" (click)="createRandom(false)">{{ 'LABYRINTH.KRUSKAL' | translate }}</button>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,12 @@ export class SharedFunctions {
|
||||
static randomEventIntFromInterval(interval: number): number {
|
||||
return Math.floor(Math.random() * (interval / 2)) * 2;
|
||||
}
|
||||
|
||||
static shuffleArray<T>(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user