import { describe, it, expect, beforeEach } from 'vitest'; import { createGameState } from '../src/core/GameState'; import { RegionType } from '../src/core/Region'; import { createRegionAction, getRegionAction, removeRegionAction, addPlacementToRegionAction, removePlacementFromRegionAction, setSlotAction, getSlotAction, clearRegionAction, getRegionPlacementCountAction, isRegionEmptyAction, isRegionFullAction, } from '../src/actions/region.actions'; import { createMeepleAction } from '../src/actions/part.actions'; import { createPlacementAction } from '../src/actions/placement.actions'; describe('Region Actions', () => { let gameState: ReturnType; beforeEach(() => { gameState = createGameState({ id: 'test-game', name: 'Test Game' }); }); describe('createRegionAction', () => { it('should create an unkeyed region', () => { const region = createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed, name: 'Draw Deck', }); expect(region.id).toBe('deck'); expect(region.type).toBe(RegionType.Unkeyed); expect(region.slots).toBeUndefined(); }); it('should create a keyed region', () => { const region = createRegionAction(gameState, { id: 'board', type: RegionType.Keyed, name: 'Game Board', }); expect(region.id).toBe('board'); expect(region.type).toBe(RegionType.Keyed); expect(region.slots).toBeDefined(); }); it('should create a region with capacity', () => { const region = createRegionAction(gameState, { id: 'hand', type: RegionType.Unkeyed, capacity: 5, }); expect(region.capacity).toBe(5); }); }); describe('getRegionAction', () => { it('should return undefined for non-existent region', () => { const region = getRegionAction(gameState, 'non-existent'); expect(region).toBeUndefined(); }); it('should return existing region', () => { createRegionAction(gameState, { id: 'board', type: RegionType.Keyed }); const region = getRegionAction(gameState, 'board'); expect(region?.id).toBe('board'); }); }); describe('removeRegionAction', () => { it('should remove a region', () => { createRegionAction(gameState, { id: 'board', type: RegionType.Keyed }); expect(getRegionAction(gameState, 'board')).toBeDefined(); removeRegionAction(gameState, 'board'); expect(getRegionAction(gameState, 'board')).toBeUndefined(); }); }); describe('addPlacementToRegionAction (unkeyed)', () => { it('should add a placement to an unkeyed region', () => { createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed }); const meeple = createMeepleAction(gameState, 'meeple-1', 'red'); const placement = createPlacementAction(gameState, { id: 'placement-1', partId: 'meeple-1', regionId: 'deck', }); addPlacementToRegionAction(gameState, 'deck', 'placement-1'); const region = getRegionAction(gameState, 'deck'); expect(region?.placements.value).toContain('placement-1'); }); it('should throw when adding to a keyed region', () => { createRegionAction(gameState, { id: 'board', type: RegionType.Keyed }); expect(() => { addPlacementToRegionAction(gameState, 'board', 'placement-1'); }).toThrow('Cannot use addPlacementToRegionAction on a keyed region'); }); it('should respect capacity limit', () => { createRegionAction(gameState, { id: 'hand', type: RegionType.Unkeyed, capacity: 2 }); createMeepleAction(gameState, 'meeple-1', 'red'); createMeepleAction(gameState, 'meeple-2', 'blue'); createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'hand' }); createPlacementAction(gameState, { id: 'p2', partId: 'meeple-2', regionId: 'hand' }); addPlacementToRegionAction(gameState, 'hand', 'p1'); addPlacementToRegionAction(gameState, 'hand', 'p2'); expect(() => { addPlacementToRegionAction(gameState, 'hand', 'p3'); }).toThrow('has reached its capacity'); }); }); describe('removePlacementFromRegionAction', () => { it('should remove a placement from an unkeyed region', () => { createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed }); const meeple = createMeepleAction(gameState, 'meeple-1', 'red'); createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'deck' }); addPlacementToRegionAction(gameState, 'deck', 'p1'); removePlacementFromRegionAction(gameState, 'deck', 'p1'); const region = getRegionAction(gameState, 'deck'); expect(region?.placements.value).not.toContain('p1'); }); it('should clear slot in keyed region', () => { createRegionAction(gameState, { id: 'board', type: RegionType.Keyed }); const meeple = createMeepleAction(gameState, 'meeple-1', 'red'); createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'board' }); setSlotAction(gameState, 'board', 'A1', 'p1'); removePlacementFromRegionAction(gameState, 'board', 'p1'); const slotValue = getSlotAction(gameState, 'board', 'A1'); expect(slotValue).toBeNull(); }); }); describe('setSlotAction (keyed)', () => { it('should set a slot in a keyed region', () => { createRegionAction(gameState, { id: 'board', type: RegionType.Keyed }); const meeple = createMeepleAction(gameState, 'meeple-1', 'red'); createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'board' }); setSlotAction(gameState, 'board', 'A1', 'p1'); const slotValue = getSlotAction(gameState, 'board', 'A1'); expect(slotValue).toBe('p1'); }); it('should throw when used on unkeyed region', () => { createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed }); expect(() => { setSlotAction(gameState, 'deck', 'slot1', 'p1'); }).toThrow('Cannot use setSlotAction on an unkeyed region'); }); it('should add placement to region list when setting slot', () => { createRegionAction(gameState, { id: 'board', type: RegionType.Keyed }); createRegionAction(gameState, { id: 'other', type: RegionType.Unkeyed }); const meeple = createMeepleAction(gameState, 'meeple-1', 'red'); createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'other' }); setSlotAction(gameState, 'board', 'A1', 'p1'); const region = getRegionAction(gameState, 'board'); expect(region?.placements.value).toContain('p1'); }); it('should clear a slot with null', () => { createRegionAction(gameState, { id: 'board', type: RegionType.Keyed }); const meeple = createMeepleAction(gameState, 'meeple-1', 'red'); createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'board' }); setSlotAction(gameState, 'board', 'A1', 'p1'); setSlotAction(gameState, 'board', 'A1', null); const slotValue = getSlotAction(gameState, 'board', 'A1'); expect(slotValue).toBeNull(); }); }); describe('getSlotAction', () => { it('should return null for empty slot', () => { createRegionAction(gameState, { id: 'board', type: RegionType.Keyed }); const slotValue = getSlotAction(gameState, 'board', 'A1'); expect(slotValue).toBeNull(); }); it('should throw when used on unkeyed region', () => { createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed }); expect(() => { getSlotAction(gameState, 'deck', 'slot1'); }).toThrow('Cannot use getSlotAction on an unkeyed region'); }); }); describe('clearRegionAction', () => { it('should clear all placements from unkeyed region', () => { createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed }); const meeple = createMeepleAction(gameState, 'meeple-1', 'red'); createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'deck' }); createPlacementAction(gameState, { id: 'p2', partId: 'meeple-1', regionId: 'deck' }); addPlacementToRegionAction(gameState, 'deck', 'p1'); addPlacementToRegionAction(gameState, 'deck', 'p2'); clearRegionAction(gameState, 'deck'); const region = getRegionAction(gameState, 'deck'); expect(region?.placements.value.length).toBe(0); }); it('should clear all slots in keyed region', () => { createRegionAction(gameState, { id: 'board', type: RegionType.Keyed }); const meeple = createMeepleAction(gameState, 'meeple-1', 'red'); createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'board' }); setSlotAction(gameState, 'board', 'A1', 'p1'); setSlotAction(gameState, 'board', 'A2', 'p1'); clearRegionAction(gameState, 'board'); const region = getRegionAction(gameState, 'board'); expect(region?.placements.value.length).toBe(0); expect(region?.slots?.value.size).toBe(0); }); }); describe('getRegionPlacementCountAction', () => { it('should return the count of placements', () => { createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed }); const meeple = createMeepleAction(gameState, 'meeple-1', 'red'); createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'deck' }); createPlacementAction(gameState, { id: 'p2', partId: 'meeple-1', regionId: 'deck' }); addPlacementToRegionAction(gameState, 'deck', 'p1'); addPlacementToRegionAction(gameState, 'deck', 'p2'); const count = getRegionPlacementCountAction(gameState, 'deck'); expect(count).toBe(2); }); it('should return 0 for non-existent region', () => { const count = getRegionPlacementCountAction(gameState, 'non-existent'); expect(count).toBe(0); }); }); describe('isRegionEmptyAction', () => { it('should return true for empty region', () => { createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed }); expect(isRegionEmptyAction(gameState, 'deck')).toBe(true); }); it('should return false for non-empty region', () => { createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed }); const meeple = createMeepleAction(gameState, 'meeple-1', 'red'); createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'deck' }); addPlacementToRegionAction(gameState, 'deck', 'p1'); expect(isRegionEmptyAction(gameState, 'deck')).toBe(false); }); }); describe('isRegionFullAction', () => { it('should return false for region without capacity', () => { createRegionAction(gameState, { id: 'deck', type: RegionType.Unkeyed }); expect(isRegionFullAction(gameState, 'deck')).toBe(false); }); it('should return true when at capacity', () => { createRegionAction(gameState, { id: 'hand', type: RegionType.Unkeyed, capacity: 2 }); const meeple = createMeepleAction(gameState, 'meeple-1', 'red'); createPlacementAction(gameState, { id: 'p1', partId: 'meeple-1', regionId: 'hand' }); createPlacementAction(gameState, { id: 'p2', partId: 'meeple-1', regionId: 'hand' }); addPlacementToRegionAction(gameState, 'hand', 'p1'); addPlacementToRegionAction(gameState, 'hand', 'p2'); expect(isRegionFullAction(gameState, 'hand')).toBe(true); }); }); });