@jjlmoya/utils-science 1.38.0 → 1.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +3 -1
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/conway-life-rule-lab/bibliography.astro +14 -0
  8. package/src/tool/conway-life-rule-lab/bibliography.ts +16 -0
  9. package/src/tool/conway-life-rule-lab/component.astro +132 -0
  10. package/src/tool/conway-life-rule-lab/conway-life-rule-lab.css +603 -0
  11. package/src/tool/conway-life-rule-lab/entry.ts +26 -0
  12. package/src/tool/conway-life-rule-lab/i18n/de.ts +50 -0
  13. package/src/tool/conway-life-rule-lab/i18n/en.ts +174 -0
  14. package/src/tool/conway-life-rule-lab/i18n/es.ts +50 -0
  15. package/src/tool/conway-life-rule-lab/i18n/fr.ts +50 -0
  16. package/src/tool/conway-life-rule-lab/i18n/id.ts +50 -0
  17. package/src/tool/conway-life-rule-lab/i18n/it.ts +50 -0
  18. package/src/tool/conway-life-rule-lab/i18n/ja.ts +50 -0
  19. package/src/tool/conway-life-rule-lab/i18n/ko.ts +50 -0
  20. package/src/tool/conway-life-rule-lab/i18n/nl.ts +50 -0
  21. package/src/tool/conway-life-rule-lab/i18n/pl.ts +50 -0
  22. package/src/tool/conway-life-rule-lab/i18n/pt.ts +50 -0
  23. package/src/tool/conway-life-rule-lab/i18n/ru.ts +50 -0
  24. package/src/tool/conway-life-rule-lab/i18n/sv.ts +50 -0
  25. package/src/tool/conway-life-rule-lab/i18n/tr.ts +50 -0
  26. package/src/tool/conway-life-rule-lab/i18n/zh.ts +50 -0
  27. package/src/tool/conway-life-rule-lab/index.ts +11 -0
  28. package/src/tool/conway-life-rule-lab/logic/LifeAchievements.ts +85 -0
  29. package/src/tool/conway-life-rule-lab/logic/LifeCanvasRenderer.ts +104 -0
  30. package/src/tool/conway-life-rule-lab/logic/LifeLabDom.ts +55 -0
  31. package/src/tool/conway-life-rule-lab/logic/LifeLabRuntime.ts +253 -0
  32. package/src/tool/conway-life-rule-lab/logic/LifePatterns.ts +35 -0
  33. package/src/tool/conway-life-rule-lab/logic/LifeRules.ts +60 -0
  34. package/src/tool/conway-life-rule-lab/logic/LifeUniverse.ts +165 -0
  35. package/src/tool/conway-life-rule-lab/logic.ts +6 -0
  36. package/src/tool/conway-life-rule-lab/seo.astro +15 -0
  37. package/src/tools.ts +2 -0
