feature/gameOfLife #12

Merged
lobo merged 6 commits from feature/gameOfLife into main 2026-02-06 22:03:48 +01:00
14 changed files with 276 additions and 98 deletions
Showing only changes of commit 59148db295 - Show all commits

View File

@@ -7,5 +7,5 @@
static readonly QUICK_SORT_WIKI = 'https://de.wikipedia.org/wiki/Quicksort'
static readonly HEAP_SORT_WIKI = 'https://de.wikipedia.org/wiki/Heapsort'
static readonly SHAKE_SORT_WIKI = 'https://de.wikipedia.org/wiki/Shakersort'
static readonly CONWAYS_WIKI = 'https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life'
static readonly CONWAYS_WIKI = 'https://de.wikipedia.org/wiki/Conways_Spiel_des_Lebens'
}

View File

@@ -4,36 +4,68 @@
</mat-card-header>
<mat-card-content>
<app-information [algorithmInformation]="algoInformation"/>
<div class="controls-panel">
<button mat-raised-button >
<mat-icon>play_arrow</mat-icon> {{ 'GOL.START' | translate }}
</button>
</div>
<div class="grid-size">
<mat-form-field appearance="outline" class="grid-field">
<mat-label>{{ 'PATHFINDING.GRID_HEIGHT' | translate }}</mat-label>
<input
matInput
type="number"
[(ngModel)]="gridRows"
[min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE"
(blur)="applyGridSize()"
(keyup.enter)="applyGridSize()"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="grid-field">
<mat-label>{{ 'PATHFINDING.GRID_WIDTH' | translate }}</mat-label>
<input
matInput
type="number"
[(ngModel)]="gridCols"
[min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE"
(blur)="applyGridSize()"
(keyup.enter)="applyGridSize()"
/>
</mat-form-field>
<div class="controls-container">
<div class="controls-panel">
<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>
</div>
<div class="grid-size">
<mat-form-field appearance="outline" class="grid-field">
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
<input
matInput
type="number"
[(ngModel)]="gridRows"
[min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE"
(blur)="applyGridSize()"
(keyup.enter)="applyGridSize()"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="grid-field">
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
<input
matInput
type="number"
[(ngModel)]="gridCols"
[min]="MIN_GRID_SIZE"
[max]="MAX_GRID_SIZE"
(blur)="applyGridSize()"
(keyup.enter)="applyGridSize()"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="grid-field">
<mat-label>{{ 'GOL.SPEED' | translate }}</mat-label>
<input
matInput
type="number"
[(ngModel)]="lifeSpeed"
[min]="MIN_TIME_PER_GENERATION"
[max]="MAX_TIME_PER_GENERATION"
(blur)="applySpeed()"
(keyup.enter)="applySpeed()"
/>
</mat-form-field>
</div>
<div class="legend">
<span><span class="legend-color alive"></span> {{ 'GOL.ALIVE' | translate }}</span>
<span><span class="legend-color empty"></span> {{ 'GOL.DEAD' | translate }}</span>
</div>
</div>
<canvas #gridCanvas></canvas>
</mat-card-content>

View File

@@ -1,6 +1,12 @@
export interface Node {
row: number;
col: number;
alive: boolean;
}
export enum Scenario {
RANDOM = 0,
EMPTY
}
export const DEFAULT_GRID_ROWS = 100;
@@ -8,4 +14,10 @@ export const DEFAULT_GRID_COLS = 100;
export const MIN_GRID_SIZE = 20;
export const MAX_GRID_SIZE = 200;
export const DEFAULT_TIME_PER_GENERATION = 50;
export const MIN_TIME_PER_GENERATION = 20;
export const MAX_TIME_PER_GENERATION = 200;
export const MAX_GRID_PX = 1000;
export const LIVE_SPAWN_PROBABILITY = 0.37;

View File

@@ -8,7 +8,9 @@ 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} from './conway-gol.models';
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';
interface GridPos { row: number; col: number }
@Component({
selector: 'app-conway-gol',
@@ -45,20 +47,57 @@ export class ConwayGol implements AfterViewInit {
disclaimerBottom: '',
disclaimerListEntry: ['GOL.EXPLANATION.DISCLAIMER_1', 'GOL.EXPLANATION.DISCLAIMER_2', 'GOL.EXPLANATION.DISCLAIMER_3', 'GOL.EXPLANATION.DISCLAIMER_4']
};
protected gridCols = DEFAULT_GRID_COLS;
protected gridRows = DEFAULT_GRID_ROWS;
protected lifeSpeed = DEFAULT_TIME_PER_GENERATION;
protected readonly MIN_GRID_SIZE = MIN_GRID_SIZE;
protected readonly MAX_GRID_SIZE = MAX_GRID_SIZE;
nodeSize = 10;
grid: Node[][] = [];
currentScenario: Scenario = 0;
@ViewChild('gridCanvas', { static: true })
canvas!: ElementRef<HTMLCanvasElement>;
private ctx!: CanvasRenderingContext2D;
private lastCell: GridPos | null = null;
isDrawing = false;
ngAfterViewInit(): void {
this.ctx = this.getContextOrThrow();
this.applyGridSize();
const el = this.canvas.nativeElement;
el.addEventListener('mousedown', (e) => this.onMouseDown(e));
el.addEventListener('mousemove', (e) => this.onMouseMove(e));
el.addEventListener('mouseup', () => this.onMouseUp());
el.addEventListener('mouseleave', () => this.onMouseUp());
el.addEventListener('touchstart', (e) => {
if(e.cancelable) e.preventDefault();
this.onMouseDown(e as never);
}, { passive: false });
el.addEventListener('touchmove', (e) => {
if(e.cancelable) e.preventDefault();
this.onMouseMove(e as never);
}, { passive: false });
el.addEventListener('touchend', () => {
this.onMouseUp();
});
}
generate(scene: Scenario): void {
this.currentScenario = scene;
this.initializeGrid();
}
setupRandomLives(): void {
for (let row = 0; row < this.gridRows; row++) {
for (let col = 0; col < this.gridCols; col++) {
this.grid[row][col].alive = Math.random() <= LIVE_SPAWN_PROBABILITY;
}
}
}
applyGridSize(): void {
@@ -73,11 +112,19 @@ export class ConwayGol implements AfterViewInit {
return;
}
this.initializeGrid();
}
applySpeed(): void {
this.lifeSpeed = Math.min(Math.max(this.lifeSpeed, MIN_TIME_PER_GENERATION), MAX_TIME_PER_GENERATION);
}
private initializeGrid(): void {
this.grid = this.createEmptyGrid();
if (this.currentScenario === Scenario.RANDOM) {
this.setupRandomLives();
}
this.drawGrid();
}
@@ -87,7 +134,7 @@ export class ConwayGol implements AfterViewInit {
for (let row = 0; row < this.gridRows; row++) {
const currentRow: Node[] = [];
for (let col = 0; col < this.gridCols; col++) {
currentRow.push(this.createNode(row, col));
currentRow.push(this.createNode(row, col, false));
}
grid.push(currentRow);
}
@@ -95,10 +142,11 @@ export class ConwayGol implements AfterViewInit {
return grid;
}
private createNode(row: number, col: number): Node {
private createNode(row: number, col: number, alive: boolean): Node {
return {
row,
col
col,
alive
};
}
@@ -113,14 +161,18 @@ export class ConwayGol implements AfterViewInit {
}
private drawNode(node: Node): void {
this.ctx.fillStyle = this.getNodeColor();
this.ctx.fillStyle = this.getNodeColor(node);
this.ctx.fillRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
this.ctx.strokeStyle = '#ccc';
this.ctx.strokeRect(node.col * this.nodeSize, node.row * this.nodeSize, this.nodeSize, this.nodeSize);
}
private getNodeColor(): string {
private getNodeColor(node: Node): string {
if (node.alive)
{
return 'black';
}
return 'lightgray';
}
@@ -150,4 +202,88 @@ export class ConwayGol implements AfterViewInit {
el.height = this.gridRows * this.nodeSize;
}
//mouse listener
private onMouseDown(event: MouseEvent): void {
const pos = this.getGridPosition(event);
if (!pos) {
return;
}
this.isDrawing = true;
this.lastCell = null;
this.applySelectionAt(pos);
}
private onMouseMove(event: MouseEvent): void {
if (!this.isDrawing) {
return;
}
const pos = this.getGridPosition(event);
if (!pos) {
return;
}
if (this.isSameCell(pos, this.lastCell)) {
return;
}
this.applySelectionAt(pos);
}
private onMouseUp(): void {
this.isDrawing = false;
this.lastCell = null;
}
// Mouse -> grid cell
private getGridPosition(event: MouseEvent | TouchEvent): GridPos | null {
const canvas = this.canvas.nativeElement;
const rect = canvas.getBoundingClientRect();
let clientX, clientY;
if (event instanceof MouseEvent) {
clientX = event.clientX;
clientY = event.clientY;
} else if (event instanceof TouchEvent && event.touches.length > 0) {
clientX = event.touches[0].clientX;
clientY = event.touches[0].clientY;
} else {
return null;
}
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const x = (clientX - rect.left) * scaleX;
const y = (clientY - rect.top) * scaleY;
const col = Math.floor(x / this.nodeSize);
const row = Math.floor(y / this.nodeSize);
if (!this.isValidPosition(row, col)) {
return null;
}
return { row, col };
}
private isValidPosition(row: number, col: number): boolean {
return row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols;
}
private isSameCell(a: GridPos, b: GridPos | null): boolean {
return !!b && a.row === b.row && a.col === b.col;
}
private applySelectionAt(pos: GridPos): void {
const node = this.grid[pos.row][pos.col];
node.alive = !node.alive;
this.lastCell = pos;
this.drawNode(node);
}
protected readonly Scenario = Scenario;
protected readonly MIN_TIME_PER_GENERATION = MIN_TIME_PER_GENERATION;
protected readonly MAX_TIME_PER_GENERATION = MAX_TIME_PER_GENERATION;
}

View File

@@ -14,6 +14,4 @@ import {AlgorithmInformation} from './information.models';
export class Information {
@Input({ required: true }) algorithmInformation!: AlgorithmInformation;
protected readonly UrlConstants = UrlConstants;
}

View File

@@ -29,7 +29,7 @@
<div class="controls">
<div class="grid-size">
<mat-form-field appearance="outline" class="grid-field">
<mat-label>{{ 'PATHFINDING.GRID_HEIGHT' | translate }}</mat-label>
<mat-label>{{ 'ALGORITHM.GRID_HEIGHT' | translate }}</mat-label>
<input
matInput
type="number"
@@ -42,7 +42,7 @@
</mat-form-field>
<mat-form-field appearance="outline" class="grid-field">
<mat-label>{{ 'PATHFINDING.GRID_WIDTH' | translate }}</mat-label>
<mat-label>{{ 'ALGORITHM.GRID_WIDTH' | translate }}</mat-label>
<input
matInput
type="number"

View File

@@ -1,13 +1,3 @@
.container {
padding: 2rem;
}
.controls-container {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
}
.controls {
display: flex;
flex-wrap: wrap;
@@ -24,26 +14,3 @@
.grid-field {
width: 150px;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
font-size: 0.9em;
.legend-color {
display: inline-block;
width: 15px;
height: 15px;
border: 1px solid #ccc;
vertical-align: middle;
margin-right: 5px;
&.start { background-color: green; }
&.end { background-color: red; }
&.wall { background-color: black; }
&.visited { background-color: skyblue; }
&.path { background-color: gold; }
}
}

View File

@@ -664,6 +664,4 @@ export class PathfindingComponent implements AfterViewInit {
private randomIntFromInterval(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1) + min);
}
protected readonly UrlConstants = UrlConstants;
}

View File

@@ -1,4 +1,4 @@
<div class="sorting-container">
<div class="container">
<mat-card class="sorting-card">
<mat-card-header>
<mat-card-title>{{ 'SORTING.TITLE' | translate }}</mat-card-title>

View File

@@ -1,11 +1,3 @@
.sorting-container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
height: 100%;
box-sizing: border-box;
.sorting-card {
width: 100%;
max-width: 1200px;
@@ -59,4 +51,3 @@
color: #FFFFFF;
}
}
}

View File

@@ -171,6 +171,4 @@ export class SortingComponent implements OnInit {
this.stopAnimations();
this.resetSortState();
}
protected readonly UrlConstants = UrlConstants;
}

View File

@@ -317,9 +317,7 @@
},
"ALERT": {
"START_END_NODES": "Bitte wählen Sie einen Start- und Endknoten aus, bevor Sie den Algorithmus starten."
},
"GRID_HEIGHT": "Höhe",
"GRID_WIDTH": "Beite"
}
},
"SORTING": {
"TITLE": "Sortieralgorithmen",
@@ -345,6 +343,11 @@
"GOL": {
"TITLE": "Conway's Spiel des Lebens",
"START": "Starten",
"RANDOM_SCENE": "Zufällig",
"EMPTY_SCENE": "Leer",
"ALIVE": "Lebend",
"DEAD": "Leer",
"SPEED": "Zeit pro Generation",
"EXPLANATION": {
"TITLE": "Erklärung",
"EXPLANATION" : "Das Spiel läuft schrittweise ab. Zunächst wird eine Anfangsgeneration von lebenden Zellen auf dem Spielfeld definiert. Aus der vorliegenden Generation (dem Gesamtbild des Spielfeldes) wird die Folgegeneration ermittelt. Der Zustand jeder einzelnen Zelle in der Folgegeneration ergibt sich dabei nach einfachen Regeln aus ihrem aktuellen Zustand sowie den aktuellen Zuständen ihrer acht Nachbarzellen (Moore-Nachbarschaft).",
@@ -369,6 +372,8 @@
"TITLE": "Conway's Game of Life",
"DESCRIPTION": "Das 'Spiel des Lebens' ist ein vom Mathematiker John Horton Conway 1970 entworfenes Spiel."
},
"NOTE": "HINWEIS"
"NOTE": "HINWEIS",
"GRID_HEIGHT": "Höhe",
"GRID_WIDTH": "Beite"
}
}

