boardgame-core/tests/placement.actions.test.ts

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([]);
});
});
});