diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.html b/src/app/pages/algorithms/conway-gol/conway-gol.html index 185995a..7352516 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.html +++ b/src/app/pages/algorithms/conway-gol/conway-gol.html @@ -6,23 +6,33 @@
+ + + - -
- + @if (gameStarted()) + { + + } @else { + + }
@@ -33,7 +43,7 @@ [(ngModel)]="gridRows" [min]="MIN_GRID_SIZE" [max]="MAX_GRID_SIZE" - (ngModelChange)="genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()" + (ngModelChange)="pauseGame(); genericGridComponent.gridRows = gridRows; genericGridComponent.applyGridSize()" /> @@ -44,7 +54,7 @@ [(ngModel)]="gridCols" [min]="MIN_GRID_SIZE" [max]="MAX_GRID_SIZE" - (ngModelChange)="genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()" + (ngModelChange)="pauseGame(); genericGridComponent.gridCols = gridCols; genericGridComponent.applyGridSize()" /> 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 f2854d6..c582cc2 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.models.ts +++ b/src/app/pages/algorithms/conway-gol/conway-gol.models.ts @@ -6,15 +6,18 @@ export interface Node { export enum Scenario { RANDOM = 0, - EMPTY + EMPTY = 1, + SIMPLE = 2, + PULSAR = 3, + GUN = 4 } -export const DEFAULT_GRID_ROWS = 100; -export const DEFAULT_GRID_COLS = 100; +export const DEFAULT_GRID_ROWS = 40; +export const DEFAULT_GRID_COLS = 40; export const MIN_GRID_SIZE = 20; -export const MAX_GRID_SIZE = 200; -export const DEFAULT_TIME_PER_GENERATION = 50; +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; diff --git a/src/app/pages/algorithms/conway-gol/conway-gol.ts b/src/app/pages/algorithms/conway-gol/conway-gol.ts index 5d6d524..9190bed 100644 --- a/src/app/pages/algorithms/conway-gol/conway-gol.ts +++ b/src/app/pages/algorithms/conway-gol/conway-gol.ts @@ -1,4 +1,4 @@ -import {AfterViewInit, Component, ViewChild} from '@angular/core'; +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'; @@ -8,7 +8,7 @@ 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, MAX_GRID_SIZE, MIN_GRID_SIZE, MAX_GRID_PX, Node, LIVE_SPAWN_PROBABILITY, Scenario, MAX_TIME_PER_GENERATION, MIN_TIME_PER_GENERATION, DEFAULT_TIME_PER_GENERATION} from './conway-gol.models'; +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({ @@ -55,7 +55,8 @@ export class ConwayGol implements AfterViewInit { protected readonly MAX_GRID_PX = MAX_GRID_PX; grid: Node[][] = []; - currentScenario: Scenario = 0; + currentScenario: Scenario = Scenario.SIMPLE; + readonly gameStarted = signal(false); @ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent; @@ -72,6 +73,7 @@ export class ConwayGol implements AfterViewInit { this.genericGridComponent.maxGridPx = this.MAX_GRID_PX; this.genericGridComponent.initializeGrid(); } + this.gameStarted.set(false); } generate(scene: Scenario): void { @@ -107,9 +109,14 @@ export class ConwayGol implements AfterViewInit { }; initializeConwayGrid = (grid: Node[][]): void => { + this.gameStarted.set(false); this.grid = grid; - if (this.currentScenario === Scenario.RANDOM) { - this.setupRandomLives(); + + 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; } }; @@ -122,8 +129,128 @@ export class ConwayGol implements AfterViewInit { } } + 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 { + 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; + } + } } diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 7e03d1a..638d321 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -343,8 +343,12 @@ "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", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 8754e04..30b4f67 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -342,8 +342,12 @@ "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",