Merge pull request 'feature/gameOfLife' (#12) from feature/gameOfLife into main
All checks were successful
Build & Push Frontend A / docker (push) Successful in 42s
All checks were successful
Build & Push Frontend A / docker (push) Successful in 42s
Reviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
180
package-lock.json
generated
180
package-lock.json
generated
@@ -306,13 +306,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/schematics": {
|
||||
"version": "21.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.1.2.tgz",
|
||||
"integrity": "sha512-PA3gkiFhHUuXd2XuP7yzKg/9N++bjw+uOl473KwIsMuZwMPhncKa4+mUYBaffDoPqaujZvjfo6mjtCBuiBv05w==",
|
||||
"version": "21.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.1.3.tgz",
|
||||
"integrity": "sha512-Ps7bRl5uOcM7WpNJHbSls/jz5/wAI0ldkTlKyiBFA7RtNeQIABAV+hvlw5DJuEb1Lo5hnK0hXj90AyZdOxzY+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "21.1.2",
|
||||
"@angular-devkit/core": "21.1.3",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"magic-string": "0.30.21",
|
||||
"ora": "9.0.0",
|
||||
@@ -324,6 +324,34 @@
|
||||
"yarn": ">= 1.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": {
|
||||
"version": "21.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz",
|
||||
"integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "8.17.1",
|
||||
"ajv-formats": "3.0.1",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"picomatch": "4.0.3",
|
||||
"rxjs": "7.8.2",
|
||||
"source-map": "0.7.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
|
||||
"npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
|
||||
"yarn": ">= 1.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"chokidar": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"chokidar": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@angular-eslint/builder": {
|
||||
"version": "21.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.2.0.tgz",
|
||||
@@ -565,19 +593,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/cli": {
|
||||
"version": "21.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.2.tgz",
|
||||
"integrity": "sha512-AHjXCBl2PEilMJct6DX3ih5Fl5PiKpNDIj0ViTyVh1YcfpYjt6NzhVlV2o++8VNPNH/vMcmf2551LZIDProXXA==",
|
||||
"version": "21.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.1.3.tgz",
|
||||
"integrity": "sha512-UPtDcpKyrKZRPfym9gTovcibPzl2O/Woy7B8sm45sAnjDH+jDUCcCvuIak7GpH47shQkC2J4yvnHZbD4c6XxcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-devkit/architect": "0.2101.2",
|
||||
"@angular-devkit/core": "21.1.2",
|
||||
"@angular-devkit/schematics": "21.1.2",
|
||||
"@angular-devkit/architect": "0.2101.3",
|
||||
"@angular-devkit/core": "21.1.3",
|
||||
"@angular-devkit/schematics": "21.1.3",
|
||||
"@inquirer/prompts": "7.10.1",
|
||||
"@listr2/prompt-adapter-inquirer": "3.0.5",
|
||||
"@modelcontextprotocol/sdk": "1.25.2",
|
||||
"@schematics/angular": "21.1.2",
|
||||
"@modelcontextprotocol/sdk": "1.26.0",
|
||||
"@schematics/angular": "21.1.3",
|
||||
"@yarnpkg/lockfile": "1.1.0",
|
||||
"algoliasearch": "5.46.2",
|
||||
"ini": "6.0.0",
|
||||
@@ -600,6 +628,53 @@
|
||||
"yarn": ">= 1.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/cli/node_modules/@angular-devkit/architect": {
|
||||
"version": "0.2101.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2101.3.tgz",
|
||||
"integrity": "sha512-vKz8aPA62W+e9+pF6ct4CRDG/MjlIH7sWFGYkxPPRst2g46ZQsRkrzfMZAWv/wnt6OZ1OwyRuO3RW83EMhag8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "21.1.3",
|
||||
"rxjs": "7.8.2"
|
||||
},
|
||||
"bin": {
|
||||
"architect": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
|
||||
"npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
|
||||
"yarn": ">= 1.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/cli/node_modules/@angular-devkit/core": {
|
||||
"version": "21.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz",
|
||||
"integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "8.17.1",
|
||||
"ajv-formats": "3.0.1",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"picomatch": "4.0.3",
|
||||
"rxjs": "7.8.2",
|
||||
"source-map": "0.7.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
|
||||
"npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
|
||||
"yarn": ">= 1.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"chokidar": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"chokidar": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@angular/common": {
|
||||
"version": "21.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@angular/common/-/common-21.1.2.tgz",
|
||||
@@ -2388,13 +2463,13 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@modelcontextprotocol/sdk": {
|
||||
"version": "1.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.25.2.tgz",
|
||||
"integrity": "sha512-LZFeo4F9M5qOhC/Uc1aQSrBHxMrvxett+9KLHt7OhcExtoiRN9DKgbZffMP/nxjutWDQpfMDfP3nkHI4X9ijww==",
|
||||
"version": "1.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
|
||||
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.7",
|
||||
"@hono/node-server": "^1.19.9",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"content-type": "^1.0.5",
|
||||
@@ -2402,14 +2477,15 @@
|
||||
"cross-spawn": "^7.0.5",
|
||||
"eventsource": "^3.0.2",
|
||||
"eventsource-parser": "^3.0.0",
|
||||
"express": "^5.0.1",
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"jose": "^6.1.1",
|
||||
"express": "^5.2.1",
|
||||
"express-rate-limit": "^8.2.1",
|
||||
"hono": "^4.11.4",
|
||||
"jose": "^6.1.3",
|
||||
"json-schema-typed": "^8.0.2",
|
||||
"pkce-challenge": "^5.0.0",
|
||||
"raw-body": "^3.0.0",
|
||||
"zod": "^3.25 || ^4.0",
|
||||
"zod-to-json-schema": "^3.25.0"
|
||||
"zod-to-json-schema": "^3.25.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -4019,14 +4095,14 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@schematics/angular": {
|
||||
"version": "21.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.1.2.tgz",
|
||||
"integrity": "sha512-kxwxhCIUrj7DfzEtDSs/pi/w+aII/WQLpPfLgoQCWE8/95v60WnTfd1afmsXsFoxikKPxkwoPWtU2YbhSoX9MQ==",
|
||||
"version": "21.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.1.3.tgz",
|
||||
"integrity": "sha512-obJvWBhzRdsYL2msM4+8bQD21vFl3VxaVsuiq6iIfYsxhU5i2Iar2wM9NaRaIIqAYhZ8ehQQ/moB9BEbWvDCTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular-devkit/core": "21.1.2",
|
||||
"@angular-devkit/schematics": "21.1.2",
|
||||
"@angular-devkit/core": "21.1.3",
|
||||
"@angular-devkit/schematics": "21.1.3",
|
||||
"jsonc-parser": "3.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4035,6 +4111,34 @@
|
||||
"yarn": ">= 1.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@schematics/angular/node_modules/@angular-devkit/core": {
|
||||
"version": "21.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.3.tgz",
|
||||
"integrity": "sha512-huEXd1tWQHwwN+0VGRT+vSVplV0KNrGFUGJzkIW6iJE1SQElxn6etMai+pSd5DJcePkx6+SuscVsxbfwf70hnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "8.17.1",
|
||||
"ajv-formats": "3.0.1",
|
||||
"jsonc-parser": "3.3.1",
|
||||
"picomatch": "4.0.3",
|
||||
"rxjs": "7.8.2",
|
||||
"source-map": "0.7.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0",
|
||||
"npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
|
||||
"yarn": ">= 1.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"chokidar": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"chokidar": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@sigstore/bundle": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-4.0.0.tgz",
|
||||
@@ -6314,11 +6418,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "7.5.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz",
|
||||
"integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==",
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz",
|
||||
"integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
@@ -6329,6 +6436,16 @@
|
||||
"express": ">= 4.11"
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit/node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -6712,12 +6829,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.11.7",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
|
||||
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
|
||||
"version": "4.11.8",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.8.tgz",
|
||||
"integrity": "sha512-eVkB/CYCCei7K2WElZW9yYQFWssG0DhaDhVvr7wy5jJ22K+ck8fWW0EsLpB0sITUTvPnc97+rrbQqIr5iqiy9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
|
||||
@@ -10,5 +10,6 @@ export const routes: Routes = [
|
||||
{ path: RouterConstants.PATHFINDING.PATH, component: RouterConstants.PATHFINDING.COMPONENT},
|
||||
{ path: RouterConstants.SORTING.PATH, component: RouterConstants.SORTING.COMPONENT},
|
||||
{ path: RouterConstants.IMPRINT.PATH, component: RouterConstants.IMPRINT.COMPONENT},
|
||||
{ path: RouterConstants.GOL.PATH, component: RouterConstants.GOL.COMPONENT}
|
||||
];
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {ImprintComponent} from '../pages/imprint/imprint.component';
|
||||
import {AlgorithmsComponent} from '../pages/algorithms/algorithms.component';
|
||||
import {PathfindingComponent} from '../pages/algorithms/pathfinding/pathfinding.component';
|
||||
import {SortingComponent} from '../pages/algorithms/sorting/sorting.component';
|
||||
import {ConwayGol} from '../pages/algorithms/conway-gol/conway-gol';
|
||||
|
||||
export class RouterConstants {
|
||||
|
||||
@@ -37,6 +38,12 @@ export class RouterConstants {
|
||||
COMPONENT: SortingComponent
|
||||
};
|
||||
|
||||
static readonly GOL = {
|
||||
PATH: 'algorithms/gol',
|
||||
LINK: '/algorithms/gol',
|
||||
COMPONENT: ConwayGol
|
||||
};
|
||||
|
||||
static readonly IMPRINT = {
|
||||
PATH: 'imprint',
|
||||
LINK: '/imprint',
|
||||
|
||||
@@ -7,4 +7,5 @@
|
||||
static readonly QUICK_SORT_WIKI = 'https://de.wikipedia.org/wiki/Quicksort'
|
||||
static readonly HEAP_SORT_WIKI = 'https://de.wikipedia.org/wiki/Heapsort'
|
||||
static readonly SHAKE_SORT_WIKI = 'https://de.wikipedia.org/wiki/Shakersort'
|
||||
static readonly CONWAYS_WIKI = 'https://de.wikipedia.org/wiki/Conways_Spiel_des_Lebens'
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
mat-card {
|
||||
cursor: pointer;
|
||||
min-width: 300px;
|
||||
max-width: 400px;
|
||||
min-width: 450px;
|
||||
max-width: 450px;
|
||||
}
|
||||
}
|
||||
|
||||
90
src/app/pages/algorithms/conway-gol/conway-gol.html
Normal file
90
src/app/pages/algorithms/conway-gol/conway-gol.html
Normal file
@@ -0,0 +1,90 @@
|
||||
<mat-card class="container">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'GOL.TITLE' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<app-information [algorithmInformation]="algoInformation"/>
|
||||
<div class="controls-container">
|
||||
<div class="controls-panel">
|
||||
<button mat-raised-button (click)="generate(Scenario.SIMPLE)">
|
||||
<mat-icon>arrow_right</mat-icon> {{ 'GOL.SIMPLE_SCENE' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button (click)="generate(Scenario.PULSAR)">
|
||||
<mat-icon>arrow_right</mat-icon> {{ 'GOL.PULSAR_SCENE' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button (click)="generate(Scenario.GUN)">
|
||||
<mat-icon>arrow_right</mat-icon> {{ 'GOL.GUN_SCENE' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button (click)="generate(Scenario.RANDOM)">
|
||||
<mat-icon>shuffle</mat-icon> {{ 'GOL.RANDOM_SCENE' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button (click)="generate(Scenario.EMPTY)">
|
||||
<mat-icon>check_box_outline_blank</mat-icon> {{ 'GOL.EMPTY_SCENE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="controls-panel">
|
||||
@if (gameStarted())
|
||||
{
|
||||
<button mat-raised-button (click)="pauseGame()">
|
||||
<mat-icon>pause</mat-icon> {{ 'GOL.PAUSE' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<button mat-raised-button (click)="startGame()">
|
||||
<mat-icon>play_arrow</mat-icon> {{ 'GOL.START' | translate }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="grid-size">
|
||||
<mat-form-field appearance="outline" class="grid-field">
|
||||
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[(ngModel)]="gridRows"
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
(ngModelChange)="pauseGame(); genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="grid-field">
|
||||
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[(ngModel)]="gridCols"
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
(ngModelChange)="pauseGame(); genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="grid-field">
|
||||
<mat-label>{{ 'GOL.SPEED' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[(ngModel)]="lifeSpeed"
|
||||
[min]="MIN_TIME_PER_GENERATION"
|
||||
[max]="MAX_TIME_PER_GENERATION"
|
||||
(blur)="applySpeed()"
|
||||
(keyup.enter)="applySpeed()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="legend">
|
||||
<span><span class="legend-color alive"></span> {{ 'GOL.ALIVE' | translate }}</span>
|
||||
<span><span class="legend-color empty"></span> {{ 'GOL.DEAD' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<app-generic-grid
|
||||
[gridRows]="gridRows"
|
||||
[gridCols]="gridCols"
|
||||
[minGridSize]="MIN_GRID_SIZE"
|
||||
[maxGridSize]="MAX_GRID_SIZE"
|
||||
[maxGridPx]="MAX_GRID_PX"
|
||||
[createNodeFn]="createConwayNode"
|
||||
[getNodeColorFn]="getConwayNodeColor"
|
||||
[applySelectionFn]="applyConwaySelection"
|
||||
(gridChange)="grid = $event"
|
||||
></app-generic-grid>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
26
src/app/pages/algorithms/conway-gol/conway-gol.models.ts
Normal file
26
src/app/pages/algorithms/conway-gol/conway-gol.models.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export interface Node {
|
||||
row: number;
|
||||
col: number;
|
||||
alive: boolean;
|
||||
}
|
||||
|
||||
export enum Scenario {
|
||||
RANDOM = 0,
|
||||
EMPTY = 1,
|
||||
SIMPLE = 2,
|
||||
PULSAR = 3,
|
||||
GUN = 4
|
||||
}
|
||||
|
||||
export const DEFAULT_GRID_ROWS = 40;
|
||||
export const DEFAULT_GRID_COLS = 40;
|
||||
|
||||
export const MIN_GRID_SIZE = 20;
|
||||
export const MAX_GRID_SIZE = 100;
|
||||
export const DEFAULT_TIME_PER_GENERATION = 30;
|
||||
|
||||
export const MIN_TIME_PER_GENERATION = 20;
|
||||
export const MAX_TIME_PER_GENERATION = 200;
|
||||
|
||||
export const MAX_GRID_PX = 1000;
|
||||
export const LIVE_SPAWN_PROBABILITY = 0.37;
|
||||
256
src/app/pages/algorithms/conway-gol/conway-gol.ts
Normal file
256
src/app/pages/algorithms/conway-gol/conway-gol.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import {AfterViewInit, Component, signal, ViewChild} from '@angular/core';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
|
||||
import {TranslatePipe} from "@ngx-translate/core";
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
import {Information} from '../information/information';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, DEFAULT_TIME_PER_GENERATION, LIVE_SPAWN_PROBABILITY, MAX_GRID_PX, MAX_GRID_SIZE, MAX_TIME_PER_GENERATION, MIN_GRID_SIZE, MIN_TIME_PER_GENERATION, Node, Scenario} from './conway-gol.models';
|
||||
import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid';
|
||||
|
||||
@Component({
|
||||
selector: 'app-conway-gol',
|
||||
imports: [
|
||||
MatCard,
|
||||
MatCardContent,
|
||||
MatCardHeader,
|
||||
MatCardTitle,
|
||||
TranslatePipe,
|
||||
Information,
|
||||
MatButton,
|
||||
MatIcon,
|
||||
MatFormField,
|
||||
MatInput,
|
||||
MatLabel,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
GenericGridComponent
|
||||
],
|
||||
templateUrl: './conway-gol.html',
|
||||
})
|
||||
export class ConwayGol implements AfterViewInit {
|
||||
|
||||
algoInformation: AlgorithmInformation = {
|
||||
title: 'GOL.EXPLANATION.TITLE',
|
||||
entries: [
|
||||
{
|
||||
name: '',
|
||||
description: 'GOL.EXPLANATION.EXPLANATION',
|
||||
link: UrlConstants.CONWAYS_WIKI
|
||||
}
|
||||
],
|
||||
disclaimer: 'GOL.EXPLANATION.DISCLAIMER',
|
||||
disclaimerBottom: '',
|
||||
disclaimerListEntry: ['GOL.EXPLANATION.DISCLAIMER_1', 'GOL.EXPLANATION.DISCLAIMER_2', 'GOL.EXPLANATION.DISCLAIMER_3', 'GOL.EXPLANATION.DISCLAIMER_4']
|
||||
};
|
||||
|
||||
protected gridCols = DEFAULT_GRID_COLS;
|
||||
protected gridRows = DEFAULT_GRID_ROWS;
|
||||
protected lifeSpeed = DEFAULT_TIME_PER_GENERATION;
|
||||
protected readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
||||
protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
|
||||
protected readonly MAX_GRID_PX = MAX_GRID_PX;
|
||||
|
||||
grid: Node[][] = [];
|
||||
currentScenario: Scenario = Scenario.SIMPLE;
|
||||
readonly gameStarted = signal(false);
|
||||
|
||||
@ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
if (this.genericGridComponent) {
|
||||
this.genericGridComponent.initializationFn = this.initializeConwayGrid;
|
||||
this.genericGridComponent.createNodeFn = this.createConwayNode;
|
||||
this.genericGridComponent.getNodeColorFn = this.getConwayNodeColor;
|
||||
this.genericGridComponent.applySelectionFn = this.applyConwaySelection;
|
||||
this.genericGridComponent.gridRows = this.gridRows;
|
||||
this.genericGridComponent.gridCols = this.gridCols;
|
||||
this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE;
|
||||
this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE;
|
||||
this.genericGridComponent.maxGridPx = this.MAX_GRID_PX;
|
||||
this.genericGridComponent.initializeGrid();
|
||||
}
|
||||
this.gameStarted.set(false);
|
||||
}
|
||||
|
||||
generate(scene: Scenario): void {
|
||||
this.currentScenario = scene;
|
||||
this.genericGridComponent.initializationFn = this.initializeConwayGrid;
|
||||
this.genericGridComponent.initializeGrid();
|
||||
}
|
||||
|
||||
applySpeed(): void {
|
||||
this.lifeSpeed = Math.min(Math.max(this.lifeSpeed, MIN_TIME_PER_GENERATION), MAX_TIME_PER_GENERATION);
|
||||
}
|
||||
|
||||
// --- Callbacks for GenericGridComponent ---
|
||||
createConwayNode = (row: number, col: number): Node => {
|
||||
return {
|
||||
row,
|
||||
col,
|
||||
alive: false
|
||||
};
|
||||
};
|
||||
|
||||
getConwayNodeColor = (node: Node): string => {
|
||||
if (node.alive) {
|
||||
return 'black';
|
||||
}
|
||||
return 'lightgray';
|
||||
};
|
||||
|
||||
applyConwaySelection = (pos: GridPos, grid: Node[][]): void => {
|
||||
this.grid = grid; // Keep internal grid in sync
|
||||
const node = grid[pos.row][pos.col];
|
||||
node.alive = !node.alive; // Toggle alive status
|
||||
};
|
||||
|
||||
initializeConwayGrid = (grid: Node[][]): void => {
|
||||
this.gameStarted.set(false);
|
||||
this.grid = grid;
|
||||
|
||||
switch(this.currentScenario) {
|
||||
case Scenario.RANDOM: this.setupRandomLives(); break;
|
||||
case Scenario.SIMPLE: this.setupSimpleLive(); break;
|
||||
case Scenario.PULSAR: this.setupPulsar(); break;
|
||||
case Scenario.GUN: this.setupGliderGun(); break;
|
||||
}
|
||||
};
|
||||
|
||||
// --- Conway-specific logic (kept local) ---
|
||||
setupRandomLives(): void {
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
this.grid[row][col].alive = Math.random() <= LIVE_SPAWN_PROBABILITY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setupSimpleLive(): void {
|
||||
this.grid[3][4].alive = true;
|
||||
this.grid[4][5].alive = true;
|
||||
this.grid[5][3].alive = true;
|
||||
this.grid[5][4].alive = true;
|
||||
this.grid[5][5].alive = true;
|
||||
}
|
||||
|
||||
setupPulsar(): void {
|
||||
const centerRow = Math.floor(this.gridRows / 2);
|
||||
const centerCol = Math.floor(this.gridCols / 2);
|
||||
|
||||
const rows = [-6, -1, 1, 6];
|
||||
const offsets = [2, 3, 4];
|
||||
|
||||
rows.forEach(r => {
|
||||
offsets.forEach(c => {
|
||||
this.setAlive(centerRow + r, centerCol + c);
|
||||
this.setAlive(centerRow + r, centerCol - c);
|
||||
this.setAlive(centerRow + c, centerCol + r);
|
||||
this.setAlive(centerRow - c, centerCol + r);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setupGliderGun(): void {
|
||||
const r = 5;
|
||||
const c = 5;
|
||||
|
||||
const dots = [
|
||||
[r+4, c], [r+4, c+1], [r+5, c], [r+5, c+1], // Block links
|
||||
[r+4, c+10], [r+5, c+10], [r+6, c+10], [r+3, c+11], [r+7, c+11],
|
||||
[r+2, c+12], [r+8, c+12], [r+2, c+13], [r+8, c+13], [r+5, c+14],
|
||||
[r+3, c+15], [r+7, c+15], [r+4, c+16], [r+5, c+16], [r+6, c+16], [r+5, c+17],
|
||||
[r+2, c+20], [r+3, c+20], [r+4, c+20], [r+2, c+21], [r+3, c+21], [r+4, c+21],
|
||||
[r+1, c+22], [r+5, c+22], [r+0, c+24], [r+1, c+24], [r+5, c+24], [r+6, c+24],
|
||||
[r+2, c+34], [r+3, c+34], [r+2, c+35], [r+3, c+35]
|
||||
];
|
||||
|
||||
dots.forEach(([row, col]) => this.setAlive(row, col));
|
||||
}
|
||||
|
||||
// --- The rules of the game
|
||||
|
||||
pauseGame(): void {
|
||||
this.gameStarted.set(false);
|
||||
}
|
||||
|
||||
async startGame(): Promise<void> {
|
||||
this.gameStarted.set(true);
|
||||
let lifeIsDead = false;
|
||||
while (this.gameStarted()){
|
||||
let gridClone = structuredClone(this.grid);
|
||||
lifeIsDead = true;
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
lifeIsDead = this.checkLifeRules(row, col, gridClone, lifeIsDead);
|
||||
}
|
||||
}
|
||||
|
||||
this.swapGrid(gridClone);
|
||||
if (lifeIsDead){
|
||||
this.gameStarted.set(false);
|
||||
}
|
||||
await this.delay(this.lifeSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
private checkLifeRules(row: number, col: number, gridClone: Node[][], lifeIsDead: boolean) {
|
||||
const itsMe = this.grid[row][col];
|
||||
let aliveNeighbors = this.howManyNeighborsAreLiving(row, col);
|
||||
if (itsMe.alive && (aliveNeighbors < 2 || aliveNeighbors > 3)) {
|
||||
gridClone[row][col].alive = false;
|
||||
lifeIsDead = false;
|
||||
} else if (!itsMe.alive && aliveNeighbors === 3) {
|
||||
gridClone[row][col].alive = true;
|
||||
lifeIsDead = false;
|
||||
}
|
||||
return lifeIsDead;
|
||||
}
|
||||
|
||||
private swapGrid(gridClone: Node[][]) {
|
||||
this.grid = gridClone;
|
||||
if (this.genericGridComponent) {
|
||||
this.genericGridComponent.grid = this.grid;
|
||||
this.genericGridComponent.drawGrid();
|
||||
}
|
||||
}
|
||||
|
||||
private howManyNeighborsAreLiving(row: number, col: number): number {
|
||||
|
||||
let aliveNeighborCount = 0;
|
||||
const minRow = Math.max(row - 1, 0);
|
||||
const minCol = Math.max(col - 1, 0);
|
||||
const maxRow = Math.min(row + 1, this.gridRows - 1);
|
||||
const maxCol = Math.min(col + 1, this.gridCols - 1);
|
||||
|
||||
for (let nRow = minRow; nRow <= maxRow; nRow++) {
|
||||
for (let nCol = minCol; nCol <= maxCol; nCol++) {
|
||||
if (nRow == row && nCol == col) {
|
||||
continue;
|
||||
}
|
||||
if (this.grid[nRow][nCol].alive) {
|
||||
aliveNeighborCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return aliveNeighborCount;
|
||||
}
|
||||
|
||||
// --- Other methods ---
|
||||
protected readonly Scenario = Scenario;
|
||||
protected readonly MIN_TIME_PER_GENERATION = MIN_TIME_PER_GENERATION;
|
||||
protected readonly MAX_TIME_PER_GENERATION = MAX_TIME_PER_GENERATION;
|
||||
|
||||
delay(ms: number) {
|
||||
return new Promise( resolve => setTimeout(resolve, ms) );
|
||||
}
|
||||
|
||||
private setAlive(r: number, c: number): void {
|
||||
if (r >= 0 && r < this.gridRows && c >= 0 && c < this.gridCols) {
|
||||
this.grid[r][c].alive = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/app/pages/algorithms/information/information.html
Normal file
35
src/app/pages/algorithms/information/information.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<div class="algo-info">
|
||||
<h3>{{ algorithmInformation.title | translate }}</h3>
|
||||
|
||||
@if(algorithmInformation.entries && algorithmInformation.entries.length > 0){
|
||||
@for (algo of algorithmInformation.entries; track algo)
|
||||
{
|
||||
<p>
|
||||
<strong>{{ algo.name }}</strong> {{ algo.description | translate }}
|
||||
<a href="{{algo.link}}" target="_blank" rel="noopener noreferrer">Wikipedia</a>
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
@if (algorithmInformation.disclaimer !== '')
|
||||
{
|
||||
<p>
|
||||
<strong>{{ 'ALGORITHM.NOTE' | translate}}</strong> {{ algorithmInformation.disclaimer | translate}}
|
||||
</p>
|
||||
@if (algorithmInformation.disclaimerListEntry && algorithmInformation.disclaimerListEntry.length > 0)
|
||||
{
|
||||
<ul>
|
||||
@for (entry of algorithmInformation.disclaimerListEntry; track entry)
|
||||
{
|
||||
<li>{{ entry | translate}}</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
@if (algorithmInformation.disclaimerBottom !== '')
|
||||
{
|
||||
<p>
|
||||
{{ algorithmInformation.disclaimerBottom | translate}}
|
||||
</p>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
14
src/app/pages/algorithms/information/information.models.ts
Normal file
14
src/app/pages/algorithms/information/information.models.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface AlgorithmInformation {
|
||||
title: string;
|
||||
entries: AlgorithmEntry[];
|
||||
disclaimer: string;
|
||||
disclaimerBottom: string;
|
||||
disclaimerListEntry: string[];
|
||||
}
|
||||
|
||||
export interface AlgorithmEntry {
|
||||
name: string;
|
||||
description: string;
|
||||
link: string;
|
||||
|
||||
}
|
||||
16
src/app/pages/algorithms/information/information.ts
Normal file
16
src/app/pages/algorithms/information/information.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {TranslatePipe} from "@ngx-translate/core";
|
||||
import {AlgorithmInformation} from './information.models';
|
||||
|
||||
@Component({
|
||||
selector: 'app-information',
|
||||
imports: [
|
||||
TranslatePipe
|
||||
],
|
||||
templateUrl: './information.html',
|
||||
styleUrl: './information.scss',
|
||||
})
|
||||
export class Information {
|
||||
|
||||
@Input({ required: true }) algorithmInformation!: AlgorithmInformation;
|
||||
}
|
||||
@@ -3,37 +3,21 @@
|
||||
<mat-card-title>{{ 'PATHFINDING.TITLE' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="algo-info">
|
||||
<h3>{{ 'PATHFINDING.EXPLANATION.TITLE' | translate }}</h3>
|
||||
|
||||
<p>
|
||||
<strong>Dijkstra</strong> {{ 'PATHFINDING.EXPLANATION.DIJKSTRA_EXPLANATION' | translate }}
|
||||
<a href="{{UrlConstants.DIJKSTRA_WIKI}}" target="_blank" rel="noopener noreferrer">Wikipedia</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>A*</strong> {{ 'PATHFINDING.EXPLANATION.ASTAR_EXPLANATION' | translate}}
|
||||
<a href="{{UrlConstants.ASTAR_WIKI}}" target="_blank" rel="noopener noreferrer">Wikipedia</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>{{ 'PATHFINDING.EXPLANATION.NOTE' | translate}}</strong> {{ 'PATHFINDING.EXPLANATION.DISCLAIMER' | translate}}
|
||||
</p>
|
||||
</div>
|
||||
<app-information [algorithmInformation]="algoInformation"/>
|
||||
|
||||
<div class="controls-container">
|
||||
<div class="controls">
|
||||
<div class="controls-panel">
|
||||
<button matButton="filled" (click)="visualize('dijkstra')">{{ 'PATHFINDING.DIJKSTRA' | translate }}</button>
|
||||
<button matButton="filled" (click)="visualize('astar')">{{ 'PATHFINDING.ASTAR' | translate }}</button>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="controls-panel">
|
||||
<button matButton="filled" (click)="createCase({withWalls: true, scenario: 'normal'})">{{ 'PATHFINDING.NORMAL_CASE' | translate }}</button>
|
||||
<button matButton="filled" (click)="createCase({withWalls: true, scenario: 'random'})">{{ 'PATHFINDING.RANDOM_CASE' | 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 class="controls">
|
||||
<div class="controls-panel">
|
||||
<mat-button-toggle-group [(ngModel)]="selectedNodeType" aria-label="Node Type Selection">
|
||||
<mat-button-toggle [value]="NodeType.Start">{{ 'PATHFINDING.START_NODE' | translate }}</mat-button-toggle>
|
||||
<mat-button-toggle [value]="NodeType.End">{{ 'PATHFINDING.END_NODE' | translate }}</mat-button-toggle>
|
||||
@@ -41,34 +25,29 @@
|
||||
<mat-button-toggle [value]="NodeType.None">{{ 'PATHFINDING.CLEAR_NODE' | translate }}</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<div class="controls-panel">
|
||||
<div class="grid-size">
|
||||
<mat-form-field appearance="outline" class="grid-field">
|
||||
<mat-label>{{ 'PATHFINDING.GRID_HEIGHT' | translate }}</mat-label>
|
||||
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
[(ngModel)]="gridRows"
|
||||
(blur)="applyGridSize()"
|
||||
(keyup.enter)="applyGridSize()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
(ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()"
|
||||
/> </mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="grid-field">
|
||||
<mat-label>{{ 'PATHFINDING.GRID_WIDTH' | translate }}</mat-label>
|
||||
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
[min]="MIN_GRID_SIZE"
|
||||
[max]="MAX_GRID_SIZE"
|
||||
[(ngModel)]="gridCols"
|
||||
(blur)="applyGridSize()"
|
||||
(keyup.enter)="applyGridSize()"
|
||||
/>
|
||||
</mat-form-field>
|
||||
(ngModelChange)="genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()"
|
||||
/> </mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -79,12 +58,22 @@
|
||||
<span><span class="legend-color visited"></span> {{ 'PATHFINDING.VISITED' | translate }}</span>
|
||||
<span><span class="legend-color path"></span> {{ 'PATHFINDING.PATH' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results-container">
|
||||
<div class="controls-panel">
|
||||
<p>{{ 'PATHFINDING.PATH_LENGTH' | translate }}: {{ pathLength }}</p>
|
||||
<p>{{ 'PATHFINDING.EXECUTION_TIME' | translate }}: {{ executionTime | number:'1.2-2' }} ms</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<canvas #gridCanvas></canvas>
|
||||
<app-generic-grid
|
||||
[gridRows]="gridRows"
|
||||
[gridCols]="gridCols"
|
||||
[minGridSize]="MIN_GRID_SIZE"
|
||||
[maxGridSize]="MAX_GRID_SIZE"
|
||||
[maxGridPx]="MAX_GRID_PX"
|
||||
[createNodeFn]="createPathfindingNode"
|
||||
[getNodeColorFn]="getPathfindingNodeColor"
|
||||
[applySelectionFn]="applyPathfindingSelection"
|
||||
(gridChange)="grid = $event"
|
||||
></app-generic-grid>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
.container {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.controls-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
|
||||
mat-button-toggle-group {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-size {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.grid-field {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
|
||||
.legend-color {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: 1px solid #ccc;
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
|
||||
&.start { background-color: green; }
|
||||
&.end { background-color: red; }
|
||||
&.wall { background-color: black; }
|
||||
&.visited { background-color: skyblue; }
|
||||
&.path { background-color: gold; }
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 1px solid #ccc;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AfterViewInit, Component, ElementRef, inject, ViewChild} from '@angular/core';
|
||||
import {AfterViewInit, Component, inject, ViewChild} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
|
||||
@@ -13,6 +13,9 @@ import {DEFAULT_GRID_COLS, DEFAULT_GRID_ROWS, MAX_GRID_PX, MAX_GRID_SIZE, MAX_RA
|
||||
import {PathfindingService} from './service/pathfinding.service';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
|
||||
import {Information} from '../information/information';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid';
|
||||
|
||||
enum NodeType {
|
||||
Start = 'start',
|
||||
@@ -21,8 +24,6 @@ enum NodeType {
|
||||
None = 'none'
|
||||
}
|
||||
|
||||
interface GridPos { row: number; col: number }
|
||||
|
||||
@Component({
|
||||
selector: 'app-pathfinding',
|
||||
standalone: true,
|
||||
@@ -37,10 +38,11 @@ interface GridPos { row: number; col: number }
|
||||
MatCard,
|
||||
MatCardHeader,
|
||||
MatCardTitle,
|
||||
MatCardContent
|
||||
MatCardContent,
|
||||
Information,
|
||||
GenericGridComponent
|
||||
],
|
||||
templateUrl: './pathfinding.component.html',
|
||||
styleUrls: ['./pathfinding.component.scss']
|
||||
})
|
||||
export class PathfindingComponent implements AfterViewInit {
|
||||
private readonly pathfindingService = inject(PathfindingService);
|
||||
@@ -49,25 +51,36 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
readonly NodeType = NodeType;
|
||||
readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
|
||||
readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
|
||||
readonly MAX_GRID_PX = MAX_GRID_PX;
|
||||
|
||||
@ViewChild('gridCanvas', { static: true })
|
||||
canvas!: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
private ctx!: CanvasRenderingContext2D;
|
||||
algoInformation: AlgorithmInformation = {
|
||||
title: 'PATHFINDING.EXPLANATION.TITLE',
|
||||
entries: [
|
||||
{
|
||||
name: 'Dijkstra',
|
||||
description: 'PATHFINDING.EXPLANATION.DIJKSTRA_EXPLANATION',
|
||||
link: UrlConstants.DIJKSTRA_WIKI
|
||||
},
|
||||
{
|
||||
name: 'A*',
|
||||
description: 'PATHFINDING.EXPLANATION.ASTAR_EXPLANATION',
|
||||
link: UrlConstants.ASTAR_WIKI
|
||||
}
|
||||
],
|
||||
disclaimer: 'PATHFINDING.EXPLANATION.DISCLAIMER',
|
||||
disclaimerBottom: '',
|
||||
disclaimerListEntry: []
|
||||
};
|
||||
|
||||
gridRows = DEFAULT_GRID_ROWS;
|
||||
gridCols = DEFAULT_GRID_COLS;
|
||||
nodeSize = 10;
|
||||
|
||||
grid: Node[][] = [];
|
||||
startNode: Node | null = null;
|
||||
endNode: Node | null = null;
|
||||
|
||||
selectedNodeType: NodeType = NodeType.None;
|
||||
|
||||
isDrawing = false;
|
||||
private lastCell: GridPos | null = null;
|
||||
private shouldAddWall = true;
|
||||
private shouldAddWall = true; // Moved here
|
||||
|
||||
animationSpeed = 3;
|
||||
pathLength = "0";
|
||||
@@ -75,58 +88,79 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
|
||||
private timeoutIds: number[] = [];
|
||||
|
||||
@ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.ctx = this.getContextOrThrow();
|
||||
this.applyGridSize(true);
|
||||
|
||||
const el = this.canvas.nativeElement;
|
||||
el.addEventListener('mousedown', (e) => this.onMouseDown(e));
|
||||
el.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
||||
el.addEventListener('mouseup', () => this.onMouseUp());
|
||||
el.addEventListener('mouseleave', () => this.onMouseUp());
|
||||
|
||||
el.addEventListener('touchstart', (e) => {
|
||||
if(e.cancelable) e.preventDefault();
|
||||
this.onMouseDown(e as never);
|
||||
}, { passive: false });
|
||||
|
||||
el.addEventListener('touchmove', (e) => {
|
||||
if(e.cancelable) e.preventDefault();
|
||||
this.onMouseMove(e as never);
|
||||
}, { passive: false });
|
||||
|
||||
el.addEventListener('touchend', () => {
|
||||
this.onMouseUp();
|
||||
});
|
||||
// Canvas logic is now handled by GenericGridComponent
|
||||
// Ensure genericGridComponent is initialized
|
||||
if (this.genericGridComponent) {
|
||||
this.genericGridComponent.initializationFn = this.initializePathfindingGrid;
|
||||
this.genericGridComponent.createNodeFn = this.createPathfindingNode;
|
||||
this.genericGridComponent.getNodeColorFn = this.getPathfindingNodeColor;
|
||||
this.genericGridComponent.applySelectionFn = this.applyPathfindingSelection;
|
||||
this.genericGridComponent.gridRows = this.gridRows;
|
||||
this.genericGridComponent.gridCols = this.gridCols;
|
||||
this.genericGridComponent.minGridSize = this.MIN_GRID_SIZE;
|
||||
this.genericGridComponent.maxGridSize = this.MAX_GRID_SIZE;
|
||||
this.genericGridComponent.maxGridPx = MAX_GRID_PX;
|
||||
this.genericGridComponent.applyGridSize(); // Trigger initial grid setup
|
||||
}
|
||||
this.createCase({withWalls: true, scenario: "normal"});
|
||||
}
|
||||
|
||||
applyGridSize(skipReset?: boolean): void {
|
||||
this.gridRows = this.clampGridSize(this.gridRows, DEFAULT_GRID_ROWS);
|
||||
this.gridCols = this.clampGridSize(this.gridCols, DEFAULT_GRID_COLS);
|
||||
this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
|
||||
this.resizeCanvas();
|
||||
// --- Callbacks for GenericGridComponent ---
|
||||
createPathfindingNode = (row: number, col: number): Node => {
|
||||
return {
|
||||
row,
|
||||
col,
|
||||
isStart: false,
|
||||
isEnd: false,
|
||||
isWall: false,
|
||||
isVisited: false,
|
||||
isPath: false,
|
||||
distance: Infinity,
|
||||
previousNode: null,
|
||||
hScore: 0,
|
||||
fScore: Infinity,
|
||||
};
|
||||
};
|
||||
|
||||
if (this.gridRows === this.grid.length && this.gridCols === this.grid[0].length)
|
||||
{
|
||||
this.drawGrid();
|
||||
return;
|
||||
getPathfindingNodeColor = (node: Node): string => {
|
||||
if (node.isStart) return 'green';
|
||||
if (node.isEnd) return 'red';
|
||||
if (node.isPath) return 'gold';
|
||||
if (node.isVisited) return 'skyblue';
|
||||
if (node.isWall) return 'black';
|
||||
return 'lightgray';
|
||||
};
|
||||
|
||||
applyPathfindingSelection = (pos: GridPos, grid: Node[][]): void => {
|
||||
this.grid = grid; // Keep internal grid in sync
|
||||
const node = 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) {
|
||||
this.shouldAddWall = !node.isWall;
|
||||
}
|
||||
|
||||
if (skipReset) {
|
||||
this.initializeGrid({withWalls: true, scenario: 'normal'});
|
||||
this.drawGrid();
|
||||
return;
|
||||
}
|
||||
switch (this.selectedNodeType) {
|
||||
case NodeType.Start:
|
||||
this.trySetStart(node);
|
||||
break;
|
||||
|
||||
this.createCase({withWalls: true, scenario: 'normal'});
|
||||
}
|
||||
case NodeType.End:
|
||||
this.trySetEnd(node);
|
||||
break;
|
||||
|
||||
createCase({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void
|
||||
{
|
||||
this.stopAnimations();
|
||||
this.initializeGrid({withWalls, scenario});
|
||||
this.drawGrid();
|
||||
case NodeType.Wall:
|
||||
this.tryToggleWall(node, this.shouldAddWall);
|
||||
break;
|
||||
|
||||
case NodeType.None:
|
||||
this.tryClearNode(node);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
visualize(algorithm: string): void {
|
||||
if (!this.ensureStartAndEnd()) {
|
||||
@@ -174,320 +208,19 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
this.animateAlgorithm(result.visitedNodesInOrder, result.nodesInShortestPathOrder);
|
||||
}
|
||||
|
||||
// Mouse interactions
|
||||
private onMouseDown(event: MouseEvent): void {
|
||||
this.stopAnimations();
|
||||
this.clearPath();
|
||||
const pos = this.getGridPosition(event);
|
||||
if (!pos) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.shouldAddWall = this.shouldStartWallStroke(pos);
|
||||
|
||||
this.isDrawing = true;
|
||||
this.lastCell = null;
|
||||
this.applySelectionAt(pos);
|
||||
}
|
||||
|
||||
private onMouseMove(event: MouseEvent): void {
|
||||
if (!this.isDrawing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pos = this.getGridPosition(event);
|
||||
if (!pos) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isSameCell(pos, this.lastCell)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.applySelectionAt(pos);
|
||||
}
|
||||
|
||||
private onMouseUp(): void {
|
||||
this.isDrawing = false;
|
||||
this.lastCell = null;
|
||||
}
|
||||
|
||||
private applySelectionAt(pos: GridPos): void {
|
||||
const node = this.grid[pos.row][pos.col];
|
||||
|
||||
switch (this.selectedNodeType) {
|
||||
case NodeType.Start:
|
||||
this.trySetStart(node);
|
||||
break;
|
||||
|
||||
case NodeType.End:
|
||||
this.trySetEnd(node);
|
||||
break;
|
||||
|
||||
case NodeType.Wall:
|
||||
this.tryToggleWall(node, this.shouldAddWall);
|
||||
break;
|
||||
|
||||
case NodeType.None:
|
||||
this.tryClearNode(node);
|
||||
break;
|
||||
}
|
||||
|
||||
this.lastCell = pos;
|
||||
this.drawNode(node);
|
||||
}
|
||||
|
||||
// Grid init
|
||||
private initializeGrid({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void {
|
||||
this.grid = this.createEmptyGrid();
|
||||
|
||||
const { start, end } = this.getScenarioStartEnd(scenario);
|
||||
initializePathfindingGrid = (grid: Node[][]): void => {
|
||||
this.grid = grid; // Update the component's grid reference
|
||||
const {start, end} = this.getScenarioStartEnd('normal'); // Default scenario
|
||||
this.startNode = this.grid[start.row][start.col];
|
||||
this.endNode = this.grid[end.row][end.col];
|
||||
|
||||
this.startNode.isStart = true;
|
||||
this.endNode.isEnd = true;
|
||||
|
||||
if (withWalls) {
|
||||
this.placeDefaultDiagonalWall(scenario);
|
||||
}
|
||||
}
|
||||
|
||||
private createEmptyGrid(): Node[][] {
|
||||
const grid: Node[][] = [];
|
||||
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
const currentRow: Node[] = [];
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
currentRow.push(this.createNode(row, col));
|
||||
}
|
||||
grid.push(currentRow);
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
private createNode(row: number, col: number): Node {
|
||||
return {
|
||||
row,
|
||||
col,
|
||||
isStart: false,
|
||||
isEnd: false,
|
||||
isWall: false,
|
||||
isVisited: false,
|
||||
isPath: false,
|
||||
distance: Infinity,
|
||||
previousNode: null,
|
||||
hScore: 0,
|
||||
fScore: Infinity,
|
||||
this.placeDefaultDiagonalWall('normal');
|
||||
};
|
||||
}
|
||||
|
||||
private getScenarioStartEnd(scenario: 'normal' | 'edge' | 'random'): { start: GridPos; end: GridPos } {
|
||||
if (scenario === 'edge') {
|
||||
return {
|
||||
start: { row: 0, col: 0 },
|
||||
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);
|
||||
}
|
||||
|
||||
return {
|
||||
start: {row: startRow, col: startCol},
|
||||
end: {row: endRow, col: endCol}
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
const len = Math.min(this.gridRows, this.gridCols);
|
||||
const startCol = Math.floor((this.gridCols - len) / 2);
|
||||
|
||||
for (let i = 0; i < Math.max(0, len - 10); i++) {
|
||||
const row = len - i - 1;
|
||||
const col = startCol + i;
|
||||
|
||||
if (!this.isValidPosition(row, col)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const node = this.grid[row][col];
|
||||
if (node.isStart || node.isEnd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
node.isWall = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Path state
|
||||
private clearPath(): void {
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
const node = this.grid[row][col];
|
||||
node.isVisited = false;
|
||||
node.isPath = false;
|
||||
node.distance = Infinity;
|
||||
node.previousNode = null;
|
||||
}
|
||||
}
|
||||
this.drawGrid();
|
||||
}
|
||||
|
||||
// Animation
|
||||
private stopAnimations(): void {
|
||||
for (const id of this.timeoutIds) {
|
||||
clearTimeout(id);
|
||||
}
|
||||
this.timeoutIds = [];
|
||||
}
|
||||
|
||||
private animateAlgorithm(visited: Node[], path: Node[]): void {
|
||||
for (let i = 0; i <= visited.length; i++) {
|
||||
if (i === visited.length) {
|
||||
const id = globalThis.setTimeout(() => this.animateShortestPath(path), this.animationSpeed * i);
|
||||
this.timeoutIds.push(id);
|
||||
return;
|
||||
}
|
||||
|
||||
const node = visited[i];
|
||||
const id = globalThis.setTimeout(() => {
|
||||
if (!node.isStart && !node.isEnd) {
|
||||
node.isVisited = true;
|
||||
this.drawNode(node);
|
||||
}
|
||||
}, this.animationSpeed * i);
|
||||
|
||||
this.timeoutIds.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
private animateShortestPath(path: Node[]): void {
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const node = path[i];
|
||||
const id = globalThis.setTimeout(() => {
|
||||
if (!node.isStart && !node.isEnd) {
|
||||
node.isPath = true;
|
||||
this.drawNode(node);
|
||||
}
|
||||
}, this.animationSpeed * i);
|
||||
|
||||
this.timeoutIds.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Drawing
|
||||
private drawGrid(): void {
|
||||
this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
|
||||
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
this.drawNode(this.grid[row][col]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private drawNode(node: Node): void {
|
||||
this.ctx.fillStyle = this.getNodeColor(node);
|
||||
this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||
|
||||
this.ctx.strokeStyle = '#ccc';
|
||||
this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||
}
|
||||
|
||||
private getNodeColor(node: Node): string {
|
||||
if (node.isStart) return 'green';
|
||||
if (node.isEnd) return 'red';
|
||||
if (node.isPath) return 'gold';
|
||||
if (node.isVisited) return 'skyblue';
|
||||
if (node.isWall) return 'black';
|
||||
return 'lightgray';
|
||||
}
|
||||
|
||||
// Placement rules (readability helpers)
|
||||
// --- Helper methods for node manipulation (kept local) ---
|
||||
private trySetStart(node: Node): void {
|
||||
if (!this.canBeStart(node)) {
|
||||
return;
|
||||
@@ -495,7 +228,7 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
|
||||
if (this.startNode) {
|
||||
this.startNode.isStart = false;
|
||||
this.drawNode(this.startNode);
|
||||
this.genericGridComponent.drawNode(this.startNode); // Redraw old start node
|
||||
}
|
||||
|
||||
node.isStart = true;
|
||||
@@ -509,7 +242,7 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
|
||||
if (this.endNode) {
|
||||
this.endNode.isEnd = false;
|
||||
this.drawNode(this.endNode);
|
||||
this.genericGridComponent.drawNode(this.endNode); // Redraw old end node
|
||||
}
|
||||
|
||||
node.isEnd = true;
|
||||
@@ -553,16 +286,197 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
return !node.isStart && !node.isEnd;
|
||||
}
|
||||
|
||||
private shouldStartWallStroke(pos: GridPos): boolean {
|
||||
if (this.selectedNodeType !== NodeType.Wall) {
|
||||
return true;
|
||||
// --- Grid manipulation for scenarios (kept local) ---
|
||||
createCase({withWalls, scenario}: { withWalls: boolean, scenario: "normal" | "edge" | "random" }): void {
|
||||
this.stopAnimations();
|
||||
// Reinitialize grid through the generic component
|
||||
this.genericGridComponent.initializationFn = (grid) => {
|
||||
this.grid = grid;
|
||||
const {start, end} = this.getScenarioStartEnd(scenario);
|
||||
this.startNode = this.grid[start.row][start.col];
|
||||
this.endNode = this.grid[end.row][end.col];
|
||||
this.startNode.isStart = true;
|
||||
this.endNode.isEnd = true;
|
||||
|
||||
if (withWalls) {
|
||||
this.placeDefaultDiagonalWall(scenario);
|
||||
}
|
||||
};
|
||||
this.genericGridComponent.initializeGrid(); // Trigger re-initialization and redraw
|
||||
}
|
||||
|
||||
const node = this.grid[pos.row][pos.col];
|
||||
return !node.isWall;
|
||||
private getScenarioStartEnd(scenario: 'normal' | 'edge' | 'random'): { start: GridPos; end: GridPos } {
|
||||
if (scenario === 'edge') {
|
||||
return {
|
||||
start: {row: 0, col: 0},
|
||||
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}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Validation
|
||||
private createRandomStartEndPosition(): { start: GridPos; end: GridPos } {
|
||||
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);
|
||||
}
|
||||
|
||||
return {
|
||||
start: {row: startRow, col: startCol},
|
||||
end: {row: endRow, col: endCol}
|
||||
};
|
||||
}
|
||||
|
||||
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.grid[row][col]) { // Use the grid passed from GenericGrid
|
||||
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.grid[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
|
||||
const len = Math.min(this.gridRows, this.gridCols);
|
||||
const startCol = Math.floor((this.gridCols - len) / 2);
|
||||
|
||||
for (let i = 0; i < Math.max(0, len - 10); i++) {
|
||||
const row = len - i - 1;
|
||||
const col = startCol + i;
|
||||
|
||||
if (!this.grid[row]?.[col]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const node = this.grid[row][col];
|
||||
if (node.isStart || node.isEnd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
node.isWall = true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Animation (adapted to use genericGridComponent for redraw) ---
|
||||
private stopAnimations(): void {
|
||||
for (const id of this.timeoutIds) {
|
||||
clearTimeout(id);
|
||||
}
|
||||
this.timeoutIds = [];
|
||||
}
|
||||
|
||||
private clearPath(): void {
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
const node = this.grid[row][col];
|
||||
node.isVisited = false;
|
||||
node.isPath = false;
|
||||
node.distance = Infinity;
|
||||
node.previousNode = null;
|
||||
}
|
||||
}
|
||||
this.genericGridComponent?.drawGrid(); // Redraw the grid via generic grid component
|
||||
}
|
||||
|
||||
private animateAlgorithm(visited: Node[], path: Node[]): void {
|
||||
for (let i = 0; i <= visited.length; i++) {
|
||||
if (i === visited.length) {
|
||||
const id = globalThis.setTimeout(() => this.animateShortestPath(path), this.animationSpeed * i);
|
||||
this.timeoutIds.push(id);
|
||||
return;
|
||||
}
|
||||
|
||||
const node = visited[i];
|
||||
const id = globalThis.setTimeout(() => {
|
||||
if (!node.isStart && !node.isEnd) {
|
||||
node.isVisited = true;
|
||||
this.genericGridComponent?.drawNode(node); // Redraw single node
|
||||
}
|
||||
}, this.animationSpeed * i);
|
||||
|
||||
this.timeoutIds.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
private animateShortestPath(path: Node[]): void {
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
const node = path[i];
|
||||
const id = globalThis.setTimeout(() => {
|
||||
if (!node.isStart && !node.isEnd) {
|
||||
node.isPath = true;
|
||||
this.genericGridComponent?.drawNode(node); // Redraw single node
|
||||
}
|
||||
}, this.animationSpeed * i);
|
||||
|
||||
this.timeoutIds.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Validation ---
|
||||
private ensureStartAndEnd(): boolean {
|
||||
if (this.startNode && this.endNode) {
|
||||
return true;
|
||||
@@ -572,76 +486,8 @@ export class PathfindingComponent implements AfterViewInit {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Grid sizing
|
||||
private clampGridSize(value: number, fallback: number): number {
|
||||
const parsed = Math.floor(Number(value));
|
||||
const safe = Number.isFinite(parsed) ? parsed : fallback;
|
||||
return Math.min(Math.max(MIN_GRID_SIZE, safe), MAX_GRID_SIZE);
|
||||
}
|
||||
|
||||
private computeNodeSize(rows: number, cols: number): number {
|
||||
const sizeByWidth = Math.floor(MAX_GRID_PX / cols);
|
||||
const sizeByHeight = Math.floor(MAX_GRID_PX / rows);
|
||||
return Math.max(1, Math.min(sizeByWidth, sizeByHeight));
|
||||
}
|
||||
|
||||
private resizeCanvas(): void {
|
||||
const el = this.canvas.nativeElement;
|
||||
el.width = this.gridCols * this.nodeSize;
|
||||
el.height = this.gridRows * this.nodeSize;
|
||||
}
|
||||
|
||||
// Mouse -> grid cell
|
||||
private getGridPosition(event: MouseEvent | TouchEvent): GridPos | null {
|
||||
const canvas = this.canvas.nativeElement;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
let clientX, clientY;
|
||||
if (event instanceof MouseEvent) {
|
||||
clientX = event.clientX;
|
||||
clientY = event.clientY;
|
||||
} else if (event instanceof TouchEvent && event.touches.length > 0) {
|
||||
clientX = event.touches[0].clientX;
|
||||
clientY = event.touches[0].clientY;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
|
||||
const x = (clientX - rect.left) * scaleX;
|
||||
const y = (clientY - rect.top) * scaleY;
|
||||
|
||||
const col = Math.floor(x / this.nodeSize);
|
||||
const row = Math.floor(y / this.nodeSize);
|
||||
|
||||
if (!this.isValidPosition(row, col)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { row, col };
|
||||
}
|
||||
|
||||
private isValidPosition(row: number, col: number): boolean {
|
||||
return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols;
|
||||
}
|
||||
|
||||
private isSameCell(a: GridPos, b: GridPos | null): boolean {
|
||||
return !!b && a.row === b.row && a.col === b.col;
|
||||
}
|
||||
|
||||
private getContextOrThrow(): CanvasRenderingContext2D {
|
||||
const ctx = this.canvas.nativeElement.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('CanvasRenderingContext2D not available.');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
// --- Utility ---
|
||||
private randomIntFromInterval(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
}
|
||||
|
||||
protected readonly UrlConstants = UrlConstants;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ export class AlgorithmsService {
|
||||
title: 'ALGORITHM.SORTING.TITLE',
|
||||
description: 'ALGORITHM.SORTING.DESCRIPTION',
|
||||
routerLink: RouterConstants.SORTING.LINK
|
||||
},
|
||||
{
|
||||
id: 'gameOfLife',
|
||||
title: 'ALGORITHM.GOL.TITLE',
|
||||
description: 'ALGORITHM.GOL.DESCRIPTION',
|
||||
routerLink: RouterConstants.GOL.LINK
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -1,50 +1,15 @@
|
||||
<div class="sorting-container">
|
||||
<mat-card class="sorting-card">
|
||||
<mat-card class="container sorting-card">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'SORTING.TITLE' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="algo-info">
|
||||
<h3>{{ 'SORTING.EXPLANATION.TITLE' | translate }}</h3>
|
||||
|
||||
<p>
|
||||
<strong>Bubble Sort</strong> {{ 'SORTING.EXPLANATION.BUBBLE_SORT_EXPLANATION' | translate }}
|
||||
<a href="{{UrlConstants.BUBBLE_SORT_WIKI}}" target="_blank" rel="noopener noreferrer">Wikipedia</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Cocktail Sort</strong> {{ 'SORTING.EXPLANATION.COCKTAIL_SORT_EXPLANATION' | translate}}
|
||||
<a href="{{UrlConstants.SHAKE_SORT_WIKI}}" target="_blank" rel="noopener noreferrer">Wikipedia</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Quick Sort</strong> {{ 'SORTING.EXPLANATION.QUICK_SORT_EXPLANATION' | translate}}
|
||||
<a href="{{UrlConstants.QUICK_SORT_WIKI}}" target="_blank" rel="noopener noreferrer">Wikipedia</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>Heap Sort</strong> {{ 'SORTING.EXPLANATION.HEAP_SORT_EXPLANATION' | translate}}
|
||||
<a href="{{UrlConstants.HEAP_SORT_WIKI}}" target="_blank" rel="noopener noreferrer">Wikipedia</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>{{ 'SORTING.EXPLANATION.NOTE' | translate}}</strong> {{ 'SORTING.EXPLANATION.DISCLAIMER' | translate}}
|
||||
</p>
|
||||
<ul>
|
||||
<li>{{ 'SORTING.EXPLANATION.DISCLAIMER_1' | translate}}</li>
|
||||
<li>{{ 'SORTING.EXPLANATION.DISCLAIMER_2' | translate}}</li>
|
||||
<li>{{ 'SORTING.EXPLANATION.DISCLAIMER_3' | translate}}</li>
|
||||
</ul>
|
||||
<p>
|
||||
{{ 'SORTING.EXPLANATION.DISCLAIMER_4' | translate}}
|
||||
</p>
|
||||
</div>
|
||||
<app-information [algorithmInformation]="algoInformation"/>
|
||||
<div class="controls-panel">
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>{{ 'SORTING.ALGORITHM' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="selectedAlgorithm">
|
||||
@for (algo of availableAlgorithms; track algo.value) {
|
||||
<mat-option [value]="algo.value">{{ algo.name }}</mat-option>
|
||||
@for (algo of algoInformation.entries; track algo.name) {
|
||||
<mat-option [value]="algo.name">{{ algo.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@@ -74,7 +39,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="info-panel">
|
||||
<div class="controls-panel">
|
||||
<p>{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime }} ms</p>
|
||||
</div>
|
||||
<div class="visualization-area">
|
||||
@@ -90,4 +55,3 @@
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
.sorting-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
.sorting-card {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
@@ -59,4 +51,3 @@
|
||||
color: #FFFFFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,12 @@ import {SortData, SortSnapshot} from './sorting.models';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import {MatInput} from '@angular/material/input';
|
||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||
import {MIN} from '@angular/forms/signals';
|
||||
import {AlgorithmInformation} from '../information/information.models';
|
||||
import {Information} from '../information/information';
|
||||
@Component({
|
||||
selector: 'app-sorting',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslateModule, FormsModule, MatInput],
|
||||
imports: [CommonModule, MatCardModule, MatFormFieldModule, MatSelectModule, MatButtonModule, MatIconModule, TranslateModule, FormsModule, MatInput, Information],
|
||||
templateUrl: './sorting.component.html',
|
||||
styleUrls: ['./sorting.component.scss']
|
||||
})
|
||||
@@ -27,21 +28,46 @@ export class SortingComponent implements OnInit {
|
||||
readonly MAX_ARRAY_SIZE: number = 200;
|
||||
readonly MIN_ARRAY_SIZE: number = 20;
|
||||
|
||||
algoInformation: AlgorithmInformation = {
|
||||
title: 'SORTING.EXPLANATION.TITLE',
|
||||
entries: [
|
||||
{
|
||||
name: 'Bubble Sort',
|
||||
description: 'SORTING.EXPLANATION.BUBBLE_SORT_EXPLANATION',
|
||||
link: UrlConstants.BUBBLE_SORT_WIKI
|
||||
},
|
||||
{
|
||||
name: 'Cocktail Sort',
|
||||
description: 'SORTING.EXPLANATION.COCKTAIL_SORT_EXPLANATION',
|
||||
link: UrlConstants.SHAKE_SORT_WIKI
|
||||
},
|
||||
{
|
||||
name: 'Quick Sort',
|
||||
description: 'SORTING.EXPLANATION.QUICK_SORT_EXPLANATION',
|
||||
link: UrlConstants.QUICK_SORT_WIKI
|
||||
},
|
||||
{
|
||||
name: 'Heap Sort',
|
||||
description: 'SORTING.EXPLANATION.HEAP_SORT_EXPLANATION',
|
||||
link: UrlConstants.HEAP_SORT_WIKI
|
||||
}
|
||||
],
|
||||
disclaimer: 'SORTING.EXPLANATION.DISCLAIMER',
|
||||
disclaimerBottom: 'SORTING.EXPLANATION.DISCLAIMER_4',
|
||||
disclaimerListEntry: [
|
||||
'SORTING.EXPLANATION.DISCLAIMER_1',
|
||||
'SORTING.EXPLANATION.DISCLAIMER_2',
|
||||
'SORTING.EXPLANATION.DISCLAIMER_3'
|
||||
]
|
||||
};
|
||||
|
||||
private timeoutIds: number[] = [];
|
||||
sortArray: SortData[] = [];
|
||||
unsortedArrayCopy: SortData[] = [];
|
||||
arraySize = 50;
|
||||
maxArrayValue = 100;
|
||||
animationSpeed = 50; // Milliseconds per step
|
||||
|
||||
// Placeholder for available sorting algorithms
|
||||
availableAlgorithms: { name: string; value: string }[] = [
|
||||
{ name: 'Bubble Sort', value: 'bubbleSort' },
|
||||
{ name: 'Cocktail Sort', value: 'cocktailSort' },
|
||||
{ name: 'Quick Sort', value: 'quickSort' },
|
||||
{ name: 'Heap Sort', value: 'heapSort' },
|
||||
];
|
||||
selectedAlgorithm: string = this.availableAlgorithms[0].value;
|
||||
selectedAlgorithm: string = this.algoInformation.entries[0].name;
|
||||
executionTime = 0;
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -93,22 +119,22 @@ export class SortingComponent implements OnInit {
|
||||
let snapshots: SortSnapshot[] = [];
|
||||
|
||||
switch (this.selectedAlgorithm) {
|
||||
case 'bubbleSort':
|
||||
case 'Bubble Sort':
|
||||
snapshots = this.sortingService.bubbleSort(this.sortArray);
|
||||
break;
|
||||
case 'quickSort':
|
||||
case 'Quick Sort':
|
||||
snapshots = this.sortingService.quickSort(this.sortArray);
|
||||
break;
|
||||
case 'heapSort':
|
||||
case 'Heap Sort':
|
||||
snapshots = this.sortingService.heapSort(this.sortArray);
|
||||
break;
|
||||
case 'cocktailSort':
|
||||
case 'Cocktail Sort':
|
||||
snapshots = this.sortingService.cocktailSort(this.sortArray);
|
||||
break;
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
this.executionTime = parseFloat((endTime - startTime).toFixed(4));
|
||||
this.executionTime = Number.parseFloat((endTime - startTime).toFixed(4));
|
||||
|
||||
console.log(snapshots.length);
|
||||
this.animateSorting(snapshots);
|
||||
@@ -145,6 +171,4 @@ export class SortingComponent implements OnInit {
|
||||
this.stopAnimations();
|
||||
this.resetSortState();
|
||||
}
|
||||
|
||||
protected readonly UrlConstants = UrlConstants;
|
||||
}
|
||||
|
||||
1
src/app/shared/components/generic-grid/generic-grid.html
Normal file
1
src/app/shared/components/generic-grid/generic-grid.html
Normal file
@@ -0,0 +1 @@
|
||||
<canvas #gridCanvas></canvas>
|
||||
213
src/app/shared/components/generic-grid/generic-grid.ts
Normal file
213
src/app/shared/components/generic-grid/generic-grid.ts
Normal file
@@ -0,0 +1,213 @@
|
||||
import {AfterViewInit, Component, ElementRef, EventEmitter, Input, Output, ViewChild} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
export interface GridPos { row: number; col: number }
|
||||
|
||||
@Component({
|
||||
selector: 'app-generic-grid',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
templateUrl: './generic-grid.html',
|
||||
styleUrl: './generic-grid.scss',
|
||||
})
|
||||
export class GenericGridComponent implements AfterViewInit {
|
||||
@ViewChild('gridCanvas', { static: true })
|
||||
canvas!: ElementRef<HTMLCanvasElement>;
|
||||
|
||||
@Input() gridRows: number = 10;
|
||||
@Input() gridCols: number = 10;
|
||||
@Input() nodeSize: number = 10; // Default node size, can be overridden by computeNodeSize
|
||||
@Input() maxGridPx: number = 500; // Max pixels for grid dimension
|
||||
@Input() minGridSize: number = 5;
|
||||
@Input() maxGridSize: number = 50;
|
||||
@Input() drawNodeBorderColor: string = '#ccc';
|
||||
|
||||
// Callbacks from parent component
|
||||
@Input() createNodeFn!: (row: number, col: number) => any;
|
||||
@Input() getNodeColorFn!: (node: any) => string;
|
||||
@Input() applySelectionFn!: (pos: GridPos, grid: any[][]) => void;
|
||||
@Input() initializationFn!: (grid: any[][]) => void;
|
||||
|
||||
@Output() gridChange = new EventEmitter<any[][]>();
|
||||
@Output() nodeClick = new EventEmitter<GridPos>();
|
||||
|
||||
private ctx!: CanvasRenderingContext2D;
|
||||
grid: any[][] = [];
|
||||
|
||||
isDrawing = false;
|
||||
private lastCell: GridPos | null = null;
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.ctx = this.getContextOrThrow();
|
||||
this.setupCanvasListeners();
|
||||
this.applyGridSize();
|
||||
}
|
||||
|
||||
setupCanvasListeners(): void {
|
||||
const el = this.canvas.nativeElement;
|
||||
el.addEventListener('mousedown', (e) => this.onMouseDown(e));
|
||||
el.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
||||
el.addEventListener('mouseup', () => this.onMouseUp());
|
||||
el.addEventListener('mouseleave', () => this.onMouseUp());
|
||||
|
||||
el.addEventListener('touchstart', (e) => {
|
||||
if (e.cancelable) e.preventDefault();
|
||||
this.onMouseDown(e as never);
|
||||
}, { passive: false });
|
||||
|
||||
el.addEventListener('touchmove', (e) => {
|
||||
if (e.cancelable) e.preventDefault();
|
||||
this.onMouseMove(e as never);
|
||||
}, { passive: false });
|
||||
|
||||
el.addEventListener('touchend', () => {
|
||||
this.onMouseUp();
|
||||
});
|
||||
}
|
||||
|
||||
applyGridSize(): void {
|
||||
this.gridRows = this.clampGridSize(this.gridRows);
|
||||
this.gridCols = this.clampGridSize(this.gridCols);
|
||||
this.nodeSize = this.computeNodeSize(this.gridRows, this.gridCols);
|
||||
this.resizeCanvas();
|
||||
if (this.gridRows === this.grid.length && this.gridCols === this.grid[0]?.length) {
|
||||
this.drawGrid();
|
||||
return;
|
||||
}
|
||||
this.initializeGrid();
|
||||
}
|
||||
|
||||
initializeGrid(): void {
|
||||
this.grid = this.createEmptyGrid();
|
||||
if (this.initializationFn) {
|
||||
this.initializationFn(this.grid);
|
||||
}
|
||||
this.drawGrid();
|
||||
this.gridChange.emit(this.grid);
|
||||
}
|
||||
|
||||
createEmptyGrid(): any[][] {
|
||||
const grid: any[][] = [];
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
const currentRow: any[] = [];
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
currentRow.push(this.createNodeFn(row, col));
|
||||
}
|
||||
grid.push(currentRow);
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
drawGrid(): void {
|
||||
this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height);
|
||||
for (let row = 0; row < this.gridRows; row++) {
|
||||
for (let col = 0; col < this.gridCols; col++) {
|
||||
this.drawNode(this.grid[row][col]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drawNode(node: any): void {
|
||||
this.ctx.fillStyle = this.getNodeColorFn(node);
|
||||
this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||
this.ctx.strokeStyle = this.drawNodeBorderColor;
|
||||
this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
|
||||
}
|
||||
|
||||
private getContextOrThrow(): CanvasRenderingContext2D {
|
||||
const ctx = this.canvas.nativeElement.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('CanvasRenderingContext2D not available.');
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
private clampGridSize(value: number): number {
|
||||
const parsed = Math.floor(Number(value));
|
||||
const safe = Number.isFinite(parsed) ? parsed : this.minGridSize; // Use minGridSize as fallback
|
||||
return Math.min(Math.max(this.minGridSize, safe), this.maxGridSize);
|
||||
}
|
||||
|
||||
private computeNodeSize(rows: number, cols: number): number {
|
||||
const sizeByWidth = Math.floor(this.maxGridPx / cols);
|
||||
const sizeByHeight = Math.floor(this.maxGridPx / rows);
|
||||
return Math.max(1, Math.min(sizeByWidth, sizeByHeight));
|
||||
}
|
||||
|
||||
private resizeCanvas(): void {
|
||||
const el = this.canvas.nativeElement;
|
||||
el.width = this.gridCols * this.nodeSize;
|
||||
el.height = this.gridRows * this.nodeSize;
|
||||
}
|
||||
|
||||
onMouseDown(event: MouseEvent | TouchEvent): void {
|
||||
this.isDrawing = true;
|
||||
this.lastCell = null;
|
||||
const pos = this.getGridPosition(event);
|
||||
if (pos) {
|
||||
this.handleInteraction(pos);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseMove(event: MouseEvent | TouchEvent): void {
|
||||
if (!this.isDrawing) {
|
||||
return;
|
||||
}
|
||||
const pos = this.getGridPosition(event);
|
||||
if (pos && !this.isSameCell(pos, this.lastCell)) {
|
||||
this.handleInteraction(pos);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseUp(): void {
|
||||
this.isDrawing = false;
|
||||
this.lastCell = null;
|
||||
}
|
||||
|
||||
private handleInteraction(pos: GridPos): void {
|
||||
this.applySelectionFn(pos, this.grid);
|
||||
this.drawNode(this.grid[pos.row][pos.col]);
|
||||
this.lastCell = pos;
|
||||
this.nodeClick.emit(pos);
|
||||
this.gridChange.emit(this.grid);
|
||||
}
|
||||
|
||||
private getGridPosition(event: MouseEvent | TouchEvent): GridPos | null {
|
||||
const canvas = this.canvas.nativeElement;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
let clientX, clientY;
|
||||
if (event instanceof MouseEvent) {
|
||||
clientX = event.clientX;
|
||||
clientY = event.clientY;
|
||||
} else if (event instanceof TouchEvent && event.touches.length > 0) {
|
||||
clientX = event.touches[0].clientX;
|
||||
clientY = event.touches[0].clientY;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const scaleX = canvas.width / rect.width;
|
||||
const scaleY = canvas.height / rect.height;
|
||||
|
||||
const x = (clientX - rect.left) * scaleX;
|
||||
const y = (clientY - rect.top) * scaleY;
|
||||
|
||||
const col = Math.floor(x / this.nodeSize);
|
||||
const row = Math.floor(y / this.nodeSize);
|
||||
|
||||
if (!this.isValidPosition(row, col)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { row, col };
|
||||
}
|
||||
|
||||
private isValidPosition(row: number, col: number): boolean {
|
||||
return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols;
|
||||
}
|
||||
|
||||
private isSameCell(a: GridPos, b: GridPos | null): boolean {
|
||||
return !!b && a.row === b.row && a.col === b.col;
|
||||
}
|
||||
}
|
||||
@@ -313,14 +313,11 @@
|
||||
"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).",
|
||||
"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."
|
||||
},
|
||||
"GRID_HEIGHT": "Höhe",
|
||||
"GRID_WIDTH": "Beite"
|
||||
}
|
||||
},
|
||||
"SORTING": {
|
||||
"TITLE": "Sortieralgorithmen",
|
||||
@@ -336,7 +333,6 @@
|
||||
"QUICK_SORT_EXPLANATION": "folgt dem \"Teile und Herrsche\"-Prinzip. Ein \"Pivot\"-Element wird gewählt, und das Array wird in zwei Hälften geteilt: Elemente kleiner als das Pivot und Elemente größer als das Pivot. Vorteil: Im Durchschnitt einer der schnellsten Sortieralgorithmen (O(n log n)); benötigt keinen zusätzlichen Speicher (In-Place). Nachteil: Im schlechtesten Fall (Worst Case) langsam (O(n²)), wenn das Pivot ungünstig gewählt wird. Ist nicht stabil (ändert Reihenfolge gleicher Elemente).",
|
||||
"HEAP_SORT_EXPLANATION": "organisiert die Daten zunächst in einer speziellen Baumstruktur (Binary Heap). Das größte Element (die Wurzel) wird entnommen und ans Ende sortiert, dann wird der Baum repariert. Vorteil: Garantiert eine schnelle Laufzeit von O(n log n), selbst im schlechtesten Fall. Benötigt fast keinen zusätzlichen Speicher. Nachteil: In der Praxis oft etwas langsamer als Quick Sort, da die Sprünge im Speicher (Heap-Struktur) den CPU-Cache schlechter nutzen.",
|
||||
"COCKTAIL_SORT_EXPLANATION" : "(auch Shaker Sort) ist eine Erweiterung des Bubble Sort. Statt nur von links nach rechts zu gehen, wechselt er bei jedem Durchlauf die Richtung und schiebt abwechselnd das größte Element nach rechts und das kleinste nach links. Vorteil: Schneller als Bubble Sort, da kleine Elemente am Ende schneller nach vorne wandern (\"Schildkröten-Problem\" gelöst). Nachteil: Bleibt in der Laufzeitklasse O(n²), also für große Datenmengen ineffizient.",
|
||||
"NOTE": "HINWEIS",
|
||||
"DISCLAIMER": "Die Wahl des \"besten\" Sortieralgorithmus hängt stark von den Daten und den Rahmenbedingungen ab. In der Informatik betrachtet man oft drei Szenarien:",
|
||||
"DISCLAIMER_1": "Best Case: Die Daten sind schon fast sortiert (hier glänzt z.B. Bubble Sort).",
|
||||
"DISCLAIMER_2": "Average Case: Der statistische Normalfall.",
|
||||
@@ -344,6 +340,28 @@
|
||||
"DISCLAIMER_4": "Zusätzlich gibt es fast immer einen Time-Space Trade-off (Zeit-Speicher-Kompromiss): Algorithmen, die extrem schnell sind (wie Merge Sort), benötigen oft viel zusätzlichen Arbeitsspeicher. Algorithmen, die direkt im vorhandenen Speicher arbeiten (wie Heap Sort), sparen Platz, sind aber manchmal komplexer oder minimal langsamer. Es gibt also keine \"One-Size-Fits-All\"-Lösung."
|
||||
}
|
||||
},
|
||||
"GOL": {
|
||||
"TITLE": "Conway's Spiel des Lebens",
|
||||
"START": "Starten",
|
||||
"PAUSE": "Pause",
|
||||
"RANDOM_SCENE": "Zufällig",
|
||||
"EMPTY_SCENE": "Leer",
|
||||
"SIMPLE_SCENE": "Simpel",
|
||||
"PULSAR_SCENE": "Pulsar",
|
||||
"GUN_SCENE": "Pistole",
|
||||
"ALIVE": "Lebend",
|
||||
"DEAD": "Leer",
|
||||
"SPEED": "Zeit pro Generation",
|
||||
"EXPLANATION": {
|
||||
"TITLE": "Erklärung",
|
||||
"EXPLANATION" : "Das Spiel läuft schrittweise ab. Zunächst wird eine Anfangsgeneration von lebenden Zellen auf dem Spielfeld definiert. Aus der vorliegenden Generation (dem Gesamtbild des Spielfeldes) wird die Folgegeneration ermittelt. Der Zustand jeder einzelnen Zelle in der Folgegeneration ergibt sich dabei nach einfachen Regeln aus ihrem aktuellen Zustand sowie den aktuellen Zuständen ihrer acht Nachbarzellen (Moore-Nachbarschaft).",
|
||||
"DISCLAIMER": "Nach Conways ursprünglicher Regel lebt eine Zelle in der nächsten Runde, wenn zuvor in ihrer 3x3-Umgebung insgesamt genau drei Zellen leben, wobei sie selbst nur bei Bedarf mitgezählt wird, das heißt:",
|
||||
"DISCLAIMER_1": "Eine lebende Zelle lebt auch in der Folgegeneration, wenn sie entweder zwei oder drei lebende Nachbarn hat.",
|
||||
"DISCLAIMER_2": "Eine tote Zelle „wird geboren“ (lebt in der Folgegeneration), wenn sie genau drei lebende Nachbarn hat.",
|
||||
"DISCLAIMER_3": "Eine lebende Zelle „stirbt“ (ist in der Folgegeneration tot), wenn sie weniger als zwei (Vereinsamung) oder mehr als drei (Übervölkerung) lebende Nachbarn hat.",
|
||||
"DISCLAIMER_4": " Eine tote Zelle bleibt tot, wenn sie nicht genau drei lebende Nachbarn hat."
|
||||
}
|
||||
},
|
||||
"ALGORITHM": {
|
||||
"TITLE": "Algorithmen",
|
||||
"PATHFINDING": {
|
||||
@@ -353,7 +371,13 @@
|
||||
"SORTING": {
|
||||
"TITLE": "Sortierung",
|
||||
"DESCRIPTION": "Visualisierung verschiedener Sortieralgorithmen."
|
||||
|
||||
}
|
||||
},
|
||||
"GOL": {
|
||||
"TITLE": "Conway's Game of Life",
|
||||
"DESCRIPTION": "Das 'Spiel des Lebens' ist ein vom Mathematiker John Horton Conway 1970 entworfenes Spiel."
|
||||
},
|
||||
"NOTE": "HINWEIS",
|
||||
"GRID_HEIGHT": "Höhe",
|
||||
"GRID_WIDTH": "Beite"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,14 +313,11 @@
|
||||
"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).",
|
||||
"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."
|
||||
},
|
||||
"GRID_HEIGHT": "Height",
|
||||
"GRID_WIDTH": "Width"
|
||||
}
|
||||
},
|
||||
"SORTING": {
|
||||
"TITLE": "Sorting Algorithms",
|
||||
@@ -335,7 +332,6 @@
|
||||
"BUBBLE_SORT_EXPLANATION": "repeatedly compares adjacent elements and swaps them if they are in the wrong order. The largest element \"bubbles\" to the end of the list like an air bubble. Advantage: Extremely simple to understand and implement; detects already sorted lists very quickly. Disadvantage: Very inefficient for large lists (runtime O(n²)). Rarely used in practice.",
|
||||
"QUICK_SORT_EXPLANATION": "follows the \"divide and conquer\" principle. A \"pivot\" element is selected, and the array is divided into two halves: elements smaller than the pivot and elements larger than the pivot. Advantage: On average one of the fastest sorting algorithms (O(n log n)); requires no additional memory (in-place). Disadvantage: Slow in the worst case (O(n²)) if the pivot is chosen poorly. Is not stable (changes order of equal elements).",
|
||||
"HEAP_SORT_EXPLANATION": "organizes the data initially into a special tree structure (Binary Heap). The largest element (the root) is extracted and sorted to the end, then the tree is repaired. Advantage: Guarantees a fast runtime of O(n log n), even in the worst case. Requires almost no additional memory. Disadvantage: Often slightly slower than Quick Sort in practice because the jumps in memory (heap structure) utilize the CPU cache less effectively.",
|
||||
"NOTE": "NOTE",
|
||||
"DISCLAIMER": "The choice of the \"best\" sorting algorithm depends heavily on the data and the constraints. In computer science, three scenarios are often considered:",
|
||||
"DISCLAIMER_1": "Best Case: The data is already nearly sorted (Bubble Sort shines here, for example).",
|
||||
"DISCLAIMER_2": "Average Case: The statistical norm.",
|
||||
@@ -343,6 +339,28 @@
|
||||
"DISCLAIMER_4": "Additionally, there is almost always a Time-Space Trade-off: Algorithms that are extremely fast (like Merge Sort) often require a lot of additional working memory. Algorithms that work directly in existing memory (like Heap Sort) save space but are sometimes more complex or slightly slower. Thus, there is no \"one-size-fits-all\" solution."
|
||||
}
|
||||
},
|
||||
"GOL": {
|
||||
"TITLE": "Conway's Game of Life",
|
||||
"START": "Start",
|
||||
"PAUSE": "Pause",
|
||||
"RANDOM_SCENE": "Random",
|
||||
"EMPTY_SCENE": "Empty",
|
||||
"SIMPLE_SCENE": "Simple",
|
||||
"PULSAR_SCENE": "Pulsar",
|
||||
"GUN_SCENE": "Gun",
|
||||
"ALIVE": "Alive",
|
||||
"DEAD": "Empty",
|
||||
"SPEED": "Time per Generation",
|
||||
"EXPLANATION": {
|
||||
"TITLE": "Erklärung",
|
||||
"EXPLANATION" : "Das Spiel läuft schrittweise ab. Zunächst wird eine Anfangsgeneration von lebenden Zellen auf dem Spielfeld definiert. Aus der vorliegenden Generation (dem Gesamtbild des Spielfeldes) wird die Folgegeneration ermittelt. Der Zustand jeder einzelnen Zelle in der Folgegeneration ergibt sich dabei nach einfachen Regeln aus ihrem aktuellen Zustand sowie den aktuellen Zuständen ihrer acht Nachbarzellen (Moore-Nachbarschaft).",
|
||||
"DISCLAIMER": "Nach Conways ursprünglicher Regel lebt eine Zelle in der nächsten Runde, wenn zuvor in ihrer 3x3-Umgebung insgesamt genau drei Zellen leben, wobei sie selbst nur bei Bedarf mitgezählt wird, das heißt:",
|
||||
"DISCLAIMER_1": "Eine lebende Zelle lebt auch in der Folgegeneration, wenn sie entweder zwei oder drei lebende Nachbarn hat.",
|
||||
"DISCLAIMER_2": "Eine tote Zelle „wird geboren“ (lebt in der Folgegeneration), wenn sie genau drei lebende Nachbarn hat.",
|
||||
"DISCLAIMER_3": "Eine lebende Zelle „stirbt“ (ist in der Folgegeneration tot), wenn sie weniger als zwei (Vereinsamung) oder mehr als drei (Übervölkerung) lebende Nachbarn hat.",
|
||||
"DISCLAIMER_4": " Eine tote Zelle bleibt tot, wenn sie nicht genau drei lebende Nachbarn hat."
|
||||
}
|
||||
},
|
||||
"ALGORITHM": {
|
||||
"TITLE": "Algorithms",
|
||||
"PATHFINDING": {
|
||||
@@ -352,6 +370,13 @@
|
||||
"SORTING": {
|
||||
"TITLE": "Sorting",
|
||||
"DESCRIPTION": "Visualizing various sorting algorithms."
|
||||
}
|
||||
},
|
||||
"GOL": {
|
||||
"TITLE:": "Conway's Game of Life",
|
||||
"DESCRIPTION": "The Game of Life is a cellular automaton devised by the British mathematician John Horton Conway in 1970."
|
||||
},
|
||||
"NOTE": "Note",
|
||||
"GRID_HEIGHT": "Height",
|
||||
"GRID_WIDTH": "Width"
|
||||
}
|
||||
}
|
||||
|
||||
109
src/styles.scss
109
src/styles.scss
@@ -110,6 +110,12 @@ a {
|
||||
transition:
|
||||
box-shadow 200ms ease,
|
||||
transform 200ms ease;
|
||||
|
||||
&.container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-mdc-card::before {
|
||||
@@ -213,6 +219,11 @@ a {
|
||||
}
|
||||
|
||||
// algos
|
||||
|
||||
.container {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.algo-info {
|
||||
margin: 0 0 1rem 0;
|
||||
padding: 0.75rem 1rem;
|
||||
@@ -231,3 +242,101 @@ a {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.controls-panel {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
font-size: 0.9em;
|
||||
|
||||
mat-button-toggle-group {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-size {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.grid-field {
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
canvas {
|
||||
border: 1px solid lightgray;
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
font-size: 0.9em;
|
||||
|
||||
.legend-color {
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border: 1px solid lightgray;
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
|
||||
&.start { background-color: green; }
|
||||
&.end { background-color: red; }
|
||||
&.wall { background-color: black; }
|
||||
&.visited { background-color: skyblue; }
|
||||
&.path { background-color: gold; }
|
||||
&.empty { background-color: lightgray; }
|
||||
&.alive { background-color: black; }
|
||||
}
|
||||
}
|
||||
|
||||
.controls-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Sorting Visualization */
|
||||
.sorting-visualization-area {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
height: 300px; /* Max height for bars */
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin-bottom: 20px;
|
||||
gap: 1px;
|
||||
background-color: #f0f0f0;
|
||||
|
||||
.sorting-bar {
|
||||
flex-grow: 1;
|
||||
background-color: #424242; /* Default unsorted color */
|
||||
transition: height 0.05s ease-in-out, background-color 0.05s ease-in-out;
|
||||
width: 10px; /* Default width, flex-grow will adjust */
|
||||
min-width: 1px; /* Ensure bars are always visible */
|
||||
|
||||
&.unsorted {
|
||||
background-color: #424242;
|
||||
}
|
||||
|
||||
&.comparing {
|
||||
background-color: #ffeb3b; /* Yellow for comparing */
|
||||
}
|
||||
|
||||
&.sorted {
|
||||
background-color: #4caf50; /* Green for sorted */
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user