@@ -0,0 +1,253 @@
1
+ import { LIFE_PATTERNS } from './LifePatterns';
2
+ import { LifeAchievements } from './LifeAchievements';
3
+ import { LifeCanvasRenderer } from './LifeCanvasRenderer';
4
+ import { fillPatternSelect, getLabels, getOutputs, type RuntimeLabels } from './LifeLabDom';
5
+ import { parseRuleNotation } from './LifeRules';
6
+ import { LifeUniverse, type LifeSnapshot } from './LifeUniverse';
7
+
8
+ interface CellPoint {
9
+ x: number;
10
+ y: number;
11
+ }
12
+
13
+ const grid = { width: 72, height: 48 };
14
+
15
+ export function startLifeLab(root: HTMLElement): void {
16
+ new LifeLabRuntime(root).start();
17
+ }
18
+
19
+ class LifeLabRuntime {
20
+ private readonly universe = new LifeUniverse(grid.width, grid.height);
21
+ private ruleSet = parseRuleNotation('B3/S23');
22
+ private running = false;
23
+ private lastTick = 0;
24
+ private latestPopulation = 0;
25
+ private drawing = false;
26
+ private drawAlive = true;
27
+ private lastDrawnCell = '';
28
+ private randomSeedDensity = 0.28;
29
+ private readonly labels: RuntimeLabels;
30
+
31
+ private readonly canvas: HTMLCanvasElement;
32
+ private readonly playButton: HTMLButtonElement;
33
+ private readonly stepButton: HTMLButtonElement;
34
+ private readonly clearButton: HTMLButtonElement;
35
+ private readonly randomizeButton: HTMLButtonElement;
36
+ private readonly placePatternButton: HTMLButtonElement;
37
+ private readonly ruleInput: HTMLInputElement;
38
+ private readonly speedInput: HTMLInputElement;
39
+ private readonly densityInput: HTMLInputElement;
40
+ private readonly patternSelect: HTMLSelectElement;
41
+ private readonly rulePresets: NodeListOf<HTMLButtonElement>;
42
+ private readonly outputs: Record<string, HTMLElement | null>;
43
+ private readonly renderer: LifeCanvasRenderer;
44
+ private readonly achievements: LifeAchievements;
45
+
46
+ public constructor(private readonly root: HTMLElement) {
47
+ this.canvas = this.byId('life-lab-canvas') as HTMLCanvasElement;
48
+ this.playButton = this.byId('life-lab-play') as HTMLButtonElement;
49
+ this.stepButton = this.byId('life-lab-step') as HTMLButtonElement;
50
+ this.clearButton = this.byId('life-lab-clear') as HTMLButtonElement;
51
+ this.randomizeButton = this.byId('life-lab-randomize') as HTMLButtonElement;
52
+ this.placePatternButton = this.byId('life-lab-place-pattern') as HTMLButtonElement;
53
+ this.ruleInput = this.byId('life-lab-rule') as HTMLInputElement;
54
+ this.speedInput = this.byId('life-lab-speed') as HTMLInputElement;
55
+ this.densityInput = this.byId('life-lab-seed-density') as HTMLInputElement;
56
+ this.patternSelect = this.byId('life-lab-pattern') as HTMLSelectElement;
57
+ this.rulePresets = document.querySelectorAll<HTMLButtonElement>('.life-lab-rule-preset');
58
+ this.outputs = getOutputs((id) => this.byId(id));
59
+ this.labels = getLabels(this.root, this.playButton);
60
+ this.renderer = new LifeCanvasRenderer(this.root, this.canvas, this.universe, grid);
61
+ this.achievements = new LifeAchievements({
62
+ pulsar: this.outputs.pulsar,
63
+ immortal: this.outputs.immortal,
64
+ bigBang: this.outputs.bigBang,
65
+ });
66
+ }
67
+
68
+ public start(): void {
69
+ fillPatternSelect(this.root, this.patternSelect);
70
+ this.bindEvents();
71
+ this.drawSnapshot(this.universe.randomize(this.randomSeedDensity));
72
+ this.rememberSeed();
73
+ }
74
+
75
+ private byId(id: string): HTMLElement {
76
+ return document.getElementById(id) as HTMLElement;
77
+ }
78
+
79
+ private bindEvents(): void {
80
+ this.playButton.addEventListener('click', () => this.setRunning(!this.running));
81
+ this.stepButton.addEventListener('click', () => this.stepOnce());
82
+ this.randomizeButton.addEventListener('click', () => this.randomize());
83
+ this.placePatternButton.addEventListener('click', () => this.placePattern());
84
+ this.clearButton.addEventListener('click', () => this.enterEditCanvas());
85
+ this.ruleInput.addEventListener('change', () => this.updateRule());
86
+ this.rulePresets.forEach((preset) => preset.addEventListener('click', () => this.selectRule(preset)));
87
+ this.canvas.addEventListener('pointerdown', (event) => this.startDrawing(event));
88
+ this.canvas.addEventListener('pointermove', (event) => this.continueDrawing(event));
89
+ this.canvas.addEventListener('pointerup', (event) => this.stopDrawing(event));
90
+ this.canvas.addEventListener('pointercancel', () => this.cancelDrawing());
91
+ window.addEventListener('resize', () => this.render());
92
+ }
93
+
94
+ private updateRule(): void {
95
+ this.rulePresets.forEach((preset) => preset.classList.toggle('active', preset.dataset.rule === this.ruleInput.value));
96
+ this.parseRuleFromInput();
97
+ }
98
+
99
+ private selectRule(preset: HTMLButtonElement): void {
100
+ this.rulePresets.forEach((button) => button.classList.remove('active'));
101
+ preset.classList.add('active');
102
+ this.ruleInput.value = preset.dataset.rule || 'B3/S23';
103
+ this.parseRuleFromInput();
104
+ }
105
+
106
+ private parseRuleFromInput(): void {
107
+ try {
108
+ this.ruleSet = parseRuleNotation(this.ruleInput.value);
109
+ this.ruleInput.classList.remove('life-lab-invalid');
110
+ this.ruleInput.removeAttribute('aria-label');
111
+ } catch {
112
+ this.ruleInput.classList.add('life-lab-invalid');
113
+ this.ruleInput.setAttribute('aria-label', this.labels.invalidRule);
114
+ }
115
+ }
116
+
117
+ private setRunning(next: boolean): void {
118
+ this.running = next;
119
+ this.playButton.setAttribute('aria-label', this.running ? this.labels.pause : this.labels.play);
120
+ this.playButton.classList.toggle('life-lab-running', this.running);
121
+ this.playButton.classList.toggle('life-lab-paused-icon', this.running);
122
+ if (this.running) requestAnimationFrame((time) => this.loop(time));
123
+ }
124
+
125
+ private loop(time: number): void {
126
+ if (!this.running) return;
127
+ const interval = 1000 / parseInt(this.speedInput.value, 10);
128
+ if (time - this.lastTick >= interval) this.stepAt(time);
129
+ requestAnimationFrame((nextTime) => this.loop(nextTime));
130
+ }
131
+
132
+ private stepAt(time: number): void {
133
+ this.parseRuleFromInput();
134
+ this.processSnapshot(this.universe.step(this.ruleSet));
135
+ this.lastTick = time;
136
+ }
137
+
138
+ private stepOnce(): void {
139
+ this.parseRuleFromInput();
140
+ this.processSnapshot(this.universe.step(this.ruleSet));
141
+ }
142
+
143
+ private processSnapshot(snapshot: LifeSnapshot): void {
144
+ this.drawSnapshot(snapshot);
145
+ this.achievements.evaluate(snapshot, this.universe.getCells(), this.randomSeedDensity);
146
+ }
147
+
148
+ private randomize(): void {
149
+ this.randomSeedDensity = parseInt(this.densityInput.value, 10) / 100;
150
+ this.drawSnapshot(this.universe.randomize(this.randomSeedDensity));
151
+ this.rememberSeed();
152
+ this.exitEditCanvas();
153
+ }
154
+
155
+ private placePattern(): void {
156
+ const pattern = LIFE_PATTERNS.find((item) => item.id === this.patternSelect.value) || LIFE_PATTERNS[0];
157
+ this.drawSnapshot(this.universe.clear());
158
+ this.drawSnapshot(this.universe.placePattern(pattern, Math.floor(grid.width * 0.36), Math.floor(grid.height * 0.36)));
159
+ this.randomSeedDensity = 1;
160
+ this.rememberSeed();
161
+ this.exitEditCanvas();
162
+ }
163
+
164
+ private enterEditCanvas(): void {
165
+ this.setRunning(false);
166
+ this.drawSnapshot(this.universe.clear());
167
+ this.rememberSeed();
168
+ this.root.classList.add('life-lab-editing');
169
+ }
170
+
171
+ private exitEditCanvas(): void {
172
+ this.root.classList.remove('life-lab-editing');
173
+ }
174
+
175
+ private drawSnapshot(snapshot: LifeSnapshot): void {
176
+ this.updateStats(snapshot);
177
+ this.render();
178
+ }
179
+
180
+ private updateStats(snapshot: LifeSnapshot): void {
181
+ const stats = snapshot.stats;
182
+ this.latestPopulation = stats.population;
183
+ this.setText('generation', stats.generation.toLocaleString());
184
+ this.setText('population', stats.population.toLocaleString());
185
+ this.setText('density', `${(stats.density * 100).toFixed(1)}%`);
186
+ this.setText('stability', `${(stats.stability * 100).toFixed(0)}%`);
187
+ this.setText('births', stats.births.toLocaleString());
188
+ this.setText('deaths', stats.deaths.toLocaleString());
189
+ this.setText('status', this.statusFor(stats.population, stats.births, stats.deaths, stats.stability));
190
+ }
191
+
192
+ private setText(key: string, value: string): void {
193
+ const output = this.outputs[key];
194
+ if (output) output.textContent = value;
195
+ }
196
+
197
+ private statusFor(population: number, births: number, deaths: number, stability: number): string {
198
+ if (population === 0 || stability > 0.9) return this.labels.frozen;
199
+ return this.activityStatus(births, deaths);
200
+ }
201
+
202
+ private activityStatus(births: number, deaths: number): string {
203
+ if (births > deaths * 1.4) return this.labels.growing;
204
+ if (deaths > births * 1.4) return this.labels.fading;
205
+ return this.labels.chaotic;
206
+ }
207
+
208
+ private render(): void {
209
+ this.renderer.render(this.latestPopulation);
210
+ }
211
+ private startDrawing(event: PointerEvent): void {
212
+ if (this.running) return;
213
+ const cell = this.renderer.canvasToCell(event);
214
+ if (!cell) return;
215
+ this.drawAlive = this.universe.getCells()[cell.y * grid.width + cell.x] === 0;
216
+ this.drawing = true;
217
+ this.lastDrawnCell = '';
218
+ this.canvas.setPointerCapture(event.pointerId);
219
+ this.paintCellAt(cell);
220
+ }
221
+
222
+ private continueDrawing(event: PointerEvent): void {
223
+ if (!this.drawing || this.running) return;
224
+ const cell = this.renderer.canvasToCell(event);
225
+ if (cell) this.paintCellAt(cell);
226
+ }
227
+
228
+ private stopDrawing(event: PointerEvent): void {
229
+ this.drawing = false;
230
+ this.lastDrawnCell = '';
231
+ this.canvas.releasePointerCapture(event.pointerId);
232
+ this.rememberSeed();
233
+ this.root.classList.add('life-lab-editing');
234
+ }
235
+
236
+ private cancelDrawing(): void {
237
+ this.drawing = false;
238
+ this.lastDrawnCell = '';
239
+ }
240
+
241
+ private paintCellAt(cell: CellPoint): void {
242
+ const key = `${cell.x}:${cell.y}`;
243
+ if (key === this.lastDrawnCell) return;
244
+ this.lastDrawnCell = key;
245
+ this.drawSnapshot(this.universe.setCell(cell.x, cell.y, this.drawAlive));
246
+ }
247
+
248
+ private rememberSeed(): void {
249
+ this.achievements.resetHistory(this.universe.getCells());
250
+ }
251
+ }
252
+
253
+
@@ -0,0 +1,35 @@
1
+ export interface LifePattern {
2
+ id: string;
3
+ cells: Array<[number, number]>;
4
+ }
5
+
6
+ export const LIFE_PATTERNS: LifePattern[] = [
7
+ {
8
+ id: 'glider',
9
+ cells: [[1, 0], [2, 1], [0, 2], [1, 2], [2, 2]],
10
+ },
11
+ {
12
+ id: 'gosper',
13
+ cells: [
14
+ [24, 0], [22, 1], [24, 1], [12, 2], [13, 2], [20, 2], [21, 2], [34, 2], [35, 2],
15
+ [11, 3], [15, 3], [20, 3], [21, 3], [34, 3], [35, 3], [0, 4], [1, 4], [10, 4],
16
+ [16, 4], [20, 4], [21, 4], [0, 5], [1, 5], [10, 5], [14, 5], [16, 5], [17, 5],
17
+ [22, 5], [24, 5], [10, 6], [16, 6], [24, 6], [11, 7], [15, 7], [12, 8], [13, 8],
18
+ ],
19
+ },
20
+ {
21
+ id: 'pulsar',
22
+ cells: [
23
+ [2, 0], [3, 0], [4, 0], [8, 0], [9, 0], [10, 0],
24
+ [0, 2], [5, 2], [7, 2], [12, 2], [0, 3], [5, 3], [7, 3], [12, 3],
25
+ [0, 4], [5, 4], [7, 4], [12, 4], [2, 5], [3, 5], [4, 5], [8, 5], [9, 5], [10, 5],
26
+ [2, 7], [3, 7], [4, 7], [8, 7], [9, 7], [10, 7], [0, 8], [5, 8], [7, 8], [12, 8],
27
+ [0, 9], [5, 9], [7, 9], [12, 9], [0, 10], [5, 10], [7, 10], [12, 10],
28
+ [2, 12], [3, 12], [4, 12], [8, 12], [9, 12], [10, 12],
29
+ ],
30
+ },
31
+ {
32
+ id: 'r-pentomino',
33
+ cells: [[1, 0], [2, 0], [0, 1], [1, 1], [1, 2]],
34
+ },
35
+ ];
@@ -0,0 +1,60 @@
1
+ export type NeighborCount = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
2
+
3
+ export interface LifeRuleSet {
4
+ birth: Set<NeighborCount>;
5
+ survival: Set<NeighborCount>;
6
+ notation: string;
7
+ }
8
+
9
+ export interface RulePreset {
10
+ id: string;
11
+ notation: string;
12
+ }
13
+
14
+ const countPattern = /^[0-8]*$/;
15
+
16
+ export const RULE_PRESETS: RulePreset[] = [
17
+ {
18
+ id: 'classic',
19
+ notation: 'B3/S23',
20
+ },
21
+ {
22
+ id: 'highlife',
23
+ notation: 'B36/S23',
24
+ },
25
+ {
26
+ id: 'seeds',
27
+ notation: 'B2/S',
28
+ },
29
+ {
30
+ id: 'day-night',
31
+ notation: 'B3678/S34678',
32
+ },
33
+ ];
34
+
35
+ export function parseRuleNotation(input: string): LifeRuleSet {
36
+ const normalized = input.trim().toUpperCase().replace(/\s+/g, '');
37
+ const match = /^B([0-8]*)\/?S([0-8]*)$/.exec(normalized);
38
+
39
+ if (!match || !countPattern.test(match[1]) || !countPattern.test(match[2])) {
40
+ throw new Error('INVALID_RULE_NOTATION');
41
+ }
42
+
43
+ return {
44
+ birth: toCountSet(match[1]),
45
+ survival: toCountSet(match[2]),
46
+ notation: `B${match[1]}/S${match[2]}`,
47
+ };
48
+ }
49
+
50
+ export function formatRuleSet(ruleSet: LifeRuleSet): string {
51
+ return `B${formatCounts(ruleSet.birth)}/S${formatCounts(ruleSet.survival)}`;
52
+ }
53
+
54
+ function toCountSet(value: string): Set<NeighborCount> {
55
+ return new Set([...value].map((count) => Number(count) as NeighborCount));
56
+ }
57
+
58
+ function formatCounts(counts: Set<NeighborCount>): string {
59
+ return [...counts].sort((a, b) => a - b).join('');
60
+ }
@@ -0,0 +1,165 @@
1
+ import type { LifePattern } from './LifePatterns';
2
+ import type { LifeRuleSet, NeighborCount } from './LifeRules';
3
+
4
+ export interface LifeGenerationStats {
5
+ generation: number;
6
+ population: number;
7
+ births: number;
8
+ deaths: number;
9
+ stability: number;
10
+ density: number;
11
+ }
12
+
13
+ export interface LifeSnapshot {
14
+ cells: Uint8Array;
15
+ stats: LifeGenerationStats;
16
+ }
17
+
18
+ export class LifeUniverse {
19
+ private current: Uint8Array;
20
+ private next: Uint8Array;
21
+ private generation = 0;
22
+ private previousPopulation = 0;
23
+
24
+ public constructor(
25
+ public readonly width: number,
26
+ public readonly height: number,
27
+ ) {
28
+ this.current = new Uint8Array(width * height);
29
+ this.next = new Uint8Array(width * height);
30
+ }
31
+
32
+ public clear(): LifeSnapshot {
33
+ this.current.fill(0);
34
+ this.next.fill(0);
35
+ this.generation = 0;
36
+ this.previousPopulation = 0;
37
+ return this.snapshot(0, 0);
38
+ }
39
+
40
+ public load(cells: Uint8Array): LifeSnapshot {
41
+ if (cells.length !== this.current.length) {
42
+ throw new Error('INVALID_LIFE_SNAPSHOT');
43
+ }
44
+
45
+ this.current.set(cells);
46
+ this.next.fill(0);
47
+ this.generation = 0;
48
+ this.previousPopulation = this.current.reduce((sum, alive) => sum + alive, 0);
49
+ return this.snapshot(0, 0);
50
+ }
51
+
52
+ public randomize(fillRatio: number): LifeSnapshot {
53
+ const clamped = Math.max(0, Math.min(1, fillRatio));
54
+ let population = 0;
55
+
56
+ for (let index = 0; index < this.current.length; index += 1) {
57
+ const alive = Math.random() < clamped ? 1 : 0;
58
+ this.current[index] = alive;
59
+ population += alive;
60
+ }
61
+
62
+ this.generation = 0;
63
+ this.previousPopulation = population;
64
+ return this.snapshot(population, 0);
65
+ }
66
+
67
+ public toggle(x: number, y: number): LifeSnapshot {
68
+ const index = this.index(x, y);
69
+ this.current[index] = this.current[index] ? 0 : 1;
70
+ return this.snapshot(0, 0);
71
+ }
72
+
73
+ public setCell(x: number, y: number, alive: boolean): LifeSnapshot {
74
+ this.current[this.index(x, y)] = alive ? 1 : 0;
75
+ return this.snapshot(0, 0);
76
+ }
77
+
78
+ public placePattern(pattern: LifePattern, originX: number, originY: number): LifeSnapshot {
79
+ for (const [x, y] of pattern.cells) {
80
+ this.current[this.index(originX + x, originY + y)] = 1;
81
+ }
82
+
83
+ return this.snapshot(0, 0);
84
+ }
85
+
86
+ public step(ruleSet: LifeRuleSet): LifeSnapshot {
87
+ let births = 0;
88
+ let deaths = 0;
89
+ let population = 0;
90
+ const previousPopulation = this.previousPopulation;
91
+
92
+ for (let y = 0; y < this.height; y += 1) {
93
+ for (let x = 0; x < this.width; x += 1) {
94
+ const result = this.stepCell(x, y, ruleSet);
95
+ population += result.population;
96
+ births += result.births;
97
+ deaths += result.deaths;
98
+ }
99
+ }
100
+
101
+ [this.current, this.next] = [this.next, this.current];
102
+ this.next.fill(0);
103
+ this.generation += 1;
104
+ this.previousPopulation = population;
105
+ return this.snapshot(births, deaths, previousPopulation);
106
+ }
107
+
108
+ public getCells(): Uint8Array {
109
+ return this.current;
110
+ }
111
+
112
+ private snapshot(births: number, deaths: number, previousPopulation = this.previousPopulation): LifeSnapshot {
113
+ const population = this.current.reduce((sum, alive) => sum + alive, 0);
114
+ const changed = births + deaths;
115
+ const stability = population === 0 ? 1 : Math.max(0, 1 - changed / Math.max(population, previousPopulation, 1));
116
+
117
+ return {
118
+ cells: this.current,
119
+ stats: {
120
+ generation: this.generation,
121
+ population,
122
+ births,
123
+ deaths,
124
+ stability,
125
+ density: population / this.current.length,
126
+ },
127
+ };
128
+ }
129
+
130
+ private countNeighbors(x: number, y: number): NeighborCount {
131
+ let total = 0;
132
+
133
+ for (let dy = -1; dy <= 1; dy += 1) {
134
+ for (let dx = -1; dx <= 1; dx += 1) {
135
+ if (dx === 0 && dy === 0) continue;
136
+ total += this.current[this.index(x + dx, y + dy)];
137
+ }
138
+ }
139
+
140
+ return total as NeighborCount;
141
+ }
142
+
143
+ private willLive(alive: boolean, neighbors: NeighborCount, ruleSet: LifeRuleSet): boolean {
144
+ if (alive) return ruleSet.survival.has(neighbors);
145
+ return ruleSet.birth.has(neighbors);
146
+ }
147
+
148
+ private stepCell(x: number, y: number, ruleSet: LifeRuleSet): { population: number; births: number; deaths: number } {
149
+ const index = this.index(x, y);
150
+ const alive = this.current[index] === 1;
151
+ const nextAlive = this.willLive(alive, this.countNeighbors(x, y), ruleSet);
152
+ this.next[index] = nextAlive ? 1 : 0;
153
+ return {
154
+ population: nextAlive ? 1 : 0,
155
+ births: !alive && nextAlive ? 1 : 0,
156
+ deaths: alive && !nextAlive ? 1 : 0,
157
+ };
158
+ }
159
+
160
+ private index(x: number, y: number): number {
161
+ const wrappedX = (x + this.width) % this.width;
162
+ const wrappedY = (y + this.height) % this.height;
163
+ return wrappedY * this.width + wrappedX;
164
+ }
165
+ }
@@ -0,0 +1,6 @@
1
+ export { LIFE_PATTERNS } from './logic/LifePatterns';
2
+ export type { LifePattern } from './logic/LifePatterns';
3
+ export { RULE_PRESETS, formatRuleSet, parseRuleNotation } from './logic/LifeRules';
4
+ export type { LifeRuleSet, NeighborCount, RulePreset } from './logic/LifeRules';
5
+ export { LifeUniverse } from './logic/LifeUniverse';
6
+ export type { LifeGenerationStats, LifeSnapshot } from './logic/LifeUniverse';
@@ -0,0 +1,15 @@
1
+ ---
2
+ import { SEORenderer } from '@jjlmoya/utils-shared';
3
+ import { conwayLifeRuleLab } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props {
7
+ locale?: KnownLocale;
8
+ }
9
+
10
+ const { locale = 'en' } = Astro.props;
11
+ const content = await conwayLifeRuleLab.i18n[locale]?.();
12
+ if (!content) return null;
13
+ ---
14
+
15
+ {content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
package/src/tools.ts CHANGED
@@ -21,6 +21,7 @@ import { THREE_BODY_PROBLEM_TOOL } from './tool/three-body-problem/index';
21
21
  import { ROCHE_LIMIT_SATELLITE_DISRUPTION_TOOL } from './tool/roche-limit-satellite-disruption/index';
22
22
  import { DYSON_SPHERE_ENERGY_CAPTURE_TOOL } from './tool/dyson-sphere-energy-capture/index';
23
23
  import { GLOBAL_ALBEDO_SNOWBALL_SIMULATOR_TOOL } from './tool/global-albedo-snowball-simulator/index';
24
+ import { CONWAY_LIFE_RULE_LAB_TOOL } from './tool/conway-life-rule-lab/index';
24
25
 
25
26
  export const ALL_TOOLS: ToolDefinition[] = [
26
27
  COLONY_COUNTER_TOOL,
@@ -44,4 +45,5 @@ export const ALL_TOOLS: ToolDefinition[] = [
44
45
  ROCHE_LIMIT_SATELLITE_DISRUPTION_TOOL,
45
46
  DYSON_SPHERE_ENERGY_CAPTURE_TOOL,
46
47
  GLOBAL_ALBEDO_SNOWBALL_SIMULATOR_TOOL,
48
+ CONWAY_LIFE_RULE_LAB_TOOL,
47
49
  ];