View File

@@ -317,9 +317,7 @@
},
"ALERT": {
"START_END_NODES": "Please select a start and end node before running the algorithm."
},
"GRID_HEIGHT": "Height",
"GRID_WIDTH": "Width"
}
},
"SORTING": {
"TITLE": "Sorting Algorithms",
@@ -344,6 +342,11 @@
"GOL": {
"TITLE": "Conway's Game of Life",
"START": "Start",
"RANDOM_SCENE": "Random",
"EMPTY_SCENE": "Empty",
"ALIVE": "Alive",
"DEAD": "Empty",
"SPEED": "Time per Generation",
"EXPLANATION": {
"TITLE": "Erklärung",
"EXPLANATION" : "Das Spiel läuft schrittweise ab. Zunächst wird eine Anfangsgeneration von lebenden Zellen auf dem Spielfeld definiert. Aus der vorliegenden Generation (dem Gesamtbild des Spielfeldes) wird die Folgegeneration ermittelt. Der Zustand jeder einzelnen Zelle in der Folgegeneration ergibt sich dabei nach einfachen Regeln aus ihrem aktuellen Zustand sowie den aktuellen Zuständen ihrer acht Nachbarzellen (Moore-Nachbarschaft).",
@@ -368,6 +371,8 @@
"TITLE:": "Conway's Game of Life",
"DESCRIPTION": "The Game of Life is a cellular automaton devised by the British mathematician John Horton Conway in 1970."
},
"NOTE": "Note"
"NOTE": "Note",
"GRID_HEIGHT": "Height",
"GRID_WIDTH": "Width"
}
}

View File

@@ -213,6 +213,11 @@ a {
}
// algos
.container {
padding: 2rem;
}
.algo-info {
margin: 0 0 1rem 0;
padding: 0.75rem 1rem;
@@ -248,7 +253,38 @@ a {
}
canvas {
border: 1px solid #ccc;
border: 1px solid lightgray;
display: block;
max-width: 100%;
}
.legend {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
font-size: 0.9em;
.legend-color {
display: inline-block;
width: 15px;
height: 15px;
border: 1px solid lightgray;
vertical-align: middle;
margin-right: 5px;
&.start { background-color: green; }
&.end { background-color: red; }
&.wall { background-color: black; }
&.visited { background-color: skyblue; }
&.path { background-color: gold; }
&.empty { background-color: lightgray; }
&.alive { background-color: black; }
}
}
.controls-container {
display: flex;
flex-direction: column;
margin-bottom: 1rem;
}