前言
由于帕鲁同人游戏预计将文本脚本格式转为twee,故需对twee的一写控制语句进行解析。而对于AVG游戏类型来说, 最重要的莫过于流程分支,即if语句。
本文记录一下解析代码的实现步骤,注:可能不是最优的解决方式。
语句格式
在twee的if语句中,其最基础的与或非等逻辑是定义如下:
key
isvaluekey
is notvalueboolean1
andboolean2boolean1
orboolean2
本文也针对这些最常见的逻辑语句进行解析实现(为了方便,这里只考虑语句合法的情况)。
解析
1. 提取 if 语句
if语句在twee中的完整代码格式如下:
1
(if:$awmscore is "不好" and $linkjxy is not "没和他表白" or $sex is "男")
可以使用正则表达式提取if语句中的逻辑部分:/\(if:(.*)\)/g,并使用其group1,可得内容如下:
1
$awmscore is "不好" and $linkjxy is not "没和他表白" or $sex is "男"
2. tokens
可以发现语句中的变量(以$开头标识)、值、逻辑操作符都是通过空格分隔的,故可使用正则/\$*(is not|is|\S+)/g进行提取,获取group1内容如下:
1
2
3
4
[
"awmscore", "is", "\"不好\"", "and", "linkjxy", "is not",
"\"没和他表白\"", "or", "sex", "is", "\"男\""
]
这里去除了变量前的$
3. 解析语句
对于一个合法的语句,各个token间是有位置关系的,如is/is not的前后必定是变量和变量值,可以直接得出结果;而and/or的前后则可以是另一组表达式或者结果,可使用分治递归的方式来处理。故我们可以有算法如下:
1
2
3
4
5
6
7
8
9
10
11
12
//token解析,evalTokenCondition
输入:tokens, ctx //ctx为一个包含变量的对象
1. 处理is,更新tokens //is token的处理会对is本身、变量和变量值进行运算,得到一个ture/false的结果,即tokens的长度应减少
2. 处理is not, 更新tokens
3. 获取and和or在tokens中的位置posAnd和PosOr
4. 遍历posAnd
4.1. 对每个and的左侧tokens和右侧tokens递归调用evalTokenCondition //and左右都可以是表达式
4.2 return 左侧结果&&右侧结果 //and使用&&运算
5. 便利posOr
5.1 对每个or的左侧tokens和右侧tokens递归调用evalTokenCondition //or左右都可以是表达式
5.2 return 左侧结果||右侧结果
5. return tokens[0] //在仅有is/or的表达式下,结果相当于reduce到了tokens[0]中
其中处理is和处理is not的算法如下:
1
2
3
4
5
6
7
8
9
10
//处理is/is not, 形式如:var is/is not "value"
输入:tokens, ctx //ctx为一个包含变量的对象
1. 获取is/is not 在tokens中的位置posIs/posIsNot
2. 创建运算结果数组res
3. 遍历posIs/posIsNot,计算对应位置前后的变量和变量值的真值
4. 创建新tokens数组newTokens
5. 遍历原数组tokens,初始化i,j为0,分别为tokens和res的指针
5.1 若下一个token为is/is not,newTokens.push(res[j++]),并跳过两个token:i += 2;
5.2 否则加入当前token:newTokens.push(tokens[i])
6. return newTokens
4. 代码实现
这里省略了提取if语句的步骤。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
const TOKEN_TYPE = {
IS: "is",
ISNOT: "is not",
AND: "and",
OR: "or",
NOT: "not",
VAR: "var",
VALUE: "value",
};
const tokenRegex = /\$*(is not|is|\S+)/g;
let text =
'$awmscore is "不好" and $linkjxy is not "没和他表白" or $sex is "男"';
let tokens = parseTokens(text, tokenRegex);
console.log(tokens);
let res = evalTokenCondition(tokens, {
awmscore: '"不好"',
linkjxy: '"没和他表白"',
sex: '"男"',
});
//text => tokens
function parseTokens(text, regex) {
let tokens = text.matchAll(regex);
let processed = [];
for (const token of tokens) {
processed.push(token[1]);
}
return processed;
}
// 找到数组中的某个值的所有下标
function findAllIndexes(arr, value) {
var indexes = [];
for (var i = 0; i < arr.length; i++) {
if (arr[i] === value) {
indexes.push(i);
}
}
return indexes;
}
function evalTokenCondition(tokens, ctx) {
let processNot = processNotToken(tokens, ctx);
tokens = processIsToken(processNot, ctx);
let andTokens = findAllIndexes(tokens, TOKEN_TYPE.AND);
let orTokens = findAllIndexes(tokens, TOKEN_TYPE.OR);
for (let i = 0; i < andTokens.length; i++) {
let leftRes = evalTokenCondition(tokens.slice(0, andTokens[i]), ctx);
let rightRes = evalTokenCondition(tokens.slice(andTokens[i] + 1), ctx);
return leftRes && rightRes;
}
for (let i = 0; i < orTokens.length; i++) {
let leftRes = evalTokenCondition(tokens.slice(0, orTokens[i]), ctx);
let rightRes = evalTokenCondition(tokens.slice(orTokens[i] + 1), ctx);
return leftRes || rightRes;
}
return tokens[0];
}
//处理is Token, 形式如:$var is "value"
function processIsToken(tokens, ctx) {
let isTokensIdx = findAllIndexes(tokens, TOKEN_TYPE.IS);
//处理is Token
let res = [];
for (let i = 0; i < isTokensIdx.length; i++) {
let varStr = tokens[isTokensIdx[i] - 1];
let valueStr = tokens[isTokensIdx[i] + 1];
res.push(ctx[varStr] == valueStr);
}
//替换原tokens数组
let newTokens = [];
for (let i = 0, j = 0; i < tokens.length; i++) {
if (tokens[i + 1] == TOKEN_TYPE.IS) {
newTokens.push(res[j++]);
i += 2;
continue;
}
newTokens.push(tokens[i]);
}
return newTokens;
}
//处理not Token, 形式如:$var not "value"
function processNotToken(tokens, ctx) {
let notTokens = findAllIndexes(tokens, TOKEN_TYPE.ISNOT);
//处理not Token
let res = [];
for (let i = 0; i < notTokens.length; i++) {
let varStr = tokens[notTokens[i] - 1];
let valueStr = tokens[notTokens[i] + 1];
res.push(ctx[varStr] != valueStr);
}
//替换原tokens数组
let newTokens = [];
for (let i = 0, j = 0; i < tokens.length; i++) {
if (tokens[i + 1] == TOKEN_TYPE.ISNOT) {
newTokens.push(res[j++]);
i += 2;
continue;
}
newTokens.push(tokens[i]);
}
return newTokens;
}
其他
这只是对if语句的简单实现,且and的优先级默认是比or高的;
此外实际上还可以有值的大小比较等逻辑,这也不难,和处理is的逻辑类似。
其实考虑到游戏本身的需求,可能已经够用了,如若需要更加通用的需求,需要更复杂的语句合法性检测、优先级处理等。