feat(pathfinding): add A* vs Dijkstra compare mode

Show A* and Dijkstra side by side on the same grid with visited count and path length. Also clamp typed grid sizes above the max instead of crashing.
This commit is contained in:
lobo committed 2026-10-14 15:40:20 +02:00
1 parent ce7db0f376
commit 718bc28bd0
8 files changed
+383 -27

No files matched your search

@@ -9,6 +9,7 @@
<div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]"> <div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]">
<button matButton="filled" (click)="visualize('dijkstra')">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button> <button matButton="filled" (click)="visualize('dijkstra')">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
<button matButton="filled" (click)="visualize('astar')">{{ 'PATHFINDING.ASTAR' | translate }}</button> <button matButton="filled" (click)="visualize('astar')">{{ 'PATHFINDING.ASTAR' | translate }}</button>
<button matButton="filled" (click)="visualizeCompare()">{{ 'PATHFINDING.COMPARE' | translate }}</button>
</div> </div>
<div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]"> <div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]">
<button matButton="filled" (click)="createCase({withWalls: true, scenario: 'normal'})">{{ 'PATHFINDING.NORMAL_CASE' | translate }}</button> <button matButton="filled" (click)="createCase({withWalls: true, scenario: 'normal'})">{{ 'PATHFINDING.NORMAL_CASE' | translate }}</button>
@@ -58,23 +59,44 @@
<span><span class="legend-swatch bg-sky-300"></span> {{ 'PATHFINDING.VISITED' | translate }}</span> <span><span class="legend-swatch bg-sky-300"></span> {{ 'PATHFINDING.VISITED' | translate }}</span>
<span><span class="legend-swatch bg-[gold]"></span> {{ 'PATHFINDING.PATH' | translate }}</span> <span><span class="legend-swatch bg-[gold]"></span> {{ 'PATHFINDING.PATH' | translate }}</span>
</div> </div>
<div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]"> @if (!compareMode()) {
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength() }}</p> <div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]">
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime() | number:'1.2-2' }} ms</p> <p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength() }}</p>
</div> <p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime() | number:'1.2-2' }} ms</p>
</div>
}
</div> </div>
<app-generic-grid <div class="flex flex-col lg:flex-row gap-4">
[gridRows]="gridRows()" <div class="flex-1 min-w-0">
[gridCols]="gridCols()" @if (compareMode()) {
[minGridSize]="MIN_GRID_SIZE" <p class="text-[0.9em] mb-2">A* · {{ 'PATHFINDING.VISITED_NODES' | translate }}: {{ astarStats()?.visitedCount }} · {{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ astarStats()?.pathLength }}</p>
[maxGridSize]="MAX_GRID_SIZE" }
[maxGridPx]="MAX_GRID_PX" <app-generic-grid
[createNodeFn]="createPathfindingNode" #primaryGrid
[getNodeColorFn]="getPathfindingNodeColor" [minGridSize]="MIN_GRID_SIZE"
[applySelectionFn]="applyPathfindingSelection" [maxGridSize]="MAX_GRID_SIZE"
[backgroundColor]="'lightgray'" [maxGridPx]="MAX_GRID_PX"
(gridChange)="grid = $event" [createNodeFn]="createPathfindingNode"
></app-generic-grid> [getNodeColorFn]="getPathfindingNodeColor"
[applySelectionFn]="applyPathfindingSelection"
[backgroundColor]="'lightgray'"
(gridChange)="grid = $event"
></app-generic-grid>
</div>
<div class="flex-1 min-w-0" [class.hidden]="!compareMode()">
<p class="text-[0.9em] mb-2">Dijkstra · {{ 'PATHFINDING.VISITED_NODES' | translate }}: {{ dijkstraStats()?.visitedCount }} · {{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ dijkstraStats()?.pathLength }}</p>
<app-generic-grid
#compareGrid
[minGridSize]="MIN_GRID_SIZE"
[maxGridSize]="MAX_GRID_SIZE"
[maxGridPx]="MAX_GRID_PX"
[createNodeFn]="createPathfindingNode"
[getNodeColorFn]="getPathfindingNodeColor"
[applySelectionFn]="applyCompareSelection"
[backgroundColor]="'lightgray'"
></app-generic-grid>
</div>
</div>
</mat-card-content> </mat-card-content>
</mat-card> </mat-card>
@@ -0,0 +1,171 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {provideZonelessChangeDetection} from '@angular/core';
import {provideNoopAnimations} from '@angular/platform-browser/animations';
import {provideTranslateService} from '@ngx-translate/core';
import {PathfindingComponent} from './pathfinding.component';
import {Node} from './pathfinding.models';
function countWalls(grid: Node[][]): number {
return grid.flat().filter(node => node.isWall).length;
}
describe('PathfindingComponent compare mode', () => {
let fixture: ComponentFixture<PathfindingComponent>;
let component: PathfindingComponent;
const originalGetContext = HTMLCanvasElement.prototype.getContext;
const compareGrid = (): Node[][] => component.compareGridComponent.grid as Node[][];
beforeEach(async () => {
vi.useFakeTimers({toFake: ['setTimeout', 'clearTimeout']});
// jsdom has no 2D context.
const fakeContext = {
fillRect: vi.fn(), strokeRect: vi.fn(), beginPath: vi.fn(), moveTo: vi.fn(), lineTo: vi.fn(), stroke: vi.fn(),
fillStyle: '', strokeStyle: '', lineWidth: 1,
};
Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
configurable: true,
value: () => fakeContext,
});
await TestBed.configureTestingModule({
imports: [PathfindingComponent],
providers: [
provideZonelessChangeDetection(),
provideNoopAnimations(),
provideTranslateService({lang: 'en', fallbackLang: 'en'}),
],
}).compileComponents();
fixture = TestBed.createComponent(PathfindingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
afterEach(() => {
vi.useRealTimers();
Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {configurable: true, value: originalGetContext});
});
it('runs both algorithms and reports stats', () => {
component.visualizeCompare();
vi.runAllTimers();
const astar = component.astarStats()!;
const dijkstra = component.dijkstraStats()!;
expect(component.compareMode()).toBe(true);
expect(astar).not.toBeNull();
expect(dijkstra).not.toBeNull();
expect(astar.pathLength).toBe(dijkstra.pathLength);
expect(astar.pathLength).not.toBe('∞');
expect(astar.visitedCount).toBeLessThanOrEqual(dijkstra.visitedCount);
});
it('mirrors an edit from the primary canvas', () => {
component.visualizeCompare();
vi.runAllTimers();
component.selectedNodeType.set(component.NodeType.Wall);
component.applyPathfindingSelection({row: 0, col: 0}, component.grid);
expect(component.grid[0][0].isWall).toBe(true);
expect(compareGrid()[0][0].isWall).toBe(true);
});
it('mirrors an edit from the compare canvas', () => {
component.visualizeCompare();
vi.runAllTimers();
component.selectedNodeType.set(component.NodeType.Wall);
component.applyCompareSelection({row: 1, col: 1});
expect(component.grid[1][1].isWall).toBe(true);
expect(compareGrid()[1][1].isWall).toBe(true);
});
it('moves the start on both grids', () => {
component.visualizeCompare();
vi.runAllTimers();
const oldStart = component.startNode!;
const {row, col} = oldStart;
component.selectedNodeType.set(component.NodeType.Start);
component.applyCompareSelection({row: 2, col: 2});
expect(component.grid[row][col].isStart).toBe(false);
expect(compareGrid()[row][col].isStart).toBe(false);
expect(component.grid[2][2].isStart).toBe(true);
expect(compareGrid()[2][2].isStart).toBe(true);
});
it('syncs the compare grid when a test case is created', () => {
component.visualizeCompare();
vi.runAllTimers();
component.createCase({withWalls: false, scenario: 'edge'});
const grid = compareGrid();
expect(grid[0][0].isStart).toBe(true);
expect(grid[49][49].isEnd).toBe(true);
expect(countWalls(grid)).toBe(0);
});
it('leaves compare mode on a single run and keeps the primary grid', () => {
component.visualizeCompare();
vi.runAllTimers();
const {row, col} = component.startNode!;
component.visualize('astar');
vi.runAllTimers();
expect(component.compareMode()).toBe(false);
expect(countWalls(component.grid)).toBeGreaterThan(0);
expect(component.startNode).toBe(component.grid[row][col]);
});
it('stops the compare animation when the grid is resized', () => {
component.visualizeCompare();
const drawn: Node[] = [];
vi.spyOn(component.compareGridComponent, 'drawNode').mockImplementation(node => {
drawn.push(node);
});
component['applyGridRows'](60);
vi.runAllTimers();
const mirror = compareGrid();
const stale = drawn.filter(node => mirror[node.row]?.[node.col] !== node);
expect(stale).toHaveLength(0);
});
it('clamps a typed grid size above the maximum and keeps the grids usable', async () => {
component.gridRows.set(200);
component['applyGridRows'](200);
fixture.detectChanges();
await fixture.whenStable();
expect(component.gridRows()).toBe(component.MAX_GRID_SIZE);
expect(component.genericGridComponent.gridRows).toBe(component.MAX_GRID_SIZE);
expect(() => component.createCase({withWalls: true, scenario: 'random'})).not.toThrow();
expect(() => component.visualizeCompare()).not.toThrow();
expect(compareGrid()).toHaveLength(component.MAX_GRID_SIZE);
});
it('shows the clamped height in the input after a second overflow', async () => {
const input = fixture.nativeElement.querySelector('input[type=number]') as HTMLInputElement;
const type = async (value: string) => {
input.value = value;
input.dispatchEvent(new Event('input', {bubbles: true}));
await fixture.whenStable();
fixture.detectChanges();
};
await type('200');
expect(input.value).toBe('150');
await type('1500');
expect(component.genericGridComponent.gridRows).toBe(150);
expect(input.value).toBe('150');
});
});
@@ -1,4 +1,4 @@
import {AfterViewInit, Component, inject, signal, ViewChild} from '@angular/core'; import {afterNextRender, AfterViewInit, Component, inject, Injector, signal, ViewChild} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {FormsModule} from '@angular/forms'; import {FormsModule} from '@angular/forms';
@@ -10,7 +10,7 @@ import {MatSnackBar} from '@angular/material/snack-bar';
import {TranslatePipe, TranslateService} from '@ngx-translate/core'; import {TranslatePipe, TranslateService} from '@ngx-translate/core';
import {createPathfindingNode, DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_PX, MAX_GRID_SIZE, MIN_GRID_SIZE, Node, PathfindingAlgorithm, PathfindingScenario} from './pathfinding.models'; import {createPathfindingNode, DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_PX, MAX_GRID_SIZE, MIN_GRID_SIZE, Node, PathfindingAlgorithm, PathfindingResult, PathfindingRunStats, PathfindingScenario} from './pathfinding.models';
import {PathfindingService} from './service/pathfinding.service'; import {PathfindingService} from './service/pathfinding.service';
import {PathfindingGridService} from './service/pathfinding-grid.service'; import {PathfindingGridService} from './service/pathfinding-grid.service';
import {PathfindingAnimator} from './pathfinding-animator'; import {PathfindingAnimator} from './pathfinding-animator';
@@ -51,6 +51,7 @@ export class PathfindingComponent implements AfterViewInit {
private readonly gridService = inject(PathfindingGridService); private readonly gridService = inject(PathfindingGridService);
private readonly translate = inject(TranslateService); private readonly translate = inject(TranslateService);
private readonly snackBar = inject(MatSnackBar); private readonly snackBar = inject(MatSnackBar);
private readonly injector = inject(Injector);
readonly NodeType = NodeType; readonly NodeType = NodeType;
readonly MIN_GRID_SIZE = MIN_GRID_SIZE; readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
@@ -90,12 +91,22 @@ export class PathfindingComponent implements AfterViewInit {
readonly pathLength = signal("0"); readonly pathLength = signal("0");
readonly executionTime = signal(0); readonly executionTime = signal(0);
readonly compareMode = signal(false);
readonly astarStats = signal<PathfindingRunStats | null>(null);
readonly dijkstraStats = signal<PathfindingRunStats | null>(null);
private readonly animator = new PathfindingAnimator( private readonly animator = new PathfindingAnimator(
node => this.genericGridComponent?.drawNode(node), node => this.genericGridComponent?.drawNode(node),
() => this.animationSpeed () => this.animationSpeed
); );
@ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent; private readonly compareAnimator = new PathfindingAnimator(
node => this.compareGridComponent?.drawNode(node),
() => this.animationSpeed
);
@ViewChild('primaryGrid') genericGridComponent!: GenericGridComponent;
@ViewChild('compareGrid') compareGridComponent!: GenericGridComponent;
ngAfterViewInit(): void { ngAfterViewInit(): void {
// Canvas logic is now handled by GenericGridComponent // Canvas logic is now handled by GenericGridComponent
@@ -112,17 +123,37 @@ export class PathfindingComponent implements AfterViewInit {
this.genericGridComponent.maxGridPx = MAX_GRID_PX; this.genericGridComponent.maxGridPx = MAX_GRID_PX;
this.genericGridComponent.applyGridSize(); // Trigger initial grid setup this.genericGridComponent.applyGridSize(); // Trigger initial grid setup
} }
if (this.compareGridComponent) {
this.compareGridComponent.initializationFn = this.initializeCompareGrid;
}
this.createCase({withWalls: true, scenario: "normal"}); this.createCase({withWalls: true, scenario: "normal"});
} }
protected applyGridRows(rows: number): void { protected applyGridRows(rows: number): void {
this.stopAnimations();
this.genericGridComponent.gridRows = rows; this.genericGridComponent.gridRows = rows;
this.genericGridComponent.applyGridSize(); this.applyClampedGridSize();
} }
protected applyGridCols(cols: number): void { protected applyGridCols(cols: number): void {
this.stopAnimations();
this.genericGridComponent.gridCols = cols; this.genericGridComponent.gridCols = cols;
this.applyClampedGridSize();
}
private applyClampedGridSize(): void {
this.genericGridComponent.applyGridSize(); this.genericGridComponent.applyGridSize();
// Only clamp down while typing; raising to the minimum would fight the user typing "100".
// After the next render: ngModel needs to see the typed value once, else a repeated overflow never rewrites the input.
afterNextRender(() => {
this.gridRows.update(rows => this.clampDown(rows, this.genericGridComponent.gridRows));
this.gridCols.update(cols => this.clampDown(cols, this.genericGridComponent.gridCols));
}, {injector: this.injector});
this.syncCompareGridIfActive();
}
private clampDown(value: number, max: number): number {
return Number.isFinite(value) && value > max ? max : value;
} }
// --- Callbacks for GenericGridComponent --- // --- Callbacks for GenericGridComponent ---
@@ -139,18 +170,27 @@ export class PathfindingComponent implements AfterViewInit {
applyPathfindingSelection = (pos: GridPos, grid: Node[][]): void => { applyPathfindingSelection = (pos: GridPos, grid: Node[][]): void => {
this.grid = grid; // Keep internal grid in sync this.grid = grid; // Keep internal grid in sync
const node = grid[pos.row][pos.col]; this.applySelection(pos, this.genericGridComponent);
};
applyCompareSelection = (pos: GridPos): void => {
this.applySelection(pos, this.compareGridComponent);
};
private applySelection(pos: GridPos, source: GenericGridComponent): void {
const node = this.grid[pos.row][pos.col];
// Determine if we should add or remove a wall // Determine if we should add or remove a wall
if (this.selectedNodeType() === NodeType.Wall && this.genericGridComponent.isDrawing && this.genericGridComponent['lastCell'] === null) { if (this.selectedNodeType() === NodeType.Wall && source.isDrawing && source['lastCell'] === null) {
this.shouldAddWall = !node.isWall; this.shouldAddWall = !node.isWall;
} }
const changed: Node[] = [node];
switch (this.selectedNodeType()) { switch (this.selectedNodeType()) {
case NodeType.Start: case NodeType.Start:
if (this.gridService.placeStart(node, this.startNode)) { if (this.gridService.placeStart(node, this.startNode)) {
if (this.startNode) { if (this.startNode) {
this.genericGridComponent.drawNode(this.startNode); // Redraw old start node changed.push(this.startNode);
} }
this.startNode = node; this.startNode = node;
} }
@@ -159,7 +199,7 @@ export class PathfindingComponent implements AfterViewInit {
case NodeType.End: case NodeType.End:
if (this.gridService.placeEnd(node, this.endNode)) { if (this.gridService.placeEnd(node, this.endNode)) {
if (this.endNode) { if (this.endNode) {
this.genericGridComponent.drawNode(this.endNode); // Redraw old end node changed.push(this.endNode);
} }
this.endNode = node; this.endNode = node;
} }
@@ -179,14 +219,29 @@ export class PathfindingComponent implements AfterViewInit {
} }
break; break;
} }
};
for (const changedNode of changed) {
this.redrawEverywhere(changedNode);
}
}
private redrawEverywhere(node: Node): void {
this.genericGridComponent.drawNode(node);
if (!this.compareMode()) {
return;
}
const mirror = this.compareNodeOf(node);
this.gridService.copyNodeStructure(node, mirror);
this.compareGridComponent.drawNode(mirror);
}
visualize(algorithm: PathfindingAlgorithm): void { visualize(algorithm: PathfindingAlgorithm): void {
if (!this.ensureStartAndEnd()) { if (!this.ensureStartAndEnd()) {
return; return;
} }
this.animator.stop(); this.stopAnimations();
this.compareMode.set(false);
this.clearPath(); this.clearPath();
const startTime = performance.now(); const startTime = performance.now();
@@ -199,15 +254,69 @@ export class PathfindingComponent implements AfterViewInit {
this.animator.animate(result); this.animator.animate(result);
} }
visualizeCompare(): void {
if (!this.ensureStartAndEnd()) {
return;
}
this.stopAnimations();
this.compareMode.set(true);
this.clearPath();
this.syncCompareGrid();
const compareGrid = this.compareGridComponent.grid as Node[][];
const compareStart = this.compareNodeOf(this.startNode!);
const compareEnd = this.compareNodeOf(this.endNode!);
const astar = this.pathfindingService.aStar(this.grid, this.startNode!, this.endNode!);
const dijkstra = this.pathfindingService.dijkstra(compareGrid, compareStart, compareEnd);
this.astarStats.set(this.toRunStats(astar));
this.dijkstraStats.set(this.toRunStats(dijkstra));
this.animator.animate(astar);
this.compareAnimator.animate(dijkstra);
}
initializePathfindingGrid = (grid: Node[][]): void => { initializePathfindingGrid = (grid: Node[][]): void => {
this.setupGrid(grid, 'normal', true); this.setupGrid(grid, 'normal', true);
}; };
initializeCompareGrid = (grid: Node[][]): void => {
this.gridService.copyGridStructure(this.grid, grid);
};
createCase({withWalls, scenario}: { withWalls: boolean, scenario: PathfindingScenario }): void { createCase({withWalls, scenario}: { withWalls: boolean, scenario: PathfindingScenario }): void {
this.animator.stop(); this.stopAnimations();
// Reinitialize grid through the generic component // Reinitialize grid through the generic component
this.genericGridComponent.initializationFn = (grid) => this.setupGrid(grid, scenario, withWalls); this.genericGridComponent.initializationFn = (grid) => this.setupGrid(grid, scenario, withWalls);
this.genericGridComponent.initializeGrid(); // Trigger re-initialization and redraw this.genericGridComponent.initializeGrid(); // Trigger re-initialization and redraw
this.syncCompareGridIfActive();
}
private syncCompareGrid(): void {
const compare = this.compareGridComponent;
compare.gridRows = this.genericGridComponent.gridRows; // clamped values, not the raw typed signals
compare.gridCols = this.genericGridComponent.gridCols;
compare.applyGridSize(); // resizes the canvas; re-inits only when the size changed
compare.initializeGrid(); // always re-init: fresh nodes = no stale search state
}
private compareNodeOf(node: Node): Node {
return this.compareGridComponent.grid[node.row][node.col] as Node;
}
private syncCompareGridIfActive(): void {
if (this.compareMode()) {
this.syncCompareGrid();
}
}
private stopAnimations(): void {
this.animator.stop();
this.compareAnimator.stop();
}
private toRunStats(result: PathfindingResult): PathfindingRunStats {
return {visitedCount: result.visitedNodesInOrder.length, pathLength: PathfindingAnimator.pathLengthLabel(result)};
} }
private setupGrid(grid: Node[][], scenario: PathfindingScenario, withWalls: boolean): void { private setupGrid(grid: Node[][], scenario: PathfindingScenario, withWalls: boolean): void {
@@ -45,3 +45,8 @@ 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; export const MAX_RANDOM_WALLS_FACTORS = 0.3;
export interface PathfindingRunStats {
visitedCount: number;
pathLength: string;
}
@@ -17,6 +17,35 @@ describe('PathfindingGridService', () => {
service = new PathfindingGridService(); service = new PathfindingGridService();
}); });
describe('copyGridStructure', () => {
it('copies start, end and walls to the same positions', () => {
const source = createGrid(5, 5);
const target = createGrid(5, 5);
source[0][0].isStart = true;
source[4][4].isEnd = true;
source[2][3].isWall = true;
service.copyGridStructure(source, target);
expect(target[0][0].isStart).toBe(true);
expect(target[4][4].isEnd).toBe(true);
expect(target[2][3].isWall).toBe(true);
expect(countWalls(target)).toBe(1);
});
it('does not copy search state', () => {
const source = createGrid(3, 3);
const target = createGrid(3, 3);
source[1][1].isVisited = true;
source[1][2].isPath = true;
service.copyGridStructure(source, target);
expect(target[1][1].isVisited).toBe(false);
expect(target[1][2].isPath).toBe(false);
});
});
describe('setupScenario', () => { describe('setupScenario', () => {
it('places start and end mid-left and mid-right with a vertical wall for "normal"', () => { it('places start and end mid-left and mid-right with a vertical wall for "normal"', () => {
const grid = createGrid(20, 20); const grid = createGrid(20, 20);
@@ -63,6 +63,22 @@ export class PathfindingGridService {
} }
} }
/** Copies the flags the user edits. Search state (isVisited, isPath, ...) is not copied. */
copyNodeStructure(source: Node, target: Node): void {
target.isStart = source.isStart;
target.isEnd = source.isEnd;
target.isWall = source.isWall;
}
/** Both grids must have the same size. */
copyGridStructure(source: Node[][], target: Node[][]): void {
for (let row = 0; row < source.length; row++) {
for (let col = 0; col < source[row].length; col++) {
this.copyNodeStructure(source[row][col], target[row][col]);
}
}
}
private getScenarioStartEnd(rows: number, cols: number, scenario: PathfindingScenario): { start: GridPos; end: GridPos } { private getScenarioStartEnd(rows: number, cols: number, scenario: PathfindingScenario): { start: GridPos; end: GridPos } {
if (scenario === 'edge') { if (scenario === 'edge') {
return { return {
+2
View File
@@ -430,6 +430,8 @@
"CLEAR_BOARD": "Leeres Gitter", "CLEAR_BOARD": "Leeres Gitter",
"VISITED": "Besucht", "VISITED": "Besucht",
"PATH": "Pfad", "PATH": "Pfad",
"COMPARE": "Vergleich (A* vs Dijkstra)",
"VISITED_NODES": "Besuchte Knoten",
"PATH_LENGTH": "Pfadlänge", "PATH_LENGTH": "Pfadlänge",
"EXECUTION_TIME": "Ausführungszeit", "EXECUTION_TIME": "Ausführungszeit",
"EXPLANATION": { "EXPLANATION": {
+2
View File
@@ -430,6 +430,8 @@
"CLEAR_BOARD": "Empty Board", "CLEAR_BOARD": "Empty Board",
"VISITED": "Visited", "VISITED": "Visited",
"PATH": "Path", "PATH": "Path",
"COMPARE": "Compare (A* vs Dijkstra)",
"VISITED_NODES": "Visited nodes",
"PATH_LENGTH": "Path length", "PATH_LENGTH": "Path length",
"EXECUTION_TIME": "Execution Time", "EXECUTION_TIME": "Execution Time",
"EXPLANATION": { "EXPLANATION": {