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:
@@ -6,23 +6,33 @@
|
|||||||
<app-information [algorithmInformation]="algoInformation"/>
|
<app-information [algorithmInformation]="algoInformation"/>
|
||||||
<div class="controls-container">
|
<div class="controls-container">
|
||||||
<div class="controls-panel">
|
<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)">
|
<button mat-raised-button (click)="generate(Scenario.RANDOM)">
|
||||||
<mat-icon>shuffle</mat-icon> {{ 'GOL.RANDOM_SCENE' | translate }}
|
<mat-icon>shuffle</mat-icon> {{ 'GOL.RANDOM_SCENE' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button mat-raised-button (click)="generate(Scenario.EMPTY)">
|
<button mat-raised-button (click)="generate(Scenario.EMPTY)">
|
||||||
<mat-icon>check_box_outline_blank</mat-icon> {{ 'GOL.EMPTY_SCENE' | translate }}
|
<mat-icon>check_box_outline_blank</mat-icon> {{ 'GOL.EMPTY_SCENE' | translate }}
|
||||||
</button>
|
</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>
|
||||||
<div class="controls-panel">
|
<div class="controls-panel">
|
||||||
<button mat-raised-button >
|
@if (gameStarted())
|
||||||
<mat-icon>play_arrow</mat-icon> {{ 'GOL.START' | translate }}
|
{
|
||||||
</button>
|
<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>
|
||||||
<div class="grid-size">
|
<div class="grid-size">
|
||||||
<mat-form-field appearance="outline" class="grid-field">
|
<mat-form-field appearance="outline" class="grid-field">
|
||||||
@@ -33,7 +43,7 @@
|
|||||||
[(ngModel)]="gridRows"
|
[(ngModel)]="gridRows"
|
||||||
[min]="MIN_GRID_SIZE"
|
[min]="MIN_GRID_SIZE"
|
||||||
[max]="MAX_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>
|
||||||
<mat-form-field appearance="outline" class="grid-field">
|
<mat-form-field appearance="outline" class="grid-field">
|
||||||
@@ -44,7 +54,7 @@
|
|||||||
[(ngModel)]="gridCols"
|
[(ngModel)]="gridCols"
|
||||||
[min]="MIN_GRID_SIZE"
|
[min]="MIN_GRID_SIZE"
|
||||||
[max]="MAX_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>
|
||||||
<mat-form-field appearance="outline" class="grid-field">
|
<mat-form-field appearance="outline" class="grid-field">
|
||||||
|
|||||||
@@ -6,15 +6,18 @@ export interface Node {
|
|||||||
|
|
||||||
export enum Scenario {
|
export enum Scenario {
|
||||||
RANDOM = 0,
|
RANDOM = 0,
|
||||||
EMPTY
|
EMPTY = 1,
|
||||||
|
SIMPLE = 2,
|
||||||
|
PULSAR = 3,
|
||||||
|
GUN = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_GRID_ROWS = 100;
|
export const DEFAULT_GRID_ROWS = 40;
|
||||||
export const DEFAULT_GRID_COLS = 100;
|
export const DEFAULT_GRID_COLS = 40;
|
||||||
|
|
||||||
export const MIN_GRID_SIZE = 20;
|
export const MIN_GRID_SIZE = 20;
|
||||||
export const MAX_GRID_SIZE = 200;
|
export const MAX_GRID_SIZE = 100;
|
||||||
export const DEFAULT_TIME_PER_GENERATION = 50;
|
export const DEFAULT_TIME_PER_GENERATION = 30;
|
||||||
|
|
||||||
export const MIN_TIME_PER_GENERATION = 20;
|
export const MIN_TIME_PER_GENERATION = 20;
|
||||||
export const MAX_TIME_PER_GENERATION = 200;
|
export const MAX_TIME_PER_GENERATION = 200;
|
||||||
|
|||||||
@@ -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 {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
|
||||||
import {TranslatePipe} from "@ngx-translate/core";
|
import {TranslatePipe} from "@ngx-translate/core";
|
||||||
import {UrlConstants} from '../../../constants/UrlConstants';
|
import {UrlConstants} from '../../../constants/UrlConstants';
|
||||||
@@ -8,7 +8,7 @@ import {MatButton} from '@angular/material/button';
|
|||||||
import {MatIcon} from '@angular/material/icon';
|
import {MatIcon} from '@angular/material/icon';
|
||||||
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
import {MatFormField, MatInput, MatLabel} from '@angular/material/input';
|
||||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
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';
|
import {GenericGridComponent, GridPos} from '../../../shared/components/generic-grid/generic-grid';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -55,7 +55,8 @@ export class ConwayGol implements AfterViewInit {
|
|||||||
protected readonly MAX_GRID_PX = MAX_GRID_PX;
|
protected readonly MAX_GRID_PX = MAX_GRID_PX;
|
||||||
|
|
||||||
grid: Node[][] = [];
|
grid: Node[][] = [];
|
||||||
currentScenario: Scenario = 0;
|
currentScenario: Scenario = Scenario.SIMPLE;
|
||||||
|
readonly gameStarted = signal(false);
|
||||||
|
|
||||||
@ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent;
|
@ViewChild(GenericGridComponent) genericGridComponent!: GenericGridComponent;
|
||||||
|
|
||||||
@@ -72,6 +73,7 @@ export class ConwayGol implements AfterViewInit {
|
|||||||
this.genericGridComponent.maxGridPx = this.MAX_GRID_PX;
|
this.genericGridComponent.maxGridPx = this.MAX_GRID_PX;
|
||||||
this.genericGridComponent.initializeGrid();
|
this.genericGridComponent.initializeGrid();
|
||||||
}
|
}
|
||||||
|
this.gameStarted.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
generate(scene: Scenario): void {
|
generate(scene: Scenario): void {
|
||||||
@@ -107,9 +109,14 @@ export class ConwayGol implements AfterViewInit {
|
|||||||
};
|
};
|
||||||
|
|
||||||
initializeConwayGrid = (grid: Node[][]): void => {
|
initializeConwayGrid = (grid: Node[][]): void => {
|
||||||
|
this.gameStarted.set(false);
|
||||||
this.grid = grid;
|
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 ---
|
// --- Other methods ---
|
||||||
protected readonly Scenario = Scenario;
|
protected readonly Scenario = Scenario;
|
||||||
protected readonly MIN_TIME_PER_GENERATION = MIN_TIME_PER_GENERATION;
|
protected readonly MIN_TIME_PER_GENERATION = MIN_TIME_PER_GENERATION;
|
||||||
protected readonly MAX_TIME_PER_GENERATION = MAX_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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -343,8 +343,12 @@
|
|||||||
"GOL": {
|
"GOL": {
|
||||||
"TITLE": "Conway's Spiel des Lebens",
|
"TITLE": "Conway's Spiel des Lebens",
|
||||||
"START": "Starten",
|
"START": "Starten",
|
||||||
|
"PAUSE": "Pause",
|
||||||
"RANDOM_SCENE": "Zufällig",
|
"RANDOM_SCENE": "Zufällig",
|
||||||
"EMPTY_SCENE": "Leer",
|
"EMPTY_SCENE": "Leer",
|
||||||
|
"SIMPLE_SCENE": "Simpel",
|
||||||
|
"PULSAR_SCENE": "Pulsar",
|
||||||
|
"GUN_SCENE": "Pistole",
|
||||||
"ALIVE": "Lebend",
|
"ALIVE": "Lebend",
|
||||||
"DEAD": "Leer",
|
"DEAD": "Leer",
|
||||||
"SPEED": "Zeit pro Generation",
|
"SPEED": "Zeit pro Generation",
|
||||||
|
|||||||
@@ -342,8 +342,12 @@
|
|||||||
"GOL": {
|
"GOL": {
|
||||||
"TITLE": "Conway's Game of Life",
|
"TITLE": "Conway's Game of Life",
|
||||||
"START": "Start",
|
"START": "Start",
|
||||||
|
"PAUSE": "Pause",
|
||||||
"RANDOM_SCENE": "Random",
|
"RANDOM_SCENE": "Random",
|
||||||
"EMPTY_SCENE": "Empty",
|
"EMPTY_SCENE": "Empty",
|
||||||
|
"SIMPLE_SCENE": "Simple",
|
||||||
|
"PULSAR_SCENE": "Pulsar",
|
||||||
|
"GUN_SCENE": "Gun",
|
||||||
"ALIVE": "Alive",
|
"ALIVE": "Alive",
|
||||||
"DEAD": "Empty",
|
"DEAD": "Empty",
|
||||||
"SPEED": "Time per Generation",
|
"SPEED": "Time per Generation",
|
||||||
|
|||||||
Reference in New Issue
Block a user