feature/gameOfLife #12

Merged
lobo merged 6 commits from feature/gameOfLife into main 2026-02-06 22:03:48 +01:00
5 changed files with 169 additions and 21 deletions
Showing only changes of commit bf46c57db0 - Show all commits

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