Skip to content

Latest commit

 

History

History
731 lines (510 loc) · 18.4 KB

GUIDE.md

File metadata and controls

731 lines (510 loc) · 18.4 KB

概述

DiceScript 被设计为一门专用于TRPG场景的脚本语言。

这门语言内嵌了各种主流的骰点语法,如常见的d20, 3d20, (4+5)d(20), 2d20k1, 2d20q1,又或者是fvtt提供的kh、kh、min和max后缀。

以及 CoC / Fate / WoD / Double Cross 规则的骰点语法。

同时,还支持整型、浮点型、字符串、数组、字典、计算数值以及函数等数据类型。

所有数据类型都是可序列化和反序列化的,包括函数在内。

此外,还支持逻辑判断和循环。DiceScript是图灵完备的。

如果你担心用户会滥用过于强大的语法,DiceScript提供了两个机制:

第一个是算力上限,当用户构造计算时间过长的表达式时,会被自动拒绝执行。

第二个是逻辑语句开关,例如你希望DiceScript只响应骰点语法,如用户输入的3d20,你可以将其关闭,这样解释器就不再处理逻辑语法(如while if等)了。

我们提供golang和js两个版本,你可以把它嵌入到任何你喜欢的地方去。

试一试

在浏览器里试用

你可以直接试一试,建议一边看语法手册一边尝试:

https://sealdice.github.io/dicescript/

语法

DiceScript的语法从JS、golang和Python上各吸取了一点东西,不过不用担心,语法非常的简单。

如果你学过包括C在内的任意一门语言,都应该很容易上手,如果是零基础的话,建议囫囵吞枣跳过不懂的部分,多用网页命令行去实践。

一点说明

在阅读教程时,你可能会觉得奇怪,因为在这个教程中看不到像是print()这样的函数,反而会有一些这样的情况:

a = d20 + 5; a

你可能会觉得最后那个a很突兀,而实际上我们总是假设DiceScript被使用在一种嵌入的场景,举例来说,你在一个跑团平台输入指令:

/r 2d20

这时期望得到的结果是,2d20被执行掉,然后返回其值。对应到开头那个句子就是:

/r a = d20 + 5; a

