commit d27948fbfc10e01c2c0b5abb8d9039be068eaa57 Author: hyper Date: Tue Mar 31 18:01:57 2026 +0800 Initial commit: boardgame-core with build fixes Co-authored-by: Qwen-Coder diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..10d6e7f --- /dev/null +++ b/README.md @@ -0,0 +1,182 @@ +# boardgame-core + +基于 Preact Signals 的桌游状态管理库。 + +## 特性 + +- **响应式状态管理**: 使用 [@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 new file mode 100644 index 0000000..d4229b8 --- /dev/null +++ b/commands/card.play.json @@ -0,0 +1,51 @@ +{ + "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 new file mode 100644 index 0000000..4b106eb --- /dev/null +++ b/commands/cli/game.cli @@ -0,0 +1,81 @@ +# 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 new file mode 100644 index 0000000..4524373 --- /dev/null +++ b/commands/cli/moves.cli @@ -0,0 +1,57 @@ +# 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 new file mode 100644 index 0000000..003d60f --- /dev/null +++ b/commands/combo.move.json @@ -0,0 +1,72 @@ +{ + "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 new file mode 100644 index 0000000..8d7425c --- /dev/null +++ b/commands/move.meeple.json @@ -0,0 +1,22 @@ +{ + "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 new file mode 100644 index 0000000..80cc9a5 --- /dev/null +++ b/commands/phase.change.json @@ -0,0 +1,44 @@ +{ + "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 new file mode 100644 index 0000000..49ceab1 --- /dev/null +++ b/commands/place.meeple.json @@ -0,0 +1,42 @@ +{ + "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 new file mode 100644 index 0000000..9f2425b --- /dev/null +++ b/commands/setup.game.json @@ -0,0 +1,44 @@ +{ + "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 new file mode 100644 index 0000000..f044620 --- /dev/null +++ b/commands/tile.place.json @@ -0,0 +1,43 @@ +{ + "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/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e18a3da --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2767 @@ +{ + "name": "boardgame-core", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "boardgame-core", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@preact/signals-core": "^1.5.1", + "boardgame-core": "file:" + }, + "devDependencies": { + "tsup": "^8.0.2", + "typescript": "^5.3.3", + "vitest": "^1.3.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@preact/signals-core": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.0.tgz", + "integrity": "sha512-AowtCcCU/33lFlh1zRFf/u+12rfrhtNakj7UpaGEsmMwUKpKWMVvcktOGcwBBNiB4lWrZWc01LhiyyzVklJyaQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/boardgame-core": { + "resolved": "", + "link": true + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e5c3f6d --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "boardgame-core", + "version": "1.0.0", + "description": "A state management library for board games using Preact Signals", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsup", + "test": "vitest", + "test:run": "vitest run" + }, + "dependencies": { + "@preact/signals-core": "^1.5.1", + "boardgame-core": "file:" + }, + "devDependencies": { + "tsup": "^8.0.2", + "typescript": "^5.3.3", + "vitest": "^1.3.1" + }, + "keywords": [ + "boardgame", + "state-management", + "preact", + "signals" + ], + "license": "MIT" +} diff --git a/src/actions/part.actions.ts b/src/actions/part.actions.ts new file mode 100644 index 0000000..e536010 --- /dev/null +++ b/src/actions/part.actions.ts @@ -0,0 +1,95 @@ +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 new file mode 100644 index 0000000..95c491c --- /dev/null +++ b/src/actions/placement.actions.ts @@ -0,0 +1,222 @@ +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 new file mode 100644 index 0000000..3e2998b --- /dev/null +++ b/src/actions/region.actions.ts @@ -0,0 +1,209 @@ +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 new file mode 100644 index 0000000..a88a32f --- /dev/null +++ b/src/commands/CliCommand.ts @@ -0,0 +1,86 @@ +/** + * 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 new file mode 100644 index 0000000..b49da63 --- /dev/null +++ b/src/commands/Command.ts @@ -0,0 +1,115 @@ +/** + * 命令步骤类型 + */ +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 new file mode 100644 index 0000000..43d8ff6 --- /dev/null +++ b/src/commands/CommandExecutor.ts @@ -0,0 +1,301 @@ +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 new file mode 100644 index 0000000..9b5c16b --- /dev/null +++ b/src/commands/CommandLog.ts @@ -0,0 +1,203 @@ +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 new file mode 100644 index 0000000..bb53966 --- /dev/null +++ b/src/commands/CommandParser.ts @@ -0,0 +1,163 @@ +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 new file mode 100644 index 0000000..6add69c --- /dev/null +++ b/src/commands/CommandRegistry.ts @@ -0,0 +1,209 @@ +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 new file mode 100644 index 0000000..4876c59 --- /dev/null +++ b/src/commands/cli.commands.ts @@ -0,0 +1,485 @@ +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 new file mode 100644 index 0000000..60c75d7 --- /dev/null +++ b/src/commands/default.commands.ts @@ -0,0 +1,285 @@ +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/core/GameState.ts b/src/core/GameState.ts new file mode 100644 index 0000000..28fc051 --- /dev/null +++ b/src/core/GameState.ts @@ -0,0 +1,312 @@ +import { signal, Signal, computed } from '@preact/signals-core'; +import type { Part } from './Part'; +import type { Placement } from './Placement'; +import type { Region } from './Region'; + +/** + * 游戏状态 + */ +export interface GameStateData { + id: string; + name: string; + phase?: string; + metadata?: Record; +} + +/** + * 游戏状态类 + * 统一管理所有 Parts, Regions, Placements + */ +export class GameState { + /** 游戏基本信息 */ + data: Signal; + + /** Parts 存储 */ + parts: Signal>; + + /** Regions 存储 */ + regions: Signal>; + + /** Placements 存储 */ + placements: Signal>; + + constructor(gameData: GameStateData) { + this.data = signal(gameData); + this.parts = signal(new Map()); + this.regions = signal(new Map()); + this.placements = signal(new Map()); + } + + // ========== Part 相关方法 ========== + + /** + * 添加 Part + */ + addPart(part: Part): void { + const parts = new Map(this.parts.value); + parts.set(part.id, part); + this.parts.value = parts; + } + + /** + * 获取 Part + */ + getPart(partId: string): Part | undefined { + return this.parts.value.get(partId); + } + + /** + * 移除 Part + */ + removePart(partId: string): void { + const parts = new Map(this.parts.value); + parts.delete(partId); + this.parts.value = parts; + } + + /** + * 更新 Part + */ + updatePart(partId: string, updates: Partial): void { + const part = this.parts.value.get(partId); + if (part) { + const updated = { ...part, ...updates } as T; + const parts = new Map(this.parts.value); + parts.set(partId, updated); + this.parts.value = parts; + } + } + + // ========== Region 相关方法 ========== + + /** + * 添加 Region + */ + addRegion(region: Region): void { + const regions = new Map(this.regions.value); + regions.set(region.id, region); + this.regions.value = regions; + } + + /** + * 获取 Region + */ + getRegion(regionId: string): Region | undefined { + return this.regions.value.get(regionId); + } + + /** + * 移除 Region + */ + removeRegion(regionId: string): void { + const regions = new Map(this.regions.value); + regions.delete(regionId); + this.regions.value = regions; + } + + // ========== Placement 相关方法 ========== + + /** + * 添加 Placement + */ + addPlacement(placement: Placement): void { + const placements = new Map(this.placements.value); + placements.set(placement.id, placement); + this.placements.value = placements; + } + + /** + * 获取 Placement + */ + getPlacement(placementId: string): Placement | undefined { + return this.placements.value.get(placementId); + } + + /** + * 移除 Placement + */ + removePlacement(placementId: string): void { + const placement = this.placements.value.get(placementId); + if (placement) { + // 从 Region 中移除 + const region = this.regions.value.get(placement.regionId); + if (region) { + 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 === '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; + } + } + } + + const placements = new Map(this.placements.value); + placements.delete(placementId); + this.placements.value = placements; + } + + /** + * 更新 Placement + */ + updatePlacement(placementId: string, updates: Partial): void { + const placement = this.placements.value.get(placementId); + if (placement) { + const updated = { ...placement, ...updates }; + const placements = new Map(this.placements.value); + placements.set(placementId, updated); + this.placements.value = placements; + } + } + + /** + * 更新 Placement 的 Part 引用 + */ + updatePlacementPart(placementId: string, part: Part | null): void { + const placement = this.placements.value.get(placementId); + if (placement) { + const updated = { ...placement, part }; + const placements = new Map(this.placements.value); + placements.set(placementId, updated); + this.placements.value = placements; + } + } + + /** + * 移动 Placement 到另一个 Region + */ + movePlacement(placementId: string, targetRegionId: string, key?: string): void { + const placement = this.placements.value.get(placementId); + if (!placement) { + throw new Error(`Placement ${placementId} not found`); + } + + const sourceRegion = this.regions.value.get(placement.regionId); + const targetRegion = this.regions.value.get(targetRegionId); + + if (!targetRegion) { + throw new Error(`Region ${targetRegionId} not found`); + } + + // 从源 Region 移除 + if (sourceRegion) { + const current = sourceRegion.placements.value; + const index = current.indexOf(placementId); + if (index !== -1) { + const updated = [...current]; + updated.splice(index, 1); + sourceRegion.placements.value = updated; + } + + // 清理源 keyed region 的 slot + if (sourceRegion.type === 'keyed' && sourceRegion.slots) { + const slots = new Map(sourceRegion.slots.value); + for (const [k, value] of slots.entries()) { + if (value === placementId) { + slots.set(k, null); + break; + } + } + sourceRegion.slots.value = slots; + } + } + + // 添加到目标 Region + if (targetRegion.type === 'keyed') { + if (key === undefined) { + throw new Error('Key is required for keyed regions'); + } + if (targetRegion.slots) { + const slots = new Map(targetRegion.slots.value); + slots.set(key, placementId); + targetRegion.slots.value = slots; + } + } + + const targetPlacements = [...targetRegion.placements.value, placementId]; + targetRegion.placements.value = targetPlacements; + + // 更新 Placement 的 regionId + const updated = { ...placement, regionId: targetRegionId }; + if (key !== undefined) { + updated.metadata = { ...updated.metadata, key }; + } + const placements = new Map(this.placements.value); + placements.set(placementId, updated); + this.placements.value = placements; + } + + // ========== 计算属性 ========== + + /** + * 获取 Region 中的所有 Placements + */ + getPlacementsInRegion(regionId: string): Placement[] { + const region = this.regions.value.get(regionId); + if (!region) { + return []; + } + + const placementIds = region.placements.value; + return placementIds + .map((id) => this.placements.value.get(id)) + .filter((p): p is Placement => p !== undefined); + } + + /** + * 获取 Part 的所有 Placements + */ + getPlacementsOfPart(partId: string): Placement[] { + const allPlacements = Array.from(this.placements.value.values()); + return allPlacements.filter((p) => p.partId === partId); + } + + /** + * 创建计算信号:获取 Region 中的 Placement 数量 + */ + createPlacementCountSignal(regionId: string): Signal { + const region = this.regions.value.get(regionId); + if (!region) { + return signal(0); + } + return computed(() => region.placements.value.length); + } + + // ========== 游戏状态管理 ========== + + /** + * 更新游戏阶段 + */ + setPhase(phase: string): void { + this.data.value = { ...this.data.value, phase }; + } + + /** + * 更新游戏元数据 + */ + updateMetadata(updates: Record): void { + this.data.value = { + ...this.data.value, + metadata: { ...this.data.value.metadata, ...updates }, + }; + } +} + +/** + * 创建游戏状态 + */ +export function createGameState(data: GameStateData): GameState { + return new GameState(data); +} diff --git a/src/core/Part.ts b/src/core/Part.ts new file mode 100644 index 0000000..56c4156 --- /dev/null +++ b/src/core/Part.ts @@ -0,0 +1,103 @@ +import { signal } from '@preact/signals-core'; + +/** + * Part 类型枚举 + */ +export enum PartType { + Meeple = 'meeple', + Card = 'card', + Tile = 'tile', +} + +/** + * Part 的基础属性 + */ +export interface PartBase { + id: string; + type: PartType; + name?: string; + metadata?: Record; +} + +/** + * Meeple 特有属性 + */ +export interface MeeplePart extends PartBase { + type: PartType.Meeple; + color: string; +} + +/** + * Card 特有属性 + */ +export interface CardPart extends PartBase { + type: PartType.Card; + suit?: string; + value?: number | string; +} + +/** + * Tile 特有属性 + */ +export interface TilePart extends PartBase { + type: PartType.Tile; + pattern?: string; + rotation?: number; +} + +/** + * Part 联合类型 + */ +export type Part = MeeplePart | CardPart | TilePart; + +/** + * Part 信号类型 + */ +export type PartSignal = ReturnType>; + +/** + * 创建 Part + */ +export function createPart(part: T): T { + return part; +} + +/** + * 创建 Meeple Part + */ +export function createMeeple(id: string, color: string, options?: { name?: string; metadata?: Record }): MeeplePart { + return { + id, + type: PartType.Meeple, + color, + ...options, + }; +} + +/** + * 创建 Card Part + */ +export function createCard( + id: string, + options?: { suit?: string; value?: number | string; name?: string; metadata?: Record } +): CardPart { + return { + id, + type: PartType.Card, + ...options, + }; +} + +/** + * 创建 Tile Part + */ +export function createTile( + id: string, + options?: { pattern?: string; rotation?: number; name?: string; metadata?: Record } +): TilePart { + return { + id, + type: PartType.Tile, + ...options, + }; +} diff --git a/src/core/Placement.ts b/src/core/Placement.ts new file mode 100644 index 0000000..5286325 --- /dev/null +++ b/src/core/Placement.ts @@ -0,0 +1,88 @@ +import { signal, Signal } from '@preact/signals-core'; +import type { Part } from './Part'; + +/** + * Placement 的位置信息 + */ +export interface Position { + x: number; + y: number; +} + +/** + * Placement 属性 + */ +export interface PlacementProperties { + id: string; + partId: string; + regionId: string; + position?: Position; + rotation?: number; + faceUp?: boolean; + metadata?: Record; +} + +/** + * Placement 类型 + */ +export interface Placement extends PlacementProperties { + part: Part | null; +} + +/** + * Placement 信号类型 + */ +export type PlacementSignal = Signal; + +/** + * 创建 Placement + */ +export function createPlacement(properties: { + id: string; + partId: string; + regionId: string; + part: Part; + position?: Position; + rotation?: number; + faceUp?: boolean; + metadata?: Record; +}): Placement { + return { + id: properties.id, + partId: properties.partId, + regionId: properties.regionId, + part: properties.part, + position: properties.position, + rotation: properties.rotation ?? 0, + faceUp: properties.faceUp ?? true, + metadata: properties.metadata, + }; +} + +/** + * 更新 Placement 的 Part 引用 + */ +export function updatePlacementPart(placement: Placement, part: Part | null): void { + placement.part = part; +} + +/** + * 更新 Placement 的位置 + */ +export function updatePlacementPosition(placement: Placement, position: Position): void { + placement.position = position; +} + +/** + * 更新 Placement 的旋转角度 + */ +export function updatePlacementRotation(placement: Placement, rotation: number): void { + placement.rotation = rotation; +} + +/** + * 翻转 Placement + */ +export function flipPlacement(placement: Placement): void { + placement.faceUp = !placement.faceUp; +} diff --git a/src/core/Region.ts b/src/core/Region.ts new file mode 100644 index 0000000..c626ad8 --- /dev/null +++ b/src/core/Region.ts @@ -0,0 +1,155 @@ +import { signal, Signal } from '@preact/signals-core'; +import type { Placement } from './Placement'; + +/** + * Region 类型 + */ +export enum RegionType { + /** + * Keyed Region - 子元素通过 key 索引 + * 适用于:玩家手牌、版图格子等有固定位置的区域 + */ + Keyed = 'keyed', + /** + * Unkeyed Region - 子元素按顺序排列 + * 适用于:牌库、弃牌堆等堆叠区域 + */ + Unkeyed = 'unkeyed', +} + +/** + * Region 属性 + */ +export interface RegionProperties { + id: string; + type: RegionType; + name?: string; + capacity?: number; + metadata?: Record; +} + +/** + * Keyed Region 的槽位 + */ +export interface Slot { + key: string; + placementId: string | null; +} + +/** + * Region 类型 + */ +export interface Region extends RegionProperties { + placements: Signal; // Placement ID 列表 + slots?: Signal>; // Keyed Region 专用:key -> placementId +} + +/** + * 创建 Region + */ +export function createRegion(properties: RegionProperties): Region { + const region: Region = { + ...properties, + placements: signal([]), + }; + + if (properties.type === RegionType.Keyed) { + region.slots = signal>(new Map()); + } + + return region; +} + +/** + * 添加 Placement ID 到 Region (unkeyed) + */ +export function addPlacementToRegion(region: Region, placementId: string): void { + if (region.type === RegionType.Keyed) { + throw new Error('Cannot use addPlacementToRegion on a keyed region. Use setSlot instead.'); + } + + const current = region.placements.value; + if (region.capacity !== undefined && current.length >= region.capacity) { + throw new Error(`Region ${region.id} has reached its capacity of ${region.capacity}`); + } + region.placements.value = [...current, placementId]; +} + +/** + * 从 Region 移除 Placement ID + */ +export function removePlacementFromRegion(region: Region, placementId: string): void { + 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 的槽位 + */ +export function setSlot(region: Region, key: string, placementId: string | null): void { + if (region.type !== RegionType.Keyed || !region.slots) { + throw new Error('Cannot use setSlot 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 getSlot(region: Region, key: string): string | null { + if (region.type !== RegionType.Keyed || !region.slots) { + throw new Error('Cannot use getSlot on an unkeyed region.'); + } + return region.slots.value.get(key) ?? null; +} + +/** + * 清空 Region + */ +export function clearRegion(region: Region): void { + region.placements.value = []; + if (region.slots) { + region.slots.value = new Map(); + } +} + +/** + * 获取 Region 中 Placement 的数量 + */ +export function getPlacementCount(region: Region): number { + return region.placements.value.length; +} + +/** + * 检查 Region 是否为空 + */ +export function isRegionEmpty(region: Region): boolean { + return region.placements.value.length === 0; +} + +/** + * 检查 Region 是否已满 + */ +export function isRegionFull(region: Region): boolean { + if (region.capacity === undefined) { + return false; + } + return region.placements.value.length >= region.capacity; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..0499ce9 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,142 @@ +/** + * boardgame-core + * 基于 Preact Signals 的桌游状态管理库 + */ + +// Core types +export { PartType } from './core/Part'; +export type { + Part, + PartBase, + MeeplePart, + CardPart, + TilePart, + PartSignal, +} from './core/Part'; + +export { RegionType } from './core/Region'; +export type { Region, RegionProperties, Slot } from './core/Region'; + +export type { Placement, PlacementProperties, Position, PlacementSignal } from './core/Placement'; + +export type { GameStateData } from './core/GameState'; + +// Core classes and functions +export { + createPart, + createMeeple, + createCard, + createTile, +} from './core/Part'; + +export { createRegion, createRegion as createRegionCore } from './core/Region'; +export type { Region as RegionClass } from './core/Region'; + +export { createPlacement } from './core/Placement'; + +export { GameState, createGameState } from './core/GameState'; + +// Part actions +export { + createPartAction, + createMeepleAction, + createCardAction, + createTileAction, + updatePartAction, + removePartAction, + getPartAction, +} from './actions/part.actions'; + +// Region actions +export { + createRegionAction, + getRegionAction, + removeRegionAction, + addPlacementToRegionAction, + removePlacementFromRegionAction, + setSlotAction, + getSlotAction, + clearRegionAction, + getRegionPlacementCountAction, + isRegionEmptyAction, + isRegionFullAction, +} from './actions/region.actions'; + +// Placement actions +export { + createPlacementAction, + getPlacementAction, + removePlacementAction, + movePlacementAction, + updatePlacementPositionAction, + updatePlacementRotationAction, + flipPlacementAction, + updatePlacementPartAction, + swapPlacementsAction, + setPlacementFaceAction, + getPlacementsInRegionAction, + getPlacementsOfPartAction, +} from './actions/placement.actions'; + +// Commands +export { + CommandActionType, + type Command, + type CommandStep, + type CommandExecutionResult, + type CommandLogEntry, + type StepResult, + type CommandStatus, + type QueuedCommand, +} from './commands/Command'; + +export { CommandExecutor } from './commands/CommandExecutor'; + +export { CommandLog, createCommandLog } from './commands/CommandLog'; + +export { + setupGameCommand, + placeMeepleCommand, + moveMeepleCommand, + drawCardCommand, + playCardCommand, + placeTileCommand, + flipTileCommand, + swapPlacementsCommand, + setPhaseCommand, + clearRegionCommand, + defaultCommands, + getDefaultCommand, +} from './commands/default.commands'; + +// CLI Commands +export { + type CliCommand, + type CliCommandArgs, + type CliCommandResult, + type CliCommandStep, + type ParsedCliCommand, +} from './commands/CliCommand'; + +export { CommandParser, createCommandParser, CommandParseError } from './commands/CommandParser'; + +export { CommandRegistry, createCommandRegistry } from './commands/CommandRegistry'; + +export { + moveCommand, + placeCommand, + flipCommand, + createCommand, + regionCommand, + drawCommand, + shuffleCommand, + discardCommand, + swapCommand, + rotateCommand, + positionCommand, + phaseCommand, + clearCommand, + removeCommand, + helpCommand, + cliCommands, +} from './commands/cli.commands'; diff --git a/tests/commands/command.executor.test.ts b/tests/commands/command.executor.test.ts new file mode 100644 index 0000000..b38cd3f --- /dev/null +++ b/tests/commands/command.executor.test.ts @@ -0,0 +1,329 @@ +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 new file mode 100644 index 0000000..99c7e28 --- /dev/null +++ b/tests/commands/command.log.test.ts @@ -0,0 +1,278 @@ +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 new file mode 100644 index 0000000..ecb9f9d --- /dev/null +++ b/tests/commands/command.parser.test.ts @@ -0,0 +1,160 @@ +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 new file mode 100644 index 0000000..e7eeaa9 --- /dev/null +++ b/tests/commands/command.registry.test.ts @@ -0,0 +1,254 @@ +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/part.actions.test.ts b/tests/part.actions.test.ts new file mode 100644 index 0000000..0704cfb --- /dev/null +++ b/tests/part.actions.test.ts @@ -0,0 +1,149 @@ +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 new file mode 100644 index 0000000..bea1440 --- /dev/null +++ b/tests/placement.actions.test.ts @@ -0,0 +1,422 @@ +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 new file mode 100644 index 0000000..a00bc1c --- /dev/null +++ b/tests/region.actions.test.ts @@ -0,0 +1,302 @@ +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/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7662224 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020"], + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..535937f --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + sourcemap: true, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..8996a04 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts'], + }, +});