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

278 lines
11 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, axisIndex=0) 对齐:
// 排序part3(x=2), part1(x=5), part2(x=7) → children=[part3, part1, part2]
// 对齐part3.position[0]=0, part1.position[0]=1, part2.position[0]=2
// 结果part3=[0,30], part1=[1,10], part2=[2,20]
//
// 第二轴 (y, axisIndex=1) 对齐:
// 排序part1(y=10), part2(y=20), part3(y=30) → children=[part1, part2, part3]
// 对齐part1.position[1]=0, part2.position[1]=1, part3.position[1]=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 按 y 轴排序后的顺序
expect(positions[0].id).toBe('p1');
expect(positions[0].position).toEqual([1, 0]);
expect(positions[1].id).toBe('p2');
expect(positions[1].position).toEqual([2, 1]);
expect(positions[2].id).toBe('p3');
expect(positions[2].position).toEqual([0, 2]);
});
});
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);
});
});
});