diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.html b/src/app/pages/algorithms/pathfinding/pathfinding.component.html index 6395d82..d457fc3 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.html +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.html @@ -13,9 +13,23 @@ A* {{ 'PATHFINDING.EXPLANATION.ASTAR_EXPLANATION' | translate}} Wikipedia

+ +

+ {{ 'PATHFINDING.EXPLANATION.NOTE' | translate}} {{ 'PATHFINDING.EXPLANATION.DISCLAIMER' | translate}} +

+
+ + +
+
+ + + +
+
{{ 'PATHFINDING.START_NODE' | translate }} @@ -25,14 +39,6 @@
-
- - - - - -
-
diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts index bbd8b49..0f9b62b 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.ts +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.ts @@ -163,6 +163,8 @@ export class PathfindingComponent implements AfterViewInit { // Mouse interactions private onMouseDown(event: MouseEvent): void { + this.stopAnimations(); + this.clearPath(); const pos = this.getGridPosition(event); if (!pos) { return; @@ -263,7 +265,8 @@ export class PathfindingComponent implements AfterViewInit { isPath: false, distance: Infinity, previousNode: null, - fScore: 0 + hScore: 0, + fScore: Infinity, }; } diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.models.ts b/src/app/pages/algorithms/pathfinding/pathfinding.models.ts index f766798..b5fb259 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.models.ts +++ b/src/app/pages/algorithms/pathfinding/pathfinding.models.ts @@ -9,6 +9,7 @@ export interface Node { distance: number; previousNode: Node | null; fScore: number; + hScore: number; } export const DEFAULT_GRID_ROWS = 50; diff --git a/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts b/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts index e6cc349..9519c84 100644 --- a/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts +++ b/src/app/pages/algorithms/pathfinding/service/pathfinding.service.ts @@ -80,9 +80,9 @@ export class PathfindingService { aStar(grid: Node[][], startNode: Node, endNode: Node): { visitedNodesInOrder: Node[], nodesInShortestPathOrder: Node[] } { const visitedNodesInOrder: Node[] = []; startNode.distance = 0; - startNode['distance'] = this.calculateHeuristic(startNode, endNode); + startNode['hScore'] = this.calculateHeuristic(startNode, endNode); // fScore = gScore + hScore - startNode['fScore'] = startNode.distance + startNode['distance']; + startNode['fScore'] = startNode.distance + startNode['hScore']; const openSet: Node[] = [startNode]; const allNodes = this.getAllNodes(grid); @@ -90,7 +90,7 @@ export class PathfindingService { this.initNodesForAStar(allNodes, startNode); while (openSet.length > 0) { - openSet.sort((nodeA, nodeB) => nodeA['fScore'] - nodeB['fScore']); + this.sortOpenSet(openSet); const currentNode = openSet.shift() as Node; if (currentNode.isWall) { @@ -125,11 +125,22 @@ export class PathfindingService { return { visitedNodesInOrder, nodesInShortestPathOrder: [] }; } + private sortOpenSet(openSet: Node[]) { + openSet.sort((a, b) => { + const f = a['fScore'] - b['fScore']; + if (f !== 0) return f; + + // tie-break: smaller heuristic first (more goal-directed) + return (a['hScore'] ?? 0) - (b['hScore'] ?? 0); + }); + } + 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['fScore'] = neighbor.distance + neighbor['distance']; + neighbor['hScore'] = this.calculateHeuristic(neighbor, endNode); + neighbor['fScore'] = neighbor.distance + neighbor['hScore']; if (!openSet.includes(neighbor)) { openSet.push(neighbor); diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 77b96d7..d30988a 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -299,10 +299,10 @@ "END_NODE": "Endknoten", "WALL": "Wand", "CLEAR_NODE": "Löschen", - "DIJKSTRA": "Dijkstra", - "ASTAR": "A*", + "DIJKSTRA": "Start Dijkstra", + "ASTAR": "Start A*", "NORMAL_CASE": "Testaufbau", - "EDGE_CASE": "A* Grenzfall", + "EDGE_CASE": "A* Grenzfall-Aufbau", "CLEAR_BOARD": "Board leeren", "VISITED": "Besucht", "PATH": "Pfad", @@ -311,7 +311,9 @@ "EXPLANATION": { "TITLE": "Algorithmen", "DIJKSTRA_EXPLANATION": " findet garantiert den kürzesten Weg, wenn alle Kantenkosten nicht-negativ sind. Vorteil: optimal und ohne Heuristik. Nachteil: besucht oft sehr viele Knoten (kann bei großen Grids langsamer wirken).", - "ASTAR_EXPLANATION": " erweitert Dijkstra um eine Heuristik (z.B. Manhattan-Distanz) und kann dadurch wesentlich zielgerichteter suchen. Vorteil: oft deutlich schneller bei guter Heuristik; bei zulässiger Heuristik bleibt der Weg optimal. Nachteil: hängt stark von der Heuristik ab (schlechte Heuristik ≈ Dijkstra)." + "ASTAR_EXPLANATION": " erweitert Dijkstra um eine Heuristik (z.B. Manhattan-Distanz) und kann dadurch wesentlich zielgerichteter suchen. Vorteil: oft deutlich schneller bei guter Heuristik; bei zulässiger Heuristik bleibt der Weg optimal. Nachteil: hängt stark von der Heuristik ab (schlechte Heuristik ≈ Dijkstra).", + "NOTE": "HINWEIS", + "DISCLAIMER": "Diese A*-Implementierung ist bewusst einfach gehalten. Es wird nur in vier Richtungen gegangen und jeder Schritt kostet 1. Die Heuristik ist minimal und dient nur dazu, das Prinzip von A* gegenüber Dijkstra zu demonstrieren. Ziel ist nicht ein optimaler oder produktionsreifer A*-Algorithmus, sondern eine anschauliche Visualisierung, wie Heuristiken die Suche beschleunigen können." }, "ALERT": { "START_END_NODES": "Bitte wählen Sie einen Start- und Endknoten aus, bevor Sie den Algorithmus starten." diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 4bb9c6e..71e8757 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -299,10 +299,10 @@ "END_NODE": "End Node", "WALL": "Wall", "CLEAR_NODE": "Clear", - "DIJKSTRA": "Dijkstra", - "ASTAR": "A*", + "DIJKSTRA": "Start Dijkstra", + "ASTAR": "Start A*", "NORMAL_CASE": "Test Scenario", - "EDGE_CASE": "A* Edge Case", + "EDGE_CASE": "A* Edge Case Scenario", "CLEAR_BOARD": "Clear Board", "VISITED": "Visited", "PATH": "Path", @@ -311,7 +311,9 @@ "EXPLANATION": { "TITLE": "Algorithms", "DIJKSTRA_EXPLANATION": " is guaranteed to find the shortest path if all edge costs are non-negative. Advantage: optimal and without heuristics. Disadvantage: often visits a large number of nodes (can be slower for large grids).", - "ASTAR_EXPLANATION": " extends Dijkstra with a heuristic (e.g. Manhattan distance) and can therefore search in a much more targeted manner. Advantage: often significantly faster with good heuristics; with permissible heuristics, the path remains optimal. Disadvantage: highly dependent on heuristics (poor heuristics ≈ Dijkstra)." + "ASTAR_EXPLANATION": " extends Dijkstra with a heuristic (e.g. Manhattan distance) and can therefore search in a much more targeted manner. Advantage: often significantly faster with good heuristics; with permissible heuristics, the path remains optimal. Disadvantage: highly dependent on heuristics (poor heuristics ≈ Dijkstra).", + "NOTE": "Note", + "DISCLAIMER": "This A* implementation is deliberately kept simple. It only moves in four directions and each step costs 1. The heuristic is minimal and only serves to demonstrate the principle of A* compared to Dijkstra. The goal is not an optimal or production-ready A* algorithm, but a clear visualisation of how heuristics can speed up the search." }, "ALERT": { "START_END_NODES": "Please select a start and end node before running the algorithm."