refactor: various improvements

This commit is contained in:
hypercross 2026-04-01 21:44:20 +08:00
parent 033fb6c894
commit dbe567ea1d
6 changed files with 65 additions and 103 deletions

View File

@ -14,11 +14,11 @@
"scripts": { "scripts": {
"build": "tsup", "build": "tsup",
"test": "vitest", "test": "vitest",
"test:run": "vitest run" "test:run": "vitest run",
"typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
"@preact/signals-core": "^1.5.1", "@preact/signals-core": "^1.5.1",
"boardgame-core": "file:",
"inline-schema": "git+https://gitea.ayi-games.online/hypercross/inline-schema" "inline-schema": "git+https://gitea.ayi-games.online/hypercross/inline-schema"
}, },
"devDependencies": { "devDependencies": {

View File

@ -10,21 +10,25 @@ export type Context = {
export const GameContext = createModel((root: Context) => { export const GameContext = createModel((root: Context) => {
const parts = createEntityCollection<Part>(); const parts = createEntityCollection<Part>();
const regions = createEntityCollection<Region>(); const regions = createEntityCollection<Region>();
const contexts = signal([signal(root)]); const contexts = signal<Signal<Context>[]>([]);
contexts.value = [signal(root)];
function pushContext(context: Context) { function pushContext(context: Context) {
const ctxSignal = signal(context); const ctxSignal = signal(context);
contexts.value = [...contexts.value, ctxSignal]; contexts.value = [...contexts.value, ctxSignal];
return context; return context;
} }
function popContext() { function popContext() {
contexts.value = contexts.value.slice(0, -1); if (contexts.value.length > 1) {
contexts.value = contexts.value.slice(0, -1);
}
} }
function latestContext<T extends Context>(type: T['type']){ function latestContext<T extends Context>(type: T['type']): Signal<T> | undefined {
for(let i = contexts.value.length - 1; i >= 0; i--){ for(let i = contexts.value.length - 1; i >= 0; i--){
if(contexts.value[i].value.type === type){ if(contexts.value[i].value.type === type){
return contexts.value[i] as Signal<T>; return contexts.value[i] as Signal<T>;
} }
} }
return undefined;
} }
return { return {

View File

@ -28,36 +28,36 @@ export type RegionAxis = {
export function applyAlign(region: Region){ export function applyAlign(region: Region){
if (region.children.length === 0) return; if (region.children.length === 0) return;
// 对每个 axis 分别处理,但保持空间关系 // Process each axis independently while preserving spatial relationships
for (let axisIndex = 0; axisIndex < region.axes.length; axisIndex++) { for (let axisIndex = 0; axisIndex < region.axes.length; axisIndex++) {
const axis = region.axes[axisIndex]; const axis = region.axes[axisIndex];
if (!axis.align) continue; if (!axis.align) continue;
// 收集当前轴上的所有唯一位置值,保持原有顺序 // Collect all unique position values on this axis, preserving original order
const positionValues = new Set<number>(); const positionValues = new Set<number>();
for (const accessor of region.children) { for (const accessor of region.children) {
positionValues.add(accessor.value.position[axisIndex] ?? 0); positionValues.add(accessor.value.position[axisIndex] ?? 0);
} }
// 排序位置值 // Sort position values
const sortedPositions = Array.from(positionValues).sort((a, b) => a - b); const sortedPositions = Array.from(positionValues).sort((a, b) => a - b);
// 创建位置映射:原位置 -> 新位置 // Create position mapping: old position -> new position
const positionMap = new Map<number, number>(); const positionMap = new Map<number, number>();
if (axis.align === 'start' && axis.min !== undefined) { if (axis.align === 'start' && axis.min !== undefined) {
// 从 min 开始紧凑排列,保持相对顺序 // Compact from min, preserving relative order
sortedPositions.forEach((pos, index) => { sortedPositions.forEach((pos, index) => {
positionMap.set(pos, axis.min! + index); positionMap.set(pos, axis.min! + index);
}); });
} else if (axis.align === 'end' && axis.max !== undefined) { } else if (axis.align === 'end' && axis.max !== undefined) {
// 从 max 开始向前紧凑排列 // Compact towards max
const count = sortedPositions.length; const count = sortedPositions.length;
sortedPositions.forEach((pos, index) => { sortedPositions.forEach((pos, index) => {
positionMap.set(pos, axis.max! - (count - 1 - index)); positionMap.set(pos, axis.max! - (count - 1 - index));
}); });
} else if (axis.align === 'center') { } else if (axis.align === 'center') {
// 居中排列 // Center alignment
const count = sortedPositions.length; const count = sortedPositions.length;
const min = axis.min ?? 0; const min = axis.min ?? 0;
const max = axis.max ?? count - 1; const max = axis.max ?? count - 1;
@ -70,14 +70,14 @@ export function applyAlign(region: Region){
}); });
} }
// 应用位置映射到所有 part // Apply position mapping to all parts
for (const accessor of region.children) { for (const accessor of region.children) {
const currentPos = accessor.value.position[axisIndex] ?? 0; const currentPos = accessor.value.position[axisIndex] ?? 0;
accessor.value.position[axisIndex] = positionMap.get(currentPos) ?? currentPos; accessor.value.position[axisIndex] = positionMap.get(currentPos) ?? currentPos;
} }
} }
// 最后按所有轴排序 children // Sort children by all axes at the end
region.children.sort((a, b) => { region.children.sort((a, b) => {
for (let i = 0; i < region.axes.length; i++) { for (let i = 0; i < region.axes.length; i++) {
const diff = (a.value.position[i] ?? 0) - (b.value.position[i] ?? 0); const diff = (a.value.position[i] ?? 0) - (b.value.position[i] ?? 0);

View File

@ -1,4 +1,4 @@
import {Context} from "./context"; import {Context} from "./context";
import {Command} from "../utils/command"; import {Command} from "../utils/command";
import {effect} from "@preact/signals-core"; import {effect} from "@preact/signals-core";
@ -17,8 +17,8 @@ export type RuleContext<T> = Context & {
* @returns * @returns
*/ */
export function invokeRuleContext<T>( export function invokeRuleContext<T>(
pushContext: (context: Context) => void, pushContext: (context: Context) => void,
type: string, type: string,
rule: Generator<string, T, Command> rule: Generator<string, T, Command>
): RuleContext<T> { ): RuleContext<T> {
const ctx: RuleContext<T> = { const ctx: RuleContext<T> = {
@ -29,41 +29,39 @@ export function invokeRuleContext<T>(
resolution: undefined, resolution: undefined,
}; };
// 执行生成器直到完成或需要等待动作 let disposed = false;
const executeRule = () => { const executeRule = () => {
if (disposed || ctx.resolution !== undefined) return;
try { try {
const result = rule.next(); const result = rule.next();
if (result.done) { if (result.done) {
// 规则执行完成,设置结果
ctx.resolution = result.value; ctx.resolution = result.value;
return; return;
} }
// 如果生成器 yield 了一个动作类型,等待处理
// 这里可以扩展为实际的动作处理逻辑
const actionType = result.value; const actionType = result.value;
// 继续执行直到有动作需要处理或规则完成 if (actionType) {
if (!result.done) { // 暂停于 yield 点,等待外部处理动作
executeRule(); // 当外部更新 actions 后effect 会重新触发
} }
} catch (error) { } catch (error) {
// 规则执行出错,抛出错误
throw error; throw error;
} }
}; };
// 使用 effect 来跟踪响应式依赖
const dispose = effect(() => { const dispose = effect(() => {
if (ctx.resolution !== undefined) { if (ctx.resolution !== undefined) {
dispose(); dispose();
disposed = true;
return; return;
} }
executeRule(); executeRule();
}); });
// 将规则上下文推入栈中
pushContext(ctx); pushContext(ctx);
return ctx; return ctx;
@ -77,12 +75,6 @@ export function invokeRuleContext<T>(
export function createRule<T>( export function createRule<T>(
type: string, type: string,
fn: (ctx: RuleContext<T>) => Generator<string, T, Command> fn: (ctx: RuleContext<T>) => Generator<string, T, Command>
): Generator<string, T, Command> { ): (ctx: RuleContext<T>) => Generator<string, T, Command> {
return fn({ return fn;
type,
actions: [],
handledActions: 0,
invocations: [],
resolution: undefined,
});
} }

View File

@ -284,10 +284,12 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
let parsedSchema: ParsedSchema | undefined; let parsedSchema: ParsedSchema | undefined;
if (paramContent.includes(':')) { if (paramContent.includes(':')) {
const [name, typeStr] = paramContent.split(':').map(s => s.trim()); const colonIndex = paramContent.indexOf(':');
const name = paramContent.slice(0, colonIndex).trim();
const typeStr = paramContent.slice(colonIndex + 1).trim();
try { try {
parsedSchema = defineSchema(typeStr); parsedSchema = defineSchema(typeStr);
} catch { } catch (e) {
// 不是有效的 schema // 不是有效的 schema
} }
paramContent = name; paramContent = name;
@ -331,7 +333,10 @@ export function parseCommandSchema(schemaStr: string): CommandSchema {
i++; i++;
} else if (token.startsWith('<') && token.endsWith('>')) { } else if (token.startsWith('<') && token.endsWith('>')) {
const isVariadic = token.endsWith('...>'); const isVariadic = token.endsWith('...>');
let paramContent = token.replace(/^[<]+|[>.>]+$/g, ''); let paramContent = token.replace(/^<+|>+$/g, '');
if (isVariadic) {
paramContent = paramContent.replace(/\.\.\.$/, '');
}
let parsedSchema: ParsedSchema | undefined; let parsedSchema: ParsedSchema | undefined;
if (paramContent.includes(':')) { if (paramContent.includes(':')) {
@ -541,14 +546,25 @@ export function validateCommand(
command: Command, command: Command,
schema: CommandSchema schema: CommandSchema
): { valid: true } | { valid: false; errors: string[] } { ): { valid: true } | { valid: false; errors: string[] } {
const errors = validateCommandCore(command, schema);
if (errors.length > 0) {
return { valid: false, errors };
}
return { valid: true };
}
/**
*
*/
function validateCommandCore(command: Command, schema: CommandSchema): string[] {
const errors: string[] = []; const errors: string[] = [];
// 验证命令名称
if (command.name !== schema.name) { if (command.name !== schema.name) {
errors.push(`命令名称不匹配:期望 "${schema.name}",实际 "${command.name}"`); errors.push(`命令名称不匹配:期望 "${schema.name}",实际 "${command.name}"`);
} }
// 验证参数数量
const requiredParams = schema.params.filter(p => p.required); const requiredParams = schema.params.filter(p => p.required);
const variadicParam = schema.params.find(p => p.variadic); const variadicParam = schema.params.find(p => p.variadic);
@ -556,30 +572,19 @@ export function validateCommand(
errors.push(`参数不足:至少需要 ${requiredParams.length} 个参数,实际 ${command.params.length}`); errors.push(`参数不足:至少需要 ${requiredParams.length} 个参数,实际 ${command.params.length}`);
} }
// 如果有可变参数,参数数量可以超过必需参数数量
// 否则,检查是否有多余参数
if (!variadicParam && command.params.length > schema.params.length) { if (!variadicParam && command.params.length > schema.params.length) {
errors.push(`参数过多:最多 ${schema.params.length} 个参数,实际 ${command.params.length}`); errors.push(`参数过多:最多 ${schema.params.length} 个参数,实际 ${command.params.length}`);
} }
// 验证必需的选项
const requiredOptions = schema.options.filter(o => o.required); const requiredOptions = schema.options.filter(o => o.required);
for (const opt of requiredOptions) { for (const opt of requiredOptions) {
// 检查长格式或短格式
const hasOption = opt.name in command.options || (opt.short && opt.short in command.options); const hasOption = opt.name in command.options || (opt.short && opt.short in command.options);
if (!hasOption) { if (!hasOption) {
errors.push(`缺少必需选项:--${opt.name}${opt.short ? ` 或 -${opt.short}` : ''}`); errors.push(`缺少必需选项:--${opt.name}${opt.short ? ` 或 -${opt.short}` : ''}`);
} }
} }
// 验证标志(标志都是可选的,除非未来扩展支持必需标志) return errors;
// 目前只检查是否有未定义的标志(可选的严格模式)
if (errors.length > 0) {
return { valid: false, errors };
}
return { valid: true };
} }
/** /**
@ -605,45 +610,13 @@ export function parseCommandWithSchema(
const schema = parseCommandSchema(schemaStr); const schema = parseCommandSchema(schemaStr);
const command = parseCommand(input); const command = parseCommand(input);
// 验证命令名称 const errors = validateCommandCore(command, schema);
if (command.name !== schema.name) {
return {
command,
valid: false,
errors: [`命令名称不匹配:期望 "${schema.name}",实际 "${command.name}"`],
};
}
const errors: string[] = [];
// 验证参数数量
const requiredParams = schema.params.filter(p => p.required);
const variadicParam = schema.params.find(p => p.variadic);
if (command.params.length < requiredParams.length) {
errors.push(`参数不足:至少需要 ${requiredParams.length} 个参数,实际 ${command.params.length}`);
return { command, valid: false, errors };
}
if (!variadicParam && command.params.length > schema.params.length) {
errors.push(`参数过多:最多 ${schema.params.length} 个参数,实际 ${command.params.length}`);
return { command, valid: false, errors };
}
// 验证必需的选项
const requiredOptions = schema.options.filter(o => o.required);
for (const opt of requiredOptions) {
const hasOption = opt.name in command.options || (opt.short && opt.short in command.options);
if (!hasOption) {
errors.push(`缺少必需选项:--${opt.name}${opt.short ? ` 或 -${opt.short}` : ''}`);
}
}
if (errors.length > 0) { if (errors.length > 0) {
return { command, valid: false, errors }; return { command, valid: false, errors };
} }
// 使用 schema 解析参数值 const parseErrors: string[] = [];
const parsedParams: unknown[] = []; const parsedParams: unknown[] = [];
for (let i = 0; i < command.params.length; i++) { for (let i = 0; i < command.params.length; i++) {
const paramValue = command.params[i]; const paramValue = command.params[i];
@ -651,21 +624,19 @@ export function parseCommandWithSchema(
if (paramSchema) { if (paramSchema) {
try { try {
// 如果是字符串值,使用 schema 解析
const parsed = typeof paramValue === 'string' const parsed = typeof paramValue === 'string'
? paramSchema.parse(paramValue) ? paramSchema.parse(paramValue)
: paramValue; : paramValue;
parsedParams.push(parsed); parsedParams.push(parsed);
} catch (e) { } catch (e) {
const err = e as ParseError; const err = e as ParseError;
errors.push(`参数 "${schema.params[i]?.name}" 解析失败:${err.message}`); parseErrors.push(`参数 "${schema.params[i]?.name}" 解析失败:${err.message}`);
} }
} else { } else {
parsedParams.push(paramValue); parsedParams.push(paramValue);
} }
} }
// 使用 schema 解析选项值
const parsedOptions: Record<string, unknown> = { ...command.options }; const parsedOptions: Record<string, unknown> = { ...command.options };
for (const [key, value] of Object.entries(command.options)) { for (const [key, value] of Object.entries(command.options)) {
const optSchema = schema.options.find(o => o.name === key || o.short === key); const optSchema = schema.options.find(o => o.name === key || o.short === key);
@ -674,13 +645,13 @@ export function parseCommandWithSchema(
parsedOptions[key] = optSchema.schema.parse(value); parsedOptions[key] = optSchema.schema.parse(value);
} catch (e) { } catch (e) {
const err = e as ParseError; const err = e as ParseError;
errors.push(`选项 "--${key}" 解析失败:${err.message}`); parseErrors.push(`选项 "--${key}" 解析失败:${err.message}`);
} }
} }
} }
if (errors.length > 0) { if (parseErrors.length > 0) {
return { command: { ...command, params: parsedParams, options: parsedOptions }, valid: false, errors }; return { command: { ...command, params: parsedParams, options: parsedOptions }, valid: false, errors: parseErrors };
} }
return { return {

View File

@ -1,4 +1,4 @@
export interface RNG { export interface RNG {
/** 设置随机数种子 */ /** 设置随机数种子 */
setSeed(seed: number): void; setSeed(seed: number): void;
@ -20,7 +20,7 @@ export function createRNG(seed?: number): RNG {
} }
/** Mulberry32RNG 类实现(用于类型兼容) */ /** Mulberry32RNG 类实现(用于类型兼容) */
export class Mulberry32RNG { export class Mulberry32RNG implements RNG {
private seed: number = 1; private seed: number = 1;
constructor(seed?: number) { constructor(seed?: number) {
@ -30,7 +30,7 @@ export class Mulberry32RNG {
} }
/** 设置随机数种子 */ /** 设置随机数种子 */
call(seed: number): void { setSeed(seed: number): void {
this.seed = seed; this.seed = seed;
} }
@ -48,11 +48,6 @@ export class Mulberry32RNG {
return Math.floor(this.next(max)); return Math.floor(this.next(max));
} }
/** 重新设置种子 */
setSeed(seed: number): void {
this.seed = seed;
}
/** 获取当前种子 */ /** 获取当前种子 */
getSeed(): number { getSeed(): number {
return this.seed; return this.seed;