diff --git a/src/app/components/layout-list/layout-list.component.html b/src/app/components/layout-list/layout-list.component.html
index 2a0f58be..c3866ba6 100644
--- a/src/app/components/layout-list/layout-list.component.html
+++ b/src/app/components/layout-list/layout-list.component.html
@@ -21,7 +21,7 @@
@if (group.isRandom) {
@for (item of group.layouts; track item) {
-
@if (item.visible) {
-
+
} @else {
}
-
{{ item.layout.name }}
+
+
+
}
diff --git a/src/app/components/layout-list/layout-list.component.scss b/src/app/components/layout-list/layout-list.component.scss
index 1adfeabe..43111898 100644
--- a/src/app/components/layout-list/layout-list.component.scss
+++ b/src/app/components/layout-list/layout-list.component.scss
@@ -258,7 +258,7 @@
@include mixins.respond-to(tiny-down) {
width: 96%;
- .preview-name {
+ .preview-name, preview-seed {
padding-top: 0;
}
}
@@ -310,6 +310,30 @@
.previews-random {
grid-template-columns: 1fr 1fr 1fr 1fr 2fr;
+
+ .preview-seed {
+ position: absolute;
+ bottom: 0;
+ width: 100%;
+ background-color: var(--card-text-color-bg);
+ box-sizing: border-box;
+
+ .seed-input {
+ width: 100%;
+ background: transparent;
+ border: none;
+ color: var(--card-text-color);
+ font-size: 0.8em;
+ text-align: center;
+ outline: none;
+ cursor: text;
+ padding: 1px 1px 4px;
+
+ &:focus {
+ background-color: var(--card-text-color-bg-hover);
+ }
+ }
+ }
}
.preview {
diff --git a/src/app/components/layout-list/layout-list.component.ts b/src/app/components/layout-list/layout-list.component.ts
index 4e6cbd32..270b68d1 100644
--- a/src/app/components/layout-list/layout-list.component.ts
+++ b/src/app/components/layout-list/layout-list.component.ts
@@ -9,6 +9,7 @@ import { DeferLoadScrollHostDirective } from '../../directives/defer-load/defer-
import { DeferLoadDirective } from '../../directives/defer-load/defer-load.directive';
import { generateRandomMapping } from '../../model/random-layout/random-layout';
import { RANDOM_LAYOUT_ID_PREFIX, type RandomSymmetry } from '../../model/random-layout/consts';
+import { seedRNG, resetRNG, generateLayoutSeed } from '../../model/rng';
import { TranslateGroupPipe } from '../../pipes/translate-group.pipe';
export interface LayoutItem {
@@ -26,6 +27,15 @@ export interface LayoutGroup {
layouts: Array;
}
+export interface RandomLayoutItem extends LayoutItem {
+ layoutSeed?: string;
+}
+
+export interface RandomLayoutGroup extends LayoutGroup {
+ isRandom: true;
+ layouts: Array;
+}
+
@Component({
selector: 'app-layout-list',
templateUrl: './layout-list.component.html',
@@ -39,7 +49,7 @@ export class LayoutListComponent implements OnInit, OnChanges {
groups: Array = [];
randomMirrorX: string = 'random';
randomMirrorY: string = 'random';
- randomGroup: LayoutGroup = {
+ randomGroup: RandomLayoutGroup = {
name: '',
layouts: [], expanded: true, isRandom: true
};
@@ -57,6 +67,7 @@ export class LayoutListComponent implements OnInit, OnChanges {
buildRandomGroup() {
this.randomGroup.name = this.translate.instant('RANDOM_GROUP');
const layoutName = this.translate.instant('RANDOM_LAYOUT');
+ this.randomGroup.layouts = [];
for (let index = 0; index < 4; index++) {
this.randomGroup.layouts.push(
{
@@ -85,12 +96,15 @@ export class LayoutListComponent implements OnInit, OnChanges {
this.generateRandomLayouts();
}
- generateRandomLayout(layoutItem: LayoutItem): void {
+ generateRandomLayout(layoutItem: RandomLayoutItem, layoutSeed?: string): void {
+ layoutItem.layoutSeed = layoutSeed ?? generateLayoutSeed();
+ seedRNG(layoutItem.layoutSeed);
const mapping = generateRandomMapping(
this.randomMirrorX as RandomSymmetry,
this.randomMirrorY as RandomSymmetry,
'random'
);
+ resetRNG();
layoutItem.layout.previewSVG = this.layoutService.generatePreview(mapping);
layoutItem.layout.mapping = mapping;
}
@@ -103,6 +117,13 @@ export class LayoutListComponent implements OnInit, OnChanges {
}
}
+ regenerateWithSeed(item: RandomLayoutItem, seed: string): void {
+ const trimmed = seed.trim();
+ if (trimmed) {
+ this.generateRandomLayout(item, trimmed);
+ }
+ }
+
ngOnChanges(_changes: SimpleChanges): void {
this.refresh();
}
diff --git a/src/app/model/random-layout/base-layer-checker.ts b/src/app/model/random-layout/base-layer-checker.ts
index 1c00b3d0..bd348187 100644
--- a/src/app/model/random-layout/base-layer-checker.ts
+++ b/src/app/model/random-layout/base-layer-checker.ts
@@ -1,5 +1,6 @@
import type { Mapping, Place } from '../types';
import { blocksOverlap, key, randChoice, randInt, shuffleArray, buildUnitGrids, buildMappingFromSetZ0 } from './utilities';
+import { rng } from '../rng';
import type { BaseLayerOptions } from './consts';
function punchHoles(base: Set, baseZ: number, xs: Array, ys: Array, minHoles: number, maxHoles: number): void {
@@ -25,7 +26,7 @@ function punchHoles(base: Set, baseZ: number, xs: Array, ys: Arr
continue;
}
// 50% chance to remove small 2x1 or 1x2 block to create bigger gaps (now using 1-step grid)
- const orient = Math.random() < 0.5 ? 'h' : 'v';
+ const orient = rng() < 0.5 ? 'h' : 'v';
const removed: Array = [];
if (orient === 'h') {
removed.push(k);
@@ -62,10 +63,10 @@ function buildInitialChecker(present: Set, xs: Array, ys: Array<
}
function computeSideCuts(): { left: number; right: number; top: number; bottom: number } {
- const left = Math.random() < 0.5 ? 0 : randChoice([0, 0, 2, 2, 4]);
- const right = Math.random() < 0.5 ? 0 : randChoice([0, 0, 2, 2, 4]);
- const top = Math.random() < 0.5 ? 0 : randChoice([0, 2, 2, 4]);
- const bottom = Math.random() < 0.5 ? 0 : randChoice([0, 2, 2, 4]);
+ const left = rng() < 0.5 ? 0 : randChoice([0, 0, 2, 2, 4]);
+ const right = rng() < 0.5 ? 0 : randChoice([0, 0, 2, 2, 4]);
+ const top = rng() < 0.5 ? 0 : randChoice([0, 2, 2, 4]);
+ const bottom = rng() < 0.5 ? 0 : randChoice([0, 2, 2, 4]);
return { left, right, top, bottom };
}
diff --git a/src/app/model/random-layout/base-layer-diamond.ts b/src/app/model/random-layout/base-layer-diamond.ts
index 3760d27e..0cac07ce 100644
--- a/src/app/model/random-layout/base-layer-diamond.ts
+++ b/src/app/model/random-layout/base-layer-diamond.ts
@@ -1,5 +1,6 @@
import type { Mapping } from '../types';
import { generateBaseLayerWithShapes, shuffleArray } from './utilities';
+import { rng } from '../rng';
import type { BaseLayerOptions } from './consts';
// Diamond (rotated-square) outline inscribed in a bounding box of (w × h) tiles.
@@ -52,7 +53,7 @@ function diamondFilledCells(x0: number, y0: number, w: number, h: number): Array
}
export function diamondCells(x0: number, y0: number, w: number, h: number): Array<[number, number]> {
- const diamond = Math.random() < 0.5 ? diamondOutlineCells : diamondFilledCells;
+ const diamond = rng() < 0.5 ? diamondOutlineCells : diamondFilledCells;
return diamond(x0, y0, w, h);
}
diff --git a/src/app/model/random-layout/base-layer-lines.ts b/src/app/model/random-layout/base-layer-lines.ts
index 874e2040..679f20a9 100644
--- a/src/app/model/random-layout/base-layer-lines.ts
+++ b/src/app/model/random-layout/base-layer-lines.ts
@@ -1,5 +1,6 @@
import type { Mapping, Place } from '../types';
import { blocksOverlap, inBounds, key, randChoice, randInt, shuffleArray, buildUnitGrids, buildMappingFromSetZ0 } from './utilities';
+import { rng } from '../rng';
import type { BaseLayerOptions } from './consts';
function addBaseLine(present: Set, mapping: Mapping, snakeKeys: Set, x: number, y: number, markSnake = false): boolean {
@@ -49,10 +50,10 @@ function buildTryDirections(direction: [number, number], wobble: number): Array<
const rev: [number, number] = [-direction[0], -direction[1]];
const orth: Array<[number, number]> = direction[0] === 0 ? [[1, 0], [-1, 0]] : [[0, 1], [0, -1]];
let tryDirections: Array<[number, number]> = [cont, ...orth, rev];
- if (Math.random() < wobble) {
+ if (rng() < wobble) {
tryDirections = shuffleArray(tryDirections);
}
- if (Math.random() < 0.2) {
+ if (rng() < 0.2) {
tryDirections = [...shuffleArray([...orth]), cont, rev];
}
return tryDirections;
@@ -86,10 +87,10 @@ function tryBurstFallback(present: Set, mapping: Mapping, snakeKeys: Set
}
function trimEdges(present: Set, snakeKeys: Set, xs: Array, ys: Array, xMax: number, yMax: number): void {
- const leftCut = Math.random() < 0.4 ? randChoice([0, 0, 2]) : 0;
- const rightCut = Math.random() < 0.4 ? randChoice([0, 0, 2]) : 0;
- const topCut = Math.random() < 0.4 ? randChoice([0, 2]) : 0;
- const bottomCut = Math.random() < 0.4 ? randChoice([0, 2]) : 0;
+ const leftCut = rng() < 0.4 ? randChoice([0, 0, 2]) : 0;
+ const rightCut = rng() < 0.4 ? randChoice([0, 0, 2]) : 0;
+ const topCut = rng() < 0.4 ? randChoice([0, 2]) : 0;
+ const bottomCut = rng() < 0.4 ? randChoice([0, 2]) : 0;
for (const yy of ys) {
for (const xx of xs) {
if (xx < leftCut || xx > xMax - rightCut || yy < topCut || yy > yMax - bottomCut) {
@@ -175,13 +176,13 @@ export function generateBaseLayerLines({ minTarget, maxTarget, xMax, yMax }: Bas
let x = sx;
let y = sy;
const directionsAll: Array<[number, number]> = [[1, 0], [-1, 0], [0, 1], [0, -1]];
- let direction: [number, number] = directionsAll[Math.floor(Math.random() * 4)];
+ let direction: [number, number] = directionsAll[randInt(0, 3)];
const targetLength = computeSnakeTargetLength(xMax, yMax);
let stuckCount = 0;
const maxStuckAttempts = 10;
for (let step = 0; step < targetLength; step++) {
- const wobble = (step % randInt(6, 12)) === 0 ? Math.random() * 0.6 + 0.2 : Math.random() * 0.3;
+ const wobble = (step % randInt(6, 12)) === 0 ? rng() * 0.6 + 0.2 : rng() * 0.3;
const moved = tryMoveStep(present, mapping, snakeKeys, x, y, direction, wobble);
if (moved) {
x = moved.x;
@@ -204,7 +205,7 @@ export function generateBaseLayerLines({ minTarget, maxTarget, xMax, yMax }: Bas
break; // Give up after multiple attempts to find a valid move
}
// Try a random direction from current position as a last resort
- const randomDirection = directionsAll[Math.floor(Math.random() * 4)];
+ const randomDirection = directionsAll[randInt(0, 3)];
const randomX = x + randomDirection[0] * 2;
const randomY = y + randomDirection[1] * 2;
if (inBounds(randomX, randomY, 0) && addBaseLine(present, mapping, snakeKeys, randomX, randomY, true)) {
diff --git a/src/app/model/random-layout/base-layer-shapes.ts b/src/app/model/random-layout/base-layer-shapes.ts
index dfe1d526..1b632c39 100644
--- a/src/app/model/random-layout/base-layer-shapes.ts
+++ b/src/app/model/random-layout/base-layer-shapes.ts
@@ -1,5 +1,5 @@
import type { Mapping } from '../types';
-import { type CellsFunction, generateBaseLayerWithShapes, shuffleArray } from './utilities';
+import { type CellsFunction, generateBaseLayerWithShapes, randInt, shuffleArray } from './utilities';
import type { BaseLayerOptions } from './consts';
import { crossCells } from './base-layer-cross';
import { diamondCells } from './base-layer-diamond';
@@ -18,7 +18,7 @@ export function generateBaseLayerShapes(options: BaseLayerOptions): Mapping {
}
shuffleArray(allSizes);
const mixedCells: CellsFunction = (x0, y0, w, h) => {
- const shapeCells = shapeFunctions[Math.floor(Math.random() * shapeFunctions.length)];
+ const shapeCells = shapeFunctions[randInt(0, shapeFunctions.length - 1)];
return shapeCells(x0, y0, w, h);
};
return generateBaseLayerWithShapes(allSizes, mixedCells, options);
diff --git a/src/app/model/random-layout/base-layer.ts b/src/app/model/random-layout/base-layer.ts
index 196b97f1..f8cef073 100644
--- a/src/app/model/random-layout/base-layer.ts
+++ b/src/app/model/random-layout/base-layer.ts
@@ -8,7 +8,7 @@ import { generateBaseLayerCross } from './base-layer-cross';
import { generateBaseLayerDiamond } from './base-layer-diamond';
import { generateBaseLayerTriangle } from './base-layer-triangle';
import { generateBaseLayerShapes } from './base-layer-shapes';
-import { blocksOverlap, inBounds, key } from './utilities';
+import { blocksOverlap, inBounds, key, randInt } from './utilities';
function mirrorBaseLayer(mirrorX: boolean, mirrorY: boolean, baseLayer: Mapping): Mapping {
if ((!mirrorX && !mirrorY) || baseLayer.length === 0) {
@@ -116,8 +116,8 @@ export function generateBaseLayerMode(mirrorX: boolean, mirrorY: boolean, mode:
const yRangeMin = mirrorY ? 6 : 12;
const yRangeMax = mirrorY ? Math.floor(Y_MAX / 2) : Y_MAX;
// choose extents favoring mid-size boards
- const xMax = Math.floor(Math.random() * (xRangeMax - xRangeMin + 1)) + xRangeMin;
- const yMax = Math.floor(Math.random() * (yRangeMax - yRangeMin + 1)) + yRangeMin;
+ const xMax = randInt(xRangeMin, xRangeMax);
+ const yMax = randInt(yRangeMin, yRangeMax);
return generateBaseLayerChecker({ minTarget, maxTarget, xMax, yMax });
}
case 'lines': {
diff --git a/src/app/model/random-layout/random-layout.ts b/src/app/model/random-layout/random-layout.ts
index 78589668..d493afa7 100644
--- a/src/app/model/random-layout/random-layout.ts
+++ b/src/app/model/random-layout/random-layout.ts
@@ -1,6 +1,7 @@
import type { Mapping } from '../types';
import { type RandomBaseLayerMode, type RandomSymmetry, TARGET_COUNT } from './consts';
import { getRandomMode, hasMultipleLevels } from './utilities';
+import { rng } from '../rng';
import { generateBaseLayer } from './base-layer';
import { fillLayout } from './upper-layers';
import { optimizeMapping } from '../../modules/editor/model/optimize';
@@ -28,8 +29,8 @@ export function generateRandomMappingRaw(mirrorX: boolean, mirrorY: boolean, mod
export function generateRandomMapping(
mirrorX: RandomSymmetry, mirrorY: RandomSymmetry, mode: RandomBaseLayerMode
): Mapping {
- const symmetricX = mirrorX === 'random' ? Math.random() < 0.5 : (mirrorX === 'true');
- const symmetricY = mirrorY === 'random' ? Math.random() < 0.5 : (mirrorY === 'true');
+ const symmetricX = mirrorX === 'random' ? rng() < 0.5 : (mirrorX === 'true');
+ const symmetricY = mirrorY === 'random' ? rng() < 0.5 : (mirrorY === 'true');
const baseLayerMode = mode === 'random' ? getRandomMode() : mode;
let mapping: Mapping = [];
let passes = 0;
diff --git a/src/app/model/random-layout/upper-layers.ts b/src/app/model/random-layout/upper-layers.ts
index d3a6c943..a04c1fb8 100644
--- a/src/app/model/random-layout/upper-layers.ts
+++ b/src/app/model/random-layout/upper-layers.ts
@@ -1,6 +1,7 @@
import type { Mapping } from '../types';
import { TARGET_COUNT, X_MAX, Y_MAX, Z_MAX } from './consts';
import { blocksOverlap, inBounds, isOdd, isSupported, key, type NonEmptyArray, randChoice, shuffleArray, tryAdd } from './utilities';
+import { rng } from '../rng';
function computeBelowWindow(current: Mapping, z: number): { minX: number; maxX: number; minY: number; maxY: number } | null {
let minX = X_MAX;
@@ -66,7 +67,7 @@ function bucketCandidates(present: Set, z: number, win: { minX: number;
function maybeProposeOverhangs(present: Set, z: number, win: { minX: number; maxX: number; minY: number; maxY: number }): Array<[number, number]> {
const overhangs: Array<[number, number]> = [];
const zb = z - 1;
- if (Math.random() < 0.25) {
+ if (rng() < 0.25) {
for (let y = win.minY; y <= win.maxY; y++) {
for (let x = win.minX; x <= win.maxX; x++) {
if (!present.has(key(zb, x, y))) {
@@ -91,7 +92,7 @@ function maybeProposeOverhangs(present: Set, z: number, win: { minX: num
}
function computeLevelBudget(remaining: number): number {
- let levelBudget = Math.trunc(Math.random() * remaining);
+ let levelBudget = Math.trunc(rng() * remaining);
levelBudget -= (levelBudget % 2);
return Math.max(levelBudget, 2);
}
diff --git a/src/app/model/random-layout/utilities.ts b/src/app/model/random-layout/utilities.ts
index b787ecf0..5f8fe184 100644
--- a/src/app/model/random-layout/utilities.ts
+++ b/src/app/model/random-layout/utilities.ts
@@ -1,8 +1,14 @@
import type { Mapping } from '../types';
import { type RandomBaseLayerMode, type BaseLayerOptions, X_MAX, Y_MAX, Z_MAX } from './consts';
-import { shuffleArray as shuffleArrayImpl } from '../array-utilities';
+import { rng } from '../rng';
-export { shuffleArray } from '../array-utilities';
+export function shuffleArray(array: Array): Array {
+ for (let index = array.length - 1; index > 0; index--) {
+ const swapIndex = Math.floor(rng() * (index + 1));
+ [array[index], array[swapIndex]] = [array[swapIndex], array[index]];
+ }
+ return array;
+}
export type NonEmptyArray = [T, ...Array];
@@ -11,11 +17,11 @@ export function key(z: number, x: number, y: number): string {
}
export function randInt(min: number, maxInclusive: number): number {
- return Math.floor(Math.random() * (maxInclusive - min + 1)) + min;
+ return Math.floor(rng() * (maxInclusive - min + 1)) + min;
}
export function randChoice(array: NonEmptyArray): T {
- return array[Math.floor(Math.random() * array.length)];
+ return array[Math.floor(rng() * array.length)];
}
export function inBounds(x: number, y: number, z: number): boolean {
@@ -237,8 +243,8 @@ export function generateBaseLayerWithShapes(
const usedSizes = new Set();
const anchors = buildEvenAnchors(xMax, yMax);
- shuffleArrayImpl(allSizes);
- shuffleArrayImpl(anchors);
+ shuffleArray(allSizes);
+ shuffleArray(anchors);
const tryPlace = (x0: number, y0: number, w: number, h: number): number => {
if (!canPlace(x0, y0, w, h, occupied, blocked, usedSizes, cellsFunction)) {
@@ -259,8 +265,8 @@ export function generateBaseLayerWithShapes(
// Phase 2: if still below minTarget, retry unused sizes with reshuffled anchors
if (total < minTarget) {
const remainingSizes = allSizes.filter(([w, h]) => !usedSizes.has(`${w}x${h}`));
- shuffleArrayImpl(remainingSizes);
- shuffleArrayImpl(anchors);
+ shuffleArray(remainingSizes);
+ shuffleArray(anchors);
total = placeSizesGeneric(total, remainingSizes, anchors, minTarget, maxTarget, tryPlace);
}
@@ -274,8 +280,8 @@ export function generateBaseLayerWithShapes(
allowReuse = true;
continue;
}
- shuffleArrayImpl(sizePool);
- shuffleArrayImpl(anchors);
+ shuffleArray(sizePool);
+ shuffleArray(anchors);
const previous = total;
total = placeSizesGeneric(total, sizePool, anchors, minTarget, maxTarget, tryPlace);
progress = total > previous;
diff --git a/src/app/model/rng.spec.ts b/src/app/model/rng.spec.ts
new file mode 100644
index 00000000..99eb3192
--- /dev/null
+++ b/src/app/model/rng.spec.ts
@@ -0,0 +1,102 @@
+import { rng, mulberry32, stringToSeed, seedRNG, resetRNG, generateLayoutSeed } from './rng';
+
+describe('rng', () => {
+ afterEach(() => {
+ resetRNG();
+ });
+
+ describe('mulberry32', () => {
+ it('produces the same sequence for the same seed', () => {
+ const gen1 = mulberry32(42);
+ const gen2 = mulberry32(42);
+ expect(gen1()).toBe(gen2());
+ expect(gen1()).toBe(gen2());
+ expect(gen1()).toBe(gen2());
+ });
+
+ it('advances state on each call', () => {
+ const gen = mulberry32(42);
+ const first = gen();
+ const second = gen();
+ const third = gen();
+ expect(first).not.toBe(second);
+ expect(second).not.toBe(third);
+ });
+
+ it('produces different sequences for different seeds', () => {
+ const gen1 = mulberry32(1);
+ const gen2 = mulberry32(2);
+ expect(gen1()).not.toBe(gen2());
+ });
+
+ it('returns values in [0, 1)', () => {
+ const gen = mulberry32(99);
+ for (let index = 0; index < 100; index++) {
+ const value = gen();
+ expect(value).toBeGreaterThanOrEqual(0);
+ expect(value).toBeLessThan(1);
+ }
+ });
+ });
+
+ describe('stringToSeed', () => {
+ it('is deterministic', () => {
+ expect(stringToSeed('hello')).toBe(stringToSeed('hello'));
+ });
+
+ it('returns different values for different strings', () => {
+ expect(stringToSeed('hello')).not.toBe(stringToSeed('world'));
+ });
+ });
+
+ describe('seedRNG / resetRNG', () => {
+ it('produces the same sequence when seeded with the same string', () => {
+ seedRNG('x');
+ const first = rng();
+ const second = rng();
+ resetRNG();
+ seedRNG('x');
+ expect(rng()).toBe(first);
+ expect(rng()).toBe(second);
+ });
+
+ it('produces different sequences for different seeds', () => {
+ seedRNG('seed-a');
+ const resultA = rng();
+ resetRNG();
+ seedRNG('seed-b');
+ expect(rng()).not.toBe(resultA);
+ });
+
+ it('after resetRNG a re-seed produces the same sequence as a fresh seed', () => {
+ seedRNG('hello');
+ const result = rng();
+ resetRNG();
+ seedRNG('hello');
+ expect(rng()).toBe(result);
+ });
+ });
+
+ describe('generateLayoutSeed', () => {
+ it('returns a 10-character string', () => {
+ expect(generateLayoutSeed()).toHaveLength(10);
+ });
+
+ it('contains only lowercase letters and digits', () => {
+ for (let index = 0; index < 20; index++) {
+ expect(generateLayoutSeed()).toMatch(/^[a-z0-9]{10}$/);
+ }
+ });
+
+ it('uses Math.random (not the seeded rng)', () => {
+ seedRNG('fixed');
+ const seeded = rng();
+ resetRNG();
+ // generateLayoutSeed uses Math.random directly, not rng() —
+ // so seeding has no effect on it
+ seedRNG('fixed');
+ generateLayoutSeed();
+ expect(rng()).toBe(seeded);
+ });
+ });
+});
diff --git a/src/app/model/rng.ts b/src/app/model/rng.ts
new file mode 100644
index 00000000..29ebe5f1
--- /dev/null
+++ b/src/app/model/rng.ts
@@ -0,0 +1,49 @@
+import { hashCode } from './hash';
+
+let _rng: () => number = Math.random;
+
+export function rng(): number {
+ return _rng();
+}
+
+export function mulberry32(seed: number): () => number {
+ // eslint-disable-next-line unicorn/prefer-math-trunc
+ let state = seed | 0;
+ return () => {
+ // | 0 wraps to int32 — required by the mulberry32 algorithm, not mere truncation
+ // eslint-disable-next-line unicorn/prefer-math-trunc
+ state = (state + 0x6D_2B_79_F5) | 0;
+ let t = Math.imul(state ^ (state >>> 15), 1 | state);
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
+ return ((t ^ (t >>> 14)) >>> 0) / 4_294_967_296;
+ };
+}
+
+function mix32(n: number): number {
+ // eslint-disable-next-line unicorn/numeric-separators-style
+ let h = Math.imul(n ^ (n >>> 16), 0x9E3779B9);
+ // eslint-disable-next-line unicorn/numeric-separators-style
+ h = Math.imul(h ^ (h >>> 16), 0x9E3779B9);
+ return h ^ (h >>> 16);
+}
+
+export function stringToSeed(s: string): number {
+ return mix32(hashCode(s));
+}
+
+export function seedRNG(seed: string): void {
+ _rng = mulberry32(stringToSeed(seed));
+}
+
+export function resetRNG(): void {
+ _rng = Math.random;
+}
+
+export function generateLayoutSeed(): string {
+ const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
+ let result = '';
+ for (let index = 0; index < 10; index++) {
+ result += chars[Math.floor(Math.random() * chars.length)];
+ }
+ return result;
+}
diff --git a/src/assets/i18n/ar.json b/src/assets/i18n/ar.json
index c26f852a..d19cb80e 100644
--- a/src/assets/i18n/ar.json
+++ b/src/assets/i18n/ar.json
@@ -37,6 +37,7 @@
"MIRROR_X": "مرآة عمودية",
"MIRROR_Y": "مرآة أفقية",
"GENERATE": "إنشاء",
+ "LAYOUT_SEED_LABEL": "بذرة",
"RANDOM_MIRROR": "عشوائي",
"YES": "نعم",
"NO": "لا",
diff --git a/src/assets/i18n/bn.json b/src/assets/i18n/bn.json
index 05528d45..2ccd2d75 100644
--- a/src/assets/i18n/bn.json
+++ b/src/assets/i18n/bn.json
@@ -37,6 +37,7 @@
"MIRROR_X": "উল্লম্ব প্রতিফলন",
"MIRROR_Y": "অনুভূমিক প্রতিফলন",
"GENERATE": "তৈরি করুন",
+ "LAYOUT_SEED_LABEL": "বীজ",
"RANDOM_MIRROR": "এলোমেলো",
"YES": "হ্যাঁ",
"NO": "না",
diff --git a/src/assets/i18n/ca.json b/src/assets/i18n/ca.json
index 40398d76..cc1355d2 100644
--- a/src/assets/i18n/ca.json
+++ b/src/assets/i18n/ca.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Mirall vertical",
"MIRROR_Y": "Mirall horitzontal",
"GENERATE": "Generar",
+ "LAYOUT_SEED_LABEL": "Llavor",
"RANDOM_MIRROR": "Aleatori",
"YES": "Sí",
"NO": "No",
diff --git a/src/assets/i18n/cs.json b/src/assets/i18n/cs.json
index e6f9d132..d2e70195 100644
--- a/src/assets/i18n/cs.json
+++ b/src/assets/i18n/cs.json
@@ -24,7 +24,7 @@
"STATS_LOSE_GAMES": "Prohry",
"STATS_BEST": "Nejlepší čas",
"STATS_AVERAGE": "Průměr",
- "STATS_EMPTY": "Zatím nebyly dokončeny žádné hry.",
+ "STATS_EMPTY": "Zatím nebyly dokončeny žádné hry.",
"LICENSE": "Licence",
"SHORTCUTS": "Klávesové zkratky",
"SETTINGS": "Nastavení",
@@ -37,6 +37,7 @@
"MIRROR_X": "Zrcadlo svisle",
"MIRROR_Y": "Zrcadlo vodorovně",
"GENERATE": "Vytvořit",
+ "LAYOUT_SEED_LABEL": "Semínko",
"RANDOM_MIRROR": "Náhodné",
"YES": "Ano",
"NO": "Ne",
diff --git a/src/assets/i18n/da.json b/src/assets/i18n/da.json
index 57308df8..2b7cf80a 100644
--- a/src/assets/i18n/da.json
+++ b/src/assets/i18n/da.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Spejl lodret",
"MIRROR_Y": "Spejl vandret",
"GENERATE": "Generer",
+ "LAYOUT_SEED_LABEL": "Frø",
"RANDOM_MIRROR": "Tilfældig",
"YES": "Ja",
"NO": "Nej",
diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json
index 24ad17ce..977439f7 100644
--- a/src/assets/i18n/de.json
+++ b/src/assets/i18n/de.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Vertikal spiegeln",
"MIRROR_Y": "Horizontal spiegeln",
"GENERATE": "Generieren",
+ "LAYOUT_SEED_LABEL": "Startwert",
"RANDOM_MIRROR": "Zufällig",
"YES": "Ja",
"NO": "Nein",
diff --git a/src/assets/i18n/el.json b/src/assets/i18n/el.json
index 151c5efc..ff49c2a4 100644
--- a/src/assets/i18n/el.json
+++ b/src/assets/i18n/el.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Κατακόρυφο καθρέφτη",
"MIRROR_Y": "Οριζόντιο καθρέφτη",
"GENERATE": "Δημιουργία",
+ "LAYOUT_SEED_LABEL": "Σπόρος",
"RANDOM_MIRROR": "Τυχαίο",
"YES": "Ναι",
"NO": "Όχι",
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 38a3761a..4c0cbbde 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Mirror vertical",
"MIRROR_Y": "Mirror horizontal",
"GENERATE": "Generate",
+ "LAYOUT_SEED_LABEL": "Seed",
"RANDOM_MIRROR": "Random",
"YES": "Yes",
"NO": "No",
diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json
index 0d7f855b..ba981d34 100644
--- a/src/assets/i18n/es.json
+++ b/src/assets/i18n/es.json
@@ -24,7 +24,7 @@
"STATS_LOSE_GAMES": "Perdidas",
"STATS_BEST": "Record",
"STATS_AVERAGE": "Promedio",
- "STATS_EMPTY": "Aún no se han terminado partidas.",
+ "STATS_EMPTY": "Aún no se han terminado partidas.",
"LICENSE": "Licencia",
"SHORTCUTS": "Atajos",
"SETTINGS": "Ajustes",
@@ -37,6 +37,7 @@
"MIRROR_X": "Espejo vertical",
"MIRROR_Y": "Espejo horizontal",
"GENERATE": "Generar",
+ "LAYOUT_SEED_LABEL": "Semilla",
"RANDOM_MIRROR": "Aleatorio",
"YES": "Sí",
"NO": "No",
diff --git a/src/assets/i18n/eu.json b/src/assets/i18n/eu.json
index 3098b892..68f1c828 100644
--- a/src/assets/i18n/eu.json
+++ b/src/assets/i18n/eu.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Ispilu bertikala",
"MIRROR_Y": "Ispilu horizontala",
"GENERATE": "Sortu",
+ "LAYOUT_SEED_LABEL": "Hazi",
"RANDOM_MIRROR": "Ausaz",
"YES": "Bai",
"NO": "Ez",
diff --git a/src/assets/i18n/fa.json b/src/assets/i18n/fa.json
index 203ee372..27220efc 100644
--- a/src/assets/i18n/fa.json
+++ b/src/assets/i18n/fa.json
@@ -37,6 +37,7 @@
"MIRROR_X": "آینه عمودی",
"MIRROR_Y": "آینه افقی",
"GENERATE": "ایجاد",
+ "LAYOUT_SEED_LABEL": "بذر",
"RANDOM_MIRROR": "تصادفی",
"YES": "بله",
"NO": "خیر",
diff --git a/src/assets/i18n/fi.json b/src/assets/i18n/fi.json
index 07475188..53d4bc95 100644
--- a/src/assets/i18n/fi.json
+++ b/src/assets/i18n/fi.json
@@ -24,7 +24,7 @@
"STATS_LOSE_GAMES": "Häviöt",
"STATS_BEST": "Paras aika",
"STATS_AVERAGE": "Keskiarvo",
- "STATS_EMPTY": "Ei pelejä suoritettu vielä.",
+ "STATS_EMPTY": "Ei pelejä suoritettu vielä.",
"LICENSE": "Lisenssi",
"SHORTCUTS": "Pikanäppäimet",
"SETTINGS": "Asetukset",
@@ -37,6 +37,7 @@
"MIRROR_X": "Peilikuva pystysuuntainen",
"MIRROR_Y": "Peilikuva vaakasuuntainen",
"GENERATE": "Luo",
+ "LAYOUT_SEED_LABEL": "Siemen",
"RANDOM_MIRROR": "Satunnainen",
"YES": "Kyllä",
"NO": "Ei",
diff --git a/src/assets/i18n/fil.json b/src/assets/i18n/fil.json
index 57982ce0..728c24c8 100644
--- a/src/assets/i18n/fil.json
+++ b/src/assets/i18n/fil.json
@@ -11,8 +11,8 @@
"UNDO_LONG": "Ibalik ang huling galaw",
"RESTART_LONG": "Magsimula ng bagong laro",
"PAUSE_LONG": "Ituloy/I-pause ang laro",
- "MSG_CONTINUE_PAUSE": "Ituloy ang Laro\u2026",
- "MSG_CONTINUE_SAVE": "Ituloy ang Naka-save na Laro\u2026",
+ "MSG_CONTINUE_PAUSE": "Ituloy ang Laro…",
+ "MSG_CONTINUE_SAVE": "Ituloy ang Naka-save na Laro…",
"MSG_START": "Simulan ang Laro",
"MSG_PLAY_AGAIN": "Maglaro muli",
"MSG_BEST": "Binabati kita, bagong pinakamabilis na oras!",
@@ -37,6 +37,7 @@
"MIRROR_X": "Salamin patayo",
"MIRROR_Y": "Salamin pahiga",
"GENERATE": "Bumuo",
+ "LAYOUT_SEED_LABEL": "Binhi",
"RANDOM_MIRROR": "Random",
"YES": "Oo",
"NO": "Hindi",
diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json
index 668efa9f..c7d11c32 100644
--- a/src/assets/i18n/fr.json
+++ b/src/assets/i18n/fr.json
@@ -24,7 +24,7 @@
"STATS_LOSE_GAMES": "Perdues",
"STATS_BEST": "Meilleur temps",
"STATS_AVERAGE": "Moyenne",
- "STATS_EMPTY": "Aucune partie terminée pour l'instant.",
+ "STATS_EMPTY": "Aucune partie terminée pour l'instant.",
"LICENSE": "Licence",
"SHORTCUTS": "Raccourcis",
"SETTINGS": "Paramètres",
@@ -37,6 +37,7 @@
"MIRROR_X": "Miroir vertical",
"MIRROR_Y": "Miroir horizontal",
"GENERATE": "Générer",
+ "LAYOUT_SEED_LABEL": "Graine",
"RANDOM_MIRROR": "Aléatoire",
"YES": "Oui",
"NO": "Non",
diff --git a/src/assets/i18n/hi.json b/src/assets/i18n/hi.json
index 4df4871b..f8832e52 100644
--- a/src/assets/i18n/hi.json
+++ b/src/assets/i18n/hi.json
@@ -37,6 +37,7 @@
"MIRROR_X": "ऊर्ध्वाधर दर्पण",
"MIRROR_Y": "क्षैतिज दर्पण",
"GENERATE": "उत्पन्न करें",
+ "LAYOUT_SEED_LABEL": "बीज",
"RANDOM_MIRROR": "यादृच्छिक",
"YES": "हाँ",
"NO": "नहीं",
diff --git a/src/assets/i18n/hu.json b/src/assets/i18n/hu.json
index f0963c24..8ea49a4e 100644
--- a/src/assets/i18n/hu.json
+++ b/src/assets/i18n/hu.json
@@ -24,7 +24,7 @@
"STATS_LOSE_GAMES": "Vesztett",
"STATS_BEST": "Legjobb idő",
"STATS_AVERAGE": "Átlag",
- "STATS_EMPTY": "Még nem fejeztél be egyetlen játékot sem.",
+ "STATS_EMPTY": "Még nem fejeztél be egyetlen játékot sem.",
"LICENSE": "Licenc",
"SHORTCUTS": "Gyorsbillentyűk",
"SETTINGS": "Beállítások",
@@ -37,6 +37,7 @@
"MIRROR_X": "Tükrözés függőlegesen",
"MIRROR_Y": "Tükrözés vízszintesen",
"GENERATE": "Generálás",
+ "LAYOUT_SEED_LABEL": "Mag",
"RANDOM_MIRROR": "Véletlen",
"YES": "Igen",
"NO": "Nem",
diff --git a/src/assets/i18n/id.json b/src/assets/i18n/id.json
index 45b12f5c..402445e8 100644
--- a/src/assets/i18n/id.json
+++ b/src/assets/i18n/id.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Cermin vertikal",
"MIRROR_Y": "Cermin horizontal",
"GENERATE": "Hasilkan",
+ "LAYOUT_SEED_LABEL": "Benih",
"RANDOM_MIRROR": "Acak",
"YES": "Ya",
"NO": "Tidak",
diff --git a/src/assets/i18n/it.json b/src/assets/i18n/it.json
index 5202ef59..3d9798ce 100644
--- a/src/assets/i18n/it.json
+++ b/src/assets/i18n/it.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Specchio verticale",
"MIRROR_Y": "Specchio orizzontale",
"GENERATE": "Genera",
+ "LAYOUT_SEED_LABEL": "Seme",
"RANDOM_MIRROR": "Casuale",
"YES": "Sì",
"NO": "No",
diff --git a/src/assets/i18n/ja.json b/src/assets/i18n/ja.json
index aa01771d..ce15393d 100644
--- a/src/assets/i18n/ja.json
+++ b/src/assets/i18n/ja.json
@@ -37,6 +37,7 @@
"MIRROR_X": "垂直にミラー",
"MIRROR_Y": "水平にミラー",
"GENERATE": "生成",
+ "LAYOUT_SEED_LABEL": "シード",
"RANDOM_MIRROR": "ランダム",
"YES": "はい",
"NO": "いいえ",
diff --git a/src/assets/i18n/ko.json b/src/assets/i18n/ko.json
index b34c32a9..1515cd7d 100644
--- a/src/assets/i18n/ko.json
+++ b/src/assets/i18n/ko.json
@@ -37,6 +37,7 @@
"MIRROR_X": "수직 미러",
"MIRROR_Y": "수평 미러",
"GENERATE": "생성",
+ "LAYOUT_SEED_LABEL": "시드",
"RANDOM_MIRROR": "무작위",
"YES": "예",
"NO": "아니오",
diff --git a/src/assets/i18n/ms.json b/src/assets/i18n/ms.json
index ae005f64..f94b8a22 100644
--- a/src/assets/i18n/ms.json
+++ b/src/assets/i18n/ms.json
@@ -11,8 +11,8 @@
"UNDO_LONG": "Buat asal langkah terakhir",
"RESTART_LONG": "Mulakan permainan baharu",
"PAUSE_LONG": "Sambung/Jeda permainan",
- "MSG_CONTINUE_PAUSE": "Sambung Permainan\u2026",
- "MSG_CONTINUE_SAVE": "Sambung Permainan Tersimpan\u2026",
+ "MSG_CONTINUE_PAUSE": "Sambung Permainan…",
+ "MSG_CONTINUE_SAVE": "Sambung Permainan Tersimpan…",
"MSG_START": "Mula Permainan",
"MSG_PLAY_AGAIN": "Main lagi",
"MSG_BEST": "Tahniah, masa terbaik baharu!",
@@ -37,6 +37,7 @@
"MIRROR_X": "Cermin menegak",
"MIRROR_Y": "Cermin mendatar",
"GENERATE": "Jana",
+ "LAYOUT_SEED_LABEL": "Benih",
"RANDOM_MIRROR": "Rawak",
"YES": "Ya",
"NO": "Tidak",
diff --git a/src/assets/i18n/nl.json b/src/assets/i18n/nl.json
index 2d665769..8fad97b2 100644
--- a/src/assets/i18n/nl.json
+++ b/src/assets/i18n/nl.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Spiegel verticaal",
"MIRROR_Y": "Spiegel horizontaal",
"GENERATE": "Genereren",
+ "LAYOUT_SEED_LABEL": "Zaad",
"RANDOM_MIRROR": "Willekeurig",
"YES": "Ja",
"NO": "Nee",
diff --git a/src/assets/i18n/no.json b/src/assets/i18n/no.json
index 53b70dea..7008c928 100644
--- a/src/assets/i18n/no.json
+++ b/src/assets/i18n/no.json
@@ -24,7 +24,7 @@
"STATS_LOSE_GAMES": "Tapt",
"STATS_BEST": "Beste tid",
"STATS_AVERAGE": "Gjennomsnitt",
- "STATS_EMPTY": "Ingen spill fullført ennå.",
+ "STATS_EMPTY": "Ingen spill fullført ennå.",
"LICENSE": "Lisens",
"SHORTCUTS": "Snarveier",
"SETTINGS": "Innstillinger",
@@ -37,6 +37,7 @@
"MIRROR_X": "Speil vertikalt",
"MIRROR_Y": "Speil horisontalt",
"GENERATE": "Generer",
+ "LAYOUT_SEED_LABEL": "Frø",
"RANDOM_MIRROR": "Tilfeldig",
"YES": "Ja",
"NO": "Nei",
diff --git a/src/assets/i18n/pl.json b/src/assets/i18n/pl.json
index 60a828e5..b1bffd16 100644
--- a/src/assets/i18n/pl.json
+++ b/src/assets/i18n/pl.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Odbicie pionowe",
"MIRROR_Y": "Odbicie poziome",
"GENERATE": "Generuj",
+ "LAYOUT_SEED_LABEL": "Ziarno",
"RANDOM_MIRROR": "Losowy",
"YES": "Tak",
"NO": "Nie",
diff --git a/src/assets/i18n/pt.json b/src/assets/i18n/pt.json
index 10d37614..475e0895 100644
--- a/src/assets/i18n/pt.json
+++ b/src/assets/i18n/pt.json
@@ -24,7 +24,7 @@
"STATS_LOSE_GAMES": "Derrotas",
"STATS_BEST": "Melhor tempo",
"STATS_AVERAGE": "Média",
- "STATS_EMPTY": "Nenhum jogo concluído ainda.",
+ "STATS_EMPTY": "Nenhum jogo concluído ainda.",
"LICENSE": "Licença",
"SHORTCUTS": "Atalhos",
"SETTINGS": "Configurações",
@@ -37,6 +37,7 @@
"MIRROR_X": "Espelho vertical",
"MIRROR_Y": "Espelho horizontal",
"GENERATE": "Gerar",
+ "LAYOUT_SEED_LABEL": "Semente",
"RANDOM_MIRROR": "Aleatório",
"YES": "Sim",
"NO": "Não",
diff --git a/src/assets/i18n/ro.json b/src/assets/i18n/ro.json
index 73621b88..1f23e1d6 100644
--- a/src/assets/i18n/ro.json
+++ b/src/assets/i18n/ro.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Oglindă verticală",
"MIRROR_Y": "Oglindă orizontală",
"GENERATE": "Generează",
+ "LAYOUT_SEED_LABEL": "Sămânță",
"RANDOM_MIRROR": "Aleatoriu",
"YES": "Da",
"NO": "Nu",
diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json
index 171ee845..2ef71eef 100644
--- a/src/assets/i18n/ru.json
+++ b/src/assets/i18n/ru.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Отобразить вертикально",
"MIRROR_Y": "Отобразить горизонтально",
"GENERATE": "Генерировать",
+ "LAYOUT_SEED_LABEL": "Зерно",
"RANDOM_MIRROR": "Случайный",
"YES": "Да",
"NO": "Нет",
diff --git a/src/assets/i18n/sv.json b/src/assets/i18n/sv.json
index adba57d2..1a524231 100644
--- a/src/assets/i18n/sv.json
+++ b/src/assets/i18n/sv.json
@@ -24,7 +24,7 @@
"STATS_LOSE_GAMES": "Förlorade",
"STATS_BEST": "Bästa tid",
"STATS_AVERAGE": "Genomsnitt",
- "STATS_EMPTY": "Inga spel avslutade ännu.",
+ "STATS_EMPTY": "Inga spel avslutade ännu.",
"LICENSE": "Licens",
"SHORTCUTS": "Genvägar",
"SETTINGS": "Inställningar",
@@ -37,6 +37,7 @@
"MIRROR_X": "Spegla vertikalt",
"MIRROR_Y": "Spegla horisontellt",
"GENERATE": "Generera",
+ "LAYOUT_SEED_LABEL": "Frö",
"RANDOM_MIRROR": "Slumpmässig",
"YES": "Ja",
"NO": "Nej",
@@ -223,7 +224,7 @@
"EDITOR_CLEAR_LAYER_LONG": "Rensa lager",
"EDITOR_DELETE_LAYER_LONG": "Ta bort lager",
"EDITOR_DISCARD_CHANGES_SURE": "Ignorera ändringar?",
- "EDITOR_MOVE_ALL_TILES_UP_LONG": "Flytta alla stenar upp",
+ "EDITOR_MOVE_ALL_TILES_UP_LONG": "Flytta alla stenar upp",
"EDITOR_MOVE_ALL_TILES_DOWN_LONG": "Flytta alla stenar ner",
"EDITOR_MOVE_ALL_TILES_LEFT_LONG": "Flytta alla stenar åt vänster",
"EDITOR_MOVE_ALL_TILES_RIGHT_LONG": "Flytta alla stenar åt höger",
diff --git a/src/assets/i18n/sw.json b/src/assets/i18n/sw.json
index 20ceaa6a..305676b2 100644
--- a/src/assets/i18n/sw.json
+++ b/src/assets/i18n/sw.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Kioo wima",
"MIRROR_Y": "Kioo mlalo",
"GENERATE": "Tengeneza",
+ "LAYOUT_SEED_LABEL": "Mbegu",
"RANDOM_MIRROR": "Nasibu",
"YES": "Ndiyo",
"NO": "Hapana",
diff --git a/src/assets/i18n/ta.json b/src/assets/i18n/ta.json
index 39f8c252..145b8b08 100644
--- a/src/assets/i18n/ta.json
+++ b/src/assets/i18n/ta.json
@@ -37,6 +37,7 @@
"MIRROR_X": "செங்குத்து பிரதிபலிப்பு",
"MIRROR_Y": "கிடைமட்ட பிரதிபலிப்பு",
"GENERATE": "உருவாக்கு",
+ "LAYOUT_SEED_LABEL": "விதை",
"RANDOM_MIRROR": "சீரற்றது",
"YES": "ஆம்",
"NO": "இல்லை",
diff --git a/src/assets/i18n/te.json b/src/assets/i18n/te.json
index 0a68ea3c..0cf90aae 100644
--- a/src/assets/i18n/te.json
+++ b/src/assets/i18n/te.json
@@ -37,6 +37,7 @@
"MIRROR_X": "నిలువు ప్రతిబింబం",
"MIRROR_Y": "అడ్డు ప్రతిబింబం",
"GENERATE": "తయారు చేయి",
+ "LAYOUT_SEED_LABEL": "విత్తనం",
"RANDOM_MIRROR": "యాదృచ్ఛికం",
"YES": "అవును",
"NO": "కాదు",
diff --git a/src/assets/i18n/th.json b/src/assets/i18n/th.json
index d4dac5f5..d01037d9 100644
--- a/src/assets/i18n/th.json
+++ b/src/assets/i18n/th.json
@@ -37,6 +37,7 @@
"MIRROR_X": "สะท้อนแนวตั้ง",
"MIRROR_Y": "สะท้อนแนวนอน",
"GENERATE": "สร้าง",
+ "LAYOUT_SEED_LABEL": "ซีด",
"RANDOM_MIRROR": "สุ่ม",
"YES": "ใช่",
"NO": "ไม่",
diff --git a/src/assets/i18n/tr.json b/src/assets/i18n/tr.json
index d2110782..375f7339 100644
--- a/src/assets/i18n/tr.json
+++ b/src/assets/i18n/tr.json
@@ -24,7 +24,7 @@
"STATS_LOSE_GAMES": "Kaybedilen",
"STATS_BEST": "En İyi Süre",
"STATS_AVERAGE": "Ortalama",
- "STATS_EMPTY": "Henüz hiçbir oyun tamamlanmadı.",
+ "STATS_EMPTY": "Henüz hiçbir oyun tamamlanmadı.",
"LICENSE": "Lisans",
"SHORTCUTS": "Kısayollar",
"SETTINGS": "Ayarlar",
@@ -37,6 +37,7 @@
"MIRROR_X": "Dikey Ayna",
"MIRROR_Y": "Yatay Ayna",
"GENERATE": "Oluştur",
+ "LAYOUT_SEED_LABEL": "Tohum",
"RANDOM_MIRROR": "Rastgele",
"YES": "Evet",
"NO": "Hayır",
diff --git a/src/assets/i18n/uk.json b/src/assets/i18n/uk.json
index d48b09ac..27d72ce9 100644
--- a/src/assets/i18n/uk.json
+++ b/src/assets/i18n/uk.json
@@ -37,6 +37,7 @@
"MIRROR_X": "Дзеркало вертикально",
"MIRROR_Y": "Дзеркало горизонтально",
"GENERATE": "Генерувати",
+ "LAYOUT_SEED_LABEL": "Зерно",
"RANDOM_MIRROR": "Випадковий",
"YES": "Так",
"NO": "Ні",
diff --git a/src/assets/i18n/ur.json b/src/assets/i18n/ur.json
index 4dcc84ff..43467dac 100644
--- a/src/assets/i18n/ur.json
+++ b/src/assets/i18n/ur.json
@@ -37,6 +37,7 @@
"MIRROR_X": "عمودی عکس",
"MIRROR_Y": "افقی عکس",
"GENERATE": "بنائیں",
+ "LAYOUT_SEED_LABEL": "بیج",
"RANDOM_MIRROR": "بے ترتیب",
"YES": "ہاں",
"NO": "نہیں",
diff --git a/src/assets/i18n/vi.json b/src/assets/i18n/vi.json
index 49ddd591..d5d7dd7e 100644
--- a/src/assets/i18n/vi.json
+++ b/src/assets/i18n/vi.json
@@ -24,7 +24,7 @@
"STATS_LOSE_GAMES": "Thua",
"STATS_BEST": "Thời Gian Tốt Nhất",
"STATS_AVERAGE": "Trung Bình",
- "STATS_EMPTY": "Chưa có trò chơi nào được hoàn thành.",
+ "STATS_EMPTY": "Chưa có trò chơi nào được hoàn thành.",
"LICENSE": "Giấy Phép",
"SHORTCUTS": "Phím Tắt",
"SETTINGS": "Cài Đặt",
@@ -37,6 +37,7 @@
"MIRROR_X": "Gương Dọc",
"MIRROR_Y": "Gương Ngang",
"GENERATE": "Tạo",
+ "LAYOUT_SEED_LABEL": "Giống",
"RANDOM_MIRROR": "Ngẫu Nhiên",
"YES": "Có",
"NO": "Không",
diff --git a/src/assets/i18n/zh.json b/src/assets/i18n/zh.json
index 9b1c6246..2745a032 100644
--- a/src/assets/i18n/zh.json
+++ b/src/assets/i18n/zh.json
@@ -37,6 +37,7 @@
"MIRROR_X": "垂直镜像",
"MIRROR_Y": "水平镜像",
"GENERATE": "生成",
+ "LAYOUT_SEED_LABEL": "种子",
"RANDOM_MIRROR": "随机",
"YES": "是",
"NO": "否",