diff --git a/README.md b/README.md index 10d6e7f..33ab2d5 100644 --- a/README.md +++ b/README.md @@ -6,177 +6,3 @@ - **响应式状态管理**: 使用 [@preact/signals-core](https://preactjs.com/guide/v10/signals/) 实现细粒度响应式 - **类型安全**: 完整的 TypeScript 支持 -- **核心概念**: - - **Part**: 游戏组件(棋子、卡牌、板块) - - **Region**: 容器区域(支持 keyed/unkeyed 两种模式) - - **Placement**: Part 在 Region 中的引用 - -## 安装 - -```bash -npm install -``` - -## 快速开始 - -### 1. 创建游戏状态 - -```typescript -import { createGameState } from 'boardgame-core'; - -const gameState = createGameState({ - id: 'game-1', - name: 'My Board Game', -}); -``` - -### 2. 创建 Parts(游戏组件) - -```typescript -import { createMeepleAction, createCardAction, createTileAction } from 'boardgame-core'; - -// 创建棋子 -const meeple = createMeepleAction(gameState, 'meeple-1', 'red', { - name: 'Player 1', -}); - -// 创建卡牌 -const card = createCardAction(gameState, 'card-1', { - suit: 'hearts', - value: 10, -}); - -// 创建板块 -const tile = createTileAction(gameState, 'tile-1', { - pattern: 'forest', - rotation: 90, -}); -``` - -### 3. 创建 Regions(区域) - -```typescript -import { createRegionAction, RegionType } from 'boardgame-core'; - -// Unkeyed Region - 适用于牌库、弃牌堆等 -const deck = createRegionAction(gameState, { - id: 'deck', - type: RegionType.Unkeyed, - name: 'Draw Deck', -}); - -// Keyed Region - 适用于版图、玩家区域等有固定位置的区域 -const board = createRegionAction(gameState, { - id: 'board', - type: RegionType.Keyed, - name: 'Game Board', -}); - -// 带容量的区域(如手牌限制) -const hand = createRegionAction(gameState, { - id: 'hand', - type: RegionType.Unkeyed, - capacity: 5, -}); -``` - -### 4. 创建 Placements(放置) - -```typescript -import { createPlacementAction } from 'boardgame-core'; - -// 将棋子放置在版图上 -const placement = createPlacementAction(gameState, { - id: 'placement-1', - partId: 'meeple-1', - regionId: 'board', - position: { x: 3, y: 4 }, -}); -``` - -### 5. 使用 Actions 操作状态 - -```typescript -import { - movePlacementAction, - flipPlacementAction, - setSlotAction, - updatePartAction, -} from 'boardgame-core'; - -// 移动 Placement 到另一个区域 -movePlacementAction(gameState, 'placement-1', 'hand'); - -// 翻转卡牌(面朝上/面朝下) -flipPlacementAction(gameState, 'placement-1'); - -// 在 Keyed Region 中设置槽位 -setSlotAction(gameState, 'board', 'A1', 'placement-1'); - -// 更新 Part 属性 -updatePartAction(gameState, 'meeple-1', { metadata: { score: 10 } }); -``` - -## API 参考 - -### Part Actions - -| Action | 描述 | -|--------|------| -| `createPartAction` | 创建通用 Part | -| `createMeepleAction` | 创建棋子 | -| `createCardAction` | 创建卡牌 | -| `createTileAction` | 创建板块 | -| `updatePartAction` | 更新 Part 属性 | -| `removePartAction` | 移除 Part | -| `getPartAction` | 获取 Part | - -### Region Actions - -| Action | 描述 | -|--------|------| -| `createRegionAction` | 创建 Region | -| `getRegionAction` | 获取 Region | -| `removeRegionAction` | 移除 Region | -| `addPlacementToRegionAction` | 添加 Placement 到 Unkeyed Region | -| `removePlacementFromRegionAction` | 从 Region 移除 Placement | -| `setSlotAction` | 设置 Keyed Region 的槽位 | -| `getSlotAction` | 获取 Keyed Region 的槽位 | -| `clearRegionAction` | 清空 Region | -| `getRegionPlacementCountAction` | 获取 Region 中 Placement 数量 | -| `isRegionEmptyAction` | 检查 Region 是否为空 | -| `isRegionFullAction` | 检查 Region 是否已满 | - -### Placement Actions - -| Action | 描述 | -|--------|------| -| `createPlacementAction` | 创建 Placement | -| `getPlacementAction` | 获取 Placement | -| `removePlacementAction` | 移除 Placement | -| `movePlacementAction` | 移动 Placement 到另一个 Region | -| `updatePlacementPositionAction` | 更新 Placement 位置 | -| `updatePlacementRotationAction` | 更新 Placement 旋转角度 | -| `flipPlacementAction` | 翻转 Placement | -| `updatePlacementPartAction` | 更新 Placement 的 Part 引用 | -| `swapPlacementsAction` | 交换两个 Placement 的位置 | -| `setPlacementFaceAction` | 设置 Placement 面朝上/下 | -| `getPlacementsInRegionAction` | 获取 Region 中的所有 Placements | -| `getPlacementsOfPartAction` | 获取 Part 的所有 Placements | - -## 运行测试 - -```bash -npm test # 监视模式 -npm run test:run # 运行一次 -``` - -## 构建 - -```bash -npm run build -``` - -## 许可证 - -MIT diff --git a/commands/card.play.json b/commands/card.play.json deleted file mode 100644 index d4229b8..0000000 --- a/commands/card.play.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "id": "card-play-001", - "name": "Play Card", - "description": "Draw a card from deck and play it to discard pile", - "steps": [ - { - "action": "createCard", - "params": { - "id": "card-hearts-10", - "suit": "hearts", - "value": 10, - "name": "10 of Hearts" - } - }, - { - "action": "createPlacement", - "params": { - "id": "placement-card-1", - "partId": "card-hearts-10", - "regionId": "player-hand", - "faceUp": false - } - }, - { - "action": "addPlacementToRegion", - "params": { - "regionId": "player-hand", - "placementId": "placement-card-1" - } - }, - { - "action": "setPlacementFace", - "params": { - "placementId": "placement-card-1", - "faceUp": true - } - }, - { - "action": "movePlacement", - "params": { - "placementId": "placement-card-1", - "targetRegionId": "discard" - } - } - ], - "metadata": { - "version": "1.0", - "category": "action", - "cardGame": true - } -} diff --git a/commands/cli/game.cli b/commands/cli/game.cli deleted file mode 100644 index 4b106eb..0000000 --- a/commands/cli/game.cli +++ /dev/null @@ -1,81 +0,0 @@ -# Board Game CLI Commands -# 命令格式: [--flags] - -# ========== 游戏设置 ========== - -# 创建游戏区域 -region board keyed --name="Game Board" -region supply unkeyed --name=Supply -region discard unkeyed --name="Discard Pile" -region hand unkeyed --name=Hand --capacity=5 - -# 设置游戏阶段 -phase setup - -# ========== 创建组件 ========== - -# 创建棋子 -create meeple red-1 --color=red --name="Red Player 1" -create meeple blue-1 --color=blue --name="Blue Player 1" - -# 创建卡牌 -create card hearts-10 --suit=hearts --value=10 -create card spades-ace --suit=spades --value=ace - -# 创建板块 -create tile forest-1 --pattern=forest --rotation=90 - -# ========== 放置组件 ========== - -# 放置棋子到版图 -place red-1 board 3 4 --rotation=0 --faceup=true - -# 放置卡牌到手牌(面朝下) -place hearts-10 hand 0 0 --faceup=false - -# ========== 移动和操作 ========== - -# 移动棋子 -move red-1 board --key=B3 - -# 翻转卡牌 -flip hearts-10 - -# 旋转板块 -rotate forest-1 180 - -# 设置位置 -position red-1 5 2 - -# 交换两个棋子 -swap red-1 blue-1 - -# ========== 卡牌操作 ========== - -# 抽牌 -draw supply 1 --to=hand - -# 洗牌(带种子) -shuffle discard --seed=2026 - -# 出牌到弃牌堆 -discard hearts-10 --to=discard - -# ========== 清理 ========== - -# 清空区域 -clear hand - -# 移除组件 -remove part red-1 -remove placement p1 -remove region temp - -# ========== 帮助 ========== - -# 显示所有命令 -help - -# 显示特定命令帮助 -help move -help shuffle diff --git a/commands/cli/moves.cli b/commands/cli/moves.cli deleted file mode 100644 index 4524373..0000000 --- a/commands/cli/moves.cli +++ /dev/null @@ -1,57 +0,0 @@ -# Movement Commands Examples -# 移动命令示例 - -# ========== 基本移动 ========== - -# 简单移动 -move piece-1 board - -# 移动到指定槽位 -move piece-1 board --key=A1 - -# ========== 位置操作 ========== - -# 设置精确位置 -position piece-1 0 0 -position piece-1 3 5 -position piece-1 -2 4 - -# 旋转 -rotate tile-1 90 -rotate tile-1 180 -rotate tile-1 270 -rotate tile-1 -45 - -# ========== 翻转 ========== - -# 翻转(切换面朝上/下) -flip card-1 -flip tile-2 - -# ========== 交换 ========== - -# 交换两个棋子位置 -swap meeple-1 meeple-2 - -# ========== 组合移动示例 ========== - -# 移动并旋转 -move knight-1 board --key=C3 -rotate knight-1 45 - -# 移动并设置位置 -move bishop-1 board -position bishop-1 4 4 - -# ========== 高级用法 ========== - -# 移动多个棋子(需要多行) -move rook-1 board --key=A1 -move rook-2 board --key=H1 -move king-1 board --key=E1 -move queen-1 board --key=D1 - -# 批量翻转 -flip card-1 -flip card-2 -flip card-3 diff --git a/commands/combo.move.json b/commands/combo.move.json deleted file mode 100644 index 003d60f..0000000 --- a/commands/combo.move.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "id": "combo-001", - "name": "Combo Move", - "description": "A complex combo: place meeple, move it, then flip a card", - "steps": [ - { - "action": "createMeeple", - "params": { - "id": "meeple-blue-1", - "color": "blue" - } - }, - { - "action": "createPlacement", - "params": { - "id": "placement-blue-1", - "partId": "meeple-blue-1", - "regionId": "supply", - "position": { - "x": 0, - "y": 0 - } - } - }, - { - "action": "movePlacement", - "params": { - "placementId": "placement-blue-1", - "targetRegionId": "board", - "key": "A1" - } - }, - { - "action": "updatePlacementPosition", - "params": { - "placementId": "placement-blue-1", - "position": { - "x": 1, - "y": 1 - } - } - }, - { - "action": "createCard", - "params": { - "id": "card-spades-ace", - "suit": "spades", - "value": "ace" - } - }, - { - "action": "createPlacement", - "params": { - "id": "placement-card-ace", - "partId": "card-spades-ace", - "regionId": "discard", - "faceUp": false - } - }, - { - "action": "flipPlacement", - "params": { - "placementId": "placement-card-ace" - } - } - ], - "metadata": { - "version": "1.0", - "category": "combo", - "difficulty": "medium" - } -} diff --git a/commands/move.meeple.json b/commands/move.meeple.json deleted file mode 100644 index 8d7425c..0000000 --- a/commands/move.meeple.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "id": "move-meeple-001", - "name": "Move Meeple", - "description": "Move a meeple from one position to another on the board", - "steps": [ - { - "action": "updatePlacementPosition", - "params": { - "placementId": "placement-red-1", - "position": { - "x": 5, - "y": 2 - } - } - } - ], - "metadata": { - "version": "1.0", - "category": "action", - "playerCount": 1 - } -} diff --git a/commands/phase.change.json b/commands/phase.change.json deleted file mode 100644 index 80cc9a5..0000000 --- a/commands/phase.change.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "id": "phase-change-001", - "name": "Phase Change", - "description": "Change the game phase with setup steps", - "steps": [ - { - "action": "setPhase", - "params": { - "phase": "setup" - } - }, - { - "action": "createRegion", - "params": { - "id": "board", - "type": "keyed", - "name": "Game Board" - } - }, - { - "action": "setPhase", - "params": { - "phase": "main" - } - }, - { - "action": "createMeeple", - "params": { - "id": "meeple-green-1", - "color": "green" - } - }, - { - "action": "setPhase", - "params": { - "phase": "end" - } - } - ], - "metadata": { - "version": "1.0", - "category": "game-flow" - } -} diff --git a/commands/place.meeple.json b/commands/place.meeple.json deleted file mode 100644 index 49ceab1..0000000 --- a/commands/place.meeple.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "id": "place-meeple-001", - "name": "Place Meeple", - "description": "Place a meeple on the board at specified position", - "steps": [ - { - "action": "createMeeple", - "params": { - "id": "meeple-red-1", - "color": "red", - "name": "Red Meeple 1" - } - }, - { - "action": "createPlacement", - "params": { - "id": "placement-red-1", - "partId": "meeple-red-1", - "regionId": "board", - "position": { - "x": 3, - "y": 4 - }, - "rotation": 0, - "faceUp": true - } - }, - { - "action": "setSlot", - "params": { - "regionId": "board", - "key": "C4", - "placementId": "placement-red-1" - } - } - ], - "metadata": { - "version": "1.0", - "category": "action", - "playerCount": 1 - } -} diff --git a/commands/setup.game.json b/commands/setup.game.json deleted file mode 100644 index 9f2425b..0000000 --- a/commands/setup.game.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "id": "setup-game-001", - "name": "Setup Game", - "description": "Initialize the game board with basic regions and starting components", - "steps": [ - { - "action": "createRegion", - "params": { - "id": "board", - "type": "keyed", - "name": "Game Board" - } - }, - { - "action": "createRegion", - "params": { - "id": "supply", - "type": "unkeyed", - "name": "Supply" - } - }, - { - "action": "createRegion", - "params": { - "id": "discard", - "type": "unkeyed", - "name": "Discard Pile" - } - }, - { - "action": "createRegion", - "params": { - "id": "player-hand", - "type": "unkeyed", - "name": "Player Hand", - "capacity": 5 - } - } - ], - "metadata": { - "version": "1.0", - "category": "setup" - } -} diff --git a/commands/tile.place.json b/commands/tile.place.json deleted file mode 100644 index f044620..0000000 --- a/commands/tile.place.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "id": "tile-place-001", - "name": "Place Tile", - "description": "Place a terrain tile on the board with rotation", - "steps": [ - { - "action": "createTile", - "params": { - "id": "tile-forest-1", - "pattern": "forest", - "rotation": 90, - "name": "Forest Tile 1" - } - }, - { - "action": "createPlacement", - "params": { - "id": "placement-tile-1", - "partId": "tile-forest-1", - "regionId": "board", - "position": { - "x": 2, - "y": 3 - }, - "rotation": 90, - "faceUp": true - } - }, - { - "action": "setSlot", - "params": { - "regionId": "board", - "key": "B3", - "placementId": "placement-tile-1" - } - } - ], - "metadata": { - "version": "1.0", - "category": "action", - "tileGame": true - } -} diff --git a/src/actions/part.actions.ts b/src/actions/part.actions.ts deleted file mode 100644 index e536010..0000000 --- a/src/actions/part.actions.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { GameState } from '../core/GameState'; -import type { Part, MeeplePart, CardPart, TilePart } from '../core/Part'; -import { PartType } from '../core/Part'; - -/** - * 创建 Part 并添加到 GameState - */ -export function createPartAction(gameState: GameState, part: T): T { - gameState.addPart(part); - return part; -} - -/** - * 创建 Meeple 并添加到 GameState - */ -export function createMeepleAction( - gameState: GameState, - id: string, - color: string, - options?: { name?: string; metadata?: Record } -): MeeplePart { - const part: MeeplePart = { - id, - type: PartType.Meeple, - color, - ...options, - }; - gameState.addPart(part); - return part; -} - -/** - * 创建 Card 并添加到 GameState - */ -export function createCardAction( - gameState: GameState, - id: string, - options?: { suit?: string; value?: number | string; name?: string; metadata?: Record } -): CardPart { - const part: CardPart = { - id, - type: PartType.Card, - ...options, - }; - gameState.addPart(part); - return part; -} - -/** - * 创建 Tile 并添加到 GameState - */ -export function createTileAction( - gameState: GameState, - id: string, - options?: { pattern?: string; rotation?: number; name?: string; metadata?: Record } -): TilePart { - const part: TilePart = { - id, - type: PartType.Tile, - ...options, - }; - gameState.addPart(part); - return part; -} - -/** - * 更新 Part - */ -export function updatePartAction( - gameState: GameState, - partId: string, - updates: Partial -): void { - gameState.updatePart(partId, updates); -} - -/** - * 移除 Part - */ -export function removePartAction(gameState: GameState, partId: string): void { - // 先移除所有引用该 Part 的 Placement - const placements = gameState.getPlacementsOfPart(partId); - for (const placement of placements) { - gameState.removePlacement(placement.id); - } - - gameState.removePart(partId); -} - -/** - * 获取 Part - */ -export function getPartAction(gameState: GameState, partId: string): Part | undefined { - return gameState.getPart(partId); -} diff --git a/src/actions/placement.actions.ts b/src/actions/placement.actions.ts deleted file mode 100644 index 95c491c..0000000 --- a/src/actions/placement.actions.ts +++ /dev/null @@ -1,222 +0,0 @@ -import type { GameState } from '../core/GameState'; -import type { Placement, Position } from '../core/Placement'; -import type { Part } from '../core/Part'; - -/** - * 创建 Placement 并添加到 GameState - */ -export function createPlacementAction( - gameState: GameState, - options: { - id: string; - partId: string; - regionId: string; - position?: Position; - rotation?: number; - faceUp?: boolean; - metadata?: Record; - } -): Placement { - const part = gameState.getPart(options.partId); - if (!part) { - throw new Error(`Part ${options.partId} not found`); - } - - const region = gameState.getRegion(options.regionId); - if (!region) { - throw new Error(`Region ${options.regionId} not found`); - } - - const placement: Placement = { - id: options.id, - partId: options.partId, - regionId: options.regionId, - part, - position: options.position, - rotation: options.rotation ?? 0, - faceUp: options.faceUp ?? true, - metadata: options.metadata, - }; - - gameState.addPlacement(placement); - return placement; -} - -/** - * 获取 Placement - */ -export function getPlacementAction(gameState: GameState, placementId: string): Placement | undefined { - return gameState.getPlacement(placementId); -} - -/** - * 移除 Placement - */ -export function removePlacementAction(gameState: GameState, placementId: string): void { - gameState.removePlacement(placementId); -} - -/** - * 移动 Placement 到另一个 Region - */ -export function movePlacementAction( - gameState: GameState, - placementId: string, - targetRegionId: string, - key?: string -): void { - gameState.movePlacement(placementId, targetRegionId, key); -} - -/** - * 更新 Placement 的位置 - */ -export function updatePlacementPositionAction( - gameState: GameState, - placementId: string, - position: Position -): void { - const placement = gameState.getPlacement(placementId); - if (!placement) { - throw new Error(`Placement ${placementId} not found`); - } - - const updated = { ...placement, position }; - const placements = new Map(gameState.placements.value); - placements.set(placementId, updated); - gameState.placements.value = placements; -} - -/** - * 更新 Placement 的旋转角度 - */ -export function updatePlacementRotationAction( - gameState: GameState, - placementId: string, - rotation: number -): void { - const placement = gameState.getPlacement(placementId); - if (!placement) { - throw new Error(`Placement ${placementId} not found`); - } - - const updated = { ...placement, rotation }; - const placements = new Map(gameState.placements.value); - placements.set(placementId, updated); - gameState.placements.value = placements; -} - -/** - * 翻转 Placement - */ -export function flipPlacementAction(gameState: GameState, placementId: string): void { - const placement = gameState.getPlacement(placementId); - if (!placement) { - throw new Error(`Placement ${placementId} not found`); - } - - const updated = { ...placement, faceUp: !placement.faceUp }; - const placements = new Map(gameState.placements.value); - placements.set(placementId, updated); - gameState.placements.value = placements; -} - -/** - * 更新 Placement 的 Part 引用 - */ -export function updatePlacementPartAction( - gameState: GameState, - placementId: string, - part: Part | null -): void { - gameState.updatePlacementPart(placementId, part); -} - -/** - * 交换两个 Placement 的位置 - */ -export function swapPlacementsAction( - gameState: GameState, - placementId1: string, - placementId2: string -): void { - const placement1 = gameState.getPlacement(placementId1); - const placement2 = gameState.getPlacement(placementId2); - - if (!placement1) { - throw new Error(`Placement ${placementId1} not found`); - } - if (!placement2) { - throw new Error(`Placement ${placementId2} not found`); - } - - if (placement1.regionId !== placement2.regionId) { - throw new Error('Cannot swap placements in different regions directly'); - } - - const region = gameState.getRegion(placement1.regionId); - if (!region) { - throw new Error(`Region ${placement1.regionId} not found`); - } - - // 如果是 keyed region,交换 slots - if (region.type === 'keyed' && region.slots) { - const slots = new Map(region.slots.value); - let key1: string | null = null; - let key2: string | null = null; - - for (const [key, value] of slots.entries()) { - if (value === placementId1) key1 = key; - if (value === placementId2) key2 = key; - } - - if (key1 && key2) { - slots.set(key1, placementId2); - slots.set(key2, placementId1); - region.slots.value = slots; - } - } else { - // unkeyed region:交换在 placements 列表中的位置 - const placements = [...region.placements.value]; - const index1 = placements.indexOf(placementId1); - const index2 = placements.indexOf(placementId2); - - if (index1 !== -1 && index2 !== -1) { - [placements[index1], placements[index2]] = [placements[index2], placements[index1]]; - region.placements.value = placements; - } - } -} - -/** - * 将 Placement 设置为面朝上/面朝下 - */ -export function setPlacementFaceAction( - gameState: GameState, - placementId: string, - faceUp: boolean -): void { - const placement = gameState.getPlacement(placementId); - if (!placement) { - throw new Error(`Placement ${placementId} not found`); - } - - const updated = { ...placement, faceUp }; - const placements = new Map(gameState.placements.value); - placements.set(placementId, updated); - gameState.placements.value = placements; -} - -/** - * 获取 Region 中的所有 Placements - */ -export function getPlacementsInRegionAction(gameState: GameState, regionId: string): Placement[] { - return gameState.getPlacementsInRegion(regionId); -} - -/** - * 获取 Part 的所有 Placements - */ -export function getPlacementsOfPartAction(gameState: GameState, partId: string): Placement[] { - return gameState.getPlacementsOfPart(partId); -} diff --git a/src/actions/region.actions.ts b/src/actions/region.actions.ts deleted file mode 100644 index 3e2998b..0000000 --- a/src/actions/region.actions.ts +++ /dev/null @@ -1,209 +0,0 @@ -import type { GameState } from '../core/GameState'; -import type { Region, RegionProperties } from '../core/Region'; -import { RegionType } from '../core/Region'; -import type { Placement } from '../core/Placement'; -import { signal } from '@preact/signals-core'; - -/** - * 创建 Region 并添加到 GameState - */ -export function createRegionAction(gameState: GameState, properties: RegionProperties): Region { - const region = { - ...properties, - placements: signal([]), - ...(properties.type === RegionType.Keyed ? { slots: signal>(new Map()) } : {}), - } as Region; - - gameState.addRegion(region); - return region; -} - -/** - * 获取 Region - */ -export function getRegionAction(gameState: GameState, regionId: string): Region | undefined { - return gameState.getRegion(regionId); -} - -/** - * 移除 Region - */ -export function removeRegionAction(gameState: GameState, regionId: string): void { - const region = gameState.getRegion(regionId); - if (region) { - // 先移除所有 Placement - const placementIds = [...region.placements.value]; - for (const placementId of placementIds) { - gameState.removePlacement(placementId); - } - } - gameState.removeRegion(regionId); -} - -/** - * 添加 Placement 到 Region (unkeyed) - */ -export function addPlacementToRegionAction( - gameState: GameState, - regionId: string, - placementId: string -): void { - const region = gameState.getRegion(regionId); - if (!region) { - throw new Error(`Region ${regionId} not found`); - } - - if (region.slots !== undefined) { - throw new Error('Cannot use addPlacementToRegionAction on a keyed region. Use setSlotAction instead.'); - } - - // 检查容量 - if (region.capacity !== undefined && region.placements.value.length >= region.capacity) { - throw new Error(`Region ${regionId} has reached its capacity of ${region.capacity}`); - } - - region.placements.value = [...region.placements.value, placementId]; -} - -/** - * 从 Region 移除 Placement - */ -export function removePlacementFromRegionAction( - gameState: GameState, - regionId: string, - placementId: string -): void { - const region = gameState.getRegion(regionId); - if (!region) { - throw new Error(`Region ${regionId} not found`); - } - - const current = region.placements.value; - const index = current.indexOf(placementId); - if (index !== -1) { - const updated = [...current]; - updated.splice(index, 1); - region.placements.value = updated; - } - - // 如果是 keyed region,清理 slot - if (region.type === RegionType.Keyed && region.slots) { - const slots = new Map(region.slots.value); - for (const [key, value] of slots.entries()) { - if (value === placementId) { - slots.set(key, null); - break; - } - } - region.slots.value = slots; - } -} - -/** - * 设置 Keyed Region 的槽位 - */ -export function setSlotAction( - gameState: GameState, - regionId: string, - key: string, - placementId: string | null -): void { - const region = gameState.getRegion(regionId); - if (!region) { - throw new Error(`Region ${regionId} not found`); - } - - if (region.type !== RegionType.Keyed || !region.slots) { - throw new Error('Cannot use setSlotAction on an unkeyed region.'); - } - - const slots = new Map(region.slots.value); - - // 如果是放置新 placement,需要更新 placements 列表 - if (placementId !== null) { - const currentPlacements = region.placements.value; - if (!currentPlacements.includes(placementId)) { - region.placements.value = [...currentPlacements, placementId]; - } - } - - slots.set(key, placementId); - region.slots.value = slots; -} - -/** - * 获取 Keyed Region 的槽位 - */ -export function getSlotAction( - gameState: GameState, - regionId: string, - key: string -): string | null { - const region = gameState.getRegion(regionId); - if (!region) { - throw new Error(`Region ${regionId} not found`); - } - - if (region.type !== RegionType.Keyed || !region.slots) { - throw new Error('Cannot use getSlotAction on an unkeyed region.'); - } - - return region.slots.value.get(key) ?? null; -} - -/** - * 清空 Region - */ -export function clearRegionAction(gameState: GameState, regionId: string): void { - const region = gameState.getRegion(regionId); - if (!region) { - throw new Error(`Region ${regionId} not found`); - } - - // 移除所有 Placement - const placementIds = [...region.placements.value]; - for (const placementId of placementIds) { - gameState.removePlacement(placementId); - } - - region.placements.value = []; - if (region.slots) { - region.slots.value = new Map(); - } -} - -/** - * 获取 Region 中 Placement 的数量 - */ -export function getRegionPlacementCountAction(gameState: GameState, regionId: string): number { - const region = gameState.getRegion(regionId); - if (!region) { - return 0; - } - return region.placements.value.length; -} - -/** - * 检查 Region 是否为空 - */ -export function isRegionEmptyAction(gameState: GameState, regionId: string): boolean { - const region = gameState.getRegion(regionId); - if (!region) { - return true; - } - return region.placements.value.length === 0; -} - -/** - * 检查 Region 是否已满 - */ -export function isRegionFullAction(gameState: GameState, regionId: string): boolean { - const region = gameState.getRegion(regionId); - if (!region) { - return false; - } - if (region.capacity === undefined) { - return false; - } - return region.placements.value.length >= region.capacity; -} diff --git a/src/commands/CliCommand.ts b/src/commands/CliCommand.ts deleted file mode 100644 index a88a32f..0000000 --- a/src/commands/CliCommand.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * CLI 命令参数 - */ -export interface CliCommandArgs { - /** 位置参数 */ - positional: string[]; - /** 标志参数 (--key=value 或 --flag) */ - flags: Record; -} - -/** - * CLI 命令定义 - */ -export interface CliCommand { - /** 命令名称 */ - name: string; - /** 命令描述 */ - description: string; - /** 使用示例 */ - usage: string; - /** 位置参数定义 */ - args?: CliCommandArgDef[]; - /** 标志参数定义 */ - flags?: CliCommandFlagDef[]; - /** 命令处理器 */ - handler: (args: CliCommandArgs) => CliCommandStep[]; -} - -/** - * 位置参数定义 - */ -export interface CliCommandArgDef { - /** 参数名称 */ - name: string; - /** 参数描述 */ - description: string; - /** 是否必需 */ - required?: boolean; - /** 默认值 */ - default?: string; -} - -/** - * 标志参数定义 - */ -export interface CliCommandFlagDef { - /** 标志名称 */ - name: string; - /** 标志描述 */ - description: string; - /** 是否必需 */ - required?: boolean; - /** 默认值 */ - default?: string | boolean; - /** 参数类型 */ - type?: 'string' | 'number' | 'boolean'; - /** 简写 */ - alias?: string; -} - -/** - * CLI 命令执行结果 - */ -export interface CliCommandResult { - success: boolean; - output?: string; - error?: string; - steps: CliCommandStep[]; -} - -/** - * CLI 命令步骤(转换为标准 CommandStep) - */ -export interface CliCommandStep { - action: string; - params: Record; -} - -/** - * 命令解析结果 - */ -export interface ParsedCliCommand { - commandName: string; - args: CliCommandArgs; - raw: string; -} diff --git a/src/commands/Command.ts b/src/commands/Command.ts deleted file mode 100644 index b49da63..0000000 --- a/src/commands/Command.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * 命令步骤类型 - */ -export enum CommandActionType { - // Part actions - CreateMeeple = 'createMeeple', - CreateCard = 'createCard', - CreateTile = 'createTile', - UpdatePart = 'updatePart', - RemovePart = 'removePart', - - // Region actions - CreateRegion = 'createRegion', - RemoveRegion = 'removeRegion', - AddPlacementToRegion = 'addPlacementToRegion', - RemovePlacementFromRegion = 'removePlacementFromRegion', - SetSlot = 'setSlot', - ClearRegion = 'clearRegion', - - // Placement actions - CreatePlacement = 'createPlacement', - RemovePlacement = 'removePlacement', - MovePlacement = 'movePlacement', - UpdatePlacementPosition = 'updatePlacementPosition', - UpdatePlacementRotation = 'updatePlacementRotation', - FlipPlacement = 'flipPlacement', - SetPlacementFace = 'setPlacementFace', - SwapPlacements = 'swapPlacements', - - // Game actions - SetPhase = 'setPhase', -} - -/** - * 命令步骤 - */ -export interface CommandStep { - action: CommandActionType; - params: Record; -} - -/** - * 命令执行结果 - */ -export interface CommandExecutionResult { - success: boolean; - error?: string; - executedSteps: number; - totalSteps: number; -} - -/** - * 命令定义 - */ -export interface Command { - /** 命令唯一标识 */ - id: string; - /** 命令名称 */ - name: string; - /** 命令描述 */ - description?: string; - /** 命令步骤 */ - steps: CommandStep[]; - /** 元数据 */ - metadata?: Record; -} - -/** - * 命令日志条目 - */ -export interface CommandLogEntry { - /** 时间戳 */ - timestamp: number; - /** 命令 ID */ - commandId: string; - /** 命令名称 */ - commandName: string; - /** 执行结果 */ - result: CommandExecutionResult; - /** 执行的步骤详情 */ - stepResults: StepResult[]; -} - -/** - * 步骤执行结果 - */ -export interface StepResult { - stepIndex: number; - action: CommandActionType; - success: boolean; - error?: string; - params: Record; -} - -/** - * 命令状态 - */ -export enum CommandStatus { - Pending = 'pending', - Executing = 'executing', - Completed = 'completed', - Failed = 'failed', -} - -/** - * 待执行命令 - */ -export interface QueuedCommand { - id: string; - command: Command; - status: CommandStatus; - queuedAt: number; - executedAt?: number; - result?: CommandExecutionResult; -} diff --git a/src/commands/CommandExecutor.ts b/src/commands/CommandExecutor.ts deleted file mode 100644 index 43d8ff6..0000000 --- a/src/commands/CommandExecutor.ts +++ /dev/null @@ -1,301 +0,0 @@ -import type { GameState } from '../core/GameState'; -import { PartType } from '../core/Part'; -import { RegionType } from '../core/Region'; -import type { Command, CommandStep, CommandExecutionResult, StepResult, CommandActionType } from './Command'; -import { - createMeepleAction, - createCardAction, - createTileAction, - updatePartAction, - removePartAction, -} from '../actions/part.actions'; -import { - createRegionAction, - removeRegionAction, - addPlacementToRegionAction, - removePlacementFromRegionAction, - setSlotAction, - clearRegionAction, -} from '../actions/region.actions'; -import { - createPlacementAction, - removePlacementAction, - movePlacementAction, - updatePlacementPositionAction, - updatePlacementRotationAction, - flipPlacementAction, - setPlacementFaceAction, - swapPlacementsAction, -} from '../actions/placement.actions'; - -/** - * 命令执行器 - * 负责解析并执行命令中的每一步 - */ -export class CommandExecutor { - private gameState: GameState; - - constructor(gameState: GameState) { - this.gameState = gameState; - } - - /** - * 执行命令 - */ - execute(command: Command): CommandExecutionResult { - const stepResults: StepResult[] = []; - let hasError = false; - let errorMessage: string | undefined; - - for (let i = 0; i < command.steps.length; i++) { - const step = command.steps[i]; - const stepResult = this.executeStep(step, i); - stepResults.push(stepResult); - - if (!stepResult.success) { - hasError = true; - errorMessage = stepResult.error; - break; // 遇到错误时停止执行 - } - } - - return { - success: !hasError, - error: errorMessage, - executedSteps: stepResults.filter((s) => s.success).length, - totalSteps: command.steps.length, - }; - } - - /** - * 执行单个步骤 - */ - private executeStep(step: CommandStep, stepIndex: number): StepResult { - try { - const { action, params } = step; - - switch (action) { - // Part actions - case 'createMeeple': - this.handleCreateMeeple(params); - break; - case 'createCard': - this.handleCreateCard(params); - break; - case 'createTile': - this.handleCreateTile(params); - break; - case 'updatePart': - this.handleUpdatePart(params); - break; - case 'removePart': - this.handleRemovePart(params); - break; - - // Region actions - case 'createRegion': - this.handleCreateRegion(params); - break; - case 'removeRegion': - this.handleRemoveRegion(params); - break; - case 'addPlacementToRegion': - this.handleAddPlacementToRegion(params); - break; - case 'removePlacementFromRegion': - this.handleRemovePlacementFromRegion(params); - break; - case 'setSlot': - this.handleSetSlot(params); - break; - case 'clearRegion': - this.handleClearRegion(params); - break; - - // Placement actions - case 'createPlacement': - this.handleCreatePlacement(params); - break; - case 'removePlacement': - this.handleRemovePlacement(params); - break; - case 'movePlacement': - this.handleMovePlacement(params); - break; - case 'updatePlacementPosition': - this.handleUpdatePlacementPosition(params); - break; - case 'updatePlacementRotation': - this.handleUpdatePlacementRotation(params); - break; - case 'flipPlacement': - this.handleFlipPlacement(params); - break; - case 'setPlacementFace': - this.handleSetPlacementFace(params); - break; - case 'swapPlacements': - this.handleSwapPlacements(params); - break; - - // Game actions - case 'setPhase': - this.handleSetPhase(params); - break; - - default: - throw new Error(`Unknown action type: ${action}`); - } - - return { - stepIndex, - action, - success: true, - params, - }; - } catch (error) { - return { - stepIndex, - action: step.action, - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - params: step.params, - }; - } - } - - // ========== Part action handlers ========== - - private handleCreateMeeple(params: Record): void { - const { id, color, name, metadata } = params; - createMeepleAction(this.gameState, id as string, color as string, { - name: name as string, - metadata: metadata as Record, - }); - } - - private handleCreateCard(params: Record): void { - const { id, suit, value, name, metadata } = params; - createCardAction(this.gameState, id as string, { - suit: suit as string, - value: value as number | string, - name: name as string, - metadata: metadata as Record, - }); - } - - private handleCreateTile(params: Record): void { - const { id, pattern, rotation, name, metadata } = params; - createTileAction(this.gameState, id as string, { - pattern: pattern as string, - rotation: rotation as number, - name: name as string, - metadata: metadata as Record, - }); - } - - private handleUpdatePart(params: Record): void { - const { partId, updates } = params; - updatePartAction(this.gameState, partId as string, updates as Record); - } - - private handleRemovePart(params: Record): void { - const { partId } = params; - removePartAction(this.gameState, partId as string); - } - - // ========== Region action handlers ========== - - private handleCreateRegion(params: Record): void { - const { id, type, name, capacity, metadata } = params; - createRegionAction(this.gameState, { - id: id as string, - type: type as RegionType, - name: name as string, - capacity: capacity as number, - metadata: metadata as Record, - }); - } - - private handleRemoveRegion(params: Record): void { - const { regionId } = params; - removeRegionAction(this.gameState, regionId as string); - } - - private handleAddPlacementToRegion(params: Record): void { - const { regionId, placementId } = params; - addPlacementToRegionAction(this.gameState, regionId as string, placementId as string); - } - - private handleRemovePlacementFromRegion(params: Record): void { - const { regionId, placementId } = params; - removePlacementFromRegionAction(this.gameState, regionId as string, placementId as string); - } - - private handleSetSlot(params: Record): void { - const { regionId, key, placementId } = params; - setSlotAction(this.gameState, regionId as string, key as string, placementId as string | null); - } - - private handleClearRegion(params: Record): void { - const { regionId } = params; - clearRegionAction(this.gameState, regionId as string); - } - - // ========== Placement action handlers ========== - - private handleCreatePlacement(params: Record): void { - const { id, partId, regionId, position, rotation, faceUp, metadata } = params; - createPlacementAction(this.gameState, { - id: id as string, - partId: partId as string, - regionId: regionId as string, - position: position as { x: number; y: number }, - rotation: rotation as number, - faceUp: faceUp as boolean, - metadata: metadata as Record, - }); - } - - private handleRemovePlacement(params: Record): void { - const { placementId } = params; - removePlacementAction(this.gameState, placementId as string); - } - - private handleMovePlacement(params: Record): void { - const { placementId, targetRegionId, key } = params; - movePlacementAction(this.gameState, placementId as string, targetRegionId as string, key as string); - } - - private handleUpdatePlacementPosition(params: Record): void { - const { placementId, position } = params; - updatePlacementPositionAction(this.gameState, placementId as string, position as { x: number; y: number }); - } - - private handleUpdatePlacementRotation(params: Record): void { - const { placementId, rotation } = params; - updatePlacementRotationAction(this.gameState, placementId as string, rotation as number); - } - - private handleFlipPlacement(params: Record): void { - const { placementId } = params; - flipPlacementAction(this.gameState, placementId as string); - } - - private handleSetPlacementFace(params: Record): void { - const { placementId, faceUp } = params; - setPlacementFaceAction(this.gameState, placementId as string, faceUp as boolean); - } - - private handleSwapPlacements(params: Record): void { - const { placementId1, placementId2 } = params; - swapPlacementsAction(this.gameState, placementId1 as string, placementId2 as string); - } - - // ========== Game action handlers ========== - - private handleSetPhase(params: Record): void { - const { phase } = params; - this.gameState.setPhase(phase as string); - } -} diff --git a/src/commands/CommandLog.ts b/src/commands/CommandLog.ts deleted file mode 100644 index 9b5c16b..0000000 --- a/src/commands/CommandLog.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { signal, Signal } from '@preact/signals-core'; -import type { Command, CommandLogEntry, CommandExecutionResult, StepResult, QueuedCommand } from './Command'; -import { CommandStatus } from './Command'; - -/** - * 命令日志过滤器 - */ -export interface CommandLogFilter { - commandId?: string; - success?: boolean; - startTime?: number; - endTime?: number; -} - -/** - * 命令日志类 - * 记录所有执行的命令及其结果 - */ -export class CommandLog { - /** 日志条目信号 */ - private entries: Signal; - - /** 待执行队列 */ - private queue: QueuedCommand[]; - - constructor() { - this.entries = signal([]); - this.queue = []; - } - - /** - * 记录命令执行 - */ - log( - command: Command, - result: CommandExecutionResult, - stepResults: StepResult[] - ): void { - const entry: CommandLogEntry = { - timestamp: Date.now(), - commandId: command.id, - commandName: command.name, - result, - stepResults, - }; - - const current = this.entries.value; - this.entries.value = [...current, entry]; - } - - /** - * 获取所有日志条目 - */ - getEntries(): CommandLogEntry[] { - return this.entries.value; - } - - /** - * 获取日志信号 - */ - getEntriesSignal(): Signal { - return this.entries; - } - - /** - * 根据过滤器获取日志条目 - */ - getFilteredEntries(filter: CommandLogFilter): CommandLogEntry[] { - return this.entries.value.filter((entry) => { - if (filter.commandId && entry.commandId !== filter.commandId) { - return false; - } - if (filter.success !== undefined && entry.result.success !== filter.success) { - return false; - } - if (filter.startTime && entry.timestamp < filter.startTime) { - return false; - } - if (filter.endTime && entry.timestamp > filter.endTime) { - return false; - } - return true; - }); - } - - /** - * 获取命令的执行历史 - */ - getCommandHistory(commandId: string): CommandLogEntry[] { - return this.getFilteredEntries({ commandId }); - } - - /** - * 获取失败的命令 - */ - getFailedCommands(): CommandLogEntry[] { - return this.getFilteredEntries({ success: false }); - } - - /** - * 获取成功的命令 - */ - getSuccessfulCommands(): CommandLogEntry[] { - return this.getFilteredEntries({ success: true }); - } - - /** - * 清空日志 - */ - clear(): void { - this.entries.value = []; - } - - /** - * 导出日志为 JSON - */ - exportToJson(): string { - return JSON.stringify(this.entries.value, null, 2); - } - - /** - * 获取日志条目数量 - */ - getCount(): number { - return this.entries.value.length; - } - - /** - * 获取最后一个日志条目 - */ - getLastEntry(): CommandLogEntry | null { - const entries = this.entries.value; - return entries.length > 0 ? entries[entries.length - 1] : null; - } - - // ========== 队列管理 ========== - - /** - * 添加命令到队列 - */ - enqueue(command: Command): QueuedCommand { - const queued: QueuedCommand = { - id: command.id, - command, - status: CommandStatus.Pending, - queuedAt: Date.now(), - }; - this.queue.push(queued); - return queued; - } - - /** - * 从队列中移除命令 - */ - dequeue(): QueuedCommand | null { - if (this.queue.length === 0) { - return null; - } - return this.queue.shift() || null; - } - - /** - * 获取队列中的所有命令 - */ - getQueue(): QueuedCommand[] { - return [...this.queue]; - } - - /** - * 更新队列中命令的状态 - */ - updateQueueStatus(commandId: string, status: CommandStatus, result?: CommandExecutionResult): void { - const index = this.queue.findIndex((q) => q.command.id === commandId); - if (index !== -1) { - this.queue[index].status = status; - if (status === CommandStatus.Completed || status === CommandStatus.Failed) { - this.queue[index].executedAt = Date.now(); - this.queue[index].result = result; - } - } - } - - /** - * 清空队列 - */ - clearQueue(): void { - this.queue = []; - } - - /** - * 获取队列长度 - */ - getQueueLength(): number { - return this.queue.length; - } -} - -/** - * 创建命令日志 - */ -export function createCommandLog(): CommandLog { - return new CommandLog(); -} diff --git a/src/commands/CommandParser.ts b/src/commands/CommandParser.ts deleted file mode 100644 index bb53966..0000000 --- a/src/commands/CommandParser.ts +++ /dev/null @@ -1,163 +0,0 @@ -import type { ParsedCliCommand, CliCommandArgs } from './CliCommand'; - -/** - * 命令解析错误 - */ -export class CommandParseError extends Error { - constructor(message: string) { - super(message); - this.name = 'CommandParseError'; - } -} - -/** - * CLI 命令解析器 - * 解析 CLI 风格的命令字符串 - */ -export class CommandParser { - /** - * 解析命令字符串 - * @param input 命令字符串,如 "move card-1 discard --faceup=true" - */ - parse(input: string): ParsedCliCommand { - const trimmed = input.trim(); - - if (!trimmed) { - throw new CommandParseError('Empty command'); - } - - const tokens = this.tokenize(trimmed); - - if (tokens.length === 0) { - throw new CommandParseError('No command found'); - } - - const commandName = tokens[0]; - const args = this.parseArgs(tokens.slice(1)); - - return { - commandName, - args, - raw: input, - }; - } - - /** - * 将命令字符串分词 - */ - private tokenize(input: string): string[] { - const tokens: string[] = []; - let current = ''; - let inQuotes = false; - let quoteChar = ''; - - for (let i = 0; i < input.length; i++) { - const char = input[i]; - - if (inQuotes) { - if (char === quoteChar) { - tokens.push(current); - current = ''; - inQuotes = false; - } else { - current += char; - } - } else if (char === '"' || char === "'") { - inQuotes = true; - quoteChar = char; - } else if (char === ' ' || char === '\t') { - if (current) { - tokens.push(current); - current = ''; - } - } else { - current += char; - } - } - - if (current) { - tokens.push(current); - } - - if (inQuotes) { - throw new CommandParseError('Unclosed quote in command'); - } - - return tokens; - } - - /** - * 解析参数 - */ - private parseArgs(tokens: string[]): CliCommandArgs { - const positional: string[] = []; - const flags: Record = {}; - - for (const token of tokens) { - if (token.startsWith('--')) { - // 长标志 --key=value 或 --flag - const flagMatch = token.match(/^--([^=]+)(?:=(.+))?$/); - if (flagMatch) { - const [, key, value] = flagMatch; - flags[key] = value !== undefined ? this.parseFlagValue(value) : true; - } - } else if (token.startsWith('-') && token.length === 2) { - // 短标志 -f 或 -k=v - const flagMatch = token.match(/^-([^=]+)(?:=(.+))?$/); - if (flagMatch) { - const [, key, value] = flagMatch; - flags[key] = value !== undefined ? this.parseFlagValue(value) : true; - } - } else { - // 位置参数 - positional.push(token); - } - } - - return { positional, flags }; - } - - /** - * 解析标志值 - */ - private parseFlagValue(value: string): string | boolean { - // 布尔值 - if (value.toLowerCase() === 'true') return true; - if (value.toLowerCase() === 'false') return false; - - // 数字转换为字符串 - return value; - } - - /** - * 格式化命令用于显示 - */ - static formatCommand(name: string, args?: CliCommandArgs): string { - if (!args) { - return name; - } - - const parts = [name]; - - // 添加位置参数 - parts.push(...args.positional); - - // 添加标志参数 - for (const [key, value] of Object.entries(args.flags)) { - if (value === true) { - parts.push(`--${key}`); - } else { - parts.push(`--${key}=${value}`); - } - } - - return parts.join(' '); - } -} - -/** - * 创建命令解析器 - */ -export function createCommandParser(): CommandParser { - return new CommandParser(); -} diff --git a/src/commands/CommandRegistry.ts b/src/commands/CommandRegistry.ts deleted file mode 100644 index 6add69c..0000000 --- a/src/commands/CommandRegistry.ts +++ /dev/null @@ -1,209 +0,0 @@ -import type { CliCommand, CliCommandArgs, CliCommandResult, CliCommandStep } from './CliCommand'; -import { CommandParser } from './CommandParser'; - -/** - * 命令注册表 - * 注册和管理 CLI 命令 - */ -export class CommandRegistry { - private commands: Map; - private parser: CommandParser; - - constructor() { - this.commands = new Map(); - this.parser = new CommandParser(); - } - - /** - * 注册命令 - */ - register(command: CliCommand): void { - this.commands.set(command.name, command); - } - - /** - * 注册多个命令 - */ - registerAll(commands: CliCommand[]): void { - for (const command of commands) { - this.register(command); - } - } - - /** - * 获取命令 - */ - get(name: string): CliCommand | undefined { - return this.commands.get(name); - } - - /** - * 检查命令是否存在 - */ - has(name: string): boolean { - return this.commands.has(name); - } - - /** - * 移除命令 - */ - unregister(name: string): void { - this.commands.delete(name); - } - - /** - * 获取所有命令 - */ - getAll(): CliCommand[] { - return Array.from(this.commands.values()); - } - - /** - * 解析并执行命令 - */ - execute(input: string): CliCommandResult { - try { - const parsed = this.parser.parse(input); - const command = this.commands.get(parsed.commandName); - - if (!command) { - return { - success: false, - error: `Unknown command: ${parsed.commandName}`, - steps: [], - }; - } - - // 验证参数 - const validationError = this.validateArgs(command, parsed.args); - if (validationError) { - return { - success: false, - error: validationError, - steps: [], - }; - } - - // 执行命令处理器 - const steps = command.handler(parsed.args); - - return { - success: true, - steps, - }; - } catch (error) { - return { - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - steps: [], - }; - } - } - - /** - * 验证参数 - */ - private validateArgs(command: CliCommand, args: CliCommandArgs): string | null { - // 验证位置参数 - if (command.args) { - for (const argDef of command.args) { - const index = command.args.indexOf(argDef); - - if (argDef.required && index >= args.positional.length) { - return `Missing required argument: ${argDef.name}`; - } - } - } - - // 验证标志参数 - if (command.flags) { - for (const flagDef of command.flags) { - if (flagDef.required && !(flagDef.name in args.flags)) { - // 检查别名 - const hasAlias = flagDef.alias && args.flags[flagDef.alias]; - if (!hasAlias) { - return `Missing required flag: --${flagDef.name}`; - } - } - } - } - - return null; - } - - /** - * 生成帮助信息 - */ - help(commandName?: string): string { - if (commandName) { - const command = this.commands.get(commandName); - if (!command) { - return `Unknown command: ${commandName}`; - } - return this.formatCommandHelp(command); - } - - // 所有命令的帮助 - const lines = ['Available commands:', '']; - for (const command of this.commands.values()) { - lines.push(` ${command.name.padEnd(15)} ${command.description}`); - } - lines.push(''); - lines.push('Use "help " for more information.'); - return lines.join('\n'); - } - - /** - * 格式化单个命令的帮助 - */ - private formatCommandHelp(command: CliCommand): string { - const lines = [ - `Command: ${command.name}`, - `Description: ${command.description}`, - `Usage: ${command.usage}`, - ]; - - if (command.args && command.args.length > 0) { - lines.push(''); - lines.push('Arguments:'); - for (const arg of command.args) { - const required = arg.required ? '(required)' : '(optional)'; - lines.push(` ${arg.name.padEnd(15)} ${arg.description} ${required}`); - } - } - - if (command.flags && command.flags.length > 0) { - lines.push(''); - lines.push('Flags:'); - for (const flag of command.flags) { - const alias = flag.alias ? `-${flag.alias}, ` : ''; - const required = flag.required ? '(required)' : '(optional)'; - const defaultVal = flag.default !== undefined ? `(default: ${flag.default})` : ''; - lines.push(` ${alias}--${flag.name.padEnd(12)} ${flag.description} ${required} ${defaultVal}`); - } - } - - return lines.join('\n'); - } - - /** - * 清除所有命令 - */ - clear(): void { - this.commands.clear(); - } - - /** - * 获取命令数量 - */ - getCount(): number { - return this.commands.size; - } -} - -/** - * 创建命令注册表 - */ -export function createCommandRegistry(): CommandRegistry { - return new CommandRegistry(); -} diff --git a/src/commands/cli.commands.ts b/src/commands/cli.commands.ts deleted file mode 100644 index 4876c59..0000000 --- a/src/commands/cli.commands.ts +++ /dev/null @@ -1,485 +0,0 @@ -import type { CliCommand } from './CliCommand'; -import { RegionType } from '../core/Region'; - -/** - * CLI 命令定义集合 - */ - -/** - * move [--key=slotKey] - * 移动 Placement 到另一个区域 - */ -export const moveCommand: CliCommand = { - name: 'move', - description: 'Move a placement to another region', - usage: 'move [--key=slotKey]', - args: [ - { name: 'placementId', description: 'The placement ID to move', required: true }, - { name: 'targetRegionId', description: 'The target region ID', required: true }, - ], - flags: [ - { name: 'key', description: 'Slot key for keyed regions', type: 'string' }, - ], - handler: (args) => { - const [placementId, targetRegionId] = args.positional; - return [ - { - action: 'movePlacement', - params: { - placementId, - targetRegionId, - key: args.flags.key as string | undefined, - }, - }, - ]; - }, -}; - -/** - * place [x] [y] [--rotation=0] [--faceup=true] - * 创建 Placement 并放置到区域 - */ -export const placeCommand: CliCommand = { - name: 'place', - description: 'Place a part in a region', - usage: 'place [x] [y] [--rotation=0] [--faceup=true]', - args: [ - { name: 'partId', description: 'The part ID to place', required: true }, - { name: 'regionId', description: 'The region ID to place in', required: true }, - { name: 'x', description: 'X position', default: '0' }, - { name: 'y', description: 'Y position', default: '0' }, - ], - flags: [ - { name: 'rotation', description: 'Rotation angle', type: 'number', default: '0' }, - { name: 'faceup', description: 'Face up or down', type: 'boolean', default: 'true' }, - ], - handler: (args) => { - const [partId, regionId] = args.positional; - const x = parseInt(args.positional[2] || '0', 10); - const y = parseInt(args.positional[3] || '0', 10); - const rotation = typeof args.flags.rotation === 'string' - ? parseInt(args.flags.rotation, 10) - : 0; - - return [ - { - action: 'createPlacement', - params: { - id: `placement-${partId}-${Date.now()}`, - partId, - regionId, - position: { x, y }, - rotation, - faceUp: args.flags.faceup === true || args.flags.faceup === 'true', - }, - }, - ]; - }, -}; - -/** - * flip - * 翻转 Placement - */ -export const flipCommand: CliCommand = { - name: 'flip', - description: 'Flip a placement face up/down', - usage: 'flip ', - args: [ - { name: 'placementId', description: 'The placement ID to flip', required: true }, - ], - handler: (args) => { - const [placementId] = args.positional; - return [ - { - action: 'flipPlacement', - params: { placementId }, - }, - ]; - }, -}; - -/** - * create [options...] - * 创建 Part(meeple/card/tile) - */ -export const createCommand: CliCommand = { - name: 'create', - description: 'Create a part (meeple, card, or tile)', - usage: 'create [--color=color] [--suit=suit] [--value=value] [--pattern=pattern]', - args: [ - { name: 'type', description: 'Part type (meeple, card, tile)', required: true }, - { name: 'id', description: 'Part ID', required: true }, - ], - flags: [ - { name: 'color', description: 'Meeple color', type: 'string' }, - { name: 'suit', description: 'Card suit', type: 'string' }, - { name: 'value', description: 'Card value', type: 'string' }, - { name: 'pattern', description: 'Tile pattern', type: 'string' }, - { name: 'rotation', description: 'Tile rotation', type: 'number' }, - { name: 'name', description: 'Part name', type: 'string' }, - ], - handler: (args) => { - const [type, id] = args.positional; - const steps = []; - - if (type === 'meeple') { - steps.push({ - action: 'createMeeple', - params: { - id, - color: (args.flags.color as string) || 'red', - name: args.flags.name as string, - }, - }); - } else if (type === 'card') { - steps.push({ - action: 'createCard', - params: { - id, - suit: args.flags.suit as string, - value: args.flags.value as string, - name: args.flags.name as string, - }, - }); - } else if (type === 'tile') { - const rotation = typeof args.flags.rotation === 'string' - ? parseInt(args.flags.rotation, 10) - : 0; - steps.push({ - action: 'createTile', - params: { - id, - pattern: args.flags.pattern as string, - rotation, - name: args.flags.name as string, - }, - }); - } else { - throw new Error(`Unknown part type: ${type}`); - } - - return steps; - }, -}; - -/** - * region [--name=name] [--capacity=n] - * 创建 Region - */ -export const regionCommand: CliCommand = { - name: 'region', - description: 'Create a region', - usage: 'region [--name=name] [--capacity=n]', - args: [ - { name: 'id', description: 'Region ID', required: true }, - { name: 'type', description: 'Region type (keyed/unkeyed)', required: true }, - ], - flags: [ - { name: 'name', description: 'Region name', type: 'string' }, - { name: 'capacity', description: 'Maximum capacity', type: 'number' }, - ], - handler: (args) => { - const [id, type] = args.positional; - const capacity = typeof args.flags.capacity === 'string' - ? parseInt(args.flags.capacity, 10) - : undefined; - return [ - { - action: 'createRegion', - params: { - id, - type: type.toLowerCase() === 'keyed' ? RegionType.Keyed : RegionType.Unkeyed, - name: args.flags.name as string, - capacity, - }, - }, - ]; - }, -}; - -/** - * draw [count] [--to=handId] - * 从牌库抽牌 - */ -export const drawCommand: CliCommand = { - name: 'draw', - description: 'Draw cards from a deck', - usage: 'draw [count] [--to=handId]', - args: [ - { name: 'deckId', description: 'Source deck/region ID', required: true }, - { name: 'count', description: 'Number of cards to draw', default: '1' }, - ], - flags: [ - { name: 'to', description: 'Target hand region ID', type: 'string', default: 'hand' }, - ], - handler: (args) => { - // 注意:这是一个简化版本,实际抽牌需要更复杂的逻辑 - const [deckId] = args.positional; - const count = parseInt(args.positional[1] || '1', 10); - const targetHand = args.flags.to as string; - - const steps = []; - for (let i = 0; i < count; i++) { - steps.push({ - action: 'createCard', - params: { - id: `card-${deckId}-${Date.now()}-${i}`, - }, - }); - } - - return steps; - }, -}; - -/** - * shuffle [--seed=number] - * 洗牌 - */ -export const shuffleCommand: CliCommand = { - name: 'shuffle', - description: 'Shuffle placements in a region', - usage: 'shuffle [--seed=number]', - args: [ - { name: 'regionId', description: 'Region ID to shuffle', required: true }, - ], - flags: [ - { name: 'seed', description: 'Random seed for reproducibility', type: 'number' }, - ], - handler: (args) => { - // shuffle 命令需要特殊的执行逻辑,这里返回一个标记步骤 - // 实际执行时需要在 CommandExecutor 中特殊处理 - const [regionId] = args.positional; - const seed = typeof args.flags.seed === 'string' - ? parseInt(args.flags.seed, 10) - : undefined; - return [ - { - action: 'shuffleRegion', - params: { - regionId, - seed, - }, - }, - ]; - }, -}; - -/** - * discard [--to=discardId] - * 将 Placement 移到弃牌堆 - */ -export const discardCommand: CliCommand = { - name: 'discard', - description: 'Move a placement to discard pile', - usage: 'discard [--to=discardId]', - args: [ - { name: 'placementId', description: 'Placement ID to discard', required: true }, - ], - flags: [ - { name: 'to', description: 'Discard region ID', type: 'string', default: 'discard' }, - ], - handler: (args) => { - const [placementId] = args.positional; - const discardId = args.flags.to as string; - return [ - { - action: 'movePlacement', - params: { - placementId, - targetRegionId: discardId, - }, - }, - ]; - }, -}; - -/** - * swap - * 交换两个 Placement - */ -export const swapCommand: CliCommand = { - name: 'swap', - description: 'Swap two placements', - usage: 'swap ', - args: [ - { name: 'placementId1', description: 'First placement ID', required: true }, - { name: 'placementId2', description: 'Second placement ID', required: true }, - ], - handler: (args) => { - const [placementId1, placementId2] = args.positional; - return [ - { - action: 'swapPlacements', - params: { placementId1, placementId2 }, - }, - ]; - }, -}; - -/** - * rotate - * 旋转 Placement - */ -export const rotateCommand: CliCommand = { - name: 'rotate', - description: 'Rotate a placement', - usage: 'rotate ', - args: [ - { name: 'placementId', description: 'Placement ID to rotate', required: true }, - { name: 'degrees', description: 'Rotation angle in degrees', required: true }, - ], - handler: (args) => { - const [placementId, degreesStr] = args.positional; - const degrees = parseInt(degreesStr, 10); - return [ - { - action: 'updatePlacementRotation', - params: { - placementId, - rotation: degrees, - }, - }, - ]; - }, -}; - -/** - * position - * 设置 Placement 位置 - */ -export const positionCommand: CliCommand = { - name: 'position', - description: 'Set placement position', - usage: 'position ', - args: [ - { name: 'placementId', description: 'Placement ID', required: true }, - { name: 'x', description: 'X coordinate', required: true }, - { name: 'y', description: 'Y coordinate', required: true }, - ], - handler: (args) => { - const [placementId, xStr, yStr] = args.positional; - return [ - { - action: 'updatePlacementPosition', - params: { - placementId, - position: { - x: parseInt(xStr, 10), - y: parseInt(yStr, 10), - }, - }, - }, - ]; - }, -}; - -/** - * phase - * 设置游戏阶段 - */ -export const phaseCommand: CliCommand = { - name: 'phase', - description: 'Set game phase', - usage: 'phase ', - args: [ - { name: 'phaseName', description: 'New phase name', required: true }, - ], - handler: (args) => { - const [phase] = args.positional; - return [ - { - action: 'setPhase', - params: { phase }, - }, - ]; - }, -}; - -/** - * clear - * 清空区域 - */ -export const clearCommand: CliCommand = { - name: 'clear', - description: 'Clear all placements from a region', - usage: 'clear ', - args: [ - { name: 'regionId', description: 'Region ID to clear', required: true }, - ], - handler: (args) => { - const [regionId] = args.positional; - return [ - { - action: 'clearRegion', - params: { regionId }, - }, - ]; - }, -}; - -/** - * remove - * 移除 Part/Placement/Region - */ -export const removeCommand: CliCommand = { - name: 'remove', - description: 'Remove a part, placement, or region', - usage: 'remove ', - args: [ - { name: 'type', description: 'Type (part/placement/region)', required: true }, - { name: 'id', description: 'ID to remove', required: true }, - ], - handler: (args) => { - const [type, id] = args.positional; - - if (type === 'part') { - return [{ action: 'removePart', params: { partId: id } }]; - } else if (type === 'placement') { - return [{ action: 'removePlacement', params: { placementId: id } }]; - } else if (type === 'region') { - return [{ action: 'removeRegion', params: { regionId: id } }]; - } else { - throw new Error(`Unknown type: ${type}`); - } - }, -}; - -/** - * help [command] - * 显示帮助信息 - */ -export const helpCommand: CliCommand = { - name: 'help', - description: 'Show help information', - usage: 'help [command]', - args: [ - { name: 'command', description: 'Command name to get help for', required: false }, - ], - handler: () => { - // help 命令由 CommandRegistry 特殊处理 - return []; - }, -}; - -/** - * 所有 CLI 命令 - */ -export const cliCommands: CliCommand[] = [ - moveCommand, - placeCommand, - flipCommand, - createCommand, - regionCommand, - drawCommand, - shuffleCommand, - discardCommand, - swapCommand, - rotateCommand, - positionCommand, - phaseCommand, - clearCommand, - removeCommand, - helpCommand, -]; diff --git a/src/commands/default.commands.ts b/src/commands/default.commands.ts deleted file mode 100644 index 60c75d7..0000000 --- a/src/commands/default.commands.ts +++ /dev/null @@ -1,285 +0,0 @@ -import type { Command } from './Command'; -import { CommandActionType } from './Command'; -import { RegionType } from '../core/Region'; - -/** - * 内置命令集合 - */ - -/** - * 设置游戏:创建基础区域 - */ -export const setupGameCommand: Command = { - id: 'setup-game', - name: 'Setup Game', - description: 'Initialize the game with basic regions', - steps: [ - { - action: CommandActionType.CreateRegion, - params: { - id: 'board', - type: RegionType.Keyed, - name: 'Game Board', - }, - }, - { - action: CommandActionType.CreateRegion, - params: { - id: 'supply', - type: RegionType.Unkeyed, - name: 'Supply', - }, - }, - { - action: CommandActionType.CreateRegion, - params: { - id: 'discard', - type: RegionType.Unkeyed, - name: 'Discard Pile', - }, - }, - ], -}; - -/** - * 放置棋子命令 - */ -export const placeMeepleCommand: Command = { - id: 'place-meeple', - name: 'Place Meeple', - description: 'Place a meeple on the board', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { - id: '${meepleId}', - color: '${color}', - }, - }, - { - action: CommandActionType.CreatePlacement, - params: { - id: '${placementId}', - partId: '${meepleId}', - regionId: 'board', - position: { x: 0, y: 0 }, - }, - }, - ], -}; - -/** - * 移动棋子命令 - */ -export const moveMeepleCommand: Command = { - id: 'move-meeple', - name: 'Move Meeple', - description: 'Move a meeple to a new position', - steps: [ - { - action: CommandActionType.UpdatePlacementPosition, - params: { - placementId: '${placementId}', - position: { x: '${x}', y: '${y}' }, - }, - }, - ], -}; - -/** - * 抽牌命令 - */ -export const drawCardCommand: Command = { - id: 'draw-card', - name: 'Draw Card', - description: 'Draw a card from the deck', - steps: [ - { - action: CommandActionType.CreateRegion, - params: { - id: 'hand', - type: RegionType.Unkeyed, - name: 'Hand', - capacity: 5, - }, - }, - { - action: CommandActionType.CreateCard, - params: { - id: '${cardId}', - suit: '${suit}', - value: '${value}', - }, - }, - { - action: CommandActionType.CreatePlacement, - params: { - id: '${placementId}', - partId: '${cardId}', - regionId: 'hand', - faceUp: false, - }, - }, - { - action: CommandActionType.AddPlacementToRegion, - params: { - regionId: 'hand', - placementId: '${placementId}', - }, - }, - ], -}; - -/** - * 出牌命令 - */ -export const playCardCommand: Command = { - id: 'play-card', - name: 'Play Card', - description: 'Play a card from hand', - steps: [ - { - action: CommandActionType.SetPlacementFace, - params: { - placementId: '${placementId}', - faceUp: true, - }, - }, - { - action: CommandActionType.MovePlacement, - params: { - placementId: '${placementId}', - targetRegionId: 'discard', - }, - }, - ], -}; - -/** - * 放置板块命令 - */ -export const placeTileCommand: Command = { - id: 'place-tile', - name: 'Place Tile', - description: 'Place a tile on the board', - steps: [ - { - action: CommandActionType.CreateTile, - params: { - id: '${tileId}', - pattern: '${pattern}', - rotation: '${rotation}', - }, - }, - { - action: CommandActionType.CreatePlacement, - params: { - id: '${placementId}', - partId: '${tileId}', - regionId: 'board', - position: { x: '${x}', y: '${y}' }, - rotation: '${rotation}', - }, - }, - { - action: CommandActionType.SetSlot, - params: { - regionId: 'board', - key: '${slotKey}', - placementId: '${placementId}', - }, - }, - ], -}; - -/** - * 翻转板块命令 - */ -export const flipTileCommand: Command = { - id: 'flip-tile', - name: 'Flip Tile', - description: 'Flip a tile face down or face up', - steps: [ - { - action: CommandActionType.FlipPlacement, - params: { - placementId: '${placementId}', - }, - }, - ], -}; - -/** - * 交换位置命令 - */ -export const swapPlacementsCommand: Command = { - id: 'swap-placements', - name: 'Swap Placements', - description: 'Swap two placements in the same region', - steps: [ - { - action: CommandActionType.SwapPlacements, - params: { - placementId1: '${placementId1}', - placementId2: '${placementId2}', - }, - }, - ], -}; - -/** - * 设置游戏阶段 - */ -export const setPhaseCommand: Command = { - id: 'set-phase', - name: 'Set Phase', - description: 'Set the current game phase', - steps: [ - { - action: CommandActionType.SetPhase, - params: { - phase: '${phase}', - }, - }, - ], -}; - -/** - * 清空区域命令 - */ -export const clearRegionCommand: Command = { - id: 'clear-region', - name: 'Clear Region', - description: 'Clear all placements from a region', - steps: [ - { - action: CommandActionType.ClearRegion, - params: { - regionId: '${regionId}', - }, - }, - ], -}; - -/** - * 所有内置命令 - */ -export const defaultCommands: Command[] = [ - setupGameCommand, - placeMeepleCommand, - moveMeepleCommand, - drawCardCommand, - playCardCommand, - placeTileCommand, - flipTileCommand, - swapPlacementsCommand, - setPhaseCommand, - clearRegionCommand, -]; - -/** - * 根据 ID 获取内置命令 - */ -export function getDefaultCommand(commandId: string): Command | undefined { - return defaultCommands.find((cmd) => cmd.id === commandId); -} diff --git a/src/games/tictactoe/TicTacToeCommands.ts b/src/games/tictactoe/TicTacToeCommands.ts deleted file mode 100644 index aa90964..0000000 --- a/src/games/tictactoe/TicTacToeCommands.ts +++ /dev/null @@ -1,210 +0,0 @@ -import type { Command } from '../../commands/Command'; -import { CommandActionType } from '../../commands/Command'; -import { RegionType } from '../../core/Region'; -import type { Player } from './TicTacToeState'; -import { getAllCellIds, DEFAULT_BOARD_CONFIG } from './TicTacToeState'; - -/** - * 井字棋游戏命令集合 - */ - -/** - * 开始游戏命令 - * 初始化 3x3 棋盘和游戏状态 - */ -export const startGameCommand: Command = { - id: 'tictactoe-start-game', - name: 'startGame', - description: 'Start a new Tic Tac Toe game', - steps: [ - // 创建棋盘区域 - { - action: CommandActionType.CreateRegion, - params: { - id: 'board', - type: RegionType.Keyed, - name: 'Tic Tac Toe Board', - }, - }, - // 创建所有单元格槽位 - ...getAllCellIds(DEFAULT_BOARD_CONFIG.size).map((cellId) => ({ - action: CommandActionType.SetSlot as CommandActionType, - params: { - regionId: 'board', - key: cellId, - placementId: null, - }, - })), - // 初始化游戏元数据 - { - action: CommandActionType.SetPhase, - params: { - phase: 'playing', - }, - }, - ], -}; - -/** - * 标记单元格命令 - * 玩家在指定单元格放置 X 或 O - */ -export const markCellCommand: (cell: string, player: Player) => Command = (cell, player) => ({ - id: `tictactoe-mark-${cell}-${player}`, - name: 'markCell', - description: `Mark cell ${cell} with ${player}`, - steps: [ - // 创建玩家标记(Part) - { - action: CommandActionType.CreateMeeple, - params: { - id: `marker-${cell}-${player}`, - color: player === 'X' ? 'blue' : 'red', - name: `${player}'s marker`, - metadata: { - player, - cell, - }, - }, - }, - // 创建放置 - { - action: CommandActionType.CreatePlacement, - params: { - id: cell, - partId: `marker-${cell}-${player}`, - regionId: 'board', - metadata: { - player, - cell, - }, - }, - }, - // 设置槽位 - { - action: CommandActionType.SetSlot, - params: { - regionId: 'board', - key: cell, - placementId: cell, - }, - }, - ], -}); - -/** - * 重置游戏命令 - * 清空棋盘,准备新游戏 - */ -export const resetGameCommand: Command = { - id: 'tictactoe-reset-game', - name: 'resetGame', - description: 'Reset the Tic Tac Toe board for a new game', - steps: [ - // 清空棋盘 - { - action: CommandActionType.ClearRegion, - params: { - regionId: 'board', - }, - }, - // 重置所有槽位 - ...getAllCellIds(DEFAULT_BOARD_CONFIG.size).map((cellId) => ({ - action: CommandActionType.SetSlot as CommandActionType, - params: { - regionId: 'board', - key: cellId, - placementId: null, - }, - })), - // 重置游戏阶段 - { - action: CommandActionType.SetPhase, - params: { - phase: 'playing', - }, - }, - ], -}; - -/** - * 设置玩家命令 - * 设置玩家 X 和 O 的信息 - */ -export const setPlayersCommand: (playerX: string, playerO: string) => Command = (playerX, playerO) => ({ - id: 'tictactoe-set-players', - name: 'setPlayers', - description: 'Set player names for X and O', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { - id: 'player-x', - color: 'blue', - name: playerX, - metadata: { - role: 'player', - symbol: 'X', - }, - }, - }, - { - action: CommandActionType.CreateMeeple, - params: { - id: 'player-o', - color: 'red', - name: playerO, - metadata: { - role: 'player', - symbol: 'O', - }, - }, - }, - ], -}); - -/** - * 获取单元格状态命令 - * 查询指定单元格的状态 - */ -export const getCellCommand: (cell: string) => Command = (cell) => ({ - id: `tictactoe-get-${cell}`, - name: 'getCell', - description: `Get the state of cell ${cell}`, - steps: [ - { - action: CommandActionType.CreatePlacement, - params: { - id: `query-${cell}`, - partId: 'query', - regionId: 'board', - metadata: { - query: true, - cell, - }, - }, - }, - ], -}); - -/** - * 所有井字棋命令 - */ -export const ticTacToeCommands: Command[] = [ - startGameCommand, - resetGameCommand, -]; - -/** - * 创建标记单元格命令的辅助函数 - */ -export function createMarkCellCommand(cell: string, player: Player): Command { - return markCellCommand(cell, player); -} - -/** - * 创建设置玩家命令的辅助函数 - */ -export function createSetPlayersCommand(playerX: string, playerO: string): Command { - return setPlayersCommand(playerX, playerO); -} diff --git a/src/games/tictactoe/TicTacToeRules.ts b/src/games/tictactoe/TicTacToeRules.ts deleted file mode 100644 index dd83014..0000000 --- a/src/games/tictactoe/TicTacToeRules.ts +++ /dev/null @@ -1,354 +0,0 @@ -import type { Rule, RuleResult, RuleContext } from '../../rules/Rule'; -import { createValidationRule, createEffectRule, createTriggerRule } from '../../rules/Rule'; -import type { Player, TicTacToeMetadata, MoveRecord } from './TicTacToeState'; -import { getWinningCombinations, parseCellId } from './TicTacToeState'; - -/** - * 井字棋游戏规则集合 - */ - -/** - * 获取当前玩家 - */ -function getCurrentPlayer(gameState: any): Player { - const metadata = gameState.data.value.metadata as Record | undefined; - const ticTacToeMetadata = metadata?.ticTacToe as TicTacToeMetadata | undefined; - return ticTacToeMetadata?.currentPlayer || 'X'; -} - -/** - * 检查游戏是否结束 - */ -function isGameEnded(gameState: any): boolean { - const metadata = gameState.data.value.metadata as Record | undefined; - const ticTacToeMetadata = metadata?.ticTacToe as TicTacToeMetadata | undefined; - return ticTacToeMetadata?.gameEnded || false; -} - -/** - * 更新游戏 metadata - */ -function updateGameMetadata(gameState: any, updates: Partial): void { - const metadata = gameState.data.value.metadata as Record | undefined; - const currentTicTacToe = (metadata?.ticTacToe as TicTacToeMetadata) || { - currentPlayer: 'X', - gameEnded: false, - winner: null, - moveHistory: [], - totalMoves: 0, - }; - gameState.data.value = { - ...gameState.data.value, - metadata: { - ...metadata, - ticTacToe: { ...currentTicTacToe, ...updates }, - }, - }; -} - -/** - * 获取单元格的玩家标记 - */ -function getCellPlayer( - context: RuleContext, - cellId: string -): Player | null { - const placement = context.gameState.placements.value.get(cellId); - if (!placement?.metadata?.player) { - return null; - } - return placement.metadata.player as Player; -} - -/** - * 检查是否有玩家获胜 - */ -function checkWin( - context: RuleContext, - size: number = 3 -): { winner: Player; combination: string[] } | null { - const combinations = getWinningCombinations(size); - - for (const combination of combinations) { - const players = combination.map((cellId) => getCellPlayer(context, cellId)); - const firstPlayer = players[0]; - - if (firstPlayer && players.every((p) => p === firstPlayer)) { - return { winner: firstPlayer, combination }; - } - } - - return null; -} - -/** - * 检查是否平局(所有单元格都被填充且无获胜者) - */ -function isDraw(context: RuleContext, size: number = 3): boolean { - const totalCells = size * size; - const filledCells = Array.from(context.gameState.placements.value.values()).filter( - (p) => p.metadata?.player - ).length; - - return filledCells === totalCells; -} - -/** - * 规则 1:验证轮到当前玩家 - * 在玩家尝试下子时检查是否是他们的回合 - */ -export const validateTurnRule = createValidationRule({ - id: 'tictactoe-validate-turn', - name: 'Validate Turn', - description: 'Check if it is the current player turn', - priority: 1, - gameType: 'tictactoe', - applicableCommands: ['markCell'], - validate: async (context: RuleContext): Promise => { - const cellId = context.command.steps[0]?.params?.cell as string; - const expectedPlayer = context.command.steps[0]?.params?.player as Player; - const currentPlayer = getCurrentPlayer(context.gameState); - - if (!cellId) { - return { - success: false, - error: 'Cell ID is required', - }; - } - - if (expectedPlayer && expectedPlayer !== currentPlayer) { - return { - success: false, - error: `It is ${currentPlayer}'s turn, not ${expectedPlayer}'s`, - }; - } - - return { success: true }; - }, -}); - -/** - * 规则 2:验证单元格为空 - * 检查目标单元格是否已经被占用 - */ -export const validateCellEmptyRule = createValidationRule({ - id: 'tictactoe-validate-cell-empty', - name: 'Validate Cell Empty', - description: 'Check if the target cell is empty', - priority: 2, - gameType: 'tictactoe', - applicableCommands: ['markCell'], - validate: async (context: RuleContext): Promise => { - const cellId = context.command.steps[0]?.params?.cell as string; - - if (!cellId) { - return { - success: false, - error: 'Cell ID is required', - }; - } - - const cellPlayer = getCellPlayer(context, cellId); - if (cellPlayer !== null) { - return { - success: false, - error: `Cell ${cellId} is already occupied by ${cellPlayer}`, - }; - } - - return { success: true }; - }, -}); - -/** - * 规则 3:验证游戏未结束 - * 游戏结束后不允许继续下子 - */ -export const validateGameNotEndedRule = createValidationRule({ - id: 'tictactoe-validate-game-not-ended', - name: 'Validate Game Not Ended', - description: 'Check if the game has already ended', - priority: 0, - gameType: 'tictactoe', - applicableCommands: ['markCell'], - validate: async (context: RuleContext): Promise => { - const gameEnded = isGameEnded(context.gameState); - - if (gameEnded) { - return { - success: false, - error: 'Game has already ended', - blockCommand: true, - }; - } - - return { success: true }; - }, -}); - -/** - * 效果规则:切换玩家 - * 在玩家下子后自动切换到下一个玩家 - */ -export const switchTurnRule = createEffectRule({ - id: 'tictactoe-switch-turn', - name: 'Switch Turn', - description: 'Switch to the next player after a move', - priority: 10, - gameType: 'tictactoe', - applicableCommands: ['markCell'], - apply: async (context: RuleContext): Promise => { - const currentPlayer = getCurrentPlayer(context.gameState); - const nextPlayer: Player = currentPlayer === 'X' ? 'O' : 'X'; - - // 直接更新 metadata - updateGameMetadata(context.gameState, { currentPlayer: nextPlayer }); - - return { - success: true, - }; - }, -}); - -/** - * 效果规则:记录移动历史 - * 记录玩家的每一步移动 - */ -export const recordMoveHistoryRule = createEffectRule({ - id: 'tictactoe-record-history', - name: 'Record Move History', - description: 'Record the move in game history', - priority: 9, - gameType: 'tictactoe', - applicableCommands: ['markCell'], - apply: async (context: RuleContext): Promise => { - const cellId = context.command.steps[0]?.params?.cell as string; - const player = context.command.steps[0]?.params?.player as Player; - const metadata = context.gameState.data.value.metadata || {}; - const ticTacToeMetadata = (metadata?.ticTacToe as TicTacToeMetadata) || { - currentPlayer: 'X', - gameEnded: false, - winner: null, - moveHistory: [], - totalMoves: 0, - }; - - const moveRecord: MoveRecord = { - player, - cellId, - timestamp: Date.now(), - }; - - const moveHistory = ticTacToeMetadata.moveHistory || []; - moveHistory.push(moveRecord); - - // 直接更新 metadata - updateGameMetadata(context.gameState, { - moveHistory, - totalMoves: (ticTacToeMetadata.totalMoves || 0) + 1, - }); - - return { - success: true, - }; - }, -}); - -/** - * 触发规则:检查获胜条件 - * 当有玩家连成一线时触发 - */ -export const checkWinConditionRule = createTriggerRule({ - id: 'tictactoe-check-win', - name: 'Check Win Condition', - description: 'Check if a player has won the game', - priority: 100, - gameType: 'tictactoe', - condition: async (context: RuleContext): Promise => { - const winResult = checkWin(context); - return winResult !== null; - }, - action: async (context: RuleContext): Promise => { - const winResult = checkWin(context); - if (!winResult) { - return { success: false, error: 'No winner detected' }; - } - - const { winner, combination } = winResult; - - // 直接更新 metadata - updateGameMetadata(context.gameState, { - gameEnded: true, - winner, - winningCombination: combination, - }); - - return { - success: true, - }; - }, -}); - -/** - * 触发规则:检查平局条件 - * 当所有单元格都被填充且无获胜者时触发 - */ -export const checkDrawConditionRule = createTriggerRule({ - id: 'tictactoe-check-draw', - name: 'Check Draw Condition', - description: 'Check if the game is a draw', - priority: 101, - gameType: 'tictactoe', - condition: async (context: RuleContext): Promise => { - const winResult = checkWin(context); - if (winResult !== null) { - return false; // 有获胜者,不是平局 - } - return isDraw(context); - }, - action: async (context: RuleContext): Promise => { - // 直接更新 metadata - updateGameMetadata(context.gameState, { - gameEnded: true, - winner: null, // null 表示平局 - }); - - return { - success: true, - }; - }, -}); - -/** - * 所有井字棋游戏规则 - */ -export const ticTacToeRules: Rule[] = [ - validateTurnRule, - validateCellEmptyRule, - validateGameNotEndedRule, - switchTurnRule, - recordMoveHistoryRule, - checkWinConditionRule, - checkDrawConditionRule, -]; - -/** - * 获取井字棋验证规则 - */ -export function getTicTacToeValidationRules(): Rule[] { - return [validateTurnRule, validateCellEmptyRule, validateGameNotEndedRule]; -} - -/** - * 获取井字棋效果规则 - */ -export function getTicTacToeEffectRules(): Rule[] { - return [switchTurnRule, recordMoveHistoryRule]; -} - -/** - * 获取井字棋触发规则 - */ -export function getTicTacToeTriggerRules(): Rule[] { - return [checkWinConditionRule, checkDrawConditionRule]; -} diff --git a/src/games/tictactoe/TicTacToeState.ts b/src/games/tictactoe/TicTacToeState.ts deleted file mode 100644 index 87acb85..0000000 --- a/src/games/tictactoe/TicTacToeState.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * 井字棋游戏状态扩展 - */ - -/** - * 玩家类型 - */ -export type Player = 'X' | 'O'; - -/** - * 单元格状态 - */ -export interface CellState { - /** 单元格 ID(如 A1, B2, C3) */ - id: string; - /** 行索引 (0-2) */ - row: number; - /** 列索引 (0-2) */ - col: number; - /** 当前玩家标记,null 表示空 */ - player: Player | null; -} - -/** - * 井字棋游戏元数据 - */ -export interface TicTacToeMetadata { - /** 当前玩家 */ - currentPlayer: Player; - /** 游戏是否结束 */ - gameEnded: boolean; - /** 获胜者,null 表示平局或未结束 */ - winner: Player | null; - /** 获胜的组合(如果有) */ - winningCombination?: string[]; - /** 游戏历史 */ - moveHistory: MoveRecord[]; - /** 总回合数 */ - totalMoves: number; -} - -/** - * 移动记录 - */ -export interface MoveRecord { - /** 移动的玩家 */ - player: Player; - /** 移动的单元格 ID */ - cellId: string; - /** 移动时间戳 */ - timestamp: number; -} - -/** - * 获胜组合类型 - */ -export type WinningLine = - | { type: 'row'; index: number } - | { type: 'column'; index: number } - | { type: 'diagonal'; direction: 'main' | 'anti' }; - -/** - * 井字棋棋盘配置 - */ -export interface TicTacToeBoardConfig { - /** 棋盘大小(默认 3x3) */ - size: number; - /** 单元格 ID 前缀 */ - cellIdPrefix: string; -} - -/** - * 默认的 3x3 棋盘配置 - */ -export const DEFAULT_BOARD_CONFIG: TicTacToeBoardConfig = { - size: 3, - cellIdPrefix: 'cell', -}; - -/** - * 获取单元格 ID - */ -export function getCellId(row: number, col: number, prefix: string = 'cell'): string { - const rowLabel = String.fromCharCode('A'.charCodeAt(0) + row); - return `${prefix}-${rowLabel}${col + 1}`; -} - -/** - * 解析单元格 ID - */ -export function parseCellId(cellId: string): { row: number; col: number } | null { - const match = cellId.match(/^cell-([A-Z])(\d+)$/); - if (!match) return null; - return { - row: match[1].charCodeAt(0) - 'A'.charCodeAt(0), - col: parseInt(match[2], 10) - 1, - }; -} - -/** - * 检查是否是有效的单元格 ID - */ -export function isValidCellId(cellId: string, size: number = 3): boolean { - const parsed = parseCellId(cellId); - if (!parsed) return false; - return parsed.row >= 0 && parsed.row < size && parsed.col >= 0 && parsed.col < size; -} - -/** - * 生成所有单元格 ID - */ -export function getAllCellIds(size: number = 3, prefix: string = 'cell'): string[] { - const cells: string[] = []; - for (let row = 0; row < size; row++) { - for (let col = 0; col < size; col++) { - cells.push(getCellId(row, col, prefix)); - } - } - return cells; -} - -/** - * 获取所有可能的获胜组合 - */ -export function getWinningCombinations(size: number = 3): string[][] { - const combinations: string[][] = []; - - // 行 - for (let row = 0; row < size; row++) { - const rowCells: string[] = []; - for (let col = 0; col < size; col++) { - rowCells.push(getCellId(row, col)); - } - combinations.push(rowCells); - } - - // 列 - for (let col = 0; col < size; col++) { - const colCells: string[] = []; - for (let row = 0; row < size; row++) { - colCells.push(getCellId(row, col)); - } - combinations.push(colCells); - } - - // 主对角线 - const mainDiagonal: string[] = []; - for (let i = 0; i < size; i++) { - mainDiagonal.push(getCellId(i, i)); - } - combinations.push(mainDiagonal); - - // 反对角线 - const antiDiagonal: string[] = []; - for (let i = 0; i < size; i++) { - antiDiagonal.push(getCellId(i, size - 1 - i)); - } - combinations.push(antiDiagonal); - - return combinations; -} diff --git a/src/games/tictactoe/index.ts b/src/games/tictactoe/index.ts deleted file mode 100644 index 250cc98..0000000 --- a/src/games/tictactoe/index.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * 井字棋游戏模块 - * Tic Tac Toe game implementation with rule enforcement - */ - -import type { Command } from '../../commands/Command'; -import type { Rule } from '../../rules/Rule'; -import { - startGameCommand, - markCellCommand, - resetGameCommand, - setPlayersCommand, - getCellCommand, - ticTacToeCommands, - createMarkCellCommand, - createSetPlayersCommand, -} from './TicTacToeCommands'; -import { - ticTacToeRules, - getTicTacToeValidationRules, - getTicTacToeEffectRules, - getTicTacToeTriggerRules, -} from './TicTacToeRules'; - -// State types -export type { - Player, - CellState, - TicTacToeMetadata, - MoveRecord, - WinningLine, - TicTacToeBoardConfig, -} from './TicTacToeState'; - -export { - DEFAULT_BOARD_CONFIG, - getCellId, - parseCellId, - isValidCellId, - getAllCellIds, - getWinningCombinations, -} from './TicTacToeState'; - -// Rules -export { - validateTurnRule, - validateCellEmptyRule, - validateGameNotEndedRule, - switchTurnRule, - recordMoveHistoryRule, - checkWinConditionRule, - checkDrawConditionRule, - ticTacToeRules, - getTicTacToeValidationRules, - getTicTacToeEffectRules, - getTicTacToeTriggerRules, -} from './TicTacToeRules'; - -// Commands -export { - startGameCommand, - markCellCommand, - resetGameCommand, - setPlayersCommand, - getCellCommand, - ticTacToeCommands, - createMarkCellCommand, - createSetPlayersCommand, -} from './TicTacToeCommands'; - -/** - * 创建井字棋游戏初始化命令 - */ -export function createTicTacToeGame(): { - commands: Command[]; - rules: Rule[]; -} { - return { - commands: [startGameCommand, resetGameCommand], - rules: ticTacToeRules, - }; -} diff --git a/src/rules/Rule.ts b/src/rules/Rule.ts deleted file mode 100644 index 00ab343..0000000 --- a/src/rules/Rule.ts +++ /dev/null @@ -1,186 +0,0 @@ -import type { GameState } from '../core/GameState'; -import type { Command } from '../commands/Command'; -import type { CommandExecutionResult } from '../commands/Command'; - -/** - * 规则执行上下文 - */ -export interface RuleContext { - /** 游戏状态 */ - gameState: GameState; - /** 当前命令 */ - command: Command; - /** 命令执行结果(执行后规则可用) */ - executionResult?: CommandExecutionResult; - /** 规则执行时的元数据 */ - metadata: Record; -} - -/** - * 规则执行结果 - */ -export interface RuleResult { - /** 规则是否通过 */ - success: boolean; - /** 错误信息(如果失败) */ - error?: string; - /** 状态修改(规则可以对状态进行修改) */ - stateUpdates?: Record; - /** 是否阻止命令执行 */ - blockCommand?: boolean; - /** 触发的额外命令 */ - triggeredCommands?: Command[]; -} - -/** - * 验证规则 - * 在命令执行前运行,用于验证命令是否合法 - */ -export interface ValidationRule { - /** 规则唯一标识 */ - id: string; - /** 规则名称 */ - name: string; - /** 规则描述 */ - description?: string; - /** 规则优先级(数字越小越先执行) */ - priority: number; - /** 适用的游戏类型 */ - gameType?: string; - /** 适用的命令名称列表 */ - applicableCommands?: string[]; - /** 验证函数 */ - validate: (context: RuleContext) => Promise; -} - -/** - * 效果规则 - * 在命令执行后运行,用于更新状态或触发额外效果 - */ -export interface EffectRule { - /** 规则唯一标识 */ - id: string; - /** 规则名称 */ - name: string; - /** 规则描述 */ - description?: string; - /** 规则优先级(数字越小越先执行) */ - priority: number; - /** 适用的游戏类型 */ - gameType?: string; - /** 适用的命令名称列表 */ - applicableCommands?: string[]; - /** 效果函数 */ - apply: (context: RuleContext) => Promise; -} - -/** - * 触发规则 - * 监听特定状态变化并触发相应动作 - */ -export interface TriggerRule { - /** 规则唯一标识 */ - id: string; - /** 规则名称 */ - name: string; - /** 规则描述 */ - description?: string; - /** 规则优先级 */ - priority: number; - /** 适用的游戏类型 */ - gameType?: string; - /** 适用的命令名称列表 */ - applicableCommands?: string[]; - /** 触发条件 */ - condition: (context: RuleContext) => Promise; - /** 触发后的动作 */ - action: (context: RuleContext) => Promise; -} - -/** - * 通用规则类型 - */ -export type Rule = ValidationRule | EffectRule | TriggerRule; - -/** - * 判断是否为验证规则 - */ -export function isValidationRule(rule: Rule): rule is ValidationRule { - return 'validate' in rule; -} - -/** - * 判断是否为效果规则 - */ -export function isEffectRule(rule: Rule): rule is EffectRule { - return 'apply' in rule; -} - -/** - * 判断是否为触发规则 - */ -export function isTriggerRule(rule: Rule): rule is TriggerRule { - return 'condition' in rule && 'action' in rule; -} - -/** - * 创建验证规则 - */ -export function createValidationRule(rule: Omit & { id?: string; name?: string }): ValidationRule { - return { - id: rule.id || `validation-${Date.now()}`, - name: rule.name || 'Unnamed Validation Rule', - description: rule.description, - priority: rule.priority ?? 0, - gameType: rule.gameType, - applicableCommands: rule.applicableCommands, - validate: rule.validate, - }; -} - -/** - * 创建效果规则 - */ -export function createEffectRule(rule: Omit & { id?: string; name?: string }): EffectRule { - return { - id: rule.id || `effect-${Date.now()}`, - name: rule.name || 'Unnamed Effect Rule', - description: rule.description, - priority: rule.priority ?? 0, - gameType: rule.gameType, - applicableCommands: rule.applicableCommands, - apply: rule.apply, - }; -} - -/** - * 创建触发规则 - */ -export function createTriggerRule(rule: Omit & { id?: string; name?: string }): TriggerRule { - return { - id: rule.id || `trigger-${Date.now()}`, - name: rule.name || 'Unnamed Trigger Rule', - description: rule.description, - priority: rule.priority ?? 0, - condition: rule.condition, - action: rule.action, - }; -} - -/** - * 规则执行日志 - */ -export interface RuleLogEntry { - /** 时间戳 */ - timestamp: number; - /** 规则 ID */ - ruleId: string; - /** 规则名称 */ - ruleName: string; - /** 规则类型 */ - ruleType: 'validation' | 'effect' | 'trigger'; - /** 执行结果 */ - result: RuleResult; - /** 命令 ID */ - commandId: string; -} diff --git a/src/rules/RuleEngine.ts b/src/rules/RuleEngine.ts deleted file mode 100644 index 1474771..0000000 --- a/src/rules/RuleEngine.ts +++ /dev/null @@ -1,378 +0,0 @@ -import type { GameState } from '../core/GameState'; -import type { Command, CommandExecutionResult } from '../commands/Command'; -import { CommandExecutor } from '../commands/CommandExecutor'; -import type { - Rule, - RuleContext, - RuleResult, - ValidationRule, - EffectRule, - TriggerRule, - RuleLogEntry, -} from './Rule'; -import { isValidationRule, isEffectRule, isTriggerRule } from './Rule'; - -/** - * 规则引擎配置 - */ -export interface RuleEngineOptions { - /** 游戏类型 */ - gameType?: string; - /** 是否启用规则日志 */ - enableLogging?: boolean; - /** 是否自动执行触发规则 */ - autoExecuteTriggers?: boolean; -} - -/** - * 规则引擎执行结果 - */ -export interface RuleEngineExecutionResult extends CommandExecutionResult { - /** 执行的验证规则 */ - validationRules: RuleLogEntry[]; - /** 执行的效果规则 */ - effectRules: RuleLogEntry[]; - /** 触发的规则 */ - triggerRules: RuleLogEntry[]; - /** 触发的额外命令 */ - triggeredCommands: Command[]; -} - -/** - * 规则引擎 - * 负责在命令执行前后运行规则,并处理触发规则 - */ -export class RuleEngine { - private gameState: GameState; - private executor: CommandExecutor; - private rules: Rule[] = []; - private options: RuleEngineOptions; - private logs: RuleLogEntry[] = []; - private isExecuting: boolean = false; - private triggerQueue: Command[] = []; - - constructor(gameState: GameState, options: RuleEngineOptions = {}) { - this.gameState = gameState; - this.executor = new CommandExecutor(gameState); - this.options = { - enableLogging: true, - autoExecuteTriggers: true, - ...options, - }; - } - - /** - * 注册规则 - */ - registerRule(rule: Rule): void { - // 如果指定了游戏类型,只有匹配时才注册 - if (this.options.gameType && rule.gameType && rule.gameType !== this.options.gameType) { - return; - } - this.rules.push(rule); - // 按优先级排序 - this.rules.sort((a, b) => a.priority - b.priority); - } - - /** - * 注册多个规则 - */ - registerRules(rules: Rule[]): void { - for (const rule of rules) { - this.registerRule(rule); - } - } - - /** - * 移除规则 - */ - unregisterRule(ruleId: string): void { - this.rules = this.rules.filter((r) => r.id !== ruleId); - } - - /** - * 清除所有规则 - */ - clearRules(): void { - this.rules = []; - } - - /** - * 获取所有规则 - */ - getRules(): Rule[] { - return [...this.rules]; - } - - /** - * 执行命令(带规则验证) - */ - async executeCommand(command: Command): Promise { - if (this.isExecuting) { - throw new Error('Rule engine is already executing a command'); - } - - this.isExecuting = true; - const validationLogs: RuleLogEntry[] = []; - const effectLogs: RuleLogEntry[] = []; - const triggerLogs: RuleLogEntry[] = []; - const triggeredCommands: Command[] = []; - - try { - // 创建规则上下文 - const context: RuleContext = { - gameState: this.gameState, - command, - metadata: {}, - }; - - // 1. 执行验证规则 - const validationRules = this.rules.filter(isValidationRule); - for (const rule of validationRules) { - if (!this.isRuleApplicable(rule, command)) { - continue; - } - - const result = await rule.validate(context); - const logEntry = this.createLogEntry(rule, 'validation', result, command.id); - validationLogs.push(logEntry); - - if (!result.success) { - return this.createFailedResult(validationLogs, effectLogs, triggerLogs, triggeredCommands, result.error); - } - - if (result.blockCommand) { - return this.createFailedResult( - validationLogs, - effectLogs, - triggerLogs, - triggeredCommands, - `Command blocked by rule: ${rule.name}` - ); - } - - // 应用状态更新 - if (result.stateUpdates) { - Object.assign(context.metadata, result.stateUpdates); - } - - // 收集触发的命令 - if (result.triggeredCommands) { - triggeredCommands.push(...result.triggeredCommands); - } - } - - // 2. 执行命令 - const executionResult = this.executor.execute(command); - context.executionResult = executionResult; - - if (!executionResult.success) { - return this.createFailedResult( - validationLogs, - effectLogs, - triggerLogs, - triggeredCommands, - executionResult.error - ); - } - - // 3. 执行效果规则 - const effectRules = this.rules.filter(isEffectRule); - for (const rule of effectRules) { - if (!this.isRuleApplicable(rule, command)) { - continue; - } - - const result = await rule.apply(context); - const logEntry = this.createLogEntry(rule, 'effect', result, command.id); - effectLogs.push(logEntry); - - if (!result.success) { - // 效果规则失败不影响命令执行,只记录日志 - continue; - } - - // 应用状态更新 - if (result.stateUpdates) { - Object.assign(context.metadata, result.stateUpdates); - } - - // 收集触发的命令 - if (result.triggeredCommands) { - triggeredCommands.push(...result.triggeredCommands); - } - } - - // 4. 执行触发规则 - if (this.options.autoExecuteTriggers) { - const triggerRules = this.rules.filter(isTriggerRule); - for (const rule of triggerRules) { - const shouldTrigger = await rule.condition(context); - if (shouldTrigger) { - const result = await rule.action(context); - const logEntry = this.createLogEntry(rule, 'trigger', result, command.id); - triggerLogs.push(logEntry); - - if (result.triggeredCommands) { - triggeredCommands.push(...result.triggeredCommands); - } - } - } - } - - // 5. 执行触发的命令(在循环外执行,避免递归) - } finally { - this.isExecuting = false; - } - - // 在主要执行完成后执行触发的命令 - for (const triggeredCommand of triggeredCommands) { - try { - const triggerResult = await this.executeCommand(triggeredCommand); - if (!triggerResult.success) { - // 触发命令失败,记录但不影响主命令 - this.logs.push({ - timestamp: Date.now(), - ruleId: 'triggered-command', - ruleName: 'Triggered Command', - ruleType: 'trigger', - result: { success: false, error: `Triggered command ${triggeredCommand.id} failed: ${triggerResult.error}` }, - commandId: triggeredCommand.id, - }); - } - } catch (error) { - // 忽略触发命令的异常 - } - } - - return { - success: true, - executedSteps: executionResult.executedSteps, - totalSteps: executionResult.totalSteps, - validationRules: validationLogs, - effectRules: effectLogs, - triggerRules: triggerLogs, - triggeredCommands, - }; - } - - /** - * 检查规则是否适用于当前命令 - */ - private isRuleApplicable( - rule: ValidationRule | EffectRule, - command: Command - ): boolean { - // 检查游戏类型 - if (rule.gameType && rule.gameType !== this.options.gameType) { - return false; - } - - // 检查命令名称 - if (rule.applicableCommands && !rule.applicableCommands.includes(command.name)) { - return false; - } - - return true; - } - - /** - * 创建日志条目 - */ - private createLogEntry( - rule: Rule, - ruleType: 'validation' | 'effect' | 'trigger', - result: RuleResult, - commandId: string - ): RuleLogEntry { - const entry: RuleLogEntry = { - timestamp: Date.now(), - ruleId: rule.id, - ruleName: rule.name, - ruleType, - result, - commandId, - }; - - if (this.options.enableLogging) { - this.logs.push(entry); - } - - return entry; - } - - /** - * 创建失败结果 - */ - private createFailedResult( - validationLogs: RuleLogEntry[], - effectLogs: RuleLogEntry[], - triggerLogs: RuleLogEntry[], - triggeredCommands: Command[], - error?: string - ): RuleEngineExecutionResult { - return { - success: false, - error, - executedSteps: 0, - totalSteps: 0, - validationRules: validationLogs, - effectRules: effectLogs, - triggerRules: triggerLogs, - triggeredCommands, - }; - } - - /** - * 获取规则日志 - */ - getLogs(): RuleLogEntry[] { - return [...this.logs]; - } - - /** - * 清除日志 - */ - clearLogs(): void { - this.logs = []; - } - - /** - * 获取游戏状态 - */ - getGameState(): GameState { - return this.gameState; - } - - /** - * 手动触发规则 - */ - async triggerRules(): Promise { - const context: RuleContext = { - gameState: this.gameState, - command: { id: 'trigger-manual', name: 'manual-trigger', steps: [] }, - metadata: {}, - }; - - const logs: RuleLogEntry[] = []; - const triggerRules = this.rules.filter(isTriggerRule); - - for (const rule of triggerRules) { - const shouldTrigger = await rule.condition(context); - if (shouldTrigger) { - const result = await rule.action(context); - const logEntry = this.createLogEntry(rule, 'trigger', result, 'manual'); - logs.push(logEntry); - } - } - - return logs; - } -} - -/** - * 创建规则引擎 - */ -export function createRuleEngine(gameState: GameState, options?: RuleEngineOptions): RuleEngine { - return new RuleEngine(gameState, options); -} diff --git a/src/rules/RuleRegistry.ts b/src/rules/RuleRegistry.ts deleted file mode 100644 index d412b69..0000000 --- a/src/rules/RuleRegistry.ts +++ /dev/null @@ -1,199 +0,0 @@ -import type { Rule, ValidationRule, EffectRule, TriggerRule } from './Rule'; -import { isValidationRule, isEffectRule, isTriggerRule } from './Rule'; - -/** - * 规则组 - */ -export interface RuleGroup { - /** 组名称 */ - name: string; - /** 组描述 */ - description?: string; - /** 规则列表 */ - rules: Rule[]; -} - -/** - * 规则注册表 - * 按游戏类型注册和管理规则 - */ -export class RuleRegistry { - private rulesByGameType: Map; - private globalRules: Rule[]; - private ruleGroups: Map; - private enabledRules: Set; - - constructor() { - this.rulesByGameType = new Map(); - this.globalRules = []; - this.ruleGroups = new Map(); - this.enabledRules = new Set(); - } - - /** - * 注册规则到特定游戏类型 - */ - register(rule: Rule, gameType?: string): void { - const targetRules = gameType - ? this.getRulesForGameType(gameType) - : this.globalRules; - - targetRules.push(rule); - this.enabledRules.add(rule.id); - - // 按优先级排序 - targetRules.sort((a, b) => a.priority - b.priority); - } - - /** - * 注册多个规则 - */ - registerAll(rules: Rule[], gameType?: string): void { - for (const rule of rules) { - this.register(rule, gameType); - } - } - - /** - * 注册规则组 - */ - registerGroup(group: RuleGroup, gameType?: string): void { - this.ruleGroups.set(group.name, group); - this.registerAll(group.rules, gameType); - } - - /** - * 获取特定游戏类型的规则 - */ - getRulesForGameType(gameType: string): Rule[] { - const gameRules = this.rulesByGameType.get(gameType) || []; - return [...gameRules, ...this.globalRules]; - } - - /** - * 获取所有规则 - */ - getAllRules(): Rule[] { - const allRules = [...this.globalRules]; - for (const rules of this.rulesByGameType.values()) { - allRules.push(...rules); - } - return allRules; - } - - /** - * 获取验证规则 - */ - getValidationRules(gameType?: string): ValidationRule[] { - const rules = gameType - ? this.getRulesForGameType(gameType) - : this.getAllRules(); - return rules.filter(isValidationRule); - } - - /** - * 获取效果规则 - */ - getEffectRules(gameType?: string): EffectRule[] { - const rules = gameType - ? this.getRulesForGameType(gameType) - : this.getAllRules(); - return rules.filter(isEffectRule); - } - - /** - * 获取触发规则 - */ - getTriggerRules(gameType?: string): TriggerRule[] { - const rules = gameType - ? this.getRulesForGameType(gameType) - : this.getAllRules(); - return rules.filter(isTriggerRule); - } - - /** - * 获取规则组 - */ - getGroup(groupName: string): RuleGroup | undefined { - return this.ruleGroups.get(groupName); - } - - /** - * 移除规则 - */ - unregister(ruleId: string): void { - this.globalRules = this.globalRules.filter((r) => r.id !== ruleId); - for (const [gameType, rules] of this.rulesByGameType.entries()) { - this.rulesByGameType.set( - gameType, - rules.filter((r) => r.id !== ruleId) - ); - } - this.enabledRules.delete(ruleId); - } - - /** - * 启用规则 - */ - enableRule(ruleId: string): void { - this.enabledRules.add(ruleId); - } - - /** - * 禁用规则 - */ - disableRule(ruleId: string): void { - this.enabledRules.delete(ruleId); - } - - /** - * 检查规则是否启用 - */ - isRuleEnabled(ruleId: string): boolean { - return this.enabledRules.has(ruleId); - } - - /** - * 获取按游戏类型分类的规则 - */ - getRulesByGameType(): Map { - return new Map(this.rulesByGameType); - } - - /** - * 清除特定游戏类型的规则 - */ - clearGameTypeRules(gameType: string): void { - this.rulesByGameType.delete(gameType); - } - - /** - * 清除所有规则 - */ - clearAllRules(): void { - this.rulesByGameType.clear(); - this.globalRules = []; - this.enabledRules.clear(); - } - - /** - * 获取规则数量 - */ - getRuleCount(): number { - return this.getAllRules().length; - } - - /** - * 获取启用的规则数量 - */ - getEnabledRuleCount(): number { - return this.enabledRules.size; - } -} - -/** - * 创建规则注册表 - */ -export function createRuleRegistry(): RuleRegistry { - return new RuleRegistry(); -} diff --git a/tests/commands/command.executor.test.ts b/tests/commands/command.executor.test.ts deleted file mode 100644 index b38cd3f..0000000 --- a/tests/commands/command.executor.test.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { createGameState } from '../../src/core/GameState'; -import { CommandExecutor } from '../../src/commands/CommandExecutor'; -import { Command, CommandActionType } from '../../src/commands/Command'; -import { RegionType } from '../../src/core/Region'; - -describe('CommandExecutor', () => { - let gameState: ReturnType; - let executor: CommandExecutor; - - beforeEach(() => { - gameState = createGameState({ id: 'test-game', name: 'Test Game' }); - executor = new CommandExecutor(gameState); - }); - - describe('execute', () => { - it('should execute a simple command successfully', () => { - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - const result = executor.execute(command); - - expect(result.success).toBe(true); - expect(result.executedSteps).toBe(1); - expect(result.totalSteps).toBe(1); - expect(gameState.getPart('meeple-1')).toBeDefined(); - }); - - it('should execute multi-step command', () => { - const command: Command = { - id: 'setup-command', - name: 'Setup Command', - steps: [ - { - action: CommandActionType.CreateRegion, - params: { id: 'board', type: RegionType.Keyed }, - }, - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'blue' }, - }, - { - action: CommandActionType.CreatePlacement, - params: { - id: 'placement-1', - partId: 'meeple-1', - regionId: 'board', - }, - }, - ], - }; - - const result = executor.execute(command); - - expect(result.success).toBe(true); - expect(result.executedSteps).toBe(3); - expect(result.totalSteps).toBe(3); - expect(gameState.getRegion('board')).toBeDefined(); - expect(gameState.getPart('meeple-1')).toBeDefined(); - expect(gameState.getPlacement('placement-1')).toBeDefined(); - }); - - it('should stop execution on error', () => { - const command: Command = { - id: 'failing-command', - name: 'Failing Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - { - action: CommandActionType.CreatePlacement, - params: { - id: 'placement-1', - partId: 'non-existent', - regionId: 'non-existent', - }, - }, - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-2', color: 'blue' }, - }, - ], - }; - - const result = executor.execute(command); - - expect(result.success).toBe(false); - expect(result.executedSteps).toBe(1); - expect(result.totalSteps).toBe(3); - expect(result.error).toBeDefined(); - expect(gameState.getPart('meeple-1')).toBeDefined(); - expect(gameState.getPart('meeple-2')).toBeUndefined(); - }); - - it('should execute createCard command', () => { - const command: Command = { - id: 'create-card', - name: 'Create Card', - steps: [ - { - action: CommandActionType.CreateCard, - params: { id: 'card-1', suit: 'hearts', value: 10 }, - }, - ], - }; - - const result = executor.execute(command); - - expect(result.success).toBe(true); - const card = gameState.getPart('card-1'); - expect(card).toBeDefined(); - expect(card?.type).toBe('card'); - }); - - it('should execute createTile command', () => { - const command: Command = { - id: 'create-tile', - name: 'Create Tile', - steps: [ - { - action: CommandActionType.CreateTile, - params: { id: 'tile-1', pattern: 'forest', rotation: 90 }, - }, - ], - }; - - const result = executor.execute(command); - - expect(result.success).toBe(true); - const tile = gameState.getPart('tile-1'); - expect(tile).toBeDefined(); - expect(tile?.type).toBe('tile'); - }); - - it('should execute movePlacement command', () => { - const setupCommand: Command = { - id: 'setup', - name: 'Setup', - steps: [ - { action: CommandActionType.CreateRegion, params: { id: 'board1', type: RegionType.Unkeyed } }, - { action: CommandActionType.CreateRegion, params: { id: 'board2', type: RegionType.Unkeyed } }, - { action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } }, - { action: CommandActionType.CreatePlacement, params: { id: 'p1', partId: 'm1', regionId: 'board1' } }, - ], - }; - executor.execute(setupCommand); - - const moveCommand: Command = { - id: 'move', - name: 'Move', - steps: [ - { - action: CommandActionType.MovePlacement, - params: { placementId: 'p1', targetRegionId: 'board2' }, - }, - ], - }; - - const result = executor.execute(moveCommand); - - expect(result.success).toBe(true); - const placement = gameState.getPlacement('p1'); - expect(placement?.regionId).toBe('board2'); - }); - - it('should execute flipPlacement command', () => { - const setupCommand: Command = { - id: 'setup', - name: 'Setup', - steps: [ - { action: CommandActionType.CreateRegion, params: { id: 'board', type: RegionType.Unkeyed } }, - { action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } }, - { action: CommandActionType.CreatePlacement, params: { id: 'p1', partId: 'm1', regionId: 'board', faceUp: true } }, - ], - }; - executor.execute(setupCommand); - - const flipCommand: Command = { - id: 'flip', - name: 'Flip', - steps: [ - { action: CommandActionType.FlipPlacement, params: { placementId: 'p1' } }, - ], - }; - - const result = executor.execute(flipCommand); - - expect(result.success).toBe(true); - const placement = gameState.getPlacement('p1'); - expect(placement?.faceUp).toBe(false); - }); - - it('should execute setPhase command', () => { - const command: Command = { - id: 'set-phase', - name: 'Set Phase', - steps: [ - { action: CommandActionType.SetPhase, params: { phase: 'midgame' } }, - ], - }; - - const result = executor.execute(command); - - expect(result.success).toBe(true); - expect(gameState.data.value.phase).toBe('midgame'); - }); - - it('should execute swapPlacements command', () => { - const setupCommand: Command = { - id: 'setup', - name: 'Setup', - steps: [ - { action: CommandActionType.CreateRegion, params: { id: 'board', type: RegionType.Unkeyed } }, - { action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } }, - { action: CommandActionType.CreateMeeple, params: { id: 'm2', color: 'blue' } }, - { action: CommandActionType.CreatePlacement, params: { id: 'p1', partId: 'm1', regionId: 'board' } }, - { action: CommandActionType.CreatePlacement, params: { id: 'p2', partId: 'm2', regionId: 'board' } }, - ], - }; - executor.execute(setupCommand); - - const region = gameState.getRegion('board'); - region!.placements.value = ['p1', 'p2']; - - const swapCommand: Command = { - id: 'swap', - name: 'Swap', - steps: [ - { action: CommandActionType.SwapPlacements, params: { placementId1: 'p1', placementId2: 'p2' } }, - ], - }; - - const result = executor.execute(swapCommand); - - expect(result.success).toBe(true); - expect(region!.placements.value).toEqual(['p2', 'p1']); - }); - - it('should execute clearRegion command', () => { - const setupCommand: Command = { - id: 'setup', - name: 'Setup', - steps: [ - { action: CommandActionType.CreateRegion, params: { id: 'board', type: RegionType.Unkeyed } }, - { action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } }, - { action: CommandActionType.CreatePlacement, params: { id: 'p1', partId: 'm1', regionId: 'board' } }, - { action: CommandActionType.AddPlacementToRegion, params: { regionId: 'board', placementId: 'p1' } }, - ], - }; - executor.execute(setupCommand); - - const clearCommand: Command = { - id: 'clear', - name: 'Clear', - steps: [ - { action: CommandActionType.ClearRegion, params: { regionId: 'board' } }, - ], - }; - - const result = executor.execute(clearCommand); - - expect(result.success).toBe(true); - const region = gameState.getRegion('board'); - expect(region?.placements.value.length).toBe(0); - }); - - it('should execute updatePart command', () => { - const setupCommand: Command = { - id: 'setup', - name: 'Setup', - steps: [ - { action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red', name: 'Original' } }, - ], - }; - executor.execute(setupCommand); - - const updateCommand: Command = { - id: 'update', - name: 'Update', - steps: [ - { action: CommandActionType.UpdatePart, params: { partId: 'm1', updates: { name: 'Updated', color: 'blue' } } }, - ], - }; - - const result = executor.execute(updateCommand); - - expect(result.success).toBe(true); - const part = gameState.getPart('m1'); - expect(part?.name).toBe('Updated'); - expect(part?.color).toBe('blue'); - }); - - it('should execute removePart command', () => { - const setupCommand: Command = { - id: 'setup', - name: 'Setup', - steps: [ - { action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } }, - ], - }; - executor.execute(setupCommand); - - expect(gameState.getPart('m1')).toBeDefined(); - - const removeCommand: Command = { - id: 'remove', - name: 'Remove', - steps: [ - { action: CommandActionType.RemovePart, params: { partId: 'm1' } }, - ], - }; - - const result = executor.execute(removeCommand); - - expect(result.success).toBe(true); - expect(gameState.getPart('m1')).toBeUndefined(); - }); - }); -}); diff --git a/tests/commands/command.log.test.ts b/tests/commands/command.log.test.ts deleted file mode 100644 index 99c7e28..0000000 --- a/tests/commands/command.log.test.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { CommandLog, createCommandLog } from '../../src/commands/CommandLog'; -import { Command, CommandActionType, CommandExecutionResult, StepResult } from '../../src/commands/Command'; - -describe('CommandLog', () => { - let log: CommandLog; - - beforeEach(() => { - log = createCommandLog(); - }); - - const sampleCommand: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { action: CommandActionType.CreateMeeple, params: { id: 'm1', color: 'red' } }, - ], - }; - - const sampleResult: CommandExecutionResult = { - success: true, - executedSteps: 1, - totalSteps: 1, - }; - - const sampleStepResults: StepResult[] = [ - { - stepIndex: 0, - action: CommandActionType.CreateMeeple, - success: true, - params: { id: 'm1', color: 'red' }, - }, - ]; - - describe('log', () => { - it('should log a command execution', () => { - log.log(sampleCommand, sampleResult, sampleStepResults); - - const entries = log.getEntries(); - expect(entries.length).toBe(1); - expect(entries[0].commandId).toBe('test-command'); - expect(entries[0].commandName).toBe('Test Command'); - expect(entries[0].result.success).toBe(true); - }); - - it('should add timestamp to log entry', () => { - const beforeTime = Date.now(); - log.log(sampleCommand, sampleResult, sampleStepResults); - const afterTime = Date.now(); - - const entry = log.getEntries()[0]; - expect(entry.timestamp).toBeGreaterThanOrEqual(beforeTime); - expect(entry.timestamp).toBeLessThanOrEqual(afterTime); - }); - - it('should log multiple entries', () => { - log.log(sampleCommand, sampleResult, sampleStepResults); - log.log(sampleCommand, sampleResult, sampleStepResults); - log.log(sampleCommand, sampleResult, sampleStepResults); - - expect(log.getEntries().length).toBe(3); - }); - }); - - describe('getFilteredEntries', () => { - it('should filter by commandId', () => { - const command1: Command = { ...sampleCommand, id: 'cmd-1', name: 'Command 1' }; - const command2: Command = { ...sampleCommand, id: 'cmd-2', name: 'Command 2' }; - - log.log(command1, sampleResult, sampleStepResults); - log.log(command2, sampleResult, sampleStepResults); - log.log(command1, sampleResult, sampleStepResults); - - const filtered = log.getFilteredEntries({ commandId: 'cmd-1' }); - expect(filtered.length).toBe(2); - }); - - it('should filter by success status', () => { - const successResult: CommandExecutionResult = { success: true, executedSteps: 1, totalSteps: 1 }; - const failResult: CommandExecutionResult = { success: false, executedSteps: 0, totalSteps: 1, error: 'Failed' }; - - log.log(sampleCommand, successResult, sampleStepResults); - log.log(sampleCommand, failResult, []); - log.log(sampleCommand, successResult, sampleStepResults); - - const failed = log.getFilteredEntries({ success: false }); - expect(failed.length).toBe(1); - - const successful = log.getFilteredEntries({ success: true }); - expect(successful.length).toBe(2); - }); - - it('should filter by time range', () => { - const startTime = Date.now(); - log.log(sampleCommand, sampleResult, sampleStepResults); - - // 模拟时间流逝 - const midTime = Date.now() + 100; - - // 手动创建一个带时间戳的条目来测试时间过滤 - const entries = log.getEntries(); - expect(entries[0].timestamp).toBeGreaterThanOrEqual(startTime); - }); - }); - - describe('getCommandHistory', () => { - it('should return history for a specific command', () => { - const command1: Command = { ...sampleCommand, id: 'cmd-1', name: 'Command 1' }; - const command2: Command = { ...sampleCommand, id: 'cmd-2', name: 'Command 2' }; - - log.log(command1, sampleResult, sampleStepResults); - log.log(command2, sampleResult, sampleStepResults); - log.log(command1, sampleResult, sampleStepResults); - - const history = log.getCommandHistory('cmd-1'); - expect(history.length).toBe(2); - }); - }); - - describe('getFailedCommands', () => { - it('should return only failed commands', () => { - const successResult: CommandExecutionResult = { success: true, executedSteps: 1, totalSteps: 1 }; - const failResult: CommandExecutionResult = { success: false, executedSteps: 0, totalSteps: 1, error: 'Error' }; - - log.log(sampleCommand, successResult, sampleStepResults); - log.log(sampleCommand, failResult, []); - log.log(sampleCommand, successResult, sampleStepResults); - - const failed = log.getFailedCommands(); - expect(failed.length).toBe(1); - expect(failed[0].result.success).toBe(false); - }); - }); - - describe('getSuccessfulCommands', () => { - it('should return only successful commands', () => { - const successResult: CommandExecutionResult = { success: true, executedSteps: 1, totalSteps: 1 }; - const failResult: CommandExecutionResult = { success: false, executedSteps: 0, totalSteps: 1, error: 'Error' }; - - log.log(sampleCommand, successResult, sampleStepResults); - log.log(sampleCommand, failResult, []); - log.log(sampleCommand, successResult, sampleStepResults); - - const successful = log.getSuccessfulCommands(); - expect(successful.length).toBe(2); - }); - }); - - describe('clear', () => { - it('should clear all log entries', () => { - log.log(sampleCommand, sampleResult, sampleStepResults); - log.log(sampleCommand, sampleResult, sampleStepResults); - - expect(log.getEntries().length).toBe(2); - - log.clear(); - - expect(log.getEntries().length).toBe(0); - }); - }); - - describe('exportToJson', () => { - it('should export logs as JSON string', () => { - log.log(sampleCommand, sampleResult, sampleStepResults); - - const json = log.exportToJson(); - const parsed = JSON.parse(json); - - expect(Array.isArray(parsed)).toBe(true); - expect(parsed.length).toBe(1); - expect(parsed[0].commandId).toBe('test-command'); - }); - }); - - describe('getCount', () => { - it('should return the number of log entries', () => { - expect(log.getCount()).toBe(0); - - log.log(sampleCommand, sampleResult, sampleStepResults); - expect(log.getCount()).toBe(1); - - log.log(sampleCommand, sampleResult, sampleStepResults); - expect(log.getCount()).toBe(2); - }); - }); - - describe('getLastEntry', () => { - it('should return the last log entry', () => { - const command1: Command = { ...sampleCommand, id: 'cmd-1', name: 'First' }; - const command2: Command = { ...sampleCommand, id: 'cmd-2', name: 'Last' }; - - log.log(command1, sampleResult, sampleStepResults); - log.log(command2, sampleResult, sampleStepResults); - - const lastEntry = log.getLastEntry(); - expect(lastEntry).not.toBeNull(); - expect(lastEntry?.commandId).toBe('cmd-2'); - }); - - it('should return null when log is empty', () => { - const lastEntry = log.getLastEntry(); - expect(lastEntry).toBeNull(); - }); - }); - - describe('Queue management', () => { - describe('enqueue', () => { - it('should add command to queue', () => { - const queued = log.enqueue(sampleCommand); - - expect(queued.id).toBe('test-command'); - expect(log.getQueueLength()).toBe(1); - }); - - it('should set initial status to Pending', () => { - const queued = log.enqueue(sampleCommand); - expect(queued.status).toBe('pending'); - }); - }); - - describe('dequeue', () => { - it('should remove and return the first command from queue', () => { - const command1: Command = { ...sampleCommand, id: 'cmd-1' }; - const command2: Command = { ...sampleCommand, id: 'cmd-2' }; - - log.enqueue(command1); - log.enqueue(command2); - - const dequeued = log.dequeue(); - expect(dequeued?.command.id).toBe('cmd-1'); - expect(log.getQueueLength()).toBe(1); - }); - - it('should return null when queue is empty', () => { - const dequeued = log.dequeue(); - expect(dequeued).toBeNull(); - }); - }); - - describe('updateQueueStatus', () => { - it('should update command status in queue', () => { - log.enqueue(sampleCommand); - - log.updateQueueStatus('test-command', 'executing'); - const queue = log.getQueue(); - expect(queue[0].status).toBe('executing'); - }); - - it('should set executedAt and result when status is Completed', () => { - const result: CommandExecutionResult = { success: true, executedSteps: 1, totalSteps: 1 }; - log.enqueue(sampleCommand); - - const beforeTime = Date.now(); - log.updateQueueStatus('test-command', 'completed', result); - const afterTime = Date.now(); - - const queue = log.getQueue(); - expect(queue[0].status).toBe('completed'); - expect(queue[0].result).toEqual(result); - expect(queue[0].executedAt).toBeGreaterThanOrEqual(beforeTime); - expect(queue[0].executedAt).toBeLessThanOrEqual(afterTime); - }); - }); - - describe('clearQueue', () => { - it('should clear all queued commands', () => { - log.enqueue(sampleCommand); - log.enqueue(sampleCommand); - - expect(log.getQueueLength()).toBe(2); - - log.clearQueue(); - - expect(log.getQueueLength()).toBe(0); - }); - }); - }); -}); diff --git a/tests/commands/command.parser.test.ts b/tests/commands/command.parser.test.ts deleted file mode 100644 index 5681e11..0000000 --- a/tests/commands/command.parser.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { CommandParser, createCommandParser } from '../../src/commands/CommandParser'; -import { CommandParseError } from '../../src/commands/CommandParser'; - -describe('CommandParser', () => { - let parser: CommandParser; - - beforeEach(() => { - parser = createCommandParser(); - }); - - describe('parse', () => { - it('should parse simple command without args', () => { - const result = parser.parse('shuffle'); - - expect(result.commandName).toBe('shuffle'); - expect(result.args.positional).toEqual([]); - expect(result.args.flags).toEqual({}); - }); - - it('should parse command with positional args', () => { - const result = parser.parse('move card-1 discard'); - - expect(result.commandName).toBe('move'); - expect(result.args.positional).toEqual(['card-1', 'discard']); - expect(result.args.flags).toEqual({}); - }); - - it('should parse command with multiple positional args', () => { - const result = parser.parse('position p1 3 5'); - - expect(result.commandName).toBe('position'); - expect(result.args.positional).toEqual(['p1', '3', '5']); - }); - - it('should parse command with flag', () => { - const result = parser.parse('shuffle discard --seed=2026'); - - expect(result.commandName).toBe('shuffle'); - expect(result.args.positional).toEqual(['discard']); - expect(result.args.flags).toEqual({ seed: '2026' }); - }); - - it('should parse command with multiple flags', () => { - const result = parser.parse('create meeple m1 --color=red --name=Player1'); - - expect(result.commandName).toBe('create'); - expect(result.args.positional).toEqual(['meeple', 'm1']); - expect(result.args.flags).toEqual({ color: 'red', name: 'Player1' }); - }); - - it('should parse command with boolean flag', () => { - const result = parser.parse('flip p1 --faceup'); - - expect(result.commandName).toBe('flip'); - expect(result.args.positional).toEqual(['p1']); - expect(result.args.flags).toEqual({ faceup: true }); - }); - - it('should parse command with short flag', () => { - const result = parser.parse('shuffle d1 -s'); - - expect(result.commandName).toBe('shuffle'); - expect(result.args.positional).toEqual(['d1']); - expect(result.args.flags).toEqual({ s: true }); - }); - - it('should parse command with short flag and value', () => { - const result = parser.parse('shuffle d1 --seed=2026'); - - expect(result.commandName).toBe('shuffle'); - expect(result.args.positional).toEqual(['d1']); - expect(result.args.flags).toEqual({ seed: '2026' }); - }); - - it('should parse command with string number value', () => { - const result = parser.parse('rotate p1 90'); - - expect(result.commandName).toBe('rotate'); - expect(result.args.positional).toEqual(['p1', '90']); - }); - - it('should parse command with negative number', () => { - const result = parser.parse('rotate p1 -45'); - - expect(result.commandName).toBe('rotate'); - expect(result.args.positional).toEqual(['p1', '-45']); - }); - - it('should parse command with float number', () => { - const result = parser.parse('rotate p1 45.5'); - - expect(result.commandName).toBe('rotate'); - expect(result.args.positional).toEqual(['p1', '45.5']); - }); - - it('should parse command with quoted string', () => { - const result = parser.parse('create meeple m1 --name="Red Player"'); - - expect(result.commandName).toBe('create'); - expect(result.args.positional).toEqual(['meeple', 'm1']); - expect(result.args.flags).toEqual({ name: 'Red Player' }); - }); - - it('should parse command with single quoted string', () => { - const result = parser.parse("create meeple m1 --name='Blue Player'"); - - expect(result.commandName).toBe('create'); - expect(result.args.positional).toEqual(['meeple', 'm1']); - expect(result.args.flags).toEqual({ name: 'Blue Player' }); - }); - - it('should handle extra whitespace', () => { - const result = parser.parse(' move card-1 discard '); - - expect(result.commandName).toBe('move'); - expect(result.args.positional).toEqual(['card-1', 'discard']); - }); - - it('should throw on empty command', () => { - expect(() => parser.parse('')).toThrow(CommandParseError); - expect(() => parser.parse(' ')).toThrow(CommandParseError); - }); - - it('should throw on unclosed quote', () => { - expect(() => parser.parse('create meeple m1 --name="Red')).toThrow(CommandParseError); - }); - }); - - describe('formatCommand', () => { - it('should format simple command', () => { - const formatted = CommandParser.formatCommand('shuffle'); - expect(formatted).toBe('shuffle'); - }); - - it('should format command with positional args', () => { - const formatted = CommandParser.formatCommand('move', { - positional: ['card-1', 'discard'], - flags: {}, - }); - expect(formatted).toBe('move card-1 discard'); - }); - - it('should format command with flags', () => { - const formatted = CommandParser.formatCommand('shuffle', { - positional: ['discard'], - flags: { seed: 2026 }, - }); - expect(formatted).toBe('shuffle discard --seed=2026'); - }); - - it('should format command with boolean flag', () => { - const formatted = CommandParser.formatCommand('flip', { - positional: ['p1'], - flags: { faceup: true }, - }); - expect(formatted).toBe('flip p1 --faceup'); - }); - }); -}); diff --git a/tests/commands/command.registry.test.ts b/tests/commands/command.registry.test.ts deleted file mode 100644 index e7eeaa9..0000000 --- a/tests/commands/command.registry.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { CommandRegistry, createCommandRegistry } from '../../src/commands/CommandRegistry'; -import type { CliCommand } from '../../src/commands/CliCommand'; - -describe('CommandRegistry', () => { - let registry: CommandRegistry; - - beforeEach(() => { - registry = createCommandRegistry(); - }); - - const sampleCommand: CliCommand = { - name: 'test', - description: 'Test command', - usage: 'test ', - args: [ - { name: 'arg', description: 'Test argument', required: true }, - ], - handler: (args) => { - return [ - { - action: 'createMeeple', - params: { id: args.positional[0], color: 'red' }, - }, - ]; - }, - }; - - describe('register', () => { - it('should register a command', () => { - registry.register(sampleCommand); - - expect(registry.has('test')).toBe(true); - expect(registry.get('test')).toBe(sampleCommand); - }); - - it('should register multiple commands', () => { - const cmd1: CliCommand = { - name: 'cmd1', - description: 'Command 1', - usage: 'cmd1', - handler: () => [], - }; - const cmd2: CliCommand = { - name: 'cmd2', - description: 'Command 2', - usage: 'cmd2', - handler: () => [], - }; - - registry.registerAll([cmd1, cmd2]); - - expect(registry.has('cmd1')).toBe(true); - expect(registry.has('cmd2')).toBe(true); - expect(registry.getCount()).toBe(2); - }); - }); - - describe('get', () => { - it('should return undefined for non-existent command', () => { - const cmd = registry.get('non-existent'); - expect(cmd).toBeUndefined(); - }); - - it('should return existing command', () => { - registry.register(sampleCommand); - const cmd = registry.get('test'); - expect(cmd?.name).toBe('test'); - }); - }); - - describe('unregister', () => { - it('should remove a command', () => { - registry.register(sampleCommand); - expect(registry.has('test')).toBe(true); - - registry.unregister('test'); - expect(registry.has('test')).toBe(false); - }); - }); - - describe('getAll', () => { - it('should return all registered commands', () => { - registry.register(sampleCommand); - - const cmd2: CliCommand = { - name: 'cmd2', - description: 'Command 2', - usage: 'cmd2', - handler: () => [], - }; - registry.register(cmd2); - - const all = registry.getAll(); - expect(all.length).toBe(2); - expect(all.map((c) => c.name)).toEqual(['test', 'cmd2']); - }); - }); - - describe('execute', () => { - it('should execute a command successfully', () => { - registry.register(sampleCommand); - - const result = registry.execute('test meeple-1'); - - expect(result.success).toBe(true); - expect(result.steps.length).toBe(1); - expect(result.steps[0].action).toBe('createMeeple'); - expect(result.steps[0].params).toEqual({ id: 'meeple-1', color: 'red' }); - }); - - it('should return error for unknown command', () => { - const result = registry.execute('unknown arg1'); - - expect(result.success).toBe(false); - expect(result.error).toContain('Unknown command'); - expect(result.steps).toEqual([]); - }); - - it('should return error for missing required argument', () => { - registry.register(sampleCommand); - - const result = registry.execute('test'); - - expect(result.success).toBe(false); - expect(result.error).toContain('Missing required argument'); - }); - - it('should execute command with flags', () => { - const cmdWithFlags: CliCommand = { - name: 'move', - description: 'Move command', - usage: 'move [--to=region]', - args: [ - { name: 'id', description: 'ID', required: true }, - ], - flags: [ - { name: 'to', description: 'Target', type: 'string' }, - ], - handler: (args) => { - return [ - { - action: 'movePlacement', - params: { - placementId: args.positional[0], - targetRegionId: args.flags.to, - }, - }, - ]; - }, - }; - registry.register(cmdWithFlags); - - const result = registry.execute('move p1 --to=board'); - - expect(result.success).toBe(true); - expect(result.steps[0].params).toEqual({ - placementId: 'p1', - targetRegionId: 'board', - }); - }); - - it('should handle command with optional args', () => { - const cmdOptional: CliCommand = { - name: 'draw', - description: 'Draw cards', - usage: 'draw [count]', - args: [ - { name: 'count', description: 'Count', required: false, default: '1' }, - ], - handler: (args) => { - return []; - }, - }; - registry.register(cmdOptional); - - const result = registry.execute('draw'); - expect(result.success).toBe(true); - }); - }); - - describe('help', () => { - beforeEach(() => { - registry.register({ - name: 'move', - description: 'Move a placement', - usage: 'move ', - args: [ - { name: 'id', description: 'Placement ID', required: true }, - { name: 'target', description: 'Target region', required: true }, - ], - flags: [ - { name: 'key', description: 'Slot key', type: 'string', alias: 'k' }, - ], - handler: () => [], - }); - - registry.register({ - name: 'flip', - description: 'Flip a placement', - usage: 'flip ', - args: [ - { name: 'id', description: 'Placement ID', required: true }, - ], - handler: () => [], - }); - }); - - it('should show all commands help', () => { - const help = registry.help(); - - expect(help).toContain('Available commands'); - expect(help).toContain('move'); - expect(help).toContain('flip'); - expect(help).toContain('help '); - }); - - it('should show specific command help', () => { - const help = registry.help('move'); - - expect(help).toContain('Command: move'); - expect(help).toContain('Move a placement'); - expect(help).toContain('Arguments:'); - expect(help).toContain('Flags:'); - }); - - it('should show error for unknown command help', () => { - const help = registry.help('unknown'); - expect(help).toContain('Unknown command'); - }); - - it('should show command with alias', () => { - const help = registry.help('move'); - expect(help).toContain('-k,'); - }); - }); - - describe('clear', () => { - it('should clear all commands', () => { - registry.register(sampleCommand); - registry.register({ - name: 'cmd2', - description: 'Command 2', - usage: 'cmd2', - handler: () => [], - }); - - expect(registry.getCount()).toBe(2); - registry.clear(); - expect(registry.getCount()).toBe(0); - expect(registry.getAll()).toEqual([]); - }); - }); -}); diff --git a/tests/games/tictactoe.test.ts b/tests/games/tictactoe.test.ts deleted file mode 100644 index 8090998..0000000 --- a/tests/games/tictactoe.test.ts +++ /dev/null @@ -1,319 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { createGameState } from '../../src/core/GameState'; -import { RuleEngine } from '../../src/rules/RuleEngine'; -import { RegionType } from '../../src/core/Region'; -import type { Player, TicTacToeMetadata } from '../../src/games/tictactoe/TicTacToeState'; -import { - getCellId, - getAllCellIds, - getWinningCombinations, -} from '../../src/games/tictactoe/TicTacToeState'; -import { - ticTacToeRules, - startGameCommand, - createMarkCellCommand, - resetGameCommand, -} from '../../src/games/tictactoe'; - -describe('Tic Tac Toe', () => { - let gameState: ReturnType; - let ruleEngine: RuleEngine; - - beforeEach(async () => { - gameState = createGameState({ - id: 'tictactoe-game', - name: 'Tic Tac Toe', - metadata: { - ticTacToe: { - currentPlayer: 'X' as Player, - gameEnded: false, - winner: null, - moveHistory: [], - totalMoves: 0, - }, - }, - }); - - ruleEngine = new RuleEngine(gameState, { gameType: 'tictactoe' }); - ruleEngine.registerRules(ticTacToeRules); - - // Start the game - await ruleEngine.executeCommand(startGameCommand); - }); - - describe('game initialization', () => { - it('should create the board region', () => { - const board = gameState.getRegion('board'); - expect(board).toBeDefined(); - expect(board?.type).toBe(RegionType.Keyed); - }); - - it('should initialize all cells', () => { - const cellIds = getAllCellIds(3); - expect(cellIds.length).toBe(9); - - const board = gameState.getRegion('board'); - expect(board).toBeDefined(); - }); - - it('should set initial game state', () => { - const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata; - expect(metadata.currentPlayer).toBe('X'); - expect(metadata.gameEnded).toBe(false); - expect(metadata.winner).toBe(null); - }); - }); - - describe('marking cells', () => { - it('should allow player X to mark an empty cell', async () => { - const command = createMarkCellCommand('cell-A1', 'X'); - const result = await ruleEngine.executeCommand(command); - - expect(result.success).toBe(true); - - const placement = gameState.getPlacement('cell-A1'); - expect(placement).toBeDefined(); - expect(placement?.metadata?.player).toBe('X'); - }); - - it('should switch to player O after X moves', async () => { - const command = createMarkCellCommand('cell-A1', 'X'); - await ruleEngine.executeCommand(command); - - const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata; - expect(metadata.currentPlayer).toBe('O'); - }); - - it('should record move history', async () => { - const command = createMarkCellCommand('cell-A1', 'X'); - await ruleEngine.executeCommand(command); - - const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata; - expect(metadata.moveHistory.length).toBe(1); - expect(metadata.moveHistory[0].player).toBe('X'); - expect(metadata.moveHistory[0].cellId).toBe('cell-A1'); - }); - - it('should not allow marking an occupied cell', async () => { - const command1 = createMarkCellCommand('cell-A1', 'X'); - await ruleEngine.executeCommand(command1); - - const command2 = createMarkCellCommand('cell-A1', 'O'); - const result = await ruleEngine.executeCommand(command2); - - expect(result.success).toBe(false); - expect(result.error).toContain('already occupied'); - }); - - it('should not allow wrong player to move', async () => { - // Try to place O when it's X's turn - const command = createMarkCellCommand('cell-A1', 'O'); - const result = await ruleEngine.executeCommand(command); - - expect(result.success).toBe(false); - expect(result.error).toContain("It is X's turn"); - }); - - it('should not allow moves after game ends', async () => { - // Set up a winning scenario - await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-A2', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'O')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-A3', 'X')); - - // Game should end with X winning - const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata; - expect(metadata.gameEnded).toBe(true); - expect(metadata.winner).toBe('X'); - - // Try to make another move - const command = createMarkCellCommand('cell-C1', 'O'); - const result = await ruleEngine.executeCommand(command); - - expect(result.success).toBe(false); - expect(result.error).toContain('Game has already ended'); - }); - }); - - describe('win conditions', () => { - it('should detect horizontal win for X', async () => { - await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-A2', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'O')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-A3', 'X')); - - const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata; - expect(metadata.gameEnded).toBe(true); - expect(metadata.winner).toBe('X'); - expect(metadata.winningCombination).toEqual(['cell-A1', 'cell-A2', 'cell-A3']); - }); - - it('should detect horizontal win for O', async () => { - await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-C1', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'O')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-C2', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B3', 'O')); - - const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata; - expect(metadata.gameEnded).toBe(true); - expect(metadata.winner).toBe('O'); - expect(metadata.winningCombination).toEqual(['cell-B1', 'cell-B2', 'cell-B3']); - }); - - it('should detect vertical win', async () => { - await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-C1', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-C2', 'O')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-C3', 'O')); - - const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata; - expect(metadata.gameEnded).toBe(true); - expect(metadata.winner).toBe('O'); - expect(metadata.winningCombination).toEqual(['cell-C1', 'cell-C2', 'cell-C3']); - }); - - it('should detect main diagonal win', async () => { - await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-C1', 'O')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-C3', 'X')); - - const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata; - expect(metadata.gameEnded).toBe(true); - expect(metadata.winner).toBe('X'); - expect(metadata.winningCombination).toEqual(['cell-A1', 'cell-B2', 'cell-C3']); - }); - - it('should detect anti-diagonal win', async () => { - await ruleEngine.executeCommand(createMarkCellCommand('cell-C1', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'O')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-A2', 'O')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-A3', 'X')); - - const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata; - expect(metadata.gameEnded).toBe(true); - expect(metadata.winner).toBe('X'); - expect(metadata.winningCombination).toEqual(['cell-A3', 'cell-B2', 'cell-C1']); - }); - }); - - describe('draw condition', () => { - it('should detect a draw when all cells are filled without winner', async () => { - // Fill the board with no winner - const moves = [ - ['cell-A1', 'X'], - ['cell-A2', 'O'], - ['cell-A3', 'X'], - ['cell-B1', 'O'], - ['cell-B3', 'X'], - ['cell-B2', 'O'], - ['cell-C2', 'X'], - ['cell-C1', 'O'], - ['cell-C3', 'X'], - ] as [string, Player][]; - - for (const [cell, player] of moves) { - const command = createMarkCellCommand(cell, player); - await ruleEngine.executeCommand(command); - } - - const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata; - expect(metadata.gameEnded).toBe(true); - expect(metadata.winner).toBe(null); // Draw - expect(metadata.totalMoves).toBe(9); - }); - }); - - describe('reset game', () => { - it('should reset the board for a new game', async () => { - // Make some moves - await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O')); - - // Reset - await ruleEngine.executeCommand(resetGameCommand); - - // Check that cells are empty - const board = gameState.getRegion('board'); - expect(board).toBeDefined(); - - // Game should be reset - const metadata = gameState.data.value.metadata?.ticTacToe as TicTacToeMetadata; - expect(metadata.currentPlayer).toBe('X'); - expect(metadata.gameEnded).toBe(false); - expect(metadata.winner).toBe(null); - }); - }); - - describe('rule engine integration', () => { - it('should execute all rules in correct order', async () => { - const command = createMarkCellCommand('cell-B2', 'X'); - const result = await ruleEngine.executeCommand(command); - - expect(result.success).toBe(true); - expect(result.validationRules.length).toBeGreaterThan(0); - expect(result.effectRules.length).toBeGreaterThan(0); - - // Check that validation rules ran - const validationRuleIds = result.validationRules.map((r) => r.ruleId); - expect(validationRuleIds).toContain('tictactoe-validate-turn'); - expect(validationRuleIds).toContain('tictactoe-validate-cell-empty'); - expect(validationRuleIds).toContain('tictactoe-validate-game-not-ended'); - - // Check that effect rules ran - const effectRuleIds = result.effectRules.map((r) => r.ruleId); - expect(effectRuleIds).toContain('tictactoe-switch-turn'); - expect(effectRuleIds).toContain('tictactoe-record-history'); - }); - - it('should trigger win condition check after each move', async () => { - // Set up a winning scenario - await ruleEngine.executeCommand(createMarkCellCommand('cell-A1', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B1', 'O')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-A2', 'X')); - await ruleEngine.executeCommand(createMarkCellCommand('cell-B2', 'O')); - - const winningMove = createMarkCellCommand('cell-A3', 'X'); - const result = await ruleEngine.executeCommand(winningMove); - - // Check that trigger rules ran - const triggerRuleIds = result.triggerRules.map((r) => r.ruleId); - expect(triggerRuleIds).toContain('tictactoe-check-win'); - }); - }); - - describe('helper functions', () => { - it('should generate correct cell IDs', () => { - expect(getCellId(0, 0)).toBe('cell-A1'); - expect(getCellId(1, 1)).toBe('cell-B2'); - expect(getCellId(2, 2)).toBe('cell-C3'); - }); - - it('should return correct winning combinations', () => { - const combinations = getWinningCombinations(3); - expect(combinations.length).toBe(8); // 3 rows + 3 columns + 2 diagonals - - // Check rows - expect(combinations[0]).toEqual(['cell-A1', 'cell-A2', 'cell-A3']); - expect(combinations[1]).toEqual(['cell-B1', 'cell-B2', 'cell-B3']); - expect(combinations[2]).toEqual(['cell-C1', 'cell-C2', 'cell-C3']); - - // Check columns - expect(combinations[3]).toEqual(['cell-A1', 'cell-B1', 'cell-C1']); - expect(combinations[4]).toEqual(['cell-A2', 'cell-B2', 'cell-C2']); - expect(combinations[5]).toEqual(['cell-A3', 'cell-B3', 'cell-C3']); - - // Check diagonals - expect(combinations[6]).toEqual(['cell-A1', 'cell-B2', 'cell-C3']); - expect(combinations[7]).toEqual(['cell-A3', 'cell-B2', 'cell-C1']); - }); - }); -}); diff --git a/tests/part.actions.test.ts b/tests/part.actions.test.ts deleted file mode 100644 index 0704cfb..0000000 --- a/tests/part.actions.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { createGameState } from '../src/core/GameState'; -import { PartType } from '../src/core/Part'; -import { - createPartAction, - createMeepleAction, - createCardAction, - createTileAction, - updatePartAction, - removePartAction, - getPartAction, -} from '../src/actions/part.actions'; - -describe('Part Actions', () => { - let gameState: ReturnType; - - beforeEach(() => { - gameState = createGameState({ id: 'test-game', name: 'Test Game' }); - }); - - describe('createPartAction', () => { - it('should create a generic part', () => { - const part = createPartAction(gameState, { - id: 'part-1', - type: PartType.Meeple, - color: 'red', - }); - - expect(part.id).toBe('part-1'); - expect(part.type).toBe(PartType.Meeple); - expect(getPartAction(gameState, 'part-1')).toBeDefined(); - }); - - it('should create a part with metadata', () => { - const part = createPartAction(gameState, { - id: 'part-1', - type: PartType.Tile, - pattern: 'forest', - metadata: { points: 5 }, - }); - - expect(part.metadata).toEqual({ points: 5 }); - }); - }); - - describe('createMeepleAction', () => { - it('should create a meeple part', () => { - const meeple = createMeepleAction(gameState, 'meeple-1', 'blue'); - - expect(meeple.id).toBe('meeple-1'); - expect(meeple.type).toBe(PartType.Meeple); - expect(meeple.color).toBe('blue'); - }); - - it('should create a meeple with name', () => { - const meeple = createMeepleAction(gameState, 'meeple-1', 'blue', { name: 'Player 1' }); - - expect(meeple.name).toBe('Player 1'); - }); - }); - - describe('createCardAction', () => { - it('should create a card part', () => { - const card = createCardAction(gameState, 'card-1', { suit: 'hearts', value: 10 }); - - expect(card.id).toBe('card-1'); - expect(card.type).toBe(PartType.Card); - expect(card.suit).toBe('hearts'); - expect(card.value).toBe(10); - }); - - it('should create a card with string value', () => { - const card = createCardAction(gameState, 'card-2', { suit: 'spades', value: 'ace' }); - - expect(card.value).toBe('ace'); - }); - }); - - describe('createTileAction', () => { - it('should create a tile part', () => { - const tile = createTileAction(gameState, 'tile-1', { pattern: 'road', rotation: 90 }); - - expect(tile.id).toBe('tile-1'); - expect(tile.type).toBe(PartType.Tile); - expect(tile.pattern).toBe('road'); - expect(tile.rotation).toBe(90); - }); - - it('should create a tile with default rotation', () => { - const tile = createTileAction(gameState, 'tile-2', { pattern: 'city' }); - - expect(tile.rotation).toBeUndefined(); - }); - }); - - describe('updatePartAction', () => { - it('should update part properties', () => { - const meeple = createMeepleAction(gameState, 'meeple-1', 'red'); - - updatePartAction(gameState, 'meeple-1', { color: 'green' as string, name: 'Updated' }); - - const updated = getPartAction(gameState, 'meeple-1'); - expect(updated?.color).toBe('green'); - expect(updated?.name).toBe('Updated'); - }); - - it('should update part metadata', () => { - createMeepleAction(gameState, 'meeple-1', 'red', { metadata: { score: 0 } }); - - updatePartAction(gameState, 'meeple-1', { metadata: { score: 10 } } as any); - - const updated = getPartAction(gameState, 'meeple-1'); - expect(updated?.metadata).toEqual({ score: 10 }); - }); - }); - - describe('removePartAction', () => { - it('should remove a part', () => { - createMeepleAction(gameState, 'meeple-1', 'red'); - - expect(getPartAction(gameState, 'meeple-1')).toBeDefined(); - - removePartAction(gameState, 'meeple-1'); - - expect(getPartAction(gameState, 'meeple-1')).toBeUndefined(); - }); - - it('should remove placements referencing the part', () => { - // 这个测试会在 placement 测试中更详细地验证 - createMeepleAction(gameState, 'meeple-1', 'red'); - removePartAction(gameState, 'meeple-1'); - - expect(getPartAction(gameState, 'meeple-1')).toBeUndefined(); - }); - }); - - describe('getPartAction', () => { - it('should return undefined for non-existent part', () => { - const part = getPartAction(gameState, 'non-existent'); - expect(part).toBeUndefined(); - }); - - it('should return existing part', () => { - createMeepleAction(gameState, 'meeple-1', 'red'); - const part = getPartAction(gameState, 'meeple-1'); - expect(part?.id).toBe('meeple-1'); - }); - }); -}); diff --git a/tests/placement.actions.test.ts b/tests/placement.actions.test.ts deleted file mode 100644 index bea1440..0000000 --- a/tests/placement.actions.test.ts +++ /dev/null @@ -1,422 +0,0 @@ -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([]); - }); - }); -}); diff --git a/tests/region.actions.test.ts b/tests/region.actions.test.ts deleted file mode 100644 index a00bc1c..0000000 --- a/tests/region.actions.test.ts +++ /dev/null @@ -1,302 +0,0 @@ -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); - }); - }); -}); diff --git a/tests/rules/rule.engine.test.ts b/tests/rules/rule.engine.test.ts deleted file mode 100644 index 915b098..0000000 --- a/tests/rules/rule.engine.test.ts +++ /dev/null @@ -1,595 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { createGameState } from '../../src/core/GameState'; -import { RuleEngine } from '../../src/rules/RuleEngine'; -import { createValidationRule, createEffectRule, createTriggerRule } from '../../src/rules/Rule'; -import type { RuleResult, RuleContext } from '../../src/rules/Rule'; -import { Command, CommandActionType } from '../../src/commands/Command'; -import { RegionType } from '../../src/core/Region'; - -describe('RuleEngine', () => { - let gameState: ReturnType; - let ruleEngine: RuleEngine; - - beforeEach(() => { - gameState = createGameState({ id: 'test-game', name: 'Test Game' }); - ruleEngine = new RuleEngine(gameState); - }); - - describe('registerRule', () => { - it('should register a validation rule', () => { - const rule = createValidationRule({ - id: 'test-validation', - name: 'Test Validation', - priority: 1, - validate: async () => ({ success: true }), - }); - - ruleEngine.registerRule(rule); - const rules = ruleEngine.getRules(); - - expect(rules.length).toBe(1); - expect(rules[0].id).toBe('test-validation'); - }); - - it('should register an effect rule', () => { - const rule = createEffectRule({ - id: 'test-effect', - name: 'Test Effect', - priority: 1, - apply: async () => ({ success: true }), - }); - - ruleEngine.registerRule(rule); - const rules = ruleEngine.getRules(); - - expect(rules.length).toBe(1); - expect(rules[0].id).toBe('test-effect'); - }); - - it('should register a trigger rule', () => { - const rule = createTriggerRule({ - id: 'test-trigger', - name: 'Test Trigger', - priority: 1, - condition: async () => true, - action: async () => ({ success: true }), - }); - - ruleEngine.registerRule(rule); - const rules = ruleEngine.getRules(); - - expect(rules.length).toBe(1); - expect(rules[0].id).toBe('test-trigger'); - }); - - it('should sort rules by priority', () => { - const rule1 = createValidationRule({ - id: 'rule-1', - name: 'Rule 1', - priority: 3, - validate: async () => ({ success: true }), - }); - const rule2 = createValidationRule({ - id: 'rule-2', - name: 'Rule 2', - priority: 1, - validate: async () => ({ success: true }), - }); - const rule3 = createValidationRule({ - id: 'rule-3', - name: 'Rule 3', - priority: 2, - validate: async () => ({ success: true }), - }); - - ruleEngine.registerRules([rule1, rule2, rule3]); - const rules = ruleEngine.getRules(); - - expect(rules[0].id).toBe('rule-2'); - expect(rules[1].id).toBe('rule-3'); - expect(rules[2].id).toBe('rule-1'); - }); - }); - - describe('executeCommand with validation rules', () => { - it('should execute command when all validation rules pass', async () => { - ruleEngine.registerRule( - createValidationRule({ - id: 'always-pass', - name: 'Always Pass', - priority: 1, - validate: async () => ({ success: true }), - }) - ); - - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - const result = await ruleEngine.executeCommand(command); - - expect(result.success).toBe(true); - expect(gameState.getPart('meeple-1')).toBeDefined(); - }); - - it('should block command when validation rule fails', async () => { - ruleEngine.registerRule( - createValidationRule({ - id: 'always-fail', - name: 'Always Fail', - priority: 1, - validate: async () => ({ - success: false, - error: 'Validation failed', - }), - }) - ); - - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - const result = await ruleEngine.executeCommand(command); - - expect(result.success).toBe(false); - expect(result.error).toBe('Validation failed'); - expect(gameState.getPart('meeple-1')).toBeUndefined(); - }); - - it('should block command when rule sets blockCommand', async () => { - ruleEngine.registerRule( - createValidationRule({ - id: 'block-command', - name: 'Block Command', - priority: 1, - validate: async () => ({ - success: true, - blockCommand: true, - }), - }) - ); - - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - const result = await ruleEngine.executeCommand(command); - - expect(result.success).toBe(false); - expect(result.error).toContain('blocked by rule'); - }); - - it('should apply state updates from validation rules', async () => { - ruleEngine.registerRule( - createValidationRule({ - id: 'set-metadata', - name: 'Set Metadata', - priority: 1, - validate: async (context) => ({ - success: true, - stateUpdates: { validated: true }, - }), - }) - ); - - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - const result = await ruleEngine.executeCommand(command); - - expect(result.success).toBe(true); - }); - }); - - describe('executeCommand with effect rules', () => { - it('should execute effect rules after command', async () => { - let effectExecuted = false; - - ruleEngine.registerRules([ - createValidationRule({ - id: 'validation', - name: 'Validation', - priority: 1, - validate: async () => ({ success: true }), - }), - createEffectRule({ - id: 'effect', - name: 'Effect', - priority: 1, - apply: async () => { - effectExecuted = true; - return { success: true }; - }, - }), - ]); - - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - await ruleEngine.executeCommand(command); - - expect(effectExecuted).toBe(true); - }); - - it('should apply state updates from effect rules', async () => { - ruleEngine.registerRule( - createEffectRule({ - id: 'update-metadata', - name: 'Update Metadata', - priority: 1, - apply: async () => ({ - success: true, - stateUpdates: { effectApplied: true }, - }), - }) - ); - - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - await ruleEngine.executeCommand(command); - - // Effect rule state updates are stored in metadata - expect(gameState.data.value.metadata).toBeDefined(); - }); - }); - - describe('executeCommand with trigger rules', () => { - it('should execute trigger rules when condition is met', async () => { - let triggerExecuted = false; - - ruleEngine.registerRule( - createTriggerRule({ - id: 'trigger', - name: 'Trigger', - priority: 1, - condition: async () => true, - action: async () => { - triggerExecuted = true; - return { success: true }; - }, - }) - ); - - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - await ruleEngine.executeCommand(command); - - expect(triggerExecuted).toBe(true); - }); - - it('should not execute trigger rules when condition is not met', async () => { - let triggerExecuted = false; - - ruleEngine.registerRule( - createTriggerRule({ - id: 'trigger', - name: 'Trigger', - priority: 1, - condition: async () => false, - action: async () => { - triggerExecuted = true; - return { success: true }; - }, - }) - ); - - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - await ruleEngine.executeCommand(command); - - expect(triggerExecuted).toBe(false); - }); - - it('should trigger commands from trigger rules', async () => { - ruleEngine.registerRule( - createTriggerRule({ - id: 'trigger-command', - name: 'Trigger Command', - priority: 1, - condition: async () => true, - action: async () => ({ - success: true, - triggeredCommands: [ - { - id: 'triggered', - name: 'Triggered Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'triggered-meeple', color: 'blue' }, - }, - ], - }, - ], - }), - }) - ); - - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - const result = await ruleEngine.executeCommand(command); - - expect(result.triggeredCommands.length).toBe(1); - expect(gameState.getPart('triggered-meeple')).toBeDefined(); - }); - }); - - describe('rule logging', () => { - it('should log rule executions', async () => { - ruleEngine.registerRule( - createValidationRule({ - id: 'logged-rule', - name: 'Logged Rule', - priority: 1, - validate: async () => ({ success: true }), - }) - ); - - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - await ruleEngine.executeCommand(command); - const logs = ruleEngine.getLogs(); - - expect(logs.length).toBe(1); - expect(logs[0].ruleId).toBe('logged-rule'); - expect(logs[0].ruleType).toBe('validation'); - }); - - it('should clear logs', async () => { - ruleEngine.registerRule( - createValidationRule({ - id: 'logged-rule', - name: 'Logged Rule', - priority: 1, - validate: async () => ({ success: true }), - }) - ); - - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - await ruleEngine.executeCommand(command); - expect(ruleEngine.getLogs().length).toBe(1); - - ruleEngine.clearLogs(); - expect(ruleEngine.getLogs().length).toBe(0); - }); - }); - - describe('game type filtering', () => { - it('should only apply rules matching the game type', async () => { - const gameTypeRuleEngine = new RuleEngine(gameState, { gameType: 'tictactoe' }); - - let tictactoeRuleExecuted = false; - let otherRuleExecuted = false; - - gameTypeRuleEngine.registerRules([ - createValidationRule({ - id: 'tictactoe-rule', - name: 'Tic Tac Toe Rule', - priority: 1, - gameType: 'tictactoe', - validate: async () => { - tictactoeRuleExecuted = true; - return { success: true }; - }, - }), - createValidationRule({ - id: 'other-rule', - name: 'Other Rule', - priority: 1, - gameType: 'chess', - validate: async () => { - otherRuleExecuted = true; - return { success: true }; - }, - }), - ]); - - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - await gameTypeRuleEngine.executeCommand(command); - - expect(tictactoeRuleExecuted).toBe(true); - expect(otherRuleExecuted).toBe(false); - }); - - it('should apply rules without game type to all games', async () => { - const gameTypeRuleEngine = new RuleEngine(gameState, { gameType: 'tictactoe' }); - - let globalRuleExecuted = false; - - gameTypeRuleEngine.registerRules([ - createValidationRule({ - id: 'global-rule', - name: 'Global Rule', - priority: 1, - validate: async () => { - globalRuleExecuted = true; - return { success: true }; - }, - }), - ]); - - const command: Command = { - id: 'test-command', - name: 'Test Command', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - await gameTypeRuleEngine.executeCommand(command); - - expect(globalRuleExecuted).toBe(true); - }); - }); - - describe('command filtering', () => { - it('should only apply rules to applicable commands', async () => { - let ruleExecuted = false; - - ruleEngine.registerRule( - createValidationRule({ - id: 'specific-command-rule', - name: 'Specific Command Rule', - priority: 1, - applicableCommands: ['specificCommand'], - validate: async () => { - ruleExecuted = true; - return { success: true }; - }, - }) - ); - - const command: Command = { - id: 'test-command', - name: 'otherCommand', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - await ruleEngine.executeCommand(command); - - expect(ruleExecuted).toBe(false); - }); - - it('should apply rules to matching commands', async () => { - let ruleExecuted = false; - - ruleEngine.registerRule( - createValidationRule({ - id: 'specific-command-rule', - name: 'Specific Command Rule', - priority: 1, - applicableCommands: ['testCommand'], - validate: async () => { - ruleExecuted = true; - return { success: true }; - }, - }) - ); - - const command: Command = { - id: 'test-command', - name: 'testCommand', - steps: [ - { - action: CommandActionType.CreateMeeple, - params: { id: 'meeple-1', color: 'red' }, - }, - ], - }; - - await ruleEngine.executeCommand(command); - - expect(ruleExecuted).toBe(true); - }); - }); -});