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:
1 parent
ce7db0f376
commit
718bc28bd0
8 files changed
+369
-13
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]">
|
||||
<button matButton="filled" (click)="visualize('dijkstra')">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
|
||||
<button matButton="filled" (click)="visualize('astar')">{{ 'PATHFINDING.ASTAR' | translate }}</button>
|
||||
<button matButton="filled" (click)="visualizeCompare()">{{ 'PATHFINDING.COMPARE' | translate }}</button>
|
||||
</div>
|
||||
<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>
|
||||
@@ -58,15 +59,21 @@
|
||||
<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>
|
||||
</div>
|
||||
@if (!compareMode()) {
|
||||
<div class="flex gap-4 mb-4 items-center flex-wrap mt-2.5 text-[0.9em]">
|
||||
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength() }}</p>
|
||||
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime() | number:'1.2-2' }} ms</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
@if (compareMode()) {
|
||||
<p class="text-[0.9em] mb-2">A* · {{ 'PATHFINDING.VISITED_NODES' | translate }}: {{ astarStats()?.visitedCount }} · {{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ astarStats()?.pathLength }}</p>
|
||||
}
|
||||
<app-generic-grid
|
||||
[gridRows]="gridRows()"
|
||||
[gridCols]="gridCols()"
|
||||
#primaryGrid
|
||||
[minGridSize]="MIN_GRID_SIZE"
|
||||
[maxGridSize]="MAX_GRID_SIZE"
|
||||
[maxGridPx]="MAX_GRID_PX"
|
||||
@@ -76,5 +83,20 @@
|
||||
[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>
|
||||
@@ -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 {FormsModule} from '@angular/forms';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {MatSnackBar} from '@angular/material/snack-bar';
|
||||
|
||||
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 {PathfindingGridService} from './service/pathfinding-grid.service';
|
||||
import {PathfindingAnimator} from './pathfinding-animator';
|
||||
@@ -51,6 +51,7 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
private readonly gridService = inject(PathfindingGridService);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly injector = inject(Injector);
|
||||
|
||||
readonly NodeType = NodeType;
|
||||
readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
||||
@@ -90,12 +91,22 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
readonly pathLength = 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(
|
||||
node => this.genericGridComponent?.drawNode(node),
|
||||
() => 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 {
|
||||
// Canvas logic is now handled by GenericGridComponent
|
||||
@@ -112,17 +123,37 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
this.genericGridComponent.maxGridPx = MAX_GRID_PX;
|
||||
this.genericGridComponent.applyGridSize(); // Trigger initial grid setup
|
||||
}
|
||||
if (this.compareGridComponent) {
|
||||
this.compareGridComponent.initializationFn = this.initializeCompareGrid;
|
||||
}
|
||||
this.createCase({withWalls: true, scenario: "normal"});
|
||||
}
|
||||
|
||||
protected applyGridRows(rows: number): void {
|
||||
this.stopAnimations();
|
||||
this.genericGridComponent.gridRows = rows;
|
||||
this.genericGridComponent.applyGridSize();
|
||||
this.applyClampedGridSize();
|
||||
}
|
||||
|
||||
protected applyGridCols(cols: number): void {
|
||||
this.stopAnimations();
|
||||
this.genericGridComponent.gridCols = cols;
|
||||
this.applyClampedGridSize();
|
||||
}
|
||||
|
||||
private applyClampedGridSize(): void {
|
||||
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 ---
|
||||
@@ -139,18 +170,27 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
|
||||
applyPathfindingSelection = (pos: GridPos, grid: Node[][]): void => {
|
||||
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
|
||||
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;
|
||||
}
|
||||
|
||||
const changed: Node[] = [node];
|
||||
switch (this.selectedNodeType()) {
|
||||
case NodeType.Start:
|
||||
if (this.gridService.placeStart(node, this.startNode)) {
|
||||
if (this.startNode) {
|
||||
this.genericGridComponent.drawNode(this.startNode); // Redraw old start node
|
||||
changed.push(this.startNode);
|
||||
}
|
||||
this.startNode = node;
|
||||
}
|
||||
@@ -159,7 +199,7 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
case NodeType.End:
|
||||
if (this.gridService.placeEnd(node, this.endNode)) {
|
||||
if (this.endNode) {
|
||||
this.genericGridComponent.drawNode(this.endNode); // Redraw old end node
|
||||
changed.push(this.endNode);
|
||||
}
|
||||
this.endNode = node;
|
||||
}
|
||||
@@ -179,14 +219,29 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
}
|
||||
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 {
|
||||
if (!this.ensureStartAndEnd()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.animator.stop();
|
||||
this.stopAnimations();
|
||||
this.compareMode.set(false);
|
||||
this.clearPath();
|
||||
|
||||
const startTime = performance.now();
|
||||
@@ -199,15 +254,69 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
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 => {
|
||||
this.setupGrid(grid, 'normal', true);
|
||||
};
|
||||
|
||||
initializeCompareGrid = (grid: Node[][]): void => {
|
||||
this.gridService.copyGridStructure(this.grid, grid);
|
||||
};
|
||||
|
||||
createCase({withWalls, scenario}: { withWalls: boolean, scenario: PathfindingScenario }): void {
|
||||
this.animator.stop();
|
||||
this.stopAnimations();
|
||||
// Reinitialize grid through the generic component
|
||||
this.genericGridComponent.initializationFn = (grid) => this.setupGrid(grid, scenario, withWalls);
|
||||
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 {
|
||||
|
||||
@@ -45,3 +45,8 @@ export const MAX_GRID_SIZE = 150;
|
||||
// Canvas max size (px)
|
||||
export const MAX_GRID_PX = 1000;
|
||||
export const MAX_RANDOM_WALLS_FACTORS = 0.3;
|
||||
|
||||
export interface PathfindingRunStats {
|
||||
visitedCount: number;
|
||||
pathLength: string;
|
||||
}
|
||||
@@ -17,6 +17,35 @@ describe('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', () => {
|
||||
it('places start and end mid-left and mid-right with a vertical wall for "normal"', () => {
|
||||
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 } {
|
||||
if (scenario === 'edge') {
|
||||
return {
|
||||
|
||||
@@ -430,6 +430,8 @@
|
||||
"CLEAR_BOARD": "Leeres Gitter",
|
||||
"VISITED": "Besucht",
|
||||
"PATH": "Pfad",
|
||||
"COMPARE": "Vergleich (A* vs Dijkstra)",
|
||||
"VISITED_NODES": "Besuchte Knoten",
|
||||
"PATH_LENGTH": "Pfadlänge",
|
||||
"EXECUTION_TIME": "Ausführungszeit",
|
||||
"EXPLANATION": {
|
||||
|
||||
@@ -430,6 +430,8 @@
|
||||
"CLEAR_BOARD": "Empty Board",
|
||||
"VISITED": "Visited",
|
||||
"PATH": "Path",
|
||||
"COMPARE": "Compare (A* vs Dijkstra)",
|
||||
"VISITED_NODES": "Visited nodes",
|
||||
"PATH_LENGTH": "Path length",
|
||||
"EXECUTION_TIME": "Execution Time",
|
||||
"EXPLANATION": {
|
||||
|
||||
Reference in new issue
Block a user