boardgame-core/src/core/region.ts

108 lines
3.6 KiB
TypeScript
Raw Normal View History

2026-04-01 13:36:16 +08:00
import {Entity, EntityAccessor} from "../utils/entity";
import {Part} from "./part";
import {RNG} from "../utils/rng";
export type Region = Entity & {
2026-04-01 17:48:40 +08:00
// aligning axes of the region, expect a part's position to have a matching number of elements
2026-04-01 13:36:16 +08:00
axes: RegionAxis[];
2026-04-01 17:34:21 +08:00
2026-04-01 13:36:16 +08:00
// current children; expect no overlapped positions
children: EntityAccessor<Part>[];
}
export type RegionAxis = {
name: string;
min?: number;
max?: number;
align?: 'start' | 'end' | 'center';
}
/**
* for each axis, try to remove gaps in positions.
* - if min exists and align is start, and there are parts at (for example) min+2 and min+4, then move them to min and min+1
* - if max exists and align is end, and there are parts at (for example) max-2 and max-4, then move them to max-1 and max-3
* - for center, move parts to the center, possibly creating parts placed at 0.5 positions
* - sort children so that they're in ascending order on each axes.
* @param region
*/
export function applyAlign(region: Region){
2026-04-01 17:48:40 +08:00
if (region.children.length === 0) return;
2026-04-01 21:44:20 +08:00
// Process each axis independently while preserving spatial relationships
2026-04-01 17:48:40 +08:00
for (let axisIndex = 0; axisIndex < region.axes.length; axisIndex++) {
const axis = region.axes[axisIndex];
if (!axis.align) continue;
2026-04-01 17:34:21 +08:00
2026-04-01 21:44:20 +08:00
// Collect all unique position values on this axis, preserving original order
2026-04-01 18:33:09 +08:00
const positionValues = new Set<number>();
for (const accessor of region.children) {
positionValues.add(accessor.value.position[axisIndex] ?? 0);
}
2026-04-01 21:44:20 +08:00
// Sort position values
2026-04-01 18:33:09 +08:00
const sortedPositions = Array.from(positionValues).sort((a, b) => a - b);
2026-04-01 21:44:20 +08:00
// Create position mapping: old position -> new position
2026-04-01 18:33:09 +08:00
const positionMap = new Map<number, number>();
2026-04-01 17:34:21 +08:00
if (axis.align === 'start' && axis.min !== undefined) {
2026-04-01 21:44:20 +08:00
// Compact from min, preserving relative order
2026-04-01 18:33:09 +08:00
sortedPositions.forEach((pos, index) => {
positionMap.set(pos, axis.min! + index);
2026-04-01 17:34:21 +08:00
});
} else if (axis.align === 'end' && axis.max !== undefined) {
2026-04-01 21:44:20 +08:00
// Compact towards max
2026-04-01 18:33:09 +08:00
const count = sortedPositions.length;
sortedPositions.forEach((pos, index) => {
positionMap.set(pos, axis.max! - (count - 1 - index));
2026-04-01 17:34:21 +08:00
});
} else if (axis.align === 'center') {
2026-04-01 21:44:20 +08:00
// Center alignment
2026-04-01 18:33:09 +08:00
const count = sortedPositions.length;
2026-04-01 17:34:21 +08:00
const min = axis.min ?? 0;
const max = axis.max ?? count - 1;
const range = max - min;
const center = min + range / 2;
2026-04-01 17:48:40 +08:00
2026-04-01 18:33:09 +08:00
sortedPositions.forEach((pos, index) => {
2026-04-01 17:34:21 +08:00
const offset = index - (count - 1) / 2;
2026-04-01 18:33:09 +08:00
positionMap.set(pos, center + offset);
2026-04-01 17:34:21 +08:00
});
}
2026-04-01 18:33:09 +08:00
2026-04-01 21:44:20 +08:00
// Apply position mapping to all parts
2026-04-01 18:33:09 +08:00
for (const accessor of region.children) {
const currentPos = accessor.value.position[axisIndex] ?? 0;
accessor.value.position[axisIndex] = positionMap.get(currentPos) ?? currentPos;
}
2026-04-01 13:36:16 +08:00
}
2026-04-01 18:33:09 +08:00
2026-04-01 21:44:20 +08:00
// Sort children by all axes at the end
2026-04-01 18:33:09 +08:00
region.children.sort((a, b) => {
for (let i = 0; i < region.axes.length; i++) {
const diff = (a.value.position[i] ?? 0) - (b.value.position[i] ?? 0);
if (diff !== 0) return diff;
}
return 0;
});
2026-04-01 13:36:16 +08:00
}
/**
* shuffle on each axis. for each axis, try to swap position.
* @param region
* @param rng
*/
export function shuffle(region: Region, rng: RNG){
2026-04-01 17:34:21 +08:00
if (region.children.length <= 1) return;
// Fisher-Yates 洗牌算法
const children = [...region.children];
for (let i = children.length - 1; i > 0; i--) {
const j = rng.nextInt(i + 1);
2026-04-01 17:48:40 +08:00
// 交换两个 part 的整个 position 数组
const temp = children[i].value.position;
children[i].value.position = children[j].value.position;
children[j].value.position = temp;
2026-04-01 17:34:21 +08:00
}
}