游戏剧情脚本twee-if语句解析

twee格式的if语句解析

Posted by Momoka7 on July 16, 2024

前言

由于帕鲁同人游戏预计将文本脚本格式转为twee,故需对twee的一写控制语句进行解析。而对于AVG游戏类型来说, 最重要的莫过于流程分支,即if语句。

本文记录一下解析代码的实现步骤,注:可能不是最优的解决方式。

语句格式

tweeif语句中,其最基础的与或非等逻辑是定义如下:

key is value

key is not value

boolean1 and boolean2

boolean1 or boolean2

本文也针对这些最常见的逻辑语句进行解析实现(为了方便,这里只考虑语句合法的情况)。

解析

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初始化ij为0分别为tokens和res的指针
5.1   若下一个token为is/is notnewTokens.push(res[j++])并跳过两个tokeni += 2;
5.2   否则加入当前tokennewTokens.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的逻辑类似。

其实考虑到游戏本身的需求,可能已经够用了,如若需要更加通用的需求,需要更复杂的语句合法性检测、优先级处理等。