update: ts

This commit is contained in:
Vick Scarlet
2026-07-29 17:03:26 +08:00
parent 1540a5d142
commit 4043fa887a
380 changed files with 4806 additions and 221348 deletions
+123
View File
@@ -0,0 +1,123 @@
import { expect, test, describe } from 'bun:test'
import { checkCondition } from '.'
// 定义属性 mock 字典的类型契约
interface MockProperties {
[key: string]: number | number[]
}
/**
* 属性注入高阶函数:模拟角色属性管理类
*/
function withProp(prop: MockProperties) {
const p = {
get(key: string): any {
return prop[key]
},
}
return (condition: string): boolean => checkCondition(p, condition)
}
describe('condition', () => {
// 实例化一个带满各种脏数据(数字、数组、空数组)的强类型测试代理
const check = withProp({
n1: 0,
n2: -10,
n3: 10,
nl1: [1, 2, 3],
nl2: [0],
nl3: [],
})
test('gt(>)', () => {
expect(check('n3>11')).toBe(false)
expect(check('n3>10')).toBe(false)
expect(check('n3>9')).toBe(true)
})
test('gte(>=)', () => {
expect(check('n3>=11')).toBe(false)
expect(check('n3>=10')).toBe(true)
expect(check('n3>=9')).toBe(true)
})
test('lt(<)', () => {
expect(check('n3<9')).toBe(false)
expect(check('n3<10')).toBe(false)
expect(check('n3<11')).toBe(true)
})
test('lte(<=)', () => {
expect(check('n3<=9')).toBe(false)
expect(check('n3<=10')).toBe(true)
expect(check('n3<=11')).toBe(true)
})
test('eq(=)', () => {
expect(check('n3=9')).toBe(false)
expect(check('n3=10')).toBe(true)
expect(check('n3=11')).toBe(false)
expect(check('nl1=0')).toBe(false)
expect(check('nl1=1')).toBe(true)
expect(check('nl1=2')).toBe(true)
expect(check('nl1=3')).toBe(true)
})
test('ne(!=)', () => {
expect(check('n3!=9')).toBe(true)
expect(check('n3!=10')).toBe(false)
expect(check('n3!=11')).toBe(true)
expect(check('nl1!=0')).toBe(true)
expect(check('nl1!=1')).toBe(false)
expect(check('nl1!=2')).toBe(false)
expect(check('nl1!=3')).toBe(false)
})
test('in(?)', () => {
expect(check('n3?[1,2,3]')).toBe(false)
expect(check('n3?[10,11,12]')).toBe(true)
expect(check('nl1?[0,1]')).toBe(true)
expect(check('nl2?[1,2,3]')).toBe(false)
expect(check('nl3?[1,2,3]')).toBe(false)
expect(check('nl2?[]')).toBe(false)
expect(check('nl3?[]')).toBe(false)
})
test('notin(!)', () => {
expect(check('n3![1,2,3]')).toBe(true)
expect(check('n3![10,11,12]')).toBe(false)
expect(check('nl1![0,1]')).toBe(false)
expect(check('nl2![1,2,3]')).toBe(true)
expect(check('nl3![1,2,3]')).toBe(true)
expect(check('nl2![]')).toBe(true)
expect(check('nl3![]')).toBe(true)
})
test('and(&)', () => {
expect(check('n1>=0&n2<0')).toBe(true)
expect(check('n2>0&n3>0')).toBe(false)
expect(check('n2<0&n3<0')).toBe(false)
expect(check('n2<0&n3>0')).toBe(true)
expect(check('n1=0&n2<0&n3>0')).toBe(true)
expect(check('n1=0&n2>0&n3>0')).toBe(false)
})
test('or(|)', () => {
expect(check('n1>=0|n2<0')).toBe(true)
expect(check('n2>0|n3>0')).toBe(true)
expect(check('n2<0|n3<0')).toBe(true)
expect(check('n2<0|n3>0')).toBe(true)
expect(check('n2>0|n3<0')).toBe(false)
expect(check('n1=0|n2<0|n3>0')).toBe(true)
expect(check('n1=0|n2>0|n3>0')).toBe(true)
expect(check('n1!=0|n2>0|n3<0')).toBe(false)
})
test('mix', () => {
expect(check('(n1=0|n2<0|n3>0)&(n1=0|n2>0|n3>0)')).toBe(true)
expect(check('(n1=0|n2<0|n3>0)&(n1!=0|n2>0|n3<0)')).toBe(false)
expect(check('n1=0|n2<0|n3>0&n1!=0|n2>0|n3<0')).toBe(true)
expect(check('(n1>0|n1?[-10,0])&(n2>0|n3![0,1])')).toBe(true)
expect(check('(n1>0&n1?[-10,0])|(n2<0&n3![0,1])')).toBe(true)
})
})
+202
View File
@@ -0,0 +1,202 @@
// 条件节点可以是一个纯字符串表达式(如 'AGE>18'),或者是一个无限嵌套自身的数组
export type ConditionNode = string | ConditionTree
export interface ConditionTree extends Array<ConditionNode> {}
// 🌟 核心类型定义 2:定义满足有 .get 提取器属性的游戏对象约束(如玩家属性管理类或原生 Map)
export interface PropertyContainer {
get(key: string): any
}
/**
* 词法解析器:将条件字符串切分为多维嵌套的语法树树(AST)
* @param condition 原始字符串表达式,例如 "AGE > 10 & (SEX = 1 | CHR >= 5)"
*/
function parseCondition(condition: string): ConditionTree {
const conditions: ConditionTree = []
const length = condition.length
const stack: ConditionTree[] = []
stack.unshift(conditions)
let cursor = 0
const catchString = (i: number): void => {
const str = condition.substring(cursor, i).trim()
cursor = i
if (str) {
stack[0]?.push(str)
}
}
for (let i = 0; i < length; i++) {
switch (condition[i]) {
case ' ':
continue
case '(': {
catchString(i)
cursor++
const sub: ConditionTree = []
stack[0]?.push(sub)
stack.unshift(sub)
break
}
case ')':
catchString(i)
cursor++
stack.shift()
break
case '|':
case '&':
catchString(i)
catchString(i + 1)
break
default:
continue
}
}
catchString(length)
return conditions
}
/**
* 外部核心接口:判定某个角色的属性是否完美符合该文本条件限制
* @param property 玩家或局内属性提取器
* @param condition 原始条件表达式
*/
export function checkCondition(
property: PropertyContainer,
condition: string,
): boolean {
const conditions = parseCondition(condition)
return checkParsedConditions(property, conditions)
}
/**
* 递归计算已解析出来的多维语法树结果
*/
function checkParsedConditions(
property: PropertyContainer,
conditions: ConditionNode,
): boolean {
// 如果已经剥离到了最底层的纯字符串表达式(如 'AGE>10'),直接移交核心原子判定器
if (!Array.isArray(conditions)) {
return checkProp(property, conditions)
}
if (conditions.length === 0) return true
if (conditions.length === 1) {
return checkParsedConditions(property, conditions[0]!)
}
let ret = checkParsedConditions(property, conditions[0]!)
for (let i = 1; i < conditions.length; i += 2) {
const operator = conditions[i]
const nextNode = conditions[i + 1]
// 防御性拦截,防止表达式残缺或配置错误引发崩溃
if (nextNode === undefined) return false
switch (operator) {
case '&':
if (ret) {
ret = checkParsedConditions(property, nextNode)
}
break
case '|':
if (ret) return true
ret = checkParsedConditions(property, nextNode)
break
default:
return false
}
}
return ret
}
/**
* 原子逻辑判定器:负责处理诸如 '>', '<', '=', '?', '!' 各种操作符的最终生死判定
*/
function checkProp(property: PropertyContainer, condition: string): boolean {
const length = condition.length
let i = condition.search(/[><!?=]/)
// 防御处理:如果没有找到任何操作符,视为非法表达式直接拒签
if (i === -1) return false
const prop = condition.substring(0, i)
// 判断是否是双字符操作符(如 >=, <=, !=),若是则向前多吞一个字符位
const isDoubleSymbol = condition[i + 1] === '='
const symbol = condition.substring(i, (i += isDoubleSymbol ? 2 : 1))
const d = condition.substring(i, length)
const propData = property.get(prop)
const conditionData: number | any[] =
d[0] === '[' ? JSON.parse(d) : Number(d)
switch (symbol) {
case '>':
return propData > conditionData
case '<':
return propData < conditionData
case '>=':
return propData >= conditionData
case '<=':
return propData <= conditionData
case '=':
if (Array.isArray(propData)) {
return propData.includes(conditionData)
}
return propData == conditionData
case '!=':
if (Array.isArray(propData)) {
return !propData.includes(conditionData)
}
return propData != conditionData
case '?': // 🌟 包含判定符(如 属性值 是否在 [1,2,3] 数组范围内)
if (Array.isArray(propData)) {
if (Array.isArray(conditionData)) {
for (const p of propData) {
if (conditionData.includes(p)) return true
}
}
return false
}
if (Array.isArray(conditionData)) {
return conditionData.includes(propData)
}
return false
case '!': // 🌟 排除判定符(如 属性值 是否不存在于 [1,2,3] 数组中)
if (Array.isArray(propData)) {
if (Array.isArray(conditionData)) {
for (const p of propData) {
if (conditionData.includes(p)) return false
}
}
return true
}
if (Array.isArray(conditionData)) {
return !conditionData.includes(propData)
}
return true
default:
return false
}
}
export function extractMaxTriggers(condition: string): number {
// Assuming only age related talents can be triggered multiple times.
const RE_AGE_CONDITION = /AGE\?\[([0-9,]+)\]/
const matchObject = RE_AGE_CONDITION.exec(condition)
if (matchObject === null) {
// Not age related, single trigger.
return 1
}
return matchObject[1]?.split(',')?.length ?? 1
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "@remake/condition",
"type": "module",
"version": "3.0.0",
"main": "./index.ts",
"author": "Vick Scarlet <vick@syaro.io>",
"scripts": {
"lint": "eslint .",
"test": "bun test"
},
"eslintConfig": {
"extends": "../../package.json"
}
}
+29
View File
@@ -0,0 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"types": ["bun"],
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": true,
"noUnusedParameters": true,
"noPropertyAccessFromIndexSignature": false,
"noImplicitAny": true
},
"include": ["**/*"]
}