Initial commit: boardgame-core with build fixes
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
@@ -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<T extends Part>(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<string, unknown> }
|
||||
): 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<string, unknown> }
|
||||
): 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<string, unknown> }
|
||||
): TilePart {
|
||||
const part: TilePart = {
|
||||
id,
|
||||
type: PartType.Tile,
|
||||
...options,
|
||||
};
|
||||
gameState.addPart(part);
|
||||
return part;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 Part
|
||||
*/
|
||||
export function updatePartAction<T extends Part>(
|
||||
gameState: GameState,
|
||||
partId: string,
|
||||
updates: Partial<T>
|
||||
): void {
|
||||
gameState.updatePart<T>(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);
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
}
|
||||
): 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);
|
||||
}
|
||||
@@ -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<string[]>([]),
|
||||
...(properties.type === RegionType.Keyed ? { slots: signal<Map<string, string | null>>(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;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* CLI 命令参数
|
||||
*/
|
||||
export interface CliCommandArgs {
|
||||
/** 位置参数 */
|
||||
positional: string[];
|
||||
/** 标志参数 (--key=value 或 --flag) */
|
||||
flags: Record<string, string | boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令解析结果
|
||||
*/
|
||||
export interface ParsedCliCommand {
|
||||
commandName: string;
|
||||
args: CliCommandArgs;
|
||||
raw: string;
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令执行结果
|
||||
*/
|
||||
export interface CommandExecutionResult {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
executedSteps: number;
|
||||
totalSteps: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令定义
|
||||
*/
|
||||
export interface Command {
|
||||
/** 命令唯一标识 */
|
||||
id: string;
|
||||
/** 命令名称 */
|
||||
name: string;
|
||||
/** 命令描述 */
|
||||
description?: string;
|
||||
/** 命令步骤 */
|
||||
steps: CommandStep[];
|
||||
/** 元数据 */
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令日志条目
|
||||
*/
|
||||
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<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 命令状态
|
||||
*/
|
||||
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;
|
||||
}
|
||||
@@ -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<string, unknown>): void {
|
||||
const { id, color, name, metadata } = params;
|
||||
createMeepleAction(this.gameState, id as string, color as string, {
|
||||
name: name as string,
|
||||
metadata: metadata as Record<string, unknown>,
|
||||
});
|
||||
}
|
||||
|
||||
private handleCreateCard(params: Record<string, unknown>): 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<string, unknown>,
|
||||
});
|
||||
}
|
||||
|
||||
private handleCreateTile(params: Record<string, unknown>): 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<string, unknown>,
|
||||
});
|
||||
}
|
||||
|
||||
private handleUpdatePart(params: Record<string, unknown>): void {
|
||||
const { partId, updates } = params;
|
||||
updatePartAction(this.gameState, partId as string, updates as Record<string, unknown>);
|
||||
}
|
||||
|
||||
private handleRemovePart(params: Record<string, unknown>): void {
|
||||
const { partId } = params;
|
||||
removePartAction(this.gameState, partId as string);
|
||||
}
|
||||
|
||||
// ========== Region action handlers ==========
|
||||
|
||||
private handleCreateRegion(params: Record<string, unknown>): 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<string, unknown>,
|
||||
});
|
||||
}
|
||||
|
||||
private handleRemoveRegion(params: Record<string, unknown>): void {
|
||||
const { regionId } = params;
|
||||
removeRegionAction(this.gameState, regionId as string);
|
||||
}
|
||||
|
||||
private handleAddPlacementToRegion(params: Record<string, unknown>): void {
|
||||
const { regionId, placementId } = params;
|
||||
addPlacementToRegionAction(this.gameState, regionId as string, placementId as string);
|
||||
}
|
||||
|
||||
private handleRemovePlacementFromRegion(params: Record<string, unknown>): void {
|
||||
const { regionId, placementId } = params;
|
||||
removePlacementFromRegionAction(this.gameState, regionId as string, placementId as string);
|
||||
}
|
||||
|
||||
private handleSetSlot(params: Record<string, unknown>): void {
|
||||
const { regionId, key, placementId } = params;
|
||||
setSlotAction(this.gameState, regionId as string, key as string, placementId as string | null);
|
||||
}
|
||||
|
||||
private handleClearRegion(params: Record<string, unknown>): void {
|
||||
const { regionId } = params;
|
||||
clearRegionAction(this.gameState, regionId as string);
|
||||
}
|
||||
|
||||
// ========== Placement action handlers ==========
|
||||
|
||||
private handleCreatePlacement(params: Record<string, unknown>): 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<string, unknown>,
|
||||
});
|
||||
}
|
||||
|
||||
private handleRemovePlacement(params: Record<string, unknown>): void {
|
||||
const { placementId } = params;
|
||||
removePlacementAction(this.gameState, placementId as string);
|
||||
}
|
||||
|
||||
private handleMovePlacement(params: Record<string, unknown>): void {
|
||||
const { placementId, targetRegionId, key } = params;
|
||||
movePlacementAction(this.gameState, placementId as string, targetRegionId as string, key as string);
|
||||
}
|
||||
|
||||
private handleUpdatePlacementPosition(params: Record<string, unknown>): void {
|
||||
const { placementId, position } = params;
|
||||
updatePlacementPositionAction(this.gameState, placementId as string, position as { x: number; y: number });
|
||||
}
|
||||
|
||||
private handleUpdatePlacementRotation(params: Record<string, unknown>): void {
|
||||
const { placementId, rotation } = params;
|
||||
updatePlacementRotationAction(this.gameState, placementId as string, rotation as number);
|
||||
}
|
||||
|
||||
private handleFlipPlacement(params: Record<string, unknown>): void {
|
||||
const { placementId } = params;
|
||||
flipPlacementAction(this.gameState, placementId as string);
|
||||
}
|
||||
|
||||
private handleSetPlacementFace(params: Record<string, unknown>): void {
|
||||
const { placementId, faceUp } = params;
|
||||
setPlacementFaceAction(this.gameState, placementId as string, faceUp as boolean);
|
||||
}
|
||||
|
||||
private handleSwapPlacements(params: Record<string, unknown>): void {
|
||||
const { placementId1, placementId2 } = params;
|
||||
swapPlacementsAction(this.gameState, placementId1 as string, placementId2 as string);
|
||||
}
|
||||
|
||||
// ========== Game action handlers ==========
|
||||
|
||||
private handleSetPhase(params: Record<string, unknown>): void {
|
||||
const { phase } = params;
|
||||
this.gameState.setPhase(phase as string);
|
||||
}
|
||||
}
|
||||
@@ -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<CommandLogEntry[]>;
|
||||
|
||||
/** 待执行队列 */
|
||||
private queue: QueuedCommand[];
|
||||
|
||||
constructor() {
|
||||
this.entries = signal<CommandLogEntry[]>([]);
|
||||
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<CommandLogEntry[]> {
|
||||
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();
|
||||
}
|
||||
@@ -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<string, string | boolean> = {};
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import type { CliCommand, CliCommandArgs, CliCommandResult, CliCommandStep } from './CliCommand';
|
||||
import { CommandParser } from './CommandParser';
|
||||
|
||||
/**
|
||||
* 命令注册表
|
||||
* 注册和管理 CLI 命令
|
||||
*/
|
||||
export class CommandRegistry {
|
||||
private commands: Map<string, CliCommand>;
|
||||
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 <command>" 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();
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
import type { CliCommand } from './CliCommand';
|
||||
import { RegionType } from '../core/Region';
|
||||
|
||||
/**
|
||||
* CLI 命令定义集合
|
||||
*/
|
||||
|
||||
/**
|
||||
* move <placementId> <targetRegionId> [--key=slotKey]
|
||||
* 移动 Placement 到另一个区域
|
||||
*/
|
||||
export const moveCommand: CliCommand = {
|
||||
name: 'move',
|
||||
description: 'Move a placement to another region',
|
||||
usage: 'move <placementId> <targetRegionId> [--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 <partId> <regionId> [x] [y] [--rotation=0] [--faceup=true]
|
||||
* 创建 Placement 并放置到区域
|
||||
*/
|
||||
export const placeCommand: CliCommand = {
|
||||
name: 'place',
|
||||
description: 'Place a part in a region',
|
||||
usage: 'place <partId> <regionId> [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 <placementId>
|
||||
* 翻转 Placement
|
||||
*/
|
||||
export const flipCommand: CliCommand = {
|
||||
name: 'flip',
|
||||
description: 'Flip a placement face up/down',
|
||||
usage: 'flip <placementId>',
|
||||
args: [
|
||||
{ name: 'placementId', description: 'The placement ID to flip', required: true },
|
||||
],
|
||||
handler: (args) => {
|
||||
const [placementId] = args.positional;
|
||||
return [
|
||||
{
|
||||
action: 'flipPlacement',
|
||||
params: { placementId },
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* create <type> <id> [options...]
|
||||
* 创建 Part(meeple/card/tile)
|
||||
*/
|
||||
export const createCommand: CliCommand = {
|
||||
name: 'create',
|
||||
description: 'Create a part (meeple, card, or tile)',
|
||||
usage: 'create <type> <id> [--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 <id> <type> [--name=name] [--capacity=n]
|
||||
* 创建 Region
|
||||
*/
|
||||
export const regionCommand: CliCommand = {
|
||||
name: 'region',
|
||||
description: 'Create a region',
|
||||
usage: 'region <id> <type> [--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 <deckId> [count] [--to=handId]
|
||||
* 从牌库抽牌
|
||||
*/
|
||||
export const drawCommand: CliCommand = {
|
||||
name: 'draw',
|
||||
description: 'Draw cards from a deck',
|
||||
usage: 'draw <deckId> [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 <regionId> [--seed=number]
|
||||
* 洗牌
|
||||
*/
|
||||
export const shuffleCommand: CliCommand = {
|
||||
name: 'shuffle',
|
||||
description: 'Shuffle placements in a region',
|
||||
usage: 'shuffle <regionId> [--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 <placementId> [--to=discardId]
|
||||
* 将 Placement 移到弃牌堆
|
||||
*/
|
||||
export const discardCommand: CliCommand = {
|
||||
name: 'discard',
|
||||
description: 'Move a placement to discard pile',
|
||||
usage: 'discard <placementId> [--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 <placementId1> <placementId2>
|
||||
* 交换两个 Placement
|
||||
*/
|
||||
export const swapCommand: CliCommand = {
|
||||
name: 'swap',
|
||||
description: 'Swap two placements',
|
||||
usage: 'swap <placementId1> <placementId2>',
|
||||
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 <placementId> <degrees>
|
||||
* 旋转 Placement
|
||||
*/
|
||||
export const rotateCommand: CliCommand = {
|
||||
name: 'rotate',
|
||||
description: 'Rotate a placement',
|
||||
usage: 'rotate <placementId> <degrees>',
|
||||
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 <placementId> <x> <y>
|
||||
* 设置 Placement 位置
|
||||
*/
|
||||
export const positionCommand: CliCommand = {
|
||||
name: 'position',
|
||||
description: 'Set placement position',
|
||||
usage: 'position <placementId> <x> <y>',
|
||||
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 <phaseName>
|
||||
* 设置游戏阶段
|
||||
*/
|
||||
export const phaseCommand: CliCommand = {
|
||||
name: 'phase',
|
||||
description: 'Set game phase',
|
||||
usage: 'phase <phaseName>',
|
||||
args: [
|
||||
{ name: 'phaseName', description: 'New phase name', required: true },
|
||||
],
|
||||
handler: (args) => {
|
||||
const [phase] = args.positional;
|
||||
return [
|
||||
{
|
||||
action: 'setPhase',
|
||||
params: { phase },
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* clear <regionId>
|
||||
* 清空区域
|
||||
*/
|
||||
export const clearCommand: CliCommand = {
|
||||
name: 'clear',
|
||||
description: 'Clear all placements from a region',
|
||||
usage: 'clear <regionId>',
|
||||
args: [
|
||||
{ name: 'regionId', description: 'Region ID to clear', required: true },
|
||||
],
|
||||
handler: (args) => {
|
||||
const [regionId] = args.positional;
|
||||
return [
|
||||
{
|
||||
action: 'clearRegion',
|
||||
params: { regionId },
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* remove <type> <id>
|
||||
* 移除 Part/Placement/Region
|
||||
*/
|
||||
export const removeCommand: CliCommand = {
|
||||
name: 'remove',
|
||||
description: 'Remove a part, placement, or region',
|
||||
usage: 'remove <type> <id>',
|
||||
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,
|
||||
];
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 游戏状态类
|
||||
* 统一管理所有 Parts, Regions, Placements
|
||||
*/
|
||||
export class GameState {
|
||||
/** 游戏基本信息 */
|
||||
data: Signal<GameStateData>;
|
||||
|
||||
/** Parts 存储 */
|
||||
parts: Signal<Map<string, Part>>;
|
||||
|
||||
/** Regions 存储 */
|
||||
regions: Signal<Map<string, Region>>;
|
||||
|
||||
/** Placements 存储 */
|
||||
placements: Signal<Map<string, Placement>>;
|
||||
|
||||
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<T extends Part>(partId: string, updates: Partial<T>): 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<Placement>): 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<number> {
|
||||
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<string, unknown>): void {
|
||||
this.data.value = {
|
||||
...this.data.value,
|
||||
metadata: { ...this.data.value.metadata, ...updates },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建游戏状态
|
||||
*/
|
||||
export function createGameState(data: GameStateData): GameState {
|
||||
return new GameState(data);
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<typeof signal<Part>>;
|
||||
|
||||
/**
|
||||
* 创建 Part
|
||||
*/
|
||||
export function createPart<T extends Part>(part: T): T {
|
||||
return part;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Meeple Part
|
||||
*/
|
||||
export function createMeeple(id: string, color: string, options?: { name?: string; metadata?: Record<string, unknown> }): 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<string, unknown> }
|
||||
): CardPart {
|
||||
return {
|
||||
id,
|
||||
type: PartType.Card,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Tile Part
|
||||
*/
|
||||
export function createTile(
|
||||
id: string,
|
||||
options?: { pattern?: string; rotation?: number; name?: string; metadata?: Record<string, unknown> }
|
||||
): TilePart {
|
||||
return {
|
||||
id,
|
||||
type: PartType.Tile,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Placement 类型
|
||||
*/
|
||||
export interface Placement extends PlacementProperties {
|
||||
part: Part | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Placement 信号类型
|
||||
*/
|
||||
export type PlacementSignal = Signal<Placement>;
|
||||
|
||||
/**
|
||||
* 创建 Placement
|
||||
*/
|
||||
export function createPlacement(properties: {
|
||||
id: string;
|
||||
partId: string;
|
||||
regionId: string;
|
||||
part: Part;
|
||||
position?: Position;
|
||||
rotation?: number;
|
||||
faceUp?: boolean;
|
||||
metadata?: Record<string, unknown>;
|
||||
}): 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;
|
||||
}
|
||||
@@ -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<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keyed Region 的槽位
|
||||
*/
|
||||
export interface Slot {
|
||||
key: string;
|
||||
placementId: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Region 类型
|
||||
*/
|
||||
export interface Region extends RegionProperties {
|
||||
placements: Signal<string[]>; // Placement ID 列表
|
||||
slots?: Signal<Map<string, string | null>>; // Keyed Region 专用:key -> placementId
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Region
|
||||
*/
|
||||
export function createRegion(properties: RegionProperties): Region {
|
||||
const region: Region = {
|
||||
...properties,
|
||||
placements: signal<string[]>([]),
|
||||
};
|
||||
|
||||
if (properties.type === RegionType.Keyed) {
|
||||
region.slots = signal<Map<string, string | null>>(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;
|
||||
}
|
||||
+142
@@ -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';
|
||||
Reference in New Issue
Block a user