From bf46c57db0beef1bdefc6aaf7de0dfa978775153 Mon Sep 17 00:00:00 2001 From: Lobo Date: Fri, 6 Feb 2026 22:03:18 +0100 Subject: [PATCH] Conway GOL: add scenarios & start/pause loop Add predefined scenarios (SIMPLE, PULSAR, GUN) and UI controls to generate them; introduce a start/pause game loop driven by an Angular signal. Reduce default grid to 40x40 and max grid to 100, speed up default generation to 30ms, and pause the game when grid size changes. Implement scenario setup helpers (simple, pulsar, glider gun), life-rule evaluation, neighbor counting, grid swapping and a delay helper. Update template to show scenario buttons and conditional start/pause button, and add corresponding i18n entries for English and German. --- .../algorithms/conway-gol/conway-gol.html | 32 ++-- .../conway-gol/conway-gol.models.ts | 13 +- .../pages/algorithms/conway-gol/conway-gol.ts | 137 +++++++++++++++++- src/assets/i18n/de.json | 4 + src/assets/i18n/en.json | 4 + 5 files changed, 169 insertions(+), 21 deletions(-) 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",