Skip to content

Commit 976862e

Browse files
committed
feat(random): add more shapes to the random layout generator
1 parent 0514ac9 commit 976862e

File tree

11 files changed

+338
-168
lines changed

11 files changed

+338
-168
lines changed
Lines changed: 4 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { Mapping } from '../types';
2-
import { shuffleArray, buildEvenAnchors, placeSizesGeneric, buildMappingFromSetZ0, canPlace, place } from './utilities';
2+
import { generateBaseLayerWithShapes, shuffleArray } from './utilities';
33
import type { BaseLayerOptions } from './consts';
44

5-
function areaCells(x0: number, y0: number, w: number, h: number): Array<[number, number]> {
5+
export function areaCells(x0: number, y0: number, w: number, h: number): Array<[number, number]> {
66
const x1 = x0 + (w - 1) * 2;
77
const y1 = y0 + (h - 1) * 2;
88
const cells: Array<[number, number]> = [];
@@ -14,72 +14,13 @@ function areaCells(x0: number, y0: number, w: number, h: number): Array<[number,
1414
return cells;
1515
}
1616

17-
export function generateBaseLayerAreas({ minTarget, maxTarget, xMax, yMax }: BaseLayerOptions): Mapping {
18-
// Random placement of filled rectangular areas on z=0 with spacing 2 and unique sizes per layer.
19-
// - Area outer width/height in [3..4]
20-
// - Filled areas (not hollow)
21-
// - Spacing: at least 2 cells empty between different areas (Chebyshev radius 2)
22-
// - Use even-even grid to cooperate with blocksOverlap safety
23-
// - Do not place two areas with identical (w,h) within the same base layer
24-
25-
const occupied = new Set<string>();
26-
const blocked = new Set<string>();
27-
const usedSizes = new Set<string>();
28-
17+
export function generateBaseLayerAreas(options: BaseLayerOptions): Mapping {
2918
const allSizes: Array<[number, number]> = [];
3019
for (let w = 2; w <= 5; w++) {
3120
for (let h = 2; h <= 5; h++) {
3221
allSizes.push([w, h]);
3322
}
3423
}
3524
shuffleArray(allSizes);
36-
37-
// Build a randomized list of even-even anchor positions within bounds
38-
const anchors = buildEvenAnchors(xMax, yMax);
39-
shuffleArray(anchors);
40-
41-
// Phase 1: randomized sizes against randomized anchors
42-
let total = placeSizesGeneric(0, allSizes, anchors, minTarget, maxTarget, (x0, y0, w, h) =>
43-
canPlace(x0, y0, w, h, occupied, blocked, usedSizes, areaCells) ? place(x0, y0, w, h, occupied, blocked, usedSizes, areaCells) : 0
44-
);
45-
46-
// Phase 2: if still below minTarget, try remaining unused sizes (reshuffle anchors and sizes)
47-
if (total < minTarget) {
48-
const remainingSizes = allSizes.filter(([w, h]) => !usedSizes.has(`${w}x${h}`));
49-
shuffleArray(remainingSizes);
50-
shuffleArray(anchors);
51-
total = placeSizesGeneric(total, remainingSizes, anchors, minTarget, maxTarget, (x0, y0, w, h) =>
52-
canPlace(x0, y0, w, h, occupied, blocked, usedSizes, areaCells) ? place(x0, y0, w, h, occupied, blocked, usedSizes, areaCells) : 0
53-
);
54-
}
55-
56-
// Phase 3: fill remaining empty spaces with areas too, up to maxTarget. Allow size reuse after unique exhausted.
57-
if (total < maxTarget) {
58-
let progress = true;
59-
let allowReuse = false;
60-
while (progress && total < maxTarget) {
61-
progress = false;
62-
const sizePool: Array<[number, number]> =
63-
allowReuse ? [...allSizes] : allSizes.filter(([w, h]) => !usedSizes.has(`${w}x${h}`));
64-
if (sizePool.length === 0 && !allowReuse) {
65-
allowReuse = true;
66-
continue;
67-
}
68-
shuffleArray(sizePool);
69-
shuffleArray(anchors);
70-
total = placeSizesGeneric(total, sizePool, anchors, minTarget, maxTarget, (x0, y0, w, h) => {
71-
if (!canPlace(x0, y0, w, h, occupied, blocked, usedSizes, areaCells)) {
72-
return 0;
73-
}
74-
const added = place(x0, y0, w, h, occupied, blocked, usedSizes, areaCells);
75-
if (added > 0) {
76-
progress = true;
77-
}
78-
return added;
79-
});
80-
}
81-
}
82-
83-
// Build mapping in y,x order (even-even)
84-
return buildMappingFromSetZ0(occupied, xMax, yMax, 2);
25+
return generateBaseLayerWithShapes(allSizes, areaCells, options);
8526
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { Mapping } from '../types';
2+
import { generateBaseLayerWithShapes, shuffleArray } from './utilities';
3+
import type { BaseLayerOptions } from './consts';
4+
5+
// Plus/cross shape within a bounding box of (w × h) tiles.
6+
// w and h must be odd so the center is on an even-coordinate grid point.
7+
export function crossCells(x0: number, y0: number, w: number, h: number): Array<[number, number]> {
8+
const cx = x0 + Math.floor(w / 2) * 2;
9+
const cy = y0 + Math.floor(h / 2) * 2;
10+
const x1 = x0 + (w - 1) * 2;
11+
const y1 = y0 + (h - 1) * 2;
12+
const cells: Array<[number, number]> = [];
13+
for (let x = x0; x <= x1; x += 2) {
14+
cells.push([x, cy]);
15+
}
16+
for (let y = y0; y <= y1; y += 2) {
17+
if (y !== cy) {
18+
cells.push([cx, y]);
19+
}
20+
}
21+
return cells;
22+
}
23+
24+
export function generateBaseLayerCross(options: BaseLayerOptions): Mapping {
25+
const allSizes: Array<[number, number]> = [];
26+
for (let w = 3; w <= 9; w += 2) {
27+
for (let h = 3; h <= 9; h += 2) {
28+
allSizes.push([w, h]);
29+
}
30+
}
31+
shuffleArray(allSizes);
32+
return generateBaseLayerWithShapes(allSizes, crossCells, options);
33+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { Mapping } from '../types';
2+
import { generateBaseLayerWithShapes, shuffleArray } from './utilities';
3+
import type { BaseLayerOptions } from './consts';
4+
5+
// Diamond (rotated-square) outline inscribed in a bounding box of (w × h) tiles.
6+
// w and h must be odd so the center and vertices land on even-coordinate grid points.
7+
function diamondOutlineCells(x0: number, y0: number, w: number, h: number): Array<[number, number]> {
8+
const cx = x0 + Math.floor(w / 2) * 2;
9+
const cy = y0 + Math.floor(h / 2) * 2;
10+
const rx = Math.floor(w / 2);
11+
const ry = Math.floor(h / 2);
12+
const seen = new Set<string>();
13+
const cells: Array<[number, number]> = [];
14+
15+
const add = (x: number, y: number) => {
16+
const k = `${x}|${y}`;
17+
if (!seen.has(k)) {
18+
seen.add(k);
19+
cells.push([x, y]);
20+
}
21+
};
22+
23+
for (let dyStep = -ry; dyStep <= ry; dyStep++) {
24+
const dxMax = Math.round(rx * (1 - Math.abs(dyStep) / ry));
25+
const y = cy + dyStep * 2;
26+
if (dxMax === 0) {
27+
add(cx, y);
28+
} else {
29+
add(cx - dxMax * 2, y);
30+
add(cx + dxMax * 2, y);
31+
}
32+
}
33+
return cells;
34+
}
35+
36+
// Diamond (rotated-square) filled inscribed in a bounding box of (w × h) tiles.
37+
function diamondFilledCells(x0: number, y0: number, w: number, h: number): Array<[number, number]> {
38+
const cx = x0 + Math.floor(w / 2) * 2;
39+
const cy = y0 + Math.floor(h / 2) * 2;
40+
const rx = Math.floor(w / 2);
41+
const ry = Math.floor(h / 2);
42+
const cells: Array<[number, number]> = [];
43+
44+
for (let dyStep = -ry; dyStep <= ry; dyStep++) {
45+
const dxMax = Math.round(rx * (1 - Math.abs(dyStep) / ry));
46+
const y = cy + dyStep * 2;
47+
for (let dxStep = -dxMax; dxStep <= dxMax; dxStep++) {
48+
cells.push([cx + dxStep * 2, y]);
49+
}
50+
}
51+
return cells;
52+
}
53+
54+
export function diamondCells(x0: number, y0: number, w: number, h: number): Array<[number, number]> {
55+
const diamond = Math.random() < 0.5 ? diamondOutlineCells : diamondFilledCells;
56+
return diamond(x0, y0, w, h);
57+
}
58+
59+
export function generateBaseLayerDiamond(options: BaseLayerOptions): Mapping {
60+
const allSizes: Array<[number, number]> = [];
61+
// Square diamonds (w === h, odd sizes 3..11)
62+
for (let s = 3; s <= 11; s += 2) {
63+
allSizes.push([s, s]);
64+
}
65+
// Asymmetric diamonds for denser packing
66+
for (let w = 3; w <= 9; w += 2) {
67+
for (let h = 3; h <= 9; h += 2) {
68+
if (w !== h) {
69+
allSizes.push([w, h]);
70+
}
71+
}
72+
}
73+
shuffleArray(allSizes);
74+
return generateBaseLayerWithShapes(allSizes, diamondCells, options);
75+
}
Lines changed: 4 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { Mapping } from '../types';
2-
import { shuffleArray, buildEvenAnchors, placeSizesGeneric, buildMappingFromSetZ0, canPlace, place } from './utilities';
2+
import { generateBaseLayerWithShapes, shuffleArray } from './utilities';
33
import type { BaseLayerOptions } from './consts';
44

5-
function ringPerimeter(x0: number, y0: number, w: number, h: number): Array<[number, number]> {
5+
export function ringPerimeter(x0: number, y0: number, w: number, h: number): Array<[number, number]> {
66
const x1 = x0 + (w - 1) * 2;
77
const y1 = y0 + (h - 1) * 2;
88
const per: Array<[number, number]> = [];
@@ -21,75 +21,13 @@ function ringPerimeter(x0: number, y0: number, w: number, h: number): Array<[num
2121
return per;
2222
}
2323

24-
export function generateBaseLayerRings({ minTarget, maxTarget, xMax, yMax }: BaseLayerOptions): Mapping {
25-
// Random placement of hollow rectangular rings on z=0 with spacing 2 and unique sizes per layer.
26-
// - Ring outer width/height in [3..8]
27-
// - Borders only, continuous per ring
28-
// - Spacing: at least 2 cells empty between different rings (Chebyshev radius 2)
29-
// - Use even-even grid to cooperate with blocksOverlap safety
30-
// - Do not place two rings with identical (w,h)
31-
32-
const occupied = new Set<string>();
33-
const blocked = new Set<string>();
34-
const usedSizes = new Set<string>();
35-
24+
export function generateBaseLayerRings(options: BaseLayerOptions): Mapping {
3625
const allSizes: Array<[number, number]> = [];
3726
for (let w = 3; w <= 8; w++) {
3827
for (let h = 3; h <= 8; h++) {
3928
allSizes.push([w, h]);
4029
}
4130
}
42-
// Randomize size order; we will still enforce uniqueness by usedSizes
4331
shuffleArray(allSizes);
44-
45-
// Build randomized list of even-even anchor positions within bounds
46-
const anchors = buildEvenAnchors(xMax, yMax);
47-
shuffleArray(anchors);
48-
49-
let total = 0;
50-
// Phase 1: try randomized sizes against randomized anchors
51-
total = placeSizesGeneric(total, allSizes, anchors, minTarget, maxTarget, (x0, y0, w, h) =>
52-
canPlace(x0, y0, w, h, occupied, blocked, usedSizes, ringPerimeter) ? place(x0, y0, w, h, occupied, blocked, usedSizes, ringPerimeter) : 0
53-
);
54-
55-
// Phase 2: if still below minTarget, try remaining unused sizes in a fresh random order and reshuffled anchors
56-
if (total < minTarget) {
57-
const remainingSizes = allSizes.filter(([w, h]) => !usedSizes.has(`${w}x${h}`));
58-
shuffleArray(remainingSizes);
59-
shuffleArray(anchors);
60-
total = placeSizesGeneric(total, remainingSizes, anchors, minTarget, maxTarget, (x0, y0, w, h) =>
61-
canPlace(x0, y0, w, h, occupied, blocked, usedSizes, ringPerimeter) ? place(x0, y0, w, h, occupied, blocked, usedSizes, ringPerimeter) : 0
62-
);
63-
}
64-
65-
// Phase 3: fill remaining empty spaces with rings too, up to maxTarget.
66-
// Keep trying any remaining unused sizes; if all sizes are used and still room, allow reuse of sizes to pack gaps.
67-
if (total < maxTarget) {
68-
let progress = true;
69-
let allowReuse = false;
70-
while (progress && total < maxTarget) {
71-
progress = false;
72-
const sizePool: Array<[number, number]> =
73-
allowReuse ? [...allSizes] : allSizes.filter(([w, h]) => !usedSizes.has(`${w}x${h}`));
74-
if (sizePool.length === 0 && !allowReuse) {
75-
allowReuse = true; // all unique sizes exhausted; permit reuse to truly fill spaces
76-
continue;
77-
}
78-
shuffleArray(sizePool);
79-
shuffleArray(anchors);
80-
total = placeSizesGeneric(total, sizePool, anchors, minTarget, maxTarget, (x0, y0, w, h) => {
81-
if (!canPlace(x0, y0, w, h, occupied, blocked, usedSizes, ringPerimeter)) {
82-
return 0;
83-
}
84-
const added = place(x0, y0, w, h, occupied, blocked, usedSizes, ringPerimeter);
85-
if (added > 0) {
86-
progress = true;
87-
}
88-
return added;
89-
});
90-
}
91-
}
92-
93-
// Build mapping in y,x order
94-
return buildMappingFromSetZ0(occupied, xMax, yMax, 2);
32+
return generateBaseLayerWithShapes(allSizes, ringPerimeter, options);
9533
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { Mapping } from '../types';
2+
import { type CellsFunction, generateBaseLayerWithShapes, shuffleArray } from './utilities';
3+
import type { BaseLayerOptions } from './consts';
4+
import { crossCells } from './base-layer-cross';
5+
import { diamondCells } from './base-layer-diamond';
6+
import { triangleCells } from './base-layer-triangle';
7+
import { areaCells } from './base-layer-areas';
8+
import { ringPerimeter } from './base-layer-rings';
9+
10+
const shapeFunctions: Array<CellsFunction> = [crossCells, diamondCells, triangleCells, areaCells, ringPerimeter];
11+
12+
export function generateBaseLayerShapes(options: BaseLayerOptions): Mapping {
13+
const allSizes: Array<[number, number]> = [];
14+
for (let w = 3; w <= 9; w++) {
15+
for (let h = 3; h <= 9; h++) {
16+
allSizes.push([w, h]);
17+
}
18+
}
19+
shuffleArray(allSizes);
20+
const mixedCells: CellsFunction = (x0, y0, w, h) => {
21+
const shapeCells = shapeFunctions[Math.floor(Math.random() * shapeFunctions.length)];
22+
return shapeCells(x0, y0, w, h);
23+
};
24+
return generateBaseLayerWithShapes(allSizes, mixedCells, options);
25+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { Mapping } from '../types';
2+
import { generateBaseLayerWithShapes, randInt, shuffleArray } from './utilities';
3+
import type { BaseLayerOptions } from './consts';
4+
5+
// Right triangle in one of four orientations. w and h are tile counts (not pixels).
6+
// All produced coordinates are multiples of 2 (even-grid).
7+
//
8+
// 0 = tip top-left 1 = tip top-right
9+
// X . . X
10+
// X X . X X
11+
// X X X X X X
12+
//
13+
// 2 = tip bottom-left 3 = tip bottom-right
14+
// X X X X X X
15+
// X X . X X
16+
// X . . X
17+
//
18+
// Orientation is fixed per call (not per cell) so that canPlace and place see the same shape.
19+
export function triangleCells(x0: number, y0: number, w: number, h: number): Array<[number, number]> {
20+
const orientation = randInt(0, 3);
21+
const cells: Array<[number, number]> = [];
22+
for (let row = 0; row < h; row++) {
23+
const y = y0 + row * 2;
24+
let count: number;
25+
let startCol: number;
26+
switch (orientation) {
27+
case 0: {
28+
count = Math.max(1, Math.round(w * (row + 1) / h));
29+
startCol = 0;
30+
break;
31+
}
32+
case 1: {
33+
count = Math.max(1, Math.round(w * (row + 1) / h));
34+
startCol = w - count;
35+
break;
36+
}
37+
case 2: {
38+
count = Math.max(1, Math.round(w * (h - row) / h));
39+
startCol = 0;
40+
break;
41+
}
42+
default: {
43+
count = Math.max(1, Math.round(w * (h - row) / h));
44+
startCol = w - count;
45+
}
46+
}
47+
for (let col = startCol; col < startCol + count; col++) {
48+
cells.push([x0 + col * 2, y]);
49+
}
50+
}
51+
return cells;
52+
}
53+
54+
export function generateBaseLayerTriangle(options: BaseLayerOptions): Mapping {
55+
const allSizes: Array<[number, number]> = [];
56+
for (let w = 3; w <= 9; w++) {
57+
for (let h = 3; h <= 9; h++) {
58+
allSizes.push([w, h]);
59+
}
60+
}
61+
shuffleArray(allSizes);
62+
// bufferRadius=4 (vs default 2) so the hypotenuse void does not let adjacent shapes
63+
// sneak into the empty bounding-box corners.
64+
return generateBaseLayerWithShapes(allSizes, (x0, y0, w, h) => triangleCells(x0, y0, w, h), options, 4);
65+
}

0 commit comments

Comments
 (0)