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
即是表达式的最终结果。
基本格式为 "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",此规则是骰出一个特殊的d6,两面为-,两面为0,两面为+,合计6面,分别对应-1 0 1
。
每次判定时,骰4次,然后将结果加和,一次典型的骰点是这样: [f=1=+0-+]
。
或许可以看成等价于 4d3 - 6
。
注:此规则语法可以使用vm.Flags.EnableDiceFate
进行开启或关闭。
基本格式为 "b骰数" 或 "p骰数",其中骰数是可选的,默认为1。
用法举例:b2
p1
。
注:此规则语法可以使用vm.Flags.EnableDiceCoC
进行开启或关闭。
这是一种多轮的骰点规则。一般为骰若干个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
进行开启或关闭
这是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代表空值。
这种类型的意思是,最终得到的值是一个式子计算的结果,例如:
&砍一刀 = 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
对应到1
,v2
对应到'测试'
。
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
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