diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.html b/src/app/pages/algorithms/conway-gol/conway-gol.html index 7352516..7bb078b 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.html +++ b/src/app/pages/algorithms/conway-gol/conway-gol.html @@ -33,6 +33,7 @@ play_arrow {{ 'GOL.START' | translate }} } +

{{ 'SORTING.EXECUTION_TIME' | translate }}: {{ executionTime }} ms

@@ -69,6 +70,7 @@ (keyup.enter)="applySpeed()" /> +
{{ 'GOL.ALIVE' | translate }} @@ -84,7 +86,8 @@ [createNodeFn]="createConwayNode" [getNodeColorFn]="getConwayNodeColor" [applySelectionFn]="applyConwaySelection" - (gridChange)="grid = $event" + [backgroundColor]="'lightgray'" + (gridChange)="readGrid = $event" > diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.models.ts b/src/app/pages/algorithms/conway-gol/conway-gol.models.ts index 9902429..f6ce9e4 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.models.ts +++ b/src/app/pages/algorithms/conway-gol/conway-gol.models.ts @@ -16,7 +16,7 @@ export const DEFAULT_GRID_ROWS = 50; export const DEFAULT_GRID_COLS = 50; export const MIN_GRID_SIZE = 20; -export const MAX_GRID_SIZE = 100; +export const MAX_GRID_SIZE = 200; export const DEFAULT_TIME_PER_GENERATION = 30; export const MIN_TIME_PER_GENERATION = 20; diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.ts b/src/app/pages/algorithms/conway-gol/conway-gol.ts index 9190bed..743965b 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.ts +++ b/src/app/pages/algorithms/conway-gol/conway-gol.ts @@ -54,7 +54,9 @@ export class ConwayGol implements AfterViewInit { protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE; protected readonly MAX_GRID_PX = MAX_GRID_PX; - grid: Node[][] = []; + readGrid: Node[][] = []; + writeGrid: Node[][] = []; + executionTime = 0; currentScenario: Scenario = Scenario.SIMPLE; readonly gameStarted = signal(false); @@ -96,45 +98,42 @@ export class ConwayGol implements AfterViewInit { }; getConwayNodeColor = (node: Node): string => { - if (node.alive) { - return 'black'; - } - return 'lightgray'; + return node.alive ? 'black' : 'lightgray'; }; applyConwaySelection = (pos: GridPos, grid: Node[][]): void => { - this.grid = grid; // Keep internal grid in sync + this.readGrid = 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; - + this.readGrid = 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; } + this.writeGrid = structuredClone(this.readGrid); }; // --- 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; + this.readGrid[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; + this.readGrid[3][4].alive = true; + this.readGrid[4][5].alive = true; + this.readGrid[5][3].alive = true; + this.readGrid[5][4].alive = true; + this.readGrid[5][5].alive = true; } setupPulsar(): void { @@ -181,39 +180,44 @@ export class ConwayGol implements AfterViewInit { this.gameStarted.set(true); let lifeIsDead = false; while (this.gameStarted()){ - let gridClone = structuredClone(this.grid); + const startTime = performance.now(); 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); + lifeIsDead = this.checkLifeRules(row, col, this.writeGrid) && lifeIsDead; } } - this.swapGrid(gridClone); + this.swapGrids(); + const endTime = performance.now(); + this.executionTime = Number.parseFloat((endTime - startTime).toFixed(4)); if (lifeIsDead){ this.gameStarted.set(false); } - await this.delay(this.lifeSpeed); + const delta = Math.max(this.lifeSpeed - this.executionTime, 0); + await this.delay(delta); } + this.executionTime = 0; } - 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 checkLifeRules(row: number, col: number, writeGrid: Node[][]): boolean { + const currentCell = this.readGrid[row][col]; + const aliveNeighbors = this.howManyNeighborsAreLiving(row, col); + const oldLifeState = currentCell.alive; + + const nextStateAlive = (currentCell.alive && (aliveNeighbors === 2 || aliveNeighbors === 3)) || (!currentCell.alive && aliveNeighbors === 3); + writeGrid[row][col].alive = nextStateAlive; + + //only if at least one cell changes the game is still alive + return (nextStateAlive == oldLifeState); } - private swapGrid(gridClone: Node[][]) { - this.grid = gridClone; + private swapGrids() { + const tmp = this.readGrid; + this.readGrid = this.writeGrid; + this.writeGrid = tmp; if (this.genericGridComponent) { - this.genericGridComponent.grid = this.grid; + this.genericGridComponent.grid = this.readGrid; this.genericGridComponent.drawGrid(); } } @@ -231,7 +235,7 @@ export class ConwayGol implements AfterViewInit { if (nRow == row && nCol == col) { continue; } - if (this.grid[nRow][nCol].alive) { + if (this.readGrid[nRow][nCol].alive) { aliveNeighborCount++; } } @@ -250,7 +254,7 @@ export class ConwayGol implements AfterViewInit { private setAlive(r: number, c: number): void { if (r >= 0 && r < this.gridRows && c >= 0 && c < this.gridCols) { - this.grid[r][c].alive = true; + this.readGrid[r][c].alive = true; } } } diff --git a/src/app/pages/algorithms/pathfinding/pathfinding.component.html b/src/app/pages/algorithms/pathfinding/pathfinding.component.html index 36d9ccd..9110a47 100644 --- a/src/app/pages/algorithms/pathfinding/pathfinding.component.html +++ b/src/app/pages/algorithms/pathfinding/pathfinding.component.html @@ -73,6 +73,7 @@ [createNodeFn]="createPathfindingNode" [getNodeColorFn]="getPathfindingNodeColor" [applySelectionFn]="applyPathfindingSelection" + [backgroundColor]="'lightgray'" (gridChange)="grid = $event" > diff --git a/src/app/pages/algorithms/sorting/sorting.component.ts b/src/app/pages/algorithms/sorting/sorting.component.ts index 569c037..440f2d9 100644 --- a/src/app/pages/algorithms/sorting/sorting.component.ts +++ b/src/app/pages/algorithms/sorting/sorting.component.ts @@ -136,7 +136,6 @@ export class SortingComponent implements OnInit { const endTime = performance.now(); this.executionTime = Number.parseFloat((endTime - startTime).toFixed(4)); - console.log(snapshots.length); this.animateSorting(snapshots); } diff --git a/src/app/shared/components/generic-grid/generic-grid.ts b/src/app/shared/components/generic-grid/generic-grid.ts index 55c8035..26791a9 100644 --- a/src/app/shared/components/generic-grid/generic-grid.ts +++ b/src/app/shared/components/generic-grid/generic-grid.ts @@ -21,6 +21,7 @@ export class GenericGridComponent implements AfterViewInit { @Input() minGridSize: number = 5; @Input() maxGridSize: number = 50; @Input() drawNodeBorderColor: string = '#ccc'; + @Input() backgroundColor: string = 'lightgray'; // Callbacks from parent component @Input() createNodeFn!: (row: number, col: number) => any; @@ -99,19 +100,70 @@ export class GenericGridComponent implements AfterViewInit { } drawGrid(): void { - this.ctx.clearRect(0, 0, this.canvas.nativeElement.width, this.canvas.nativeElement.height); + if (!this.ctx || !this.grid.length) return; + + const width = this.canvas.nativeElement.width; + const height = this.canvas.nativeElement.height; + const size = this.nodeSize; + + this.ctx.fillStyle = this.backgroundColor; + this.ctx.fillRect(0, 0, width, height); + this.ctx.fillStyle = 'black'; + for (let row = 0; row < this.gridRows; row++) { for (let col = 0; col < this.gridCols; col++) { - this.drawNode(this.grid[row][col]); + const node = this.grid[row][col]; + + const color = this.getNodeColorFn(node); + + if (color !== this.backgroundColor) { + if (this.ctx.fillStyle !== color) { + this.ctx.fillStyle = color; + } + + const x = col * this.nodeSize; + const y = row * this.nodeSize; + + this.ctx.fillRect(x, y, this.nodeSize, this.nodeSize); + + } } } + + if (size > 2) { + this.drawGridLines(width, height); + } + + } + + private drawGridLines(width: number, height: number): void { + this.ctx.beginPath(); + this.ctx.strokeStyle = this.drawNodeBorderColor; + this.ctx.lineWidth = 1; + + for (let col = 0; col <= this.gridCols; col++) { + const x = col * this.nodeSize; + this.ctx.moveTo(x, 0); + this.ctx.lineTo(x, height); + } + + for (let row = 0; row <= this.gridRows; row++) { + const y = row * this.nodeSize; + this.ctx.moveTo(0, y); + this.ctx.lineTo(width, y); + } + + this.ctx.stroke(); } 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); + + if (this.nodeSize > 4) { + this.ctx.strokeStyle = this.drawNodeBorderColor; + this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize); + } } private getContextOrThrow(): CanvasRenderingContext2D {