boardgame-core/tests/core/region.test.ts

325 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { describe, it, expect } from 'vitest';
import { applyAlign, shuffle, type Region, type RegionAxis } from '../../src/core/region';
import { createRNG } from '../../src/utils/rng';
import { createEntityCollection } from '../../src/utils/entity';
import { type Part } from '../../src/core/part';
describe('Region', () => {
function createPart(id: string, position: number[]): Part {
const collection = createEntityCollection<Part>();
const part: Part = {
id,
sides: 1,
side: 0,
region: { id: 'region1', value: {} as Region },
position: [...position]
};
collection.add(part);
return part;
}
function createRegion(axes: RegionAxis[], parts: Part[]): Region {
const region: Region = {
id: 'region1',
axes: [...axes],
children: parts.map(p => ({ id: p.id, value: p }))
};
return region;
}
describe('applyAlign', () => {
it('should do nothing with empty region', () => {
const region = createRegion([{ name: 'x', min: 0, align: 'start' }], []);
applyAlign(region);
expect(region.children).toHaveLength(0);
});
it('should align parts to start on first axis', () => {
const part1 = createPart('p1', [5, 10]);
const part2 = createPart('p2', [7, 20]);
const part3 = createPart('p3', [2, 30]);
const region = createRegion(
[{ name: 'x', min: 0, align: 'start' }, { name: 'y' }],
[part1, part2, part3]
);
applyAlign(region);
// 排序后应该是 part3(2), part1(5), part2(7) -> 对齐到 0, 1, 2 (第一轴)
expect(region.children[0].value.position[0]).toBe(0);
expect(region.children[1].value.position[0]).toBe(1);
expect(region.children[2].value.position[0]).toBe(2);
// 第二轴保持不变
expect(region.children[0].value.position[1]).toBe(30);
expect(region.children[1].value.position[1]).toBe(10);
expect(region.children[2].value.position[1]).toBe(20);
});
it('should align parts to start with custom min', () => {
const part1 = createPart('p1', [5, 100]);
const part2 = createPart('p2', [7, 200]);
const region = createRegion(
[{ name: 'x', min: 10, align: 'start' }, { name: 'y' }],
[part1, part2]
);
applyAlign(region);
expect(region.children[0].value.position[0]).toBe(10);
expect(region.children[1].value.position[0]).toBe(11);
// 第二轴保持不变
expect(region.children[0].value.position[1]).toBe(100);
expect(region.children[1].value.position[1]).toBe(200);
});
it('should align parts to end on first axis', () => {
const part1 = createPart('p1', [2, 50]);
const part2 = createPart('p2', [4, 60]);
const part3 = createPart('p3', [1, 70]);
const region = createRegion(
[{ name: 'x', max: 10, align: 'end' }, { name: 'y' }],
[part1, part2, part3]
);
applyAlign(region);
// 3 个部分,对齐到 end(max=10),应该是 8, 9, 10
expect(region.children[0].value.position[0]).toBe(8);
expect(region.children[1].value.position[0]).toBe(9);
expect(region.children[2].value.position[0]).toBe(10);
});
it('should align parts to center on first axis', () => {
const part1 = createPart('p1', [0, 5]);
const part2 = createPart('p2', [1, 6]);
const part3 = createPart('p3', [2, 7]);
const region = createRegion(
[{ name: 'x', min: 0, max: 10, align: 'center' }, { name: 'y' }],
[part1, part2, part3]
);
applyAlign(region);
// 中心是 53 个部分应该是 4, 5, 6
expect(region.children[0].value.position[0]).toBe(4);
expect(region.children[1].value.position[0]).toBe(5);
expect(region.children[2].value.position[0]).toBe(6);
});
it('should handle even count center alignment', () => {
const part1 = createPart('p1', [0, 10]);
const part2 = createPart('p2', [1, 20]);
const region = createRegion(
[{ name: 'x', min: 0, max: 10, align: 'center' }, { name: 'y' }],
[part1, part2]
);
applyAlign(region);
// 中心是 52 个部分应该是 4.5, 5.5
expect(region.children[0].value.position[0]).toBe(4.5);
expect(region.children[1].value.position[0]).toBe(5.5);
});
it('should sort children by position on current axis', () => {
const part1 = createPart('p1', [5, 100]);
const part2 = createPart('p2', [1, 200]);
const part3 = createPart('p3', [3, 300]);
const region = createRegion(
[{ name: 'x', min: 0, align: 'start' }, { name: 'y' }],
[part1, part2, part3]
);
applyAlign(region);
// children 应该按第一轴位置排序
expect(region.children[0].value.id).toBe('p2');
expect(region.children[1].value.id).toBe('p3');
expect(region.children[2].value.id).toBe('p1');
});
it('should align on multiple axes', () => {
const part1 = createPart('p1', [5, 10]);
const part2 = createPart('p2', [7, 20]);
const part3 = createPart('p3', [2, 30]);
const region = createRegion(
[
{ name: 'x', min: 0, align: 'start' },
{ name: 'y', min: 0, align: 'start' }
],
[part1, part2, part3]
);
applyAlign(region);
// X 轴对齐:
// 唯一位置值:[2, 5, 7] -> 映射到 [0, 1, 2]
// part3: 2->0, part1: 5->1, part2: 7->2
// 结果part3=[0,30], part1=[1,10], part2=[2,20]
//
// Y 轴对齐:
// 唯一位置值:[10, 20, 30] -> 映射到 [0, 1, 2]
// part1: 10->0, part2: 20->1, part3: 30->2
// 最终part1=[1,0], part2=[2,1], part3=[0,2]
const positions = region.children.map(c => ({
id: c.value.id,
position: c.value.position
}));
// children 按位置排序后的顺序
expect(positions[0].id).toBe('p3');
expect(positions[0].position).toEqual([0, 2]);
expect(positions[1].id).toBe('p1');
expect(positions[1].position).toEqual([1, 0]);
expect(positions[2].id).toBe('p2');
expect(positions[2].position).toEqual([2, 1]);
});
it('should align 4 elements on rectangle corners', () => {
// 4 个元素放在矩形的四个角:(0,0), (10,0), (10,1), (0,1)
// 期望:保持矩形布局,只是紧凑到 (0,0), (1,0), (1,1), (0,1)
const part1 = createPart('p1', [0, 0]); // 左下角
const part2 = createPart('p2', [10, 0]); // 右下角
const part3 = createPart('p3', [10, 1]); // 右上角
const part4 = createPart('p4', [0, 1]); // 左上角
const region = createRegion(
[
{ name: 'x', min: 0, max: 10, align: 'start' },
{ name: 'y', min: 0, max: 10, align: 'start' }
],
[part1, part2, part3, part4]
);
applyAlign(region);
// X 轴对齐:
// 唯一位置值:[0, 10] -> 映射到 [0, 1]
// part1: 0->0, part4: 0->0, part2: 10->1, part3: 10->1
// 结果part1=[0,0], part4=[0,1], part2=[1,0], part3=[1,1]
//
// Y 轴对齐:
// 唯一位置值:[0, 1] -> 映射到 [0, 1] (已经是紧凑的)
// part1: 0->0, part2: 0->0, part4: 1->1, part3: 1->1
// 最终part1=[0,0], part2=[1,0], part4=[0,1], part3=[1,1]
const positions = region.children.map(c => ({
id: c.value.id,
position: c.value.position
}));
// children 按位置排序后的顺序:(0,0), (0,1), (1,0), (1,1)
expect(positions[0].id).toBe('p1');
expect(positions[0].position).toEqual([0, 0]);
expect(positions[1].id).toBe('p4');
expect(positions[1].position).toEqual([0, 1]);
expect(positions[2].id).toBe('p2');
expect(positions[2].position).toEqual([1, 0]);
expect(positions[3].id).toBe('p3');
expect(positions[3].position).toEqual([1, 1]);
});
});
describe('shuffle', () => {
it('should do nothing with empty region', () => {
const region = createRegion([], []);
const rng = createRNG(42);
shuffle(region, rng);
expect(region.children).toHaveLength(0);
});
it('should do nothing with single part', () => {
const part = createPart('p1', [0, 0, 0]);
const region = createRegion([], [part]);
const rng = createRNG(42);
shuffle(region, rng);
expect(region.children[0].value.position).toEqual([0, 0, 0]);
});
it('should shuffle positions of multiple parts', () => {
const part1 = createPart('p1', [0, 100]);
const part2 = createPart('p2', [1, 200]);
const part3 = createPart('p3', [2, 300]);
const region = createRegion([], [part1, part2, part3]);
const rng = createRNG(42);
const originalPositions = region.children.map(c => [...c.value.position]);
shuffle(region, rng);
// 位置应该被交换
const newPositions = region.children.map(c => c.value.position);
// 验证所有原始位置仍然存在(只是被交换了)
originalPositions.forEach(origPos => {
const found = newPositions.some(newPos =>
newPos[0] === origPos[0] && newPos[1] === origPos[1]
);
expect(found).toBe(true);
});
});
it('should be deterministic with same seed', () => {
const createRegionForTest = () => {
const part1 = createPart('p1', [0, 10]);
const part2 = createPart('p2', [1, 20]);
const part3 = createPart('p3', [2, 30]);
return createRegion([], [part1, part2, part3]);
};
const region1 = createRegionForTest();
const region2 = createRegionForTest();
const rng1 = createRNG(42);
const rng2 = createRNG(42);
shuffle(region1, rng1);
shuffle(region2, rng2);
const positions1 = region1.children.map(c => c.value.position);
const positions2 = region2.children.map(c => c.value.position);
expect(positions1).toEqual(positions2);
});
it('should produce different results with different seeds', () => {
const createRegionForTest = () => {
const part1 = createPart('p1', [0, 10]);
const part2 = createPart('p2', [1, 20]);
const part3 = createPart('p3', [2, 30]);
const part4 = createPart('p4', [3, 40]);
const part5 = createPart('p5', [4, 50]);
return createRegion([], [part1, part2, part3, part4, part5]);
};
const results = new Set<string>();
// 尝试多个种子,确保大多数产生不同结果
for (let seed = 1; seed <= 10; seed++) {
const region = createRegionForTest();
const rng = createRNG(seed);
shuffle(region, rng);
const positions = JSON.stringify(region.children.map(c => c.value.position));
results.add(positions);
}
// 10 个种子中至少应该有 5 个不同的结果
expect(results.size).toBeGreaterThan(5);
});
});
});