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