chore: clean up

This commit is contained in:
hyper
2026-04-01 17:08:08 +08:00
parent 6740584fc8
commit ea337acacb
37 changed files with 0 additions and 7381 deletions
-95
View File
@@ -1,95 +0,0 @@
import type { GameState } from '../core/GameState';
import type { Part, MeeplePart, CardPart, TilePart } from '../core/Part';
import { PartType } from '../core/Part';
/**
* 创建 Part 并添加到 GameState
*/
export function createPartAction<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);
}
-222
View File
@@ -1,222 +0,0 @@
import type { GameState } from '../core/GameState';
import type { Placement, Position } from '../core/Placement';
import type { Part } from '../core/Part';
/**
* 创建 Placement 并添加到 GameState
*/
export function createPlacementAction(
gameState: GameState,
options: {
id: string;
partId: string;
regionId: string;
position?: Position;
rotation?: number;
faceUp?: boolean;
metadata?: Record<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);
}
-209
View File
@@ -1,209 +0,0 @@
import type { GameState } from '../core/GameState';
import type { Region, RegionProperties } from '../core/Region';
import { RegionType } from '../core/Region';
import type { Placement } from '../core/Placement';
import { signal } from '@preact/signals-core';
/**
* 创建 Region 并添加到 GameState
*/
export function createRegionAction(gameState: GameState, properties: RegionProperties): Region {
const region = {
...properties,
placements: signal<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;
}
-86
View File
@@ -1,86 +0,0 @@
/**
* 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;
}
-115
View File
@@ -1,115 +0,0 @@
/**
* 命令步骤类型
*/
export enum CommandActionType {
// Part actions
CreateMeeple = 'createMeeple',
CreateCard = 'createCard',
CreateTile = 'createTile',
UpdatePart = 'updatePart',
RemovePart = 'removePart',
// Region actions
CreateRegion = 'createRegion',
RemoveRegion = 'removeRegion',
AddPlacementToRegion = 'addPlacementToRegion',
RemovePlacementFromRegion = 'removePlacementFromRegion',
SetSlot = 'setSlot',
ClearRegion = 'clearRegion',
// Placement actions
CreatePlacement = 'createPlacement',
RemovePlacement = 'removePlacement',
MovePlacement = 'movePlacement',
UpdatePlacementPosition = 'updatePlacementPosition',
UpdatePlacementRotation = 'updatePlacementRotation',
FlipPlacement = 'flipPlacement',
SetPlacementFace = 'setPlacementFace',
SwapPlacements = 'swapPlacements',
// Game actions
SetPhase = 'setPhase',
}
/**
* 命令步骤
*/
export interface CommandStep {
action: CommandActionType;
params: Record<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;
}
-301
View File
@@ -1,301 +0,0 @@
import type { GameState } from '../core/GameState';
import { PartType } from '../core/Part';
import { RegionType } from '../core/Region';
import type { Command, CommandStep, CommandExecutionResult, StepResult, CommandActionType } from './Command';
import {
createMeepleAction,
createCardAction,
createTileAction,
updatePartAction,
removePartAction,
} from '../actions/part.actions';
import {
createRegionAction,
removeRegionAction,
addPlacementToRegionAction,
removePlacementFromRegionAction,
setSlotAction,
clearRegionAction,
} from '../actions/region.actions';
import {
createPlacementAction,
removePlacementAction,
movePlacementAction,
updatePlacementPositionAction,
updatePlacementRotationAction,
flipPlacementAction,
setPlacementFaceAction,
swapPlacementsAction,
} from '../actions/placement.actions';
/**
* 命令执行器
* 负责解析并执行命令中的每一步
*/
export class CommandExecutor {
private gameState: GameState;
constructor(gameState: GameState) {
this.gameState = gameState;
}
/**
* 执行命令
*/
execute(command: Command): CommandExecutionResult {
const stepResults: StepResult[] = [];
let hasError = false;
let errorMessage: string | undefined;
for (let i = 0; i < command.steps.length; i++) {
const step = command.steps[i];
const stepResult = this.executeStep(step, i);
stepResults.push(stepResult);
if (!stepResult.success) {
hasError = true;
errorMessage = stepResult.error;
break; // 遇到错误时停止执行
}
}
return {
success: !hasError,
error: errorMessage,
executedSteps: stepResults.filter((s) => s.success).length,
totalSteps: command.steps.length,
};
}
/**
* 执行单个步骤
*/
private executeStep(step: CommandStep, stepIndex: number): StepResult {
try {
const { action, params } = step;
switch (action) {
// Part actions
case 'createMeeple':
this.handleCreateMeeple(params);
break;
case 'createCard':
this.handleCreateCard(params);
break;
case 'createTile':
this.handleCreateTile(params);
break;
case 'updatePart':
this.handleUpdatePart(params);
break;
case 'removePart':
this.handleRemovePart(params);
break;
// Region actions
case 'createRegion':
this.handleCreateRegion(params);
break;
case 'removeRegion':
this.handleRemoveRegion(params);
break;
case 'addPlacementToRegion':
this.handleAddPlacementToRegion(params);
break;
case 'removePlacementFromRegion':
this.handleRemovePlacementFromRegion(params);
break;
case 'setSlot':
this.handleSetSlot(params);
break;
case 'clearRegion':
this.handleClearRegion(params);
break;
// Placement actions
case 'createPlacement':
this.handleCreatePlacement(params);
break;
case 'removePlacement':
this.handleRemovePlacement(params);
break;
case 'movePlacement':
this.handleMovePlacement(params);
break;
case 'updatePlacementPosition':
this.handleUpdatePlacementPosition(params);
break;
case 'updatePlacementRotation':
this.handleUpdatePlacementRotation(params);
break;
case 'flipPlacement':
this.handleFlipPlacement(params);
break;
case 'setPlacementFace':
this.handleSetPlacementFace(params);
break;
case 'swapPlacements':
this.handleSwapPlacements(params);
break;
// Game actions
case 'setPhase':
this.handleSetPhase(params);
break;
default:
throw new Error(`Unknown action type: ${action}`);
}
return {
stepIndex,
action,
success: true,
params,
};
} catch (error) {
return {
stepIndex,
action: step.action,
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
params: step.params,
};
}
}
// ========== Part action handlers ==========
private handleCreateMeeple(params: Record<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);
}
}
-203
View File
@@ -1,203 +0,0 @@
import { signal, Signal } from '@preact/signals-core';
import type { Command, CommandLogEntry, CommandExecutionResult, StepResult, QueuedCommand } from './Command';
import { CommandStatus } from './Command';
/**
* 命令日志过滤器
*/
export interface CommandLogFilter {
commandId?: string;
success?: boolean;
startTime?: number;
endTime?: number;
}
/**
* 命令日志类
* 记录所有执行的命令及其结果
*/
export class CommandLog {
/** 日志条目信号 */
private entries: Signal<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();
}
-163
View File
@@ -1,163 +0,0 @@
import type { ParsedCliCommand, CliCommandArgs } from './CliCommand';
/**
* 命令解析错误
*/
export class CommandParseError extends Error {
constructor(message: string) {
super(message);
this.name = 'CommandParseError';
}
}
/**
* CLI 命令解析器
* 解析 CLI 风格的命令字符串
*/
export class CommandParser {
/**
* 解析命令字符串
* @param input 命令字符串,如 "move card-1 discard --faceup=true"
*/
parse(input: string): ParsedCliCommand {
const trimmed = input.trim();
if (!trimmed) {
throw new CommandParseError('Empty command');
}
const tokens = this.tokenize(trimmed);
if (tokens.length === 0) {
throw new CommandParseError('No command found');
}
const commandName = tokens[0];
const args = this.parseArgs(tokens.slice(1));
return {
commandName,
args,
raw: input,
};
}
/**
* 将命令字符串分词
*/
private tokenize(input: string): string[] {
const tokens: string[] = [];
let current = '';
let inQuotes = false;
let quoteChar = '';
for (let i = 0; i < input.length; i++) {
const char = input[i];
if (inQuotes) {
if (char === quoteChar) {
tokens.push(current);
current = '';
inQuotes = false;
} else {
current += char;
}
} else if (char === '"' || char === "'") {
inQuotes = true;
quoteChar = char;
} else if (char === ' ' || char === '\t') {
if (current) {
tokens.push(current);
current = '';
}
} else {
current += char;
}
}
if (current) {
tokens.push(current);
}
if (inQuotes) {
throw new CommandParseError('Unclosed quote in command');
}
return tokens;
}
/**
* 解析参数
*/
private parseArgs(tokens: string[]): CliCommandArgs {
const positional: string[] = [];
const flags: Record<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();
}
-209
View File
@@ -1,209 +0,0 @@
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();
}
-485
View File
@@ -1,485 +0,0 @@
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...]
* 创建 Partmeeple/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,
];
-285
View File
@@ -1,285 +0,0 @@
import type { Command } from './Command';
import { CommandActionType } from './Command';
import { RegionType } from '../core/Region';
/**
* 内置命令集合
*/
/**
* 设置游戏:创建基础区域
*/
export const setupGameCommand: Command = {
id: 'setup-game',
name: 'Setup Game',
description: 'Initialize the game with basic regions',
steps: [
{
action: CommandActionType.CreateRegion,
params: {
id: 'board',
type: RegionType.Keyed,
name: 'Game Board',
},
},
{
action: CommandActionType.CreateRegion,
params: {
id: 'supply',
type: RegionType.Unkeyed,
name: 'Supply',
},
},
{
action: CommandActionType.CreateRegion,
params: {
id: 'discard',
type: RegionType.Unkeyed,
name: 'Discard Pile',
},
},
],
};
/**
* 放置棋子命令
*/
export const placeMeepleCommand: Command = {
id: 'place-meeple',
name: 'Place Meeple',
description: 'Place a meeple on the board',
steps: [
{
action: CommandActionType.CreateMeeple,
params: {
id: '${meepleId}',
color: '${color}',
},
},
{
action: CommandActionType.CreatePlacement,
params: {
id: '${placementId}',
partId: '${meepleId}',
regionId: 'board',
position: { x: 0, y: 0 },
},
},
],
};
/**
* 移动棋子命令
*/
export const moveMeepleCommand: Command = {
id: 'move-meeple',
name: 'Move Meeple',
description: 'Move a meeple to a new position',
steps: [
{
action: CommandActionType.UpdatePlacementPosition,
params: {
placementId: '${placementId}',
position: { x: '${x}', y: '${y}' },
},
},
],
};
/**
* 抽牌命令
*/
export const drawCardCommand: Command = {
id: 'draw-card',
name: 'Draw Card',
description: 'Draw a card from the deck',
steps: [
{
action: CommandActionType.CreateRegion,
params: {
id: 'hand',
type: RegionType.Unkeyed,
name: 'Hand',
capacity: 5,
},
},
{
action: CommandActionType.CreateCard,
params: {
id: '${cardId}',
suit: '${suit}',
value: '${value}',
},
},
{
action: CommandActionType.CreatePlacement,
params: {
id: '${placementId}',
partId: '${cardId}',
regionId: 'hand',
faceUp: false,
},
},
{
action: CommandActionType.AddPlacementToRegion,
params: {
regionId: 'hand',
placementId: '${placementId}',
},
},
],
};
/**
* 出牌命令
*/
export const playCardCommand: Command = {
id: 'play-card',
name: 'Play Card',
description: 'Play a card from hand',
steps: [
{
action: CommandActionType.SetPlacementFace,
params: {
placementId: '${placementId}',
faceUp: true,
},
},
{
action: CommandActionType.MovePlacement,
params: {
placementId: '${placementId}',
targetRegionId: 'discard',
},
},
],
};
/**
* 放置板块命令
*/
export const placeTileCommand: Command = {
id: 'place-tile',
name: 'Place Tile',
description: 'Place a tile on the board',
steps: [
{
action: CommandActionType.CreateTile,
params: {
id: '${tileId}',
pattern: '${pattern}',
rotation: '${rotation}',
},
},
{
action: CommandActionType.CreatePlacement,
params: {
id: '${placementId}',
partId: '${tileId}',
regionId: 'board',
position: { x: '${x}', y: '${y}' },
rotation: '${rotation}',
},
},
{
action: CommandActionType.SetSlot,
params: {
regionId: 'board',
key: '${slotKey}',
placementId: '${placementId}',
},
},
],
};
/**
* 翻转板块命令
*/
export const flipTileCommand: Command = {
id: 'flip-tile',
name: 'Flip Tile',
description: 'Flip a tile face down or face up',
steps: [
{
action: CommandActionType.FlipPlacement,
params: {
placementId: '${placementId}',
},
},
],
};
/**
* 交换位置命令
*/
export const swapPlacementsCommand: Command = {
id: 'swap-placements',
name: 'Swap Placements',
description: 'Swap two placements in the same region',
steps: [
{
action: CommandActionType.SwapPlacements,
params: {
placementId1: '${placementId1}',
placementId2: '${placementId2}',
},
},
],
};
/**
* 设置游戏阶段
*/
export const setPhaseCommand: Command = {
id: 'set-phase',
name: 'Set Phase',
description: 'Set the current game phase',
steps: [
{
action: CommandActionType.SetPhase,
params: {
phase: '${phase}',
},
},
],
};
/**
* 清空区域命令
*/
export const clearRegionCommand: Command = {
id: 'clear-region',
name: 'Clear Region',
description: 'Clear all placements from a region',
steps: [
{
action: CommandActionType.ClearRegion,
params: {
regionId: '${regionId}',
},
},
],
};
/**
* 所有内置命令
*/
export const defaultCommands: Command[] = [
setupGameCommand,
placeMeepleCommand,
moveMeepleCommand,
drawCardCommand,
playCardCommand,
placeTileCommand,
flipTileCommand,
swapPlacementsCommand,
setPhaseCommand,
clearRegionCommand,
];
/**
* 根据 ID 获取内置命令
*/
export function getDefaultCommand(commandId: string): Command | undefined {
return defaultCommands.find((cmd) => cmd.id === commandId);
}
-210
View File
@@ -1,210 +0,0 @@
import type { Command } from '../../commands/Command';
import { CommandActionType } from '../../commands/Command';
import { RegionType } from '../../core/Region';
import type { Player } from './TicTacToeState';
import { getAllCellIds, DEFAULT_BOARD_CONFIG } from './TicTacToeState';
/**
* 井字棋游戏命令集合
*/
/**
* 开始游戏命令
* 初始化 3x3 棋盘和游戏状态
*/
export const startGameCommand: Command = {
id: 'tictactoe-start-game',
name: 'startGame',
description: 'Start a new Tic Tac Toe game',
steps: [
// 创建棋盘区域
{
action: CommandActionType.CreateRegion,
params: {
id: 'board',
type: RegionType.Keyed,
name: 'Tic Tac Toe Board',
},
},
// 创建所有单元格槽位
...getAllCellIds(DEFAULT_BOARD_CONFIG.size).map((cellId) => ({
action: CommandActionType.SetSlot as CommandActionType,
params: {
regionId: 'board',
key: cellId,
placementId: null,
},
})),
// 初始化游戏元数据
{
action: CommandActionType.SetPhase,
params: {
phase: 'playing',
},
},
],
};
/**
* 标记单元格命令
* 玩家在指定单元格放置 X 或 O
*/
export const markCellCommand: (cell: string, player: Player) => Command = (cell, player) => ({
id: `tictactoe-mark-${cell}-${player}`,
name: 'markCell',
description: `Mark cell ${cell} with ${player}`,
steps: [
// 创建玩家标记(Part
{
action: CommandActionType.CreateMeeple,
params: {
id: `marker-${cell}-${player}`,
color: player === 'X' ? 'blue' : 'red',
name: `${player}'s marker`,
metadata: {
player,
cell,
},
},
},
// 创建放置
{
action: CommandActionType.CreatePlacement,
params: {
id: cell,
partId: `marker-${cell}-${player}`,
regionId: 'board',
metadata: {
player,
cell,
},
},
},
// 设置槽位
{
action: CommandActionType.SetSlot,
params: {
regionId: 'board',
key: cell,
placementId: cell,
},
},
],
});
/**
* 重置游戏命令
* 清空棋盘,准备新游戏
*/
export const resetGameCommand: Command = {
id: 'tictactoe-reset-game',
name: 'resetGame',
description: 'Reset the Tic Tac Toe board for a new game',
steps: [
// 清空棋盘
{
action: CommandActionType.ClearRegion,
params: {
regionId: 'board',
},
},
// 重置所有槽位
...getAllCellIds(DEFAULT_BOARD_CONFIG.size).map((cellId) => ({
action: CommandActionType.SetSlot as CommandActionType,
params: {
regionId: 'board',
key: cellId,
placementId: null,
},
})),
// 重置游戏阶段
{
action: CommandActionType.SetPhase,
params: {
phase: 'playing',
},
},
],
};
/**
* 设置玩家命令
* 设置玩家 X 和 O 的信息
*/
export const setPlayersCommand: (playerX: string, playerO: string) => Command = (playerX, playerO) => ({
id: 'tictactoe-set-players',
name: 'setPlayers',
description: 'Set player names for X and O',
steps: [
{
action: CommandActionType.CreateMeeple,
params: {
id: 'player-x',
color: 'blue',
name: playerX,
metadata: {
role: 'player',
symbol: 'X',
},
},
},
{
action: CommandActionType.CreateMeeple,
params: {
id: 'player-o',
color: 'red',
name: playerO,
metadata: {
role: 'player',
symbol: 'O',
},
},
},
],
});
/**
* 获取单元格状态命令
* 查询指定单元格的状态
*/
export const getCellCommand: (cell: string) => Command = (cell) => ({
id: `tictactoe-get-${cell}`,
name: 'getCell',
description: `Get the state of cell ${cell}`,
steps: [
{
action: CommandActionType.CreatePlacement,
params: {
id: `query-${cell}`,
partId: 'query',
regionId: 'board',
metadata: {
query: true,
cell,
},
},
},
],
});
/**
* 所有井字棋命令
*/
export const ticTacToeCommands: Command[] = [
startGameCommand,
resetGameCommand,
];
/**
* 创建标记单元格命令的辅助函数
*/
export function createMarkCellCommand(cell: string, player: Player): Command {
return markCellCommand(cell, player);
}
/**
* 创建设置玩家命令的辅助函数
*/
export function createSetPlayersCommand(playerX: string, playerO: string): Command {
return setPlayersCommand(playerX, playerO);
}
-354
View File
@@ -1,354 +0,0 @@
import type { Rule, RuleResult, RuleContext } from '../../rules/Rule';
import { createValidationRule, createEffectRule, createTriggerRule } from '../../rules/Rule';
import type { Player, TicTacToeMetadata, MoveRecord } from './TicTacToeState';
import { getWinningCombinations, parseCellId } from './TicTacToeState';
/**
* 井字棋游戏规则集合
*/
/**
* 获取当前玩家
*/
function getCurrentPlayer(gameState: any): Player {
const metadata = gameState.data.value.metadata as Record<string, unknown> | undefined;
const ticTacToeMetadata = metadata?.ticTacToe as TicTacToeMetadata | undefined;
return ticTacToeMetadata?.currentPlayer || 'X';
}
/**
* 检查游戏是否结束
*/
function isGameEnded(gameState: any): boolean {
const metadata = gameState.data.value.metadata as Record<string, unknown> | undefined;
const ticTacToeMetadata = metadata?.ticTacToe as TicTacToeMetadata | undefined;
return ticTacToeMetadata?.gameEnded || false;
}
/**
* 更新游戏 metadata
*/
function updateGameMetadata(gameState: any, updates: Partial<TicTacToeMetadata>): void {
const metadata = gameState.data.value.metadata as Record<string, unknown> | undefined;
const currentTicTacToe = (metadata?.ticTacToe as TicTacToeMetadata) || {
currentPlayer: 'X',
gameEnded: false,
winner: null,
moveHistory: [],
totalMoves: 0,
};
gameState.data.value = {
...gameState.data.value,
metadata: {
...metadata,
ticTacToe: { ...currentTicTacToe, ...updates },
},
};
}
/**
* 获取单元格的玩家标记
*/
function getCellPlayer(
context: RuleContext,
cellId: string
): Player | null {
const placement = context.gameState.placements.value.get(cellId);
if (!placement?.metadata?.player) {
return null;
}
return placement.metadata.player as Player;
}
/**
* 检查是否有玩家获胜
*/
function checkWin(
context: RuleContext,
size: number = 3
): { winner: Player; combination: string[] } | null {
const combinations = getWinningCombinations(size);
for (const combination of combinations) {
const players = combination.map((cellId) => getCellPlayer(context, cellId));
const firstPlayer = players[0];
if (firstPlayer && players.every((p) => p === firstPlayer)) {
return { winner: firstPlayer, combination };
}
}
return null;
}
/**
* 检查是否平局(所有单元格都被填充且无获胜者)
*/
function isDraw(context: RuleContext, size: number = 3): boolean {
const totalCells = size * size;
const filledCells = Array.from(context.gameState.placements.value.values()).filter(
(p) => p.metadata?.player
).length;
return filledCells === totalCells;
}
/**
* 规则 1:验证轮到当前玩家
* 在玩家尝试下子时检查是否是他们的回合
*/
export const validateTurnRule = createValidationRule({
id: 'tictactoe-validate-turn',
name: 'Validate Turn',
description: 'Check if it is the current player turn',
priority: 1,
gameType: 'tictactoe',
applicableCommands: ['markCell'],
validate: async (context: RuleContext): Promise<RuleResult> => {
const cellId = context.command.steps[0]?.params?.cell as string;
const expectedPlayer = context.command.steps[0]?.params?.player as Player;
const currentPlayer = getCurrentPlayer(context.gameState);
if (!cellId) {
return {
success: false,
error: 'Cell ID is required',
};
}
if (expectedPlayer && expectedPlayer !== currentPlayer) {
return {
success: false,
error: `It is ${currentPlayer}'s turn, not ${expectedPlayer}'s`,
};
}
return { success: true };
},
});
/**
* 规则 2:验证单元格为空
* 检查目标单元格是否已经被占用
*/
export const validateCellEmptyRule = createValidationRule({
id: 'tictactoe-validate-cell-empty',
name: 'Validate Cell Empty',
description: 'Check if the target cell is empty',
priority: 2,
gameType: 'tictactoe',
applicableCommands: ['markCell'],
validate: async (context: RuleContext): Promise<RuleResult> => {
const cellId = context.command.steps[0]?.params?.cell as string;
if (!cellId) {
return {
success: false,
error: 'Cell ID is required',
};
}
const cellPlayer = getCellPlayer(context, cellId);
if (cellPlayer !== null) {
return {
success: false,
error: `Cell ${cellId} is already occupied by ${cellPlayer}`,
};
}
return { success: true };
},
});
/**
* 规则 3:验证游戏未结束
* 游戏结束后不允许继续下子
*/
export const validateGameNotEndedRule = createValidationRule({
id: 'tictactoe-validate-game-not-ended',
name: 'Validate Game Not Ended',
description: 'Check if the game has already ended',
priority: 0,
gameType: 'tictactoe',
applicableCommands: ['markCell'],
validate: async (context: RuleContext): Promise<RuleResult> => {
const gameEnded = isGameEnded(context.gameState);
if (gameEnded) {
return {
success: false,
error: 'Game has already ended',
blockCommand: true,
};
}
return { success: true };
},
});
/**
* 效果规则:切换玩家
* 在玩家下子后自动切换到下一个玩家
*/
export const switchTurnRule = createEffectRule({
id: 'tictactoe-switch-turn',
name: 'Switch Turn',
description: 'Switch to the next player after a move',
priority: 10,
gameType: 'tictactoe',
applicableCommands: ['markCell'],
apply: async (context: RuleContext): Promise<RuleResult> => {
const currentPlayer = getCurrentPlayer(context.gameState);
const nextPlayer: Player = currentPlayer === 'X' ? 'O' : 'X';
// 直接更新 metadata
updateGameMetadata(context.gameState, { currentPlayer: nextPlayer });
return {
success: true,
};
},
});
/**
* 效果规则:记录移动历史
* 记录玩家的每一步移动
*/
export const recordMoveHistoryRule = createEffectRule({
id: 'tictactoe-record-history',
name: 'Record Move History',
description: 'Record the move in game history',
priority: 9,
gameType: 'tictactoe',
applicableCommands: ['markCell'],
apply: async (context: RuleContext): Promise<RuleResult> => {
const cellId = context.command.steps[0]?.params?.cell as string;
const player = context.command.steps[0]?.params?.player as Player;
const metadata = context.gameState.data.value.metadata || {};
const ticTacToeMetadata = (metadata?.ticTacToe as TicTacToeMetadata) || {
currentPlayer: 'X',
gameEnded: false,
winner: null,
moveHistory: [],
totalMoves: 0,
};
const moveRecord: MoveRecord = {
player,
cellId,
timestamp: Date.now(),
};
const moveHistory = ticTacToeMetadata.moveHistory || [];
moveHistory.push(moveRecord);
// 直接更新 metadata
updateGameMetadata(context.gameState, {
moveHistory,
totalMoves: (ticTacToeMetadata.totalMoves || 0) + 1,
});
return {
success: true,
};
},
});
/**
* 触发规则:检查获胜条件
* 当有玩家连成一线时触发
*/
export const checkWinConditionRule = createTriggerRule({
id: 'tictactoe-check-win',
name: 'Check Win Condition',
description: 'Check if a player has won the game',
priority: 100,
gameType: 'tictactoe',
condition: async (context: RuleContext): Promise<boolean> => {
const winResult = checkWin(context);
return winResult !== null;
},
action: async (context: RuleContext): Promise<RuleResult> => {
const winResult = checkWin(context);
if (!winResult) {
return { success: false, error: 'No winner detected' };
}
const { winner, combination } = winResult;
// 直接更新 metadata
updateGameMetadata(context.gameState, {
gameEnded: true,
winner,
winningCombination: combination,
});
return {
success: true,
};
},
});
/**
* 触发规则:检查平局条件
* 当所有单元格都被填充且无获胜者时触发
*/
export const checkDrawConditionRule = createTriggerRule({
id: 'tictactoe-check-draw',
name: 'Check Draw Condition',
description: 'Check if the game is a draw',
priority: 101,
gameType: 'tictactoe',
condition: async (context: RuleContext): Promise<boolean> => {
const winResult = checkWin(context);
if (winResult !== null) {
return false; // 有获胜者,不是平局
}
return isDraw(context);
},
action: async (context: RuleContext): Promise<RuleResult> => {
// 直接更新 metadata
updateGameMetadata(context.gameState, {
gameEnded: true,
winner: null, // null 表示平局
});
return {
success: true,
};
},
});
/**
* 所有井字棋游戏规则
*/
export const ticTacToeRules: Rule[] = [
validateTurnRule,
validateCellEmptyRule,
validateGameNotEndedRule,
switchTurnRule,
recordMoveHistoryRule,
checkWinConditionRule,
checkDrawConditionRule,
];
/**
* 获取井字棋验证规则
*/
export function getTicTacToeValidationRules(): Rule[] {
return [validateTurnRule, validateCellEmptyRule, validateGameNotEndedRule];
}
/**
* 获取井字棋效果规则
*/
export function getTicTacToeEffectRules(): Rule[] {
return [switchTurnRule, recordMoveHistoryRule];
}
/**
* 获取井字棋触发规则
*/
export function getTicTacToeTriggerRules(): Rule[] {
return [checkWinConditionRule, checkDrawConditionRule];
}
-161
View File
@@ -1,161 +0,0 @@
/**
* 井字棋游戏状态扩展
*/
/**
* 玩家类型
*/
export type Player = 'X' | 'O';
/**
* 单元格状态
*/
export interface CellState {
/** 单元格 ID(如 A1, B2, C3 */
id: string;
/** 行索引 (0-2) */
row: number;
/** 列索引 (0-2) */
col: number;
/** 当前玩家标记,null 表示空 */
player: Player | null;
}
/**
* 井字棋游戏元数据
*/
export interface TicTacToeMetadata {
/** 当前玩家 */
currentPlayer: Player;
/** 游戏是否结束 */
gameEnded: boolean;
/** 获胜者,null 表示平局或未结束 */
winner: Player | null;
/** 获胜的组合(如果有) */
winningCombination?: string[];
/** 游戏历史 */
moveHistory: MoveRecord[];
/** 总回合数 */
totalMoves: number;
}
/**
* 移动记录
*/
export interface MoveRecord {
/** 移动的玩家 */
player: Player;
/** 移动的单元格 ID */
cellId: string;
/** 移动时间戳 */
timestamp: number;
}
/**
* 获胜组合类型
*/
export type WinningLine =
| { type: 'row'; index: number }
| { type: 'column'; index: number }
| { type: 'diagonal'; direction: 'main' | 'anti' };
/**
* 井字棋棋盘配置
*/
export interface TicTacToeBoardConfig {
/** 棋盘大小(默认 3x3 */
size: number;
/** 单元格 ID 前缀 */
cellIdPrefix: string;
}
/**
* 默认的 3x3 棋盘配置
*/
export const DEFAULT_BOARD_CONFIG: TicTacToeBoardConfig = {
size: 3,
cellIdPrefix: 'cell',
};
/**
* 获取单元格 ID
*/
export function getCellId(row: number, col: number, prefix: string = 'cell'): string {
const rowLabel = String.fromCharCode('A'.charCodeAt(0) + row);
return `${prefix}-${rowLabel}${col + 1}`;
}
/**
* 解析单元格 ID
*/
export function parseCellId(cellId: string): { row: number; col: number } | null {
const match = cellId.match(/^cell-([A-Z])(\d+)$/);
if (!match) return null;
return {
row: match[1].charCodeAt(0) - 'A'.charCodeAt(0),
col: parseInt(match[2], 10) - 1,
};
}
/**
* 检查是否是有效的单元格 ID
*/
export function isValidCellId(cellId: string, size: number = 3): boolean {
const parsed = parseCellId(cellId);
if (!parsed) return false;
return parsed.row >= 0 && parsed.row < size && parsed.col >= 0 && parsed.col < size;
}
/**
* 生成所有单元格 ID
*/
export function getAllCellIds(size: number = 3, prefix: string = 'cell'): string[] {
const cells: string[] = [];
for (let row = 0; row < size; row++) {
for (let col = 0; col < size; col++) {
cells.push(getCellId(row, col, prefix));
}
}
return cells;
}
/**
* 获取所有可能的获胜组合
*/
export function getWinningCombinations(size: number = 3): string[][] {
const combinations: string[][] = [];
// 行
for (let row = 0; row < size; row++) {
const rowCells: string[] = [];
for (let col = 0; col < size; col++) {
rowCells.push(getCellId(row, col));
}
combinations.push(rowCells);
}
// 列
for (let col = 0; col < size; col++) {
const colCells: string[] = [];
for (let row = 0; row < size; row++) {
colCells.push(getCellId(row, col));
}
combinations.push(colCells);
}
// 主对角线
const mainDiagonal: string[] = [];
for (let i = 0; i < size; i++) {
mainDiagonal.push(getCellId(i, i));
}
combinations.push(mainDiagonal);
// 反对角线
const antiDiagonal: string[] = [];
for (let i = 0; i < size; i++) {
antiDiagonal.push(getCellId(i, size - 1 - i));
}
combinations.push(antiDiagonal);
return combinations;
}
-82
View File
@@ -1,82 +0,0 @@
/**
* 井字棋游戏模块
* Tic Tac Toe game implementation with rule enforcement
*/
import type { Command } from '../../commands/Command';
import type { Rule } from '../../rules/Rule';
import {
startGameCommand,
markCellCommand,
resetGameCommand,
setPlayersCommand,
getCellCommand,
ticTacToeCommands,
createMarkCellCommand,
createSetPlayersCommand,
} from './TicTacToeCommands';
import {
ticTacToeRules,
getTicTacToeValidationRules,
getTicTacToeEffectRules,
getTicTacToeTriggerRules,
} from './TicTacToeRules';
// State types
export type {
Player,
CellState,
TicTacToeMetadata,
MoveRecord,
WinningLine,
TicTacToeBoardConfig,
} from './TicTacToeState';
export {
DEFAULT_BOARD_CONFIG,
getCellId,
parseCellId,
isValidCellId,
getAllCellIds,
getWinningCombinations,
} from './TicTacToeState';
// Rules
export {
validateTurnRule,
validateCellEmptyRule,
validateGameNotEndedRule,
switchTurnRule,
recordMoveHistoryRule,
checkWinConditionRule,
checkDrawConditionRule,
ticTacToeRules,
getTicTacToeValidationRules,
getTicTacToeEffectRules,
getTicTacToeTriggerRules,
} from './TicTacToeRules';
// Commands
export {
startGameCommand,
markCellCommand,
resetGameCommand,
setPlayersCommand,
getCellCommand,
ticTacToeCommands,
createMarkCellCommand,
createSetPlayersCommand,
} from './TicTacToeCommands';
/**
* 创建井字棋游戏初始化命令
*/
export function createTicTacToeGame(): {
commands: Command[];
rules: Rule[];
} {
return {
commands: [startGameCommand, resetGameCommand],
rules: ticTacToeRules,
};
}
-186
View File
@@ -1,186 +0,0 @@
import type { GameState } from '../core/GameState';
import type { Command } from '../commands/Command';
import type { CommandExecutionResult } from '../commands/Command';
/**
* 规则执行上下文
*/
export interface RuleContext {
/** 游戏状态 */
gameState: GameState;
/** 当前命令 */
command: Command;
/** 命令执行结果(执行后规则可用) */
executionResult?: CommandExecutionResult;
/** 规则执行时的元数据 */
metadata: Record<string, unknown>;
}
/**
* 规则执行结果
*/
export interface RuleResult {
/** 规则是否通过 */
success: boolean;
/** 错误信息(如果失败) */
error?: string;
/** 状态修改(规则可以对状态进行修改) */
stateUpdates?: Record<string, unknown>;
/** 是否阻止命令执行 */
blockCommand?: boolean;
/** 触发的额外命令 */
triggeredCommands?: Command[];
}
/**
* 验证规则
* 在命令执行前运行,用于验证命令是否合法
*/
export interface ValidationRule {
/** 规则唯一标识 */
id: string;
/** 规则名称 */
name: string;
/** 规则描述 */
description?: string;
/** 规则优先级(数字越小越先执行) */
priority: number;
/** 适用的游戏类型 */
gameType?: string;
/** 适用的命令名称列表 */
applicableCommands?: string[];
/** 验证函数 */
validate: (context: RuleContext) => Promise<RuleResult>;
}
/**
* 效果规则
* 在命令执行后运行,用于更新状态或触发额外效果
*/
export interface EffectRule {
/** 规则唯一标识 */
id: string;
/** 规则名称 */
name: string;
/** 规则描述 */
description?: string;
/** 规则优先级(数字越小越先执行) */
priority: number;
/** 适用的游戏类型 */
gameType?: string;
/** 适用的命令名称列表 */
applicableCommands?: string[];
/** 效果函数 */
apply: (context: RuleContext) => Promise<RuleResult>;
}
/**
* 触发规则
* 监听特定状态变化并触发相应动作
*/
export interface TriggerRule {
/** 规则唯一标识 */
id: string;
/** 规则名称 */
name: string;
/** 规则描述 */
description?: string;
/** 规则优先级 */
priority: number;
/** 适用的游戏类型 */
gameType?: string;
/** 适用的命令名称列表 */
applicableCommands?: string[];
/** 触发条件 */
condition: (context: RuleContext) => Promise<boolean>;
/** 触发后的动作 */
action: (context: RuleContext) => Promise<RuleResult>;
}
/**
* 通用规则类型
*/
export type Rule = ValidationRule | EffectRule | TriggerRule;
/**
* 判断是否为验证规则
*/
export function isValidationRule(rule: Rule): rule is ValidationRule {
return 'validate' in rule;
}
/**
* 判断是否为效果规则
*/
export function isEffectRule(rule: Rule): rule is EffectRule {
return 'apply' in rule;
}
/**
* 判断是否为触发规则
*/
export function isTriggerRule(rule: Rule): rule is TriggerRule {
return 'condition' in rule && 'action' in rule;
}
/**
* 创建验证规则
*/
export function createValidationRule(rule: Omit<ValidationRule, 'id' | 'name'> & { id?: string; name?: string }): ValidationRule {
return {
id: rule.id || `validation-${Date.now()}`,
name: rule.name || 'Unnamed Validation Rule',
description: rule.description,
priority: rule.priority ?? 0,
gameType: rule.gameType,
applicableCommands: rule.applicableCommands,
validate: rule.validate,
};
}
/**
* 创建效果规则
*/
export function createEffectRule(rule: Omit<EffectRule, 'id' | 'name'> & { id?: string; name?: string }): EffectRule {
return {
id: rule.id || `effect-${Date.now()}`,
name: rule.name || 'Unnamed Effect Rule',
description: rule.description,
priority: rule.priority ?? 0,
gameType: rule.gameType,
applicableCommands: rule.applicableCommands,
apply: rule.apply,
};
}
/**
* 创建触发规则
*/
export function createTriggerRule(rule: Omit<TriggerRule, 'id' | 'name'> & { id?: string; name?: string }): TriggerRule {
return {
id: rule.id || `trigger-${Date.now()}`,
name: rule.name || 'Unnamed Trigger Rule',
description: rule.description,
priority: rule.priority ?? 0,
condition: rule.condition,
action: rule.action,
};
}
/**
* 规则执行日志
*/
export interface RuleLogEntry {
/** 时间戳 */
timestamp: number;
/** 规则 ID */
ruleId: string;
/** 规则名称 */
ruleName: string;
/** 规则类型 */
ruleType: 'validation' | 'effect' | 'trigger';
/** 执行结果 */
result: RuleResult;
/** 命令 ID */
commandId: string;
}
-378
View File
@@ -1,378 +0,0 @@
import type { GameState } from '../core/GameState';
import type { Command, CommandExecutionResult } from '../commands/Command';
import { CommandExecutor } from '../commands/CommandExecutor';
import type {
Rule,
RuleContext,
RuleResult,
ValidationRule,
EffectRule,
TriggerRule,
RuleLogEntry,
} from './Rule';
import { isValidationRule, isEffectRule, isTriggerRule } from './Rule';
/**
* 规则引擎配置
*/
export interface RuleEngineOptions {
/** 游戏类型 */
gameType?: string;
/** 是否启用规则日志 */
enableLogging?: boolean;
/** 是否自动执行触发规则 */
autoExecuteTriggers?: boolean;
}
/**
* 规则引擎执行结果
*/
export interface RuleEngineExecutionResult extends CommandExecutionResult {
/** 执行的验证规则 */
validationRules: RuleLogEntry[];
/** 执行的效果规则 */
effectRules: RuleLogEntry[];
/** 触发的规则 */
triggerRules: RuleLogEntry[];
/** 触发的额外命令 */
triggeredCommands: Command[];
}
/**
* 规则引擎
* 负责在命令执行前后运行规则,并处理触发规则
*/
export class RuleEngine {
private gameState: GameState;
private executor: CommandExecutor;
private rules: Rule[] = [];
private options: RuleEngineOptions;
private logs: RuleLogEntry[] = [];
private isExecuting: boolean = false;
private triggerQueue: Command[] = [];
constructor(gameState: GameState, options: RuleEngineOptions = {}) {
this.gameState = gameState;
this.executor = new CommandExecutor(gameState);
this.options = {
enableLogging: true,
autoExecuteTriggers: true,
...options,
};
}
/**
* 注册规则
*/
registerRule(rule: Rule): void {
// 如果指定了游戏类型,只有匹配时才注册
if (this.options.gameType && rule.gameType && rule.gameType !== this.options.gameType) {
return;
}
this.rules.push(rule);
// 按优先级排序
this.rules.sort((a, b) => a.priority - b.priority);
}
/**
* 注册多个规则
*/
registerRules(rules: Rule[]): void {
for (const rule of rules) {
this.registerRule(rule);
}
}
/**
* 移除规则
*/
unregisterRule(ruleId: string): void {
this.rules = this.rules.filter((r) => r.id !== ruleId);
}
/**
* 清除所有规则
*/
clearRules(): void {
this.rules = [];
}
/**
* 获取所有规则
*/
getRules(): Rule[] {
return [...this.rules];
}
/**
* 执行命令(带规则验证)
*/
async executeCommand(command: Command): Promise<RuleEngineExecutionResult> {
if (this.isExecuting) {
throw new Error('Rule engine is already executing a command');
}
this.isExecuting = true;
const validationLogs: RuleLogEntry[] = [];
const effectLogs: RuleLogEntry[] = [];
const triggerLogs: RuleLogEntry[] = [];
const triggeredCommands: Command[] = [];
try {
// 创建规则上下文
const context: RuleContext = {
gameState: this.gameState,
command,
metadata: {},
};
// 1. 执行验证规则
const validationRules = this.rules.filter(isValidationRule);
for (const rule of validationRules) {
if (!this.isRuleApplicable(rule, command)) {
continue;
}
const result = await rule.validate(context);
const logEntry = this.createLogEntry(rule, 'validation', result, command.id);
validationLogs.push(logEntry);
if (!result.success) {
return this.createFailedResult(validationLogs, effectLogs, triggerLogs, triggeredCommands, result.error);
}
if (result.blockCommand) {
return this.createFailedResult(
validationLogs,
effectLogs,
triggerLogs,
triggeredCommands,
`Command blocked by rule: ${rule.name}`
);
}
// 应用状态更新
if (result.stateUpdates) {
Object.assign(context.metadata, result.stateUpdates);
}
// 收集触发的命令
if (result.triggeredCommands) {
triggeredCommands.push(...result.triggeredCommands);
}
}
// 2. 执行命令
const executionResult = this.executor.execute(command);
context.executionResult = executionResult;
if (!executionResult.success) {
return this.createFailedResult(
validationLogs,
effectLogs,
triggerLogs,
triggeredCommands,
executionResult.error
);
}
// 3. 执行效果规则
const effectRules = this.rules.filter(isEffectRule);
for (const rule of effectRules) {
if (!this.isRuleApplicable(rule, command)) {
continue;
}
const result = await rule.apply(context);
const logEntry = this.createLogEntry(rule, 'effect', result, command.id);
effectLogs.push(logEntry);
if (!result.success) {
// 效果规则失败不影响命令执行,只记录日志
continue;
}
// 应用状态更新
if (result.stateUpdates) {
Object.assign(context.metadata, result.stateUpdates);
}
// 收集触发的命令
if (result.triggeredCommands) {
triggeredCommands.push(...result.triggeredCommands);
}
}
// 4. 执行触发规则
if (this.options.autoExecuteTriggers) {
const triggerRules = this.rules.filter(isTriggerRule);
for (const rule of triggerRules) {
const shouldTrigger = await rule.condition(context);
if (shouldTrigger) {
const result = await rule.action(context);
const logEntry = this.createLogEntry(rule, 'trigger', result, command.id);
triggerLogs.push(logEntry);
if (result.triggeredCommands) {
triggeredCommands.push(...result.triggeredCommands);
}
}
}
}
// 5. 执行触发的命令(在循环外执行,避免递归)
} finally {
this.isExecuting = false;
}
// 在主要执行完成后执行触发的命令
for (const triggeredCommand of triggeredCommands) {
try {
const triggerResult = await this.executeCommand(triggeredCommand);
if (!triggerResult.success) {
// 触发命令失败,记录但不影响主命令
this.logs.push({
timestamp: Date.now(),
ruleId: 'triggered-command',
ruleName: 'Triggered Command',
ruleType: 'trigger',
result: { success: false, error: `Triggered command ${triggeredCommand.id} failed: ${triggerResult.error}` },
commandId: triggeredCommand.id,
});
}
} catch (error) {
// 忽略触发命令的异常
}
}
return {
success: true,
executedSteps: executionResult.executedSteps,
totalSteps: executionResult.totalSteps,
validationRules: validationLogs,
effectRules: effectLogs,
triggerRules: triggerLogs,
triggeredCommands,
};
}
/**
* 检查规则是否适用于当前命令
*/
private isRuleApplicable(
rule: ValidationRule | EffectRule,
command: Command
): boolean {
// 检查游戏类型
if (rule.gameType && rule.gameType !== this.options.gameType) {
return false;
}
// 检查命令名称
if (rule.applicableCommands && !rule.applicableCommands.includes(command.name)) {
return false;
}
return true;
}
/**
* 创建日志条目
*/
private createLogEntry(
rule: Rule,
ruleType: 'validation' | 'effect' | 'trigger',
result: RuleResult,
commandId: string
): RuleLogEntry {
const entry: RuleLogEntry = {
timestamp: Date.now(),
ruleId: rule.id,
ruleName: rule.name,
ruleType,
result,
commandId,
};
if (this.options.enableLogging) {
this.logs.push(entry);
}
return entry;
}
/**
* 创建失败结果
*/
private createFailedResult(
validationLogs: RuleLogEntry[],
effectLogs: RuleLogEntry[],
triggerLogs: RuleLogEntry[],
triggeredCommands: Command[],
error?: string
): RuleEngineExecutionResult {
return {
success: false,
error,
executedSteps: 0,
totalSteps: 0,
validationRules: validationLogs,
effectRules: effectLogs,
triggerRules: triggerLogs,
triggeredCommands,
};
}
/**
* 获取规则日志
*/
getLogs(): RuleLogEntry[] {
return [...this.logs];
}
/**
* 清除日志
*/
clearLogs(): void {
this.logs = [];
}
/**
* 获取游戏状态
*/
getGameState(): GameState {
return this.gameState;
}
/**
* 手动触发规则
*/
async triggerRules(): Promise<RuleLogEntry[]> {
const context: RuleContext = {
gameState: this.gameState,
command: { id: 'trigger-manual', name: 'manual-trigger', steps: [] },
metadata: {},
};
const logs: RuleLogEntry[] = [];
const triggerRules = this.rules.filter(isTriggerRule);
for (const rule of triggerRules) {
const shouldTrigger = await rule.condition(context);
if (shouldTrigger) {
const result = await rule.action(context);
const logEntry = this.createLogEntry(rule, 'trigger', result, 'manual');
logs.push(logEntry);
}
}
return logs;
}
}
/**
* 创建规则引擎
*/
export function createRuleEngine(gameState: GameState, options?: RuleEngineOptions): RuleEngine {
return new RuleEngine(gameState, options);
}
-199
View File
@@ -1,199 +0,0 @@
import type { Rule, ValidationRule, EffectRule, TriggerRule } from './Rule';
import { isValidationRule, isEffectRule, isTriggerRule } from './Rule';
/**
* 规则组
*/
export interface RuleGroup {
/** 组名称 */
name: string;
/** 组描述 */
description?: string;
/** 规则列表 */
rules: Rule[];
}
/**
* 规则注册表
* 按游戏类型注册和管理规则
*/
export class RuleRegistry {
private rulesByGameType: Map<string, Rule[]>;
private globalRules: Rule[];
private ruleGroups: Map<string, RuleGroup>;
private enabledRules: Set<string>;
constructor() {
this.rulesByGameType = new Map();
this.globalRules = [];
this.ruleGroups = new Map();
this.enabledRules = new Set();
}
/**
* 注册规则到特定游戏类型
*/
register(rule: Rule, gameType?: string): void {
const targetRules = gameType
? this.getRulesForGameType(gameType)
: this.globalRules;
targetRules.push(rule);
this.enabledRules.add(rule.id);
// 按优先级排序
targetRules.sort((a, b) => a.priority - b.priority);
}
/**
* 注册多个规则
*/
registerAll(rules: Rule[], gameType?: string): void {
for (const rule of rules) {
this.register(rule, gameType);
}
}
/**
* 注册规则组
*/
registerGroup(group: RuleGroup, gameType?: string): void {
this.ruleGroups.set(group.name, group);
this.registerAll(group.rules, gameType);
}
/**
* 获取特定游戏类型的规则
*/
getRulesForGameType(gameType: string): Rule[] {
const gameRules = this.rulesByGameType.get(gameType) || [];
return [...gameRules, ...this.globalRules];
}
/**
* 获取所有规则
*/
getAllRules(): Rule[] {
const allRules = [...this.globalRules];
for (const rules of this.rulesByGameType.values()) {
allRules.push(...rules);
}
return allRules;
}
/**
* 获取验证规则
*/
getValidationRules(gameType?: string): ValidationRule[] {
const rules = gameType
? this.getRulesForGameType(gameType)
: this.getAllRules();
return rules.filter(isValidationRule);
}
/**
* 获取效果规则
*/
getEffectRules(gameType?: string): EffectRule[] {
const rules = gameType
? this.getRulesForGameType(gameType)
: this.getAllRules();
return rules.filter(isEffectRule);
}
/**
* 获取触发规则
*/
getTriggerRules(gameType?: string): TriggerRule[] {
const rules = gameType
? this.getRulesForGameType(gameType)
: this.getAllRules();
return rules.filter(isTriggerRule);
}
/**
* 获取规则组
*/
getGroup(groupName: string): RuleGroup | undefined {
return this.ruleGroups.get(groupName);
}
/**
* 移除规则
*/
unregister(ruleId: string): void {
this.globalRules = this.globalRules.filter((r) => r.id !== ruleId);
for (const [gameType, rules] of this.rulesByGameType.entries()) {
this.rulesByGameType.set(
gameType,
rules.filter((r) => r.id !== ruleId)
);
}
this.enabledRules.delete(ruleId);
}
/**
* 启用规则
*/
enableRule(ruleId: string): void {
this.enabledRules.add(ruleId);
}
/**
* 禁用规则
*/
disableRule(ruleId: string): void {
this.enabledRules.delete(ruleId);
}
/**
* 检查规则是否启用
*/
isRuleEnabled(ruleId: string): boolean {
return this.enabledRules.has(ruleId);
}
/**
* 获取按游戏类型分类的规则
*/
getRulesByGameType(): Map<string, Rule[]> {
return new Map(this.rulesByGameType);
}
/**
* 清除特定游戏类型的规则
*/
clearGameTypeRules(gameType: string): void {
this.rulesByGameType.delete(gameType);
}
/**
* 清除所有规则
*/
clearAllRules(): void {
this.rulesByGameType.clear();
this.globalRules = [];
this.enabledRules.clear();
}
/**
* 获取规则数量
*/
getRuleCount(): number {
return this.getAllRules().length;
}
/**
* 获取启用的规则数量
*/
getEnabledRuleCount(): number {
return this.enabledRules.size;
}
}
/**
* 创建规则注册表
*/
export function createRuleRegistry(): RuleRegistry {
return new RuleRegistry();
}