因此,我们一般情况下不需要使用print()函数。如果你正在试图将DiceScript嵌入到其他地方去,那么在vm.Run('xxx'')之后,vm.Ret即是表达式的最终结果。

骰子算符

d 常规骰子算符,用法举例 d20 2d20k1 d20优势

基本格式为 "XdY后缀",X代表骰数,Y代表面数,后缀可为:

  • kl/q 取低(keep lowest),例如 3d20q1 3d20kl 3d20kl1 (这三句意思都为,三个d20取最低)
  • kh/k 取高(keep highest),例如 3d20k1 3d20kh 3d20kh1 (这三句意思都为,三个d20取最高)
  • dl 排除最低(drop lowest),例如 3d20dl 3d20dl1 (三个d20排除最低值)
  • dh 排除最高(drop highest),例如 3d20dh 3d20dh1 (三个d20排除最高值)
  • min 界定下限,例如 3d20min10 (三个d20,每个骰子结果至少为10)
  • max 界定上限,例如 3d20max10 (三个d20,每个骰子结果至多为10)
  • 优势,例如 d20优势,相当于 2d20kh,梨骰算符
  • 劣势,例如 d20劣势,相当于 2d20kl,梨骰算符

f 命运骰,随机骰4次,每骰结果可能是-1 0 1,记为- 0 +

基本格式为 "f",此规则是骰出一个特殊的d6,两面为-,两面为0,两面为+,合计6面,分别对应-1 0 1

每次判定时,骰4次,然后将结果加和,一次典型的骰点是这样: [f=1=+0-+]

或许可以看成等价于 4d3 - 6

注:此规则语法可以使用vm.Flags.EnableDiceFate进行开启或关闭。

b/p CoC奖励骰/惩罚骰

基本格式为 "b骰数" 或 "p骰数",其中骰数是可选的,默认为1。

用法举例:b2 p1

注:此规则语法可以使用vm.Flags.EnableDiceCoC进行开启或关闭。

c 双十字规则骰点

这是一种多轮的骰点规则。一般为骰若干个d10,并指定暴击值。

基本格式为:XcYmZ,X骰数,Y暴击值,Z骰子面数。 其中XY是必须的,Z如不设置则为10。

如果达到暴击值,则所有暴击骰子进入下一轮,直到全部骰子都不能暴击。

最终的骰点结果为:暴击轮数 * 10 + 最后一轮中最大的点数。

用法举例:4c3m7,结果为:

4c3m7=[出目32/9 轮数:4 {<4>,2,<4>,<5>},{<7>,1,2},{<7>},{2}]=32
//骰4个d7,暴击值为3。若不设置m则为4个d10。

注:此规则语法可以使用vm.Flags.EnableDiceDoubleCross进行开启或关闭

a WoD无限规则骰点

这是WOD骰点规则,国内更多见于无限团,默认使用d10。

基本格式为:XaYmZkNqM,X骰数,Y加骰线,Z骰子面数,N阈值(>=),M阈值(<=)。 其中Y是必须的,骰数默认为1,成功线N默认为8。

这是一个多轮骰点规则,骰X个d10,每有一个≥成功线的骰,成功数+1,每有一个≥加骰线的骰,加骰数+1,随后下一轮骰点使用上一轮的加骰数作为骰池数量。

特别的,若Y=0则不加骰,这也可以用到其他一些TRPG游戏中去。

M值与N值相对,可使用M参数计算≤M的骰数。

5a9k2=8=成功8/8 轮数:3 {2*,<10*>,<9*>,5*,2*},{<9*>,2*},{2*}

注:此规则语法可以使用vm.Flags.EnableDiceWoD进行开启或关闭。

注释

以 // 开头的行为注释。

// 这是一行注释

保留字

这些名字不能用于变量名:

'while' / 'if' / 'else' / 'continue' / 'break' / 'return' / 'func'

变量名

变量命名使用主流规则,即可以使用中英文以及下划线作为变量名,但首个字符不能是数字。

特殊的,DiceScript允许冒号作为变量名的一部分,但尽量不要主动去使用,因为这是设计给全局配置文件的。

hi
camelCase
PascalCase
abc123
ALL_CAPS
测试
part:end

换行规则和语句块

DiceScript的代码中,对换行的要求并不严格。类似于JS等语言,DiceScript支持在语句当中换行:

a
=
2;a
//输出2,即赋值后的a值

但是出于可读性、可能的奇特语句截断导致的未知bug的考量,还是推荐使用把一个语句放在同一行中的情况,或严格遵守一定的代码规范。可以参考JS的代码规范: https://www.runoob.com/js/js-conventions.html

DiceScript 的语句必须以 ; 分割,除非是最后一条语句。

最后一条语句可以省略分号,举例:

123
a = 1; b = 2; 123

语句块则是使用大括号,如:

if true {
    // xxx
}

当语句和语句块混写的时候,语句块后面不用写分号:

a = d20
if a > 10 {
    // xxx
}
b = 5

算符优先级

暂略,参考c / python / js。

变量

在DiceScript中,变量不需要声明即可使用(类似Python):

v1 = 1
v2 = '123'
v3 = [1,2,3]
v4 = {}

语句块不影响变量的生命周期。

if d10 > 5 {
    a = true
}

if a {
    '运气不错!' 
}
//在本例中,a所在的第一个语句块结束运行后,a的值仍然保留,并且在第二个语句块的判断中应用。

函数中的变量有单独的变量空间:

a = 2

func f1() {
    a = 10
    return a
}

[f1(), a]
//在本例中,函数语句中的a被赋值,但不影响函数外的a被赋值为2。

这段代码会得到 [10, 2] 这样一个结果。

类型

数字

DiceScript 允许两种数字类型,整数和浮点数,足以应对各种向上取整、向下取整、四舍五入的TRPG规则。

整数和浮点数互相运算时会自动进行类型转换,注意任何int与float运算的操作都会使得结果成为float。

以下是合法的数字类型举例:

0
1234
-5678
3.14159
1.0
-12.34
0.0314159
.0314159 // DiceScript会在这样的数字前加上0,本例等于0.0314159

此外,DiceScript没有布尔类型,true的值为整数1,false的值为整数0。

字符串

DiceScript 允许两种字符串的常规写法:

'hello world'
"hello world"

以及一种带格式化的模板语法:

name1 = 'Alice';
name2 = 'Bob';

`Hello, {name1} & {name2}, {d100} is today's lucky number`

你会得到这样的结果:

Hello, Alice & Bob, 22 is today's lucky number

请注意,在常规写法中使用变量与算符会导致不输出预期结果的情况。

字符串可以使用加号连接:

text = 'Hello, ' + 'world' + '!'

同时,字符串支持分片语法,写法同python,暂不支持步长:

'12345'[2:4]  // 34

以及两种模板写法,首先是``:

`玩家的生命值为,{hp}`

{hp} 表示会带入hp变量并格式化输出。

`玩家目前状态:{%
  if float(hp) / hpmax < 0.1 {
    stat = '生命垂危'
  } else {
    stat = '还顶得住'
  }
  stat // 最后一项非语句块内容会被输出
%}`

{} 和 {% %} 的行为是相同的,都是输出其中的内容。

不过,推荐在{}中写简短的表达式,表达式可以看成是不带if while的句子,较为抽象的定义说法是不带语句块的句子。

而 {% %} 推荐复杂一些的逻辑,这样会容易区分一些。目前的语句语法有四种:if while return 函数定义

{} / {% %} 段落会返回里面最后一个表达式的值,如果没有值会自动补空字符串。

第二种隐藏的模板语法,用的符号是 \x1e,他跟`的作用完全相同,也支持 f-string。

这个是专用于开发者进行接入的,解决的唯一问题就是不占用`这个符号,让用户的输入有更大自由度。

举个例子,你希望用户能在一段文本中插入变量,比如他会这样:

User: /text 我的生命值是{hp}

你可以这么做:

vm.Run("\x1e" + input + "\x1e")

如果将上面代码中的"\x1e"换成`,效果是一样的,但是这限制了用户在文本中输入`字符,因此使用一个不可见字符替代。

null

null代表空值。

计算类型

这种类型的意思是,最终得到的值是一个式子计算的结果,例如:

&砍一刀 = D20 + 4
砍一刀 + 10  // 此时为 D20 + 4 + 10,每次调用时会动态计算一遍。

同时我们可以实现更高级的功能:

&a = this.x + d10 //
&a.x = 5

这里的this是指该变量内部的一个空间,如果你有其他编程语言经验,可以理解为函数的内部变量。

this的解释请参考:https://www.runoob.com/js/js-this.html ,在DiceScript中this的用法基本与JS相同。

a // 结果为:a = 5[a.x = 5] + d10 

这对一些二级属性非常有用,例如可以提前定义公式,只改变其中的变量。

海豹1.x的RollVM中,DND的技能实际上就是这样实现的。

数组

有两种方式定义一个数组。一是[1,2,3,4,5],二是[1..5]会生成一个包含12345的数组,也可以写[5..1]生成出一个反的数组。

还有一种可用于生成大量重复数的数组方法:

a = [1] * 10
//a = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

a=[1]*2+[2]*3
//a=[1, 1, 2, 2, 2]

数组可以装入任意类型,也可以装入多维数组。

a = [1,2,'test', [4,5,6]]

通过下标可以取得数组内容:

a[0]
a[3][1]

分片语法:

[1,2,3,4,5][2:4]  // [3,4]

a = [1,2,3]; a[2:3] = [4,5,6] // a == [1, 2, 4, 5, 6]

支持两个fvtt的特殊语法,如[1d20, 10]kh,是为取最高,还有一个是后缀kl取最低,如:

[1,2,3]kl
[1,2,3]kl2 //结果为 3 = 1 + 2,会将取出来的结果求和
[1,2,3]kh

数组函数:

[1,2,3].sum() // 加和 6
[1,2,3].kl() // 取最低的1个值,1
[1,2,3].kl(2) // 取最低的2个值并相加,3
[1,2,3].kh() //  取最高的1个值,3
[1,2,3].kh(2) //  取最高的2个值并相加,得到5
[1,2,3].shuffle() // 打乱顺序,[3,1,2]
[1,2,3].len() // 求长度,3
[1,2,3].rand() // 随机取其中一项并返回其值,如 2
[1,2,3].randSize(2) // 随机取其中2项并返回其值,如 [3,2]
[1,2,3].push(4) // 加入一个值,效果同[1,2,3] + [4],[1,2,3,4]
[1,2,3].shift() // 取最前方的一个值,并将其弹出数组,获得1,数组变为[2,3]
[1,2,3].pop() // 取最后方的一个值,并将其弹出数组,获得3,数组变为[1,2]

字典

字典是一种存放对应关系的数据结构。

例如下方的代码,将v1对应到1v2对应到'测试'

d = { 'v1': 1, 'v2': '测试', 1: 'test' }

这里v1和v2被称为键,另外两个被称为值,这样一组关系叫做一个键值对。字典就是键值对的集合。

然后我们可以这样得到其中的内容:

d.v1 // 1
d['v1'] // 1

这样修改或者添加内容:

d.v3 = 4
d['v4'] = 5

请特别注意,字典的键必须为字符串,实际操作中也允许数字类型,但是会自动转换为字符串。

函数

定义和调用函数

func test(n) {
    return n + 1;
}

test(11) // 获得12

此外,函数可以被装入字典或数组。

示例,斐波那契数列计算:

func fib(n) {
  this.n == 0 ? 0,
  this.n == 1 ? 1,
  this.n == 2 ? 1,
   1 ? fib(this.n-1)+fib(this.n-2)
}
fib(11) // 89

this的解释请参考:https://www.runoob.com/js/js-this.html ,在DiceScript中this的用法基本与JS相同。

另一种写法:

func fib(n) {
  if this.n == 0 { 0 }
  else if this.n == 1 { 1 }
  else if this.n == 2 { 1 } else {
    fib(this.n-1) + fib(this.n-2)
  }
}
fib(10) // 55

流程控制

if else

t0 = d20

if t0 > 10 {
    t1 = "aaa"
} else {
    t1 = 'bbb'
}
//在条件为真时,执行语句块内语句;在条件为假时,执行else内语句,随后执行下一个语句。

循环

t1 = 0;
while t1 < 10 {
	t1 = t1 + 1
}
//在条件为真时,执行语句块内语句,并再次判断条件是否为真。条件为假后,结束循环,执行下一个语句。

逻辑算符

条件判断相关的逻辑算符有:

> // 判断左侧是否大于右侧
< // 判断左侧是否小于右侧
== // 判断左右两侧是否相等,请注意 = 会被视为赋值,不会视为判断
!= // 判断左右两侧是否不相等
>= // 判断左侧是否大于等于右侧
<= // 判断左侧是否小于等于右侧
&& // 逻辑与,即都为真时才为真,有一个不为真时为假
|| // 逻辑或,即有一个为真时即为真,都不为真时为假

逻辑与&&:以 expr1 && expr2 为例, 如果 expr1 能被转换为 false,那么返回 expr1;否则,返回expr2。因此,&&用于布尔值时,当操作数都为 true 时返回 true;否则返回 false。

expr1 && expr2 为例, 如果 expr1 能被转换为 false,那么返回 expr1;否则,返回expr2。因此,&&用于布尔值时,当操作数都为 true 时返回 true;否则返回 false

if v1 > 10 && v2 > 15 {
    // ...
}

|| 为逻辑或,只要有一个条件满足,后面的就不会执行:

a = [1,2,3];
if '' || 'OK' || a.push(4) {
    // 这里a.push(4)不会触发,因为 'OK' 已经满足条件
} 

同时可以这样使用:

val1 = expr1 || expr2 

如果 expr1 能被转换为 true,那么返回 expr1;否则,返回expr2。因此,|| 用于布尔值时,当任何一个操作数为 true 则返回 true;如果操作数都是 false 则返回 false。

其他算符

按位与/按位或 & | //按照二进制进行计算,不是十进制
加减乘除余 + -* / % //余,即取余运算,计算前一个数被后一个数除后剩下的余数
乘方 ^ ** // 2 ** 3 或 2 ^ 3 即2的3次方

三目运算符/多重条件运算符

例如你设计了一个类CoC规则的TRPG,有一种叫做“灵视”的属性,知道的越多越接近疯狂,可以编写这样的判定语句:

灵视 = d100;
灵视 >= 40 ? '如果灵视达到40以上,你就能看到这句话' : '无知亦是幸运'

再比如进行更细致的判定:

灵视 = d100;

灵视 >= 80 ? '看得很清楚吗?',
灵视 >= 50 ? '不错,再靠近一点……',
灵视 >= 30 ? '仔细听……',
灵视 >= 0 ? '呵,无知之人。'

空值合并算符

?? 如果左侧为null,则使用右侧值。

expr1 ?? expr2:当 expr1 不为 null 时取 expr1,为空时取 expr2

举例: 0 ?? 1 为 0 null ?? 1 为 1

内置函数

floor(num) // 对int/float类型向下取整
ceil(num) // 对int/float类型向上取整
round(num) // 对int/float类型四舍五入
abs(num) // 取绝对值

int(num) // 转化为int类型,向下取整
float(num) // 转化为float类型
str(obj) // 转化为str类型
bool(obj) // 将对象二值化,结果为0或1

repr(obj) // 将对象转化为供解释器读取的形式,类似于python的同名函数
load(name) // 读取变量名为name的变量,拿到其值
loadRaw(name) // 读取变量名为name的变量,与load()不同,如果该变量是计算类型,那么不会返回计算后结果

repr(obj) // 将对象转化为供解释器读取的形式
load(name) // 根据给出的名字,获取对象。 load('a') == a
dir(obj) // 查看这个对象的方法函数,可用于字典、数组等
typeId(obj) // 获取某个对象的类型ID,值为数字

特殊宏

在脚本中,可以通过特殊宏来开关骰点规则,例如,初始时:

a2
> 1[a2=1=成功1/4 轮数:4 {<2>},{<2>},{<8*>},{1}]

执行此宏后,再执行a2,将被当做变量而非语法处理。

// #EnableDiceWoD false
a2
> null[a2=null] // 此时当作变量处理,因此获得null

可用的四个宏为:

// #EnableDiceCoC true
// #EnableDiceWoD true
// #EnableDiceFate true
// #EnableDiceDoubleCross true

请注意,前面的//并不代表这是注释。将true改为false,即可获得关闭用宏。

开发者

集成到你的项目

Golang:

package main

import (
	"fmt"
	dice "github.com/sealdice/dicescript"
)

func main() {
	vm := dice.NewVM()
	if err := vm.Run(`d20`); err == nil {
		fmt.Printf("结果: %s\n", vm.Ret.ToString())
	} else {
		fmt.Printf("错误: %s\n", err.Error())
	}
}

JavaScript // 还会再调整API

function roll(text) {
    let ctx = dice.newVM();
    try {
        ctx.Run(text)
        if (ctx.Error) {
            console.log(`错误: ${ctx.Error.Error()}`)
        } else {
            console.log(`结果: ${ctx.Ret.ToString()}`)
        }
    } catch (e) {
        this.items.push(`错误: 未知错误`)
    }
}

编译

依次执行:

go mod install
go install github.com/fy0/pigeon@latest
go install github.com/gopherjs/[email protected]
pigeon -nolint -optimize-parser -optimize-ref-expr-by-index -o .\roll.peg.go .\roll.peg

如果你使用golang:

go build

如果你使用JS:

gopherjs build github.com/sealdice/dicescript/jsport -o jsport/dicescript.cjs