Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions src/app/components/layout-list/layout-list.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
@if (group.isRandom) {
<div class="previews previews-random" appDeferLoadScrollHost>
@for (item of group.layouts; track item) {
<div class="preview"
<div class="preview preview-seed-card"
tabindex="0"
[id]="'item-'+item.layout.id"
[class.selected]="item.selected"
Expand All @@ -30,15 +30,23 @@
(appDeferLoad)="item.visible=true"
>
@if (item.visible) {
<app-layout-preview class="svg-board" [svg]="item.layout.previewSVG" [alt]="item.layout.name"></app-layout-preview>
<app-layout-preview class="svg-board" [svg]="item.layout.previewSVG"
[alt]="item.layout.name"></app-layout-preview>
} @else {
<div class="preview-placeholder"
[id]="'item-'+item.layout.id"
[class.selected]="item.selected"
>
</div>
}
<div class="preview-name">{{ item.layout.name }}</div>
<div class="preview-seed" (click)="$event.stopPropagation()">
<input type="text" class="seed-input"
[value]="$any(item).layoutSeed ?? ''"
(keyup)="$event.stopPropagation(); regenerateWithSeed($any(item), $any($event.target).value)"
[attr.aria-label]="'LAYOUT_SEED_LABEL' | translate"
[attr.title]="'LAYOUT_SEED_LABEL' | translate"
/>
</div>
</div>
}
<div class="preview-random">
Expand Down
26 changes: 25 additions & 1 deletion src/app/components/layout-list/layout-list.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@
@include mixins.respond-to(tiny-down) {
width: 96%;

.preview-name {
.preview-name, preview-seed {
padding-top: 0;
}
}
Expand Down Expand Up @@ -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 {
Expand Down
25 changes: 23 additions & 2 deletions src/app/components/layout-list/layout-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -26,6 +27,15 @@ export interface LayoutGroup {
layouts: Array<LayoutItem>;
}

export interface RandomLayoutItem extends LayoutItem {
layoutSeed?: string;
}

export interface RandomLayoutGroup extends LayoutGroup {
isRandom: true;
layouts: Array<RandomLayoutItem>;
}

@Component({
selector: 'app-layout-list',
templateUrl: './layout-list.component.html',
Expand All @@ -39,7 +49,7 @@ export class LayoutListComponent implements OnInit, OnChanges {
groups: Array<LayoutGroup> = [];
randomMirrorX: string = 'random';
randomMirrorY: string = 'random';
randomGroup: LayoutGroup = {
randomGroup: RandomLayoutGroup = {
name: '',
layouts: [], expanded: true, isRandom: true
};
Expand All @@ -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(
{
Expand Down Expand Up @@ -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;
}
Expand All @@ -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();
}
Expand Down
11 changes: 6 additions & 5 deletions src/app/model/random-layout/base-layer-checker.ts
Original file line number Diff line number Diff line change
@@ -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<string>, baseZ: number, xs: Array<number>, ys: Array<number>, minHoles: number, maxHoles: number): void {
Expand All @@ -25,7 +26,7 @@ function punchHoles(base: Set<string>, baseZ: number, xs: Array<number>, 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<string> = [];
if (orient === 'h') {
removed.push(k);
Expand Down Expand Up @@ -62,10 +63,10 @@ function buildInitialChecker(present: Set<string>, xs: Array<number>, 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 };
}

Expand Down
3 changes: 2 additions & 1 deletion src/app/model/random-layout/base-layer-diamond.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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);
}

Expand Down
19 changes: 10 additions & 9 deletions src/app/model/random-layout/base-layer-lines.ts
Original file line number Diff line number Diff line change
@@ -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<string>, mapping: Mapping, snakeKeys: Set<string>, x: number, y: number, markSnake = false): boolean {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -86,10 +87,10 @@ function tryBurstFallback(present: Set<string>, mapping: Mapping, snakeKeys: Set
}

function trimEdges(present: Set<string>, snakeKeys: Set<string>, xs: Array<number>, ys: Array<number>, 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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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)) {
Expand Down
4 changes: 2 additions & 2 deletions src/app/model/random-layout/base-layer-shapes.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions src/app/model/random-layout/base-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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': {
Expand Down
5 changes: 3 additions & 2 deletions src/app/model/random-layout/random-layout.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 3 additions & 2 deletions src/app/model/random-layout/upper-layers.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -66,7 +67,7 @@ function bucketCandidates(present: Set<string>, z: number, win: { minX: number;
function maybeProposeOverhangs(present: Set<string>, 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))) {
Expand All @@ -91,7 +92,7 @@ function maybeProposeOverhangs(present: Set<string>, 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);
}
Expand Down
26 changes: 16 additions & 10 deletions src/app/model/random-layout/utilities.ts
Original file line number Diff line number Diff line change
@@ -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<T>(array: Array<T>): Array<T> {
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> = [T, ...Array<T>];

Expand All @@ -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<T>(array: NonEmptyArray<T>): 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 {
Expand Down Expand Up @@ -237,8 +243,8 @@ export function generateBaseLayerWithShapes(
const usedSizes = new Set<string>();
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)) {
Expand All @@ -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);
}

Expand All @@ -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;
Expand Down
Loading