Merge pull request 'feature/pathfinding-finetuning' (#11) from feature/pathfinding-finetuning into main
All checks were successful
Build & Push Frontend A / docker (push) Successful in 50s

Reviewed-on: #11
This commit was merged in pull request #11.
This commit is contained in:
2026-02-05 09:25:41 +01:00
7 changed files with 149 additions and 70 deletions

View File

@@ -66,7 +66,7 @@
</mat-card> </mat-card>
<mat-card class="experdience"> <mat-card class="experdience">
<h2>{{ 'ABOUT.SECTION.EXPERIENCE' | translate }}</h2> <h2 style="margin-left: 0.5rem;">{{ 'ABOUT.SECTION.EXPERIENCE' | translate }}</h2>
<div class="xp-list"> <div class="xp-list">
@for (entry of xpKeys; track entry.key) { @for (entry of xpKeys; track entry.key) {
<div class="xp-item"> <div class="xp-item">

View File

@@ -47,7 +47,6 @@
/* Skills block */ /* Skills block */
.skills { .skills {
padding: 5px; padding: 5px;
h2 { margin-top: .25rem; margin-left: .25rem; } h2 { margin-top: .25rem; margin-left: .25rem; }
.chip-groups { .chip-groups {
margin-left: .25rem; margin-left: .25rem;
@@ -64,11 +63,7 @@
/* Experience block */ /* Experience block */
.experience { .experience {
padding: 5px; padding: 5px;
h2 { margin-top: .25rem;margin-left: .25rem; } h2 { margin-top: .25rem; margin-left: .25rem; }
.xp-list {
margin-left: .25rem;
display: grid; gap: .75rem;
}
.xp-item { .xp-item {
.xp-head { .xp-head {
display:flex; align-items:baseline; gap:.5rem; display:flex; align-items:baseline; gap:.5rem;

View File

@@ -23,13 +23,14 @@
<div class="controls-container"> <div class="controls-container">
<div class="controls"> <div class="controls">
<button matButton="filled" (click)="visualizeDijkstra()">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button> <button matButton="filled" (click)="visualize('dijkstra')">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
<button matButton="filled" (click)="visualizeAStar()">{{ 'PATHFINDING.ASTAR' | translate }}</button> <button matButton="filled" (click)="visualize('astar')">{{ 'PATHFINDING.ASTAR' | translate }}</button>
</div> </div>
<div class="controls"> <div class="controls">
<button matButton="filled" (click)="normalCase()">{{ 'PATHFINDING.NORMAL_CASE' | translate }}</button> <button matButton="filled" (click)="createCase({withWalls: true, scenario: 'normal'})">{{ 'PATHFINDING.NORMAL_CASE' | translate }}</button>
<button matButton="filled" (click)="edgeCase()">{{ 'PATHFINDING.EDGE_CASE' | translate }}</button> <button matButton="filled" (click)="createCase({withWalls: true, scenario: 'random'})">{{ 'PATHFINDING.RANDOM_CASE' | translate }}</button>
<button matButton="filled" (click)="clearBoard()">{{ 'PATHFINDING.CLEAR_BOARD' | translate }}</button> <button matButton="filled" (click)="createCase({withWalls: true, scenario: 'edge'})">{{ 'PATHFINDING.EDGE_CASE' | translate }}</button>
<button matButton="filled" (click)="createCase({withWalls: false, scenario: 'normal'})">{{ 'PATHFINDING.CLEAR_BOARD' | translate }}</button>
</div> </div>
<div class="controls"> <div class="controls">

View File

@@ -9,7 +9,7 @@ import {MatInputModule} from '@angular/material/input';
import {TranslateModule, TranslateService} from '@ngx-translate/core'; import {TranslateModule, TranslateService} from '@ngx-translate/core';
import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_PX, MAX_GRID_SIZE, MIN_GRID_SIZE, Node} from './pathfinding.models'; import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_PX, MAX_GRID_SIZE, MAX_RANDOM_WALLS_FACTORS, MIN_GRID_SIZE, Node} from './pathfinding.models';
import {PathfindingService} from './service/pathfinding.service'; import {PathfindingService} from './service/pathfinding.service';
import {UrlConstants} from '../../../constants/UrlConstants'; import {UrlConstants} from '../../../constants/UrlConstants';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card'; import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
@@ -70,7 +70,7 @@ export class PathfindingComponent implements AfterViewInit {
private shouldAddWall = true; private shouldAddWall = true;
animationSpeed = 3; animationSpeed = 3;
pathLength = 0; pathLength = "0";
executionTime = 0; executionTime = 0;
private timeoutIds: number[] = []; private timeoutIds: number[] = [];
@@ -103,40 +103,32 @@ export class PathfindingComponent implements AfterViewInit {
applyGridSize(skipReset?: boolean): void { applyGridSize(skipReset?: boolean): void {
this.gridRows = this.clampGridSize(this.gridRows, DEFAULT_GRID_ROWS); this.gridRows = this.clampGridSize(this.gridRows, DEFAULT_GRID_ROWS);
this.gridCols = this.clampGridSize(this.gridCols, DEFAULT_GRID_COLS); this.gridCols = this.clampGridSize(this.gridCols, DEFAULT_GRID_COLS);
this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols); this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
this.resizeCanvas(); this.resizeCanvas();
if (skipReset) { if (this.gridRows === this.grid.length && this.gridCols === this.grid[0].length)
this.initializeGrid(true, 'edge'); {
this.drawGrid(); this.drawGrid();
return; return;
} }
// Default after size changes: pick one consistent scenario if (skipReset) {
this.edgeCase(); this.initializeGrid({withWalls: true, scenario: 'normal'});
this.drawGrid();
return;
}
this.createCase({withWalls: true, scenario: 'normal'});
} }
// Scenarios (buttons) createCase({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void
normalCase(): void { {
this.stopAnimations(); this.stopAnimations();
this.initializeGrid(true, 'normal'); this.initializeGrid({withWalls, scenario});
this.drawGrid(); this.drawGrid();
} }
edgeCase(): void { visualize(algorithm: string): void {
this.stopAnimations();
this.initializeGrid(true, 'edge');
this.drawGrid();
}
clearBoard(): void {
this.stopAnimations();
this.initializeGrid(false, 'edge');
this.drawGrid();
}
visualizeDijkstra(): void {
if (!this.ensureStartAndEnd()) { if (!this.ensureStartAndEnd()) {
return; return;
} }
@@ -145,36 +137,38 @@ export class PathfindingComponent implements AfterViewInit {
this.clearPath(); this.clearPath();
const startTime = performance.now(); const startTime = performance.now();
const result = this.pathfindingService.dijkstra( let result;
this.grid,
this.grid[this.startNode!.row][this.startNode!.col],
this.grid[this.endNode!.row][this.endNode!.col]
);
const endTime = performance.now();
this.pathLength = result.nodesInShortestPathOrder.length; switch (algorithm) {
this.executionTime = endTime - startTime; 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;
}
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder); if (!result)
} {
visualizeAStar(): void {
if (!this.ensureStartAndEnd()) {
return; return;
} }
this.stopAnimations();
this.clearPath();
const startTime = performance.now();
const result = this.pathfindingService.aStar(
this.grid,
this.grid[this.startNode!.row][this.startNode!.col],
this.grid[this.endNode!.row][this.endNode!.col]
);
const endTime = performance.now(); const endTime = performance.now();
const lengthOfShortestPath = result.nodesInShortestPathOrder.length;
this.pathLength = result.nodesInShortestPathOrder.length; if (lengthOfShortestPath === 0)
{
this.pathLength = "∞"
}
else
{
this.pathLength = result.nodesInShortestPathOrder.length + "";
}
this.executionTime = endTime - startTime; this.executionTime = endTime - startTime;
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder); this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
@@ -244,7 +238,7 @@ export class PathfindingComponent implements AfterViewInit {
} }
// Grid init // Grid init
private initializeGrid(withWalls: boolean, scenario: 'normal' | 'edge'): void { private initializeGrid({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void {
this.grid = this.createEmptyGrid(); this.grid = this.createEmptyGrid();
const { start, end } = this.getScenarioStartEnd(scenario); const { start, end } = this.getScenarioStartEnd(scenario);
@@ -255,7 +249,7 @@ export class PathfindingComponent implements AfterViewInit {
this.endNode.isEnd = true; this.endNode.isEnd = true;
if (withWalls) { if (withWalls) {
this.placeDefaultDiagonalWall(); this.placeDefaultDiagonalWall(scenario);
} }
} }
@@ -289,23 +283,105 @@ export class PathfindingComponent implements AfterViewInit {
}; };
} }
private getScenarioStartEnd(scenario: 'normal' | 'edge'): { start: GridPos; end: GridPos } { private getScenarioStartEnd(scenario: 'normal' | 'edge' | 'random'): { start: GridPos; end: GridPos } {
if (scenario === 'edge') { if (scenario === 'edge') {
return { return {
start: { row: 0, col: 0 }, start: { row: 0, col: 0 },
end: { row: this.gridRows - 1, col: this.gridCols - 1 } end: { row: this.gridRows - 1, col: this.gridCols - 1 }
}; };
} }
else if (scenario === 'random') {
return this.createRandomStartEndPosition();
}
else {
// normal: mid-left -> mid-right
const midRow = Math.floor(this.gridRows / 2);
return {
start: { row: midRow, col: 0 },
end: { row: midRow, col: this.gridCols - 1 }
};
}
}
private createRandomStartEndPosition() {
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 endRow: number = this.randomIntFromInterval(0, this.gridRows - 1);
let endCol: number;
if (startCol <= midCol) {
endCol = this.randomIntFromInterval(midCol + 1, this.gridCols - 1);
} else {
endCol = this.randomIntFromInterval(0, midCol);
}
// normal: mid-left -> mid-right
const midRow = Math.floor(this.gridRows / 2);
return { return {
start: { row: midRow, col: 0 }, start: {row: startRow, col: startCol},
end: { row: midRow, col: this.gridCols - 1 } end: {row: endRow, col: endCol}
}; };
} }
private placeDefaultDiagonalWall(): void { private placeDefaultDiagonalWall(scenario: 'normal' | 'edge' | 'random'): void {
if (scenario === 'edge') {
this.createDiagonalWall();
}
else if (scenario === 'normal') {
this.createVerticalWall();
}
else if (scenario === 'random') {
this.createRandomWalls();
}
}
private createRandomWalls(){
const maxNumberOfWalls = Math.floor(MAX_RANDOM_WALLS_FACTORS * this.gridCols * this.gridRows);
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);
if (!this.isValidPosition(row, col)) {
wall--;
continue;
}
const node = this.grid[row][col];
if (node.isStart || node.isEnd) {
wall--;
continue;
}
node.isWall = true;
}
}
private createVerticalWall() {
const height = this.gridRows;
const startCol = Math.floor(this.gridCols / 2);
for (let i = 5; i < (height - 5); i++) {
const row = i;
if (!this.isValidPosition(row, startCol)) {
continue;
}
const node = this.grid[row][startCol];
if (node.isStart || node.isEnd) {
continue;
}
node.isWall = true;
}
}
private createDiagonalWall() {
// Diagonal-ish wall; avoids start/end // Diagonal-ish wall; avoids start/end
const len = Math.min(this.gridRows, this.gridCols); const len = Math.min(this.gridRows, this.gridCols);
const startCol = Math.floor((this.gridCols - len) / 2); const startCol = Math.floor((this.gridCols - len) / 2);
@@ -327,7 +403,7 @@ export class PathfindingComponent implements AfterViewInit {
} }
} }
// Path state // Path state
private clearPath(): void { private clearPath(): void {
for (let row = 0; row < this.gridRows; row++) { for (let row = 0; row < this.gridRows; row++) {
for (let col = 0; col < this.gridCols; col++) { for (let col = 0; col < this.gridCols; col++) {
@@ -563,5 +639,9 @@ export class PathfindingComponent implements AfterViewInit {
return ctx; return ctx;
} }
private randomIntFromInterval(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1) + min);
}
protected readonly UrlConstants = UrlConstants; protected readonly UrlConstants = UrlConstants;
} }

View File

@@ -20,3 +20,4 @@ export const MAX_GRID_SIZE = 150;
// Canvas max size (px) // Canvas max size (px)
export const MAX_GRID_PX = 1000; export const MAX_GRID_PX = 1000;
export const MAX_RANDOM_WALLS_FACTORS = 0.3;

View File

@@ -303,7 +303,8 @@
"ASTAR": "Start A*", "ASTAR": "Start A*",
"NORMAL_CASE": "Testaufbau", "NORMAL_CASE": "Testaufbau",
"EDGE_CASE": "A* Grenzfall-Aufbau", "EDGE_CASE": "A* Grenzfall-Aufbau",
"CLEAR_BOARD": "Board leeren", "RANDOM_CASE": "Zufälliger-Aufbau",
"CLEAR_BOARD": "Leeres Gitter",
"VISITED": "Besucht", "VISITED": "Besucht",
"PATH": "Pfad", "PATH": "Pfad",
"PATH_LENGTH": "Pfadlänge", "PATH_LENGTH": "Pfadlänge",

View File

@@ -303,7 +303,8 @@
"ASTAR": "Start A*", "ASTAR": "Start A*",
"NORMAL_CASE": "Test Scenario", "NORMAL_CASE": "Test Scenario",
"EDGE_CASE": "A* Edge Case Scenario", "EDGE_CASE": "A* Edge Case Scenario",
"CLEAR_BOARD": "Clear Board", "RANDOM_CASE": "Random Case",
"CLEAR_BOARD": "Empty Board",
"VISITED": "Visited", "VISITED": "Visited",
"PATH": "Path", "PATH": "Path",
"PATH_LENGTH": "Path length", "PATH_LENGTH": "Path length",