423 lines
14 KiB
TypeScript
423 lines
14 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { createGameState } from '../src/core/GameState';
|
|
import { RegionType } from '../src/core/Region';
|
|
import { createMeepleAction } from '../src/actions/part.actions';
|
|
import { createRegionAction } from '../src/actions/region.actions';
|
|
import {
|
|
createPlacementAction,
|
|
getPlacementAction,
|
|
removePlacementAction,
|
|
movePlacementAction,
|
|
updatePlacementPositionAction,
|
|
updatePlacementRotationAction,
|
|
flipPlacementAction,
|
|
updatePlacementPartAction,
|
|
swapPlacementsAction,
|
|
setPlacementFaceAction,
|
|
getPlacementsInRegionAction,
|
|
getPlacementsOfPartAction,
|
|
} from '../src/actions/placement.actions';
|
|
|
|
describe('Placement Actions', () => {
|
|
let gameState: ReturnType<typeof createGameState>;
|
|
|
|
beforeEach(() => {
|
|
gameState = createGameState({ id: 'test-game', name: 'Test Game' });
|
|
});
|
|
|
|
describe('createPlacementAction', () => {
|
|
it('should create a placement', () => {
|
|
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
|
|
const placement = createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
});
|
|
|
|
expect(placement.id).toBe('placement-1');
|
|
expect(placement.partId).toBe('meeple-1');
|
|
expect(placement.regionId).toBe('board');
|
|
expect(placement.part).toBeDefined();
|
|
});
|
|
|
|
it('should create a placement with position', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
|
|
const placement = createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
position: { x: 3, y: 4 },
|
|
});
|
|
|
|
expect(placement.position).toEqual({ x: 3, y: 4 });
|
|
});
|
|
|
|
it('should throw if part does not exist', () => {
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
|
|
expect(() => {
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'non-existent',
|
|
regionId: 'board',
|
|
});
|
|
}).toThrow('Part non-existent not found');
|
|
});
|
|
|
|
it('should throw if region does not exist', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
|
|
expect(() => {
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'non-existent',
|
|
});
|
|
}).toThrow('Region non-existent not found');
|
|
});
|
|
});
|
|
|
|
describe('getPlacementAction', () => {
|
|
it('should return undefined for non-existent placement', () => {
|
|
const placement = getPlacementAction(gameState, 'non-existent');
|
|
expect(placement).toBeUndefined();
|
|
});
|
|
|
|
it('should return existing placement', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
});
|
|
|
|
const placement = getPlacementAction(gameState, 'placement-1');
|
|
expect(placement?.id).toBe('placement-1');
|
|
});
|
|
});
|
|
|
|
describe('removePlacementAction', () => {
|
|
it('should remove a placement', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
});
|
|
|
|
expect(getPlacementAction(gameState, 'placement-1')).toBeDefined();
|
|
|
|
removePlacementAction(gameState, 'placement-1');
|
|
|
|
expect(getPlacementAction(gameState, 'placement-1')).toBeUndefined();
|
|
});
|
|
|
|
it('should remove placement from region', () => {
|
|
const region = createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
});
|
|
|
|
region.placements.value = ['placement-1'];
|
|
removePlacementAction(gameState, 'placement-1');
|
|
|
|
expect(region.placements.value).not.toContain('placement-1');
|
|
});
|
|
});
|
|
|
|
describe('movePlacementAction', () => {
|
|
it('should move placement to another region', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
createRegionAction(gameState, { id: 'supply', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
});
|
|
|
|
movePlacementAction(gameState, 'placement-1', 'supply');
|
|
|
|
const placement = getPlacementAction(gameState, 'placement-1');
|
|
expect(placement?.regionId).toBe('supply');
|
|
});
|
|
|
|
it('should move placement to keyed region with key', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
});
|
|
|
|
movePlacementAction(gameState, 'placement-1', 'board', 'B2');
|
|
|
|
const slotValue = gameState.regions.value.get('board')?.slots?.value.get('B2');
|
|
expect(slotValue).toBe('placement-1');
|
|
});
|
|
|
|
it('should throw if key is required but not provided', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
});
|
|
|
|
expect(() => {
|
|
movePlacementAction(gameState, 'placement-1', 'board');
|
|
}).toThrow('Key is required for keyed regions');
|
|
});
|
|
});
|
|
|
|
describe('updatePlacementPositionAction', () => {
|
|
it('should update placement position', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
position: { x: 0, y: 0 },
|
|
});
|
|
|
|
updatePlacementPositionAction(gameState, 'placement-1', { x: 5, y: 3 });
|
|
|
|
const placement = getPlacementAction(gameState, 'placement-1');
|
|
expect(placement?.position).toEqual({ x: 5, y: 3 });
|
|
});
|
|
|
|
it('should throw if placement does not exist', () => {
|
|
expect(() => {
|
|
updatePlacementPositionAction(gameState, 'non-existent', { x: 1, y: 1 });
|
|
}).toThrow('Placement non-existent not found');
|
|
});
|
|
});
|
|
|
|
describe('updatePlacementRotationAction', () => {
|
|
it('should update placement rotation', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
rotation: 0,
|
|
});
|
|
|
|
updatePlacementRotationAction(gameState, 'placement-1', 90);
|
|
|
|
const placement = getPlacementAction(gameState, 'placement-1');
|
|
expect(placement?.rotation).toBe(90);
|
|
});
|
|
});
|
|
|
|
describe('flipPlacementAction', () => {
|
|
it('should flip placement faceUp state', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
faceUp: true,
|
|
});
|
|
|
|
flipPlacementAction(gameState, 'placement-1');
|
|
|
|
const placement = getPlacementAction(gameState, 'placement-1');
|
|
expect(placement?.faceUp).toBe(false);
|
|
|
|
flipPlacementAction(gameState, 'placement-1');
|
|
expect(getPlacementAction(gameState, 'placement-1')?.faceUp).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('setPlacementFaceAction', () => {
|
|
it('should set placement face up', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
faceUp: false,
|
|
});
|
|
|
|
setPlacementFaceAction(gameState, 'placement-1', true);
|
|
|
|
expect(getPlacementAction(gameState, 'placement-1')?.faceUp).toBe(true);
|
|
});
|
|
|
|
it('should set placement face down', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
faceUp: true,
|
|
});
|
|
|
|
setPlacementFaceAction(gameState, 'placement-1', false);
|
|
|
|
expect(getPlacementAction(gameState, 'placement-1')?.faceUp).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('updatePlacementPartAction', () => {
|
|
it('should update the part reference', () => {
|
|
const meeple1 = createMeepleAction(gameState, 'meeple-1', 'red');
|
|
const meeple2 = createMeepleAction(gameState, 'meeple-2', 'blue');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
});
|
|
|
|
updatePlacementPartAction(gameState, 'placement-1', meeple2);
|
|
|
|
const placement = getPlacementAction(gameState, 'placement-1');
|
|
expect(placement?.part?.id).toBe('meeple-2');
|
|
});
|
|
|
|
it('should set part reference to null', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'placement-1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
});
|
|
|
|
updatePlacementPartAction(gameState, 'placement-1', null);
|
|
|
|
expect(getPlacementAction(gameState, 'placement-1')?.part).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('swapPlacementsAction', () => {
|
|
it('should swap two placements in unkeyed region', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createMeepleAction(gameState, 'meeple-2', 'blue');
|
|
const region = createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'p1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
});
|
|
createPlacementAction(gameState, {
|
|
id: 'p2',
|
|
partId: 'meeple-2',
|
|
regionId: 'board',
|
|
});
|
|
|
|
region.placements.value = ['p1', 'p2'];
|
|
|
|
swapPlacementsAction(gameState, 'p1', 'p2');
|
|
|
|
expect(region.placements.value).toEqual(['p2', 'p1']);
|
|
});
|
|
|
|
it('should swap two placements in keyed region', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createMeepleAction(gameState, 'meeple-2', 'blue');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Keyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'p1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board',
|
|
});
|
|
createPlacementAction(gameState, {
|
|
id: 'p2',
|
|
partId: 'meeple-2',
|
|
regionId: 'board',
|
|
});
|
|
|
|
// 设置初始槽位
|
|
const region = gameState.getRegion('board');
|
|
region?.slots?.value.set('A1', 'p1');
|
|
region?.slots?.value.set('A2', 'p2');
|
|
|
|
swapPlacementsAction(gameState, 'p1', 'p2');
|
|
|
|
expect(region?.slots?.value.get('A1')).toBe('p2');
|
|
expect(region?.slots?.value.get('A2')).toBe('p1');
|
|
});
|
|
|
|
it('should throw if placements are in different regions', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board1', type: RegionType.Unkeyed });
|
|
createRegionAction(gameState, { id: 'board2', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, {
|
|
id: 'p1',
|
|
partId: 'meeple-1',
|
|
regionId: 'board1',
|
|
});
|
|
createPlacementAction(gameState, {
|
|
id: 'p2',
|
|
partId: 'meeple-1',
|
|
regionId: 'board2',
|
|
});
|
|
|
|
expect(() => {
|
|
swapPlacementsAction(gameState, 'p1', 'p2');
|
|
}).toThrow('Cannot swap placements in different regions directly');
|
|
});
|
|
});
|
|
|
|
describe('getPlacementsInRegionAction', () => {
|
|
it('should return all placements in a region', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'board' });
|
|
createPlacementAction(gameState, { id: 'p2', partId: 'meeple-1', regionId: 'board' });
|
|
|
|
const region = gameState.getRegion('board');
|
|
region!.placements.value = ['p1', 'p2'];
|
|
|
|
const placements = getPlacementsInRegionAction(gameState, 'board');
|
|
expect(placements.length).toBe(2);
|
|
expect(placements.map((p) => p.id)).toEqual(['p1', 'p2']);
|
|
});
|
|
|
|
it('should return empty array for non-existent region', () => {
|
|
const placements = getPlacementsInRegionAction(gameState, 'non-existent');
|
|
expect(placements).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('getPlacementsOfPartAction', () => {
|
|
it('should return all placements of a part', () => {
|
|
const meeple = createMeepleAction(gameState, 'meeple-1', 'red');
|
|
createRegionAction(gameState, { id: 'board1', type: RegionType.Unkeyed });
|
|
createRegionAction(gameState, { id: 'board2', type: RegionType.Unkeyed });
|
|
createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'board1' });
|
|
createPlacementAction(gameState, { id: 'p2', partId: 'meeple-1', regionId: 'board2' });
|
|
|
|
const placements = getPlacementsOfPartAction(gameState, 'meeple-1');
|
|
expect(placements.length).toBe(2);
|
|
expect(placements.map((p) => p.partId)).toEqual(['meeple-1', 'meeple-1']);
|
|
});
|
|
|
|
it('should return empty array for part with no placements', () => {
|
|
createMeepleAction(gameState, 'meeple-1', 'red');
|
|
|
|
const placements = getPlacementsOfPartAction(gameState, 'meeple-1');
|
|
expect(placements).toEqual([]);
|
|
});
|
|
});
|
|
});
|