250 lines
7.9 KiB
TypeScript
250 lines
7.9 KiB
TypeScript
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
import { flip, flipTo, roll, findPartById, isCellOccupied, getPartAtPosition, isCellOccupiedByRegion, getPartAtPositionInRegion, Part } from '@/core/part';
|
||
|
|
import { createRegion } from '@/core/region';
|
||
|
|
import { createRNG } from '@/utils/rng';
|
||
|
|
|
||
|
|
function createTestPart<TMeta = {}>(overrides: Partial<Part<TMeta>> & TMeta): Part<TMeta> {
|
||
|
|
return {
|
||
|
|
id: 'test-part',
|
||
|
|
regionId: 'test-region',
|
||
|
|
position: [0, 0],
|
||
|
|
...overrides,
|
||
|
|
} as Part<TMeta>;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('flip', () => {
|
||
|
|
it('should cycle to next side when sides is defined', () => {
|
||
|
|
const part = createTestPart({ sides: 2, side: 0 });
|
||
|
|
flip(part);
|
||
|
|
expect(part.side).toBe(1);
|
||
|
|
flip(part);
|
||
|
|
expect(part.side).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should wrap around when side reaches sides count', () => {
|
||
|
|
const part = createTestPart({ sides: 6, side: 5 });
|
||
|
|
flip(part);
|
||
|
|
expect(part.side).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should do nothing when sides is undefined', () => {
|
||
|
|
const part = createTestPart({ side: 3 });
|
||
|
|
flip(part);
|
||
|
|
expect(part.side).toBe(3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should initialize side to 0 when side is undefined', () => {
|
||
|
|
const part = createTestPart({ sides: 4 });
|
||
|
|
flip(part);
|
||
|
|
expect(part.side).toBe(1);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('flipTo', () => {
|
||
|
|
it('should set side to specified value', () => {
|
||
|
|
const part = createTestPart({ sides: 6, side: 0 });
|
||
|
|
flipTo(part, 3);
|
||
|
|
expect(part.side).toBe(3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should do nothing when sides is undefined', () => {
|
||
|
|
const part = createTestPart({ side: 2 });
|
||
|
|
flipTo(part, 5);
|
||
|
|
expect(part.side).toBe(2);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should do nothing when side exceeds sides count', () => {
|
||
|
|
const part = createTestPart({ sides: 6, side: 0 });
|
||
|
|
flipTo(part, 6);
|
||
|
|
expect(part.side).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should allow side equal to sides - 1', () => {
|
||
|
|
const part = createTestPart({ sides: 6, side: 0 });
|
||
|
|
flipTo(part, 5);
|
||
|
|
expect(part.side).toBe(5);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('roll', () => {
|
||
|
|
it('should randomize side when sides is defined', () => {
|
||
|
|
const part = createTestPart({ sides: 6 });
|
||
|
|
const rng = createRNG(12345);
|
||
|
|
roll(part, rng);
|
||
|
|
expect(part.side).toBeGreaterThanOrEqual(0);
|
||
|
|
expect(part.side).toBeLessThan(6);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should do nothing when sides is undefined', () => {
|
||
|
|
const part = createTestPart({ side: 3 });
|
||
|
|
const rng = createRNG(12345);
|
||
|
|
roll(part, rng);
|
||
|
|
expect(part.side).toBe(3);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should produce deterministic results with same seed', () => {
|
||
|
|
const part1 = createTestPart({ sides: 6 });
|
||
|
|
const part2 = createTestPart({ sides: 6 });
|
||
|
|
const rng1 = createRNG(42);
|
||
|
|
const rng2 = createRNG(42);
|
||
|
|
roll(part1, rng1);
|
||
|
|
roll(part2, rng2);
|
||
|
|
expect(part1.side).toBe(part2.side);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('findPartById', () => {
|
||
|
|
it('should find part by id', () => {
|
||
|
|
const parts: Record<string, Part> = {
|
||
|
|
'part-1': createTestPart({ id: 'part-1' }),
|
||
|
|
'part-2': createTestPart({ id: 'part-2' }),
|
||
|
|
};
|
||
|
|
const result = findPartById(parts, 'part-1');
|
||
|
|
expect(result).toBeDefined();
|
||
|
|
expect(result!.id).toBe('part-1');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return undefined for non-existent id', () => {
|
||
|
|
const parts: Record<string, Part> = {
|
||
|
|
'part-1': createTestPart({ id: 'part-1' }),
|
||
|
|
};
|
||
|
|
const result = findPartById(parts, 'part-99');
|
||
|
|
expect(result).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should work with empty record', () => {
|
||
|
|
const parts: Record<string, Part> = {};
|
||
|
|
const result = findPartById(parts, 'any');
|
||
|
|
expect(result).toBeUndefined();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('isCellOccupied', () => {
|
||
|
|
it('should return true for occupied cell', () => {
|
||
|
|
const parts: Record<string, Part> = {
|
||
|
|
'p1': createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] }),
|
||
|
|
};
|
||
|
|
expect(isCellOccupied(parts, 'board', [1, 2])).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return false for empty cell', () => {
|
||
|
|
const parts: Record<string, Part> = {
|
||
|
|
'p1': createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] }),
|
||
|
|
};
|
||
|
|
expect(isCellOccupied(parts, 'board', [3, 4])).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return false for different region', () => {
|
||
|
|
const parts: Record<string, Part> = {
|
||
|
|
'p1': createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] }),
|
||
|
|
};
|
||
|
|
expect(isCellOccupied(parts, 'hand', [1, 2])).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should work with empty record', () => {
|
||
|
|
const parts: Record<string, Part> = {};
|
||
|
|
expect(isCellOccupied(parts, 'board', [0, 0])).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should handle multi-dimensional positions', () => {
|
||
|
|
const parts: Record<string, Part> = {
|
||
|
|
'p1': createTestPart({ id: 'p1', regionId: 'board', position: [1, 2, 3] }),
|
||
|
|
};
|
||
|
|
expect(isCellOccupied(parts, 'board', [1, 2, 3])).toBe(true);
|
||
|
|
expect(isCellOccupied(parts, 'board', [1, 2])).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('getPartAtPosition', () => {
|
||
|
|
it('should return part at specified position', () => {
|
||
|
|
const part1 = createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] });
|
||
|
|
const parts: Record<string, Part> = { 'p1': part1 };
|
||
|
|
const result = getPartAtPosition(parts, 'board', [1, 2]);
|
||
|
|
expect(result).toBe(part1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return undefined for empty cell', () => {
|
||
|
|
const parts: Record<string, Part> = {
|
||
|
|
'p1': createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] }),
|
||
|
|
};
|
||
|
|
const result = getPartAtPosition(parts, 'board', [3, 4]);
|
||
|
|
expect(result).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return undefined for different region', () => {
|
||
|
|
const parts: Record<string, Part> = {
|
||
|
|
'p1': createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] }),
|
||
|
|
};
|
||
|
|
const result = getPartAtPosition(parts, 'hand', [1, 2]);
|
||
|
|
expect(result).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should work with empty record', () => {
|
||
|
|
const parts: Record<string, Part> = {};
|
||
|
|
const result = getPartAtPosition(parts, 'board', [0, 0]);
|
||
|
|
expect(result).toBeUndefined();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('isCellOccupiedByRegion', () => {
|
||
|
|
it('should return true for occupied cell', () => {
|
||
|
|
const region = createRegion('board', [
|
||
|
|
{ name: 'x', min: 0, max: 2 },
|
||
|
|
{ name: 'y', min: 0, max: 2 },
|
||
|
|
]);
|
||
|
|
const part = createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] });
|
||
|
|
region.childIds.push(part.id);
|
||
|
|
region.partMap['1,2'] = part.id;
|
||
|
|
|
||
|
|
expect(isCellOccupiedByRegion(region, [1, 2])).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return false for empty cell', () => {
|
||
|
|
const region = createRegion('board', [
|
||
|
|
{ name: 'x', min: 0, max: 2 },
|
||
|
|
{ name: 'y', min: 0, max: 2 },
|
||
|
|
]);
|
||
|
|
const part = createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] });
|
||
|
|
region.childIds.push(part.id);
|
||
|
|
region.partMap['1,2'] = part.id;
|
||
|
|
|
||
|
|
expect(isCellOccupiedByRegion(region, [0, 0])).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should work with empty region', () => {
|
||
|
|
const region = createRegion('board', []);
|
||
|
|
expect(isCellOccupiedByRegion(region, [0, 0])).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('getPartAtPositionInRegion', () => {
|
||
|
|
it('should return part at specified position', () => {
|
||
|
|
const region = createRegion('board', []);
|
||
|
|
const part1 = createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] });
|
||
|
|
const parts: Record<string, Part> = { 'p1': part1 };
|
||
|
|
region.childIds.push(part1.id);
|
||
|
|
region.partMap['1,2'] = part1.id;
|
||
|
|
|
||
|
|
const result = getPartAtPositionInRegion(region, parts, [1, 2]);
|
||
|
|
expect(result).toBe(part1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should return undefined for empty cell', () => {
|
||
|
|
const region = createRegion('board', []);
|
||
|
|
const part1 = createTestPart({ id: 'p1', regionId: 'board', position: [1, 2] });
|
||
|
|
const parts: Record<string, Part> = { 'p1': part1 };
|
||
|
|
region.childIds.push(part1.id);
|
||
|
|
region.partMap['1,2'] = part1.id;
|
||
|
|
|
||
|
|
const result = getPartAtPositionInRegion(region, parts, [0, 0]);
|
||
|
|
expect(result).toBeUndefined();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should work with empty region', () => {
|
||
|
|
const region = createRegion('board', []);
|
||
|
|
const parts: Record<string, Part> = {};
|
||
|
|
const result = getPartAtPositionInRegion(region, parts, [0, 0]);
|
||
|
|
expect(result).toBeUndefined();
|
||
|
|
});
|
||
|
|
});
|