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.
This commit is contained in:
2026-02-06 22:03:18 +01:00
parent 930f0110b0
commit bf46c57db0
5 changed files with 169 additions and 21 deletions

View File

@@ -6,23 +6,33 @@
<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>
<button mat-raised-button >
<mat-icon>play_arrow</mat-icon> {{ 'GOL.START' | translate }}
</button>
<button mat-raised-button >
<mat-icon>play_arrow</mat-icon> {{ 'GOL.START' | translate }}
</button>
</div>
<div class="controls-panel">
<button mat-raised-button >
<mat-icon>play_arrow</mat-icon> {{ 'GOL.START' | translate }}
</button>
@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">
@@ -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()"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="grid-field">
@@ -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()"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="grid-field">

View File

@@ -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;

View File

@@ -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<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;
}
}
}

View File

@@ -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",

View File

@@ -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",