我的世界命令设计
我的世界命令设计
Mybbs —— 开源MC社区第 1 节
本节最后修改于 2022 / 11 / 12
条件判断
条件判断在我的世界指令设计中具有极大的用途。条件判断逻辑一般以两种形式体现在指令中,分别是“有条件的命令方块”和“选择器”。下面分别介绍这两种形式。
有条件命令方块
对于一个开启的有条件连锁命令方块,如果指向它的命令方块的指令执行成功,则这个命令方块中的指令将会被执行。否则将不会被执行。这是有条件命令方块较为普遍的使用方法。
1
2
3
4
5# 例子
# 如果有人背包里有床,就提示“有床啊!”
[~,~,~,~] clear @a bed
[+,L,+,0] say 有床啊!选择器
选择器也是条件判断的一种形式,这个判断是对实体的判断:每次选择都可以看作把全部实体挨个判断一次,符合选择器条件就会被选中,否则就不会被选中。
通常的用法就是通常的用法:选择作用的实体。选择器参数可以使选择更加精准。
1
2
3
4
5# 例子
# 我们希望给所有叫做“重工业玉米”的玩家打上“帅比”标签
# 或者可以换个说法:我们判断一个人叫不叫“重工业玉米”,如果叫,那么打上“帅比”标签。
[~,~,~,~] tag @a[name=重工业玉米] add 帅比选择器还可以搭配
execute
指令,用来代替testfor
指令搭配有条件命令方块实现的判断。这个用法在以实体为主的指令设计中十分实用——本教程系列就是以实体为主的指令设计。1
2
3
4# 例子
# 检测有没有玩家叫“重工业玉米”,如果有,就提示“玉米大人驾到!”
[~,~,~,~] execute @a[name=重工业玉米] ~~~ say 玉米大人驾到!
条件判断和逻辑在后面会继续讨论,感兴趣的可以跳跃阅读。
MP过程
条件判断很常用,但过多的条件和过长的指令分支会让构建和维护较为滑稽。在以实体为主的指令设计中,我们可以打标签来“标记”符合条件的实体,通过此标签来进一步操作实体。操作完就删除标签,取消“标记”。这样优点很多,比如可以减少条件判断的数量,降低指令执行中的不可预测性,防止标签泄露,还有愉悦我们的心情之类。
因为我的世界中指令单线程执行,相邻连锁命令方块之间指令的执行几乎没有时间间隔,所以这种方法有一定的封闭性。为了方便,我们可以给这个方法起个名。在本文中,叫做“过程”。一串命令方块,从打上标记到取消标记的部分,可以称作一个“过程”。
为了区分这种我的世界指令的“过程”和其他各种过程,我们可以管这种我的世界指令中的“过程”叫做MP过程,“MP”是“我的世界指令设计模式(Minecraft command design Pattern)”的简写。打上的标记就可以叫MP过程标记
1 | # 例子 |
第 2 节
本节最后修改于 2022 / 11 / 12
MP事件
我们上一节讲到MP过程是在一串连锁命令方块中,通过给符合特定条件的实体打标签,使用此标签对其操作,最后去掉标签来结束MP过程——一共三步来对实体更方便安全地进行操作。
我们同时还讲到,选择器体现了一种条件判断的思想。MP过程的第一步打标签,就需要选择器进行判断。
那么我们换个视角来看,能不能把这套流程解释为“判断有无符合特定条件的实体,如果有则进行操作,否则不进行操作”呢?当然是可以的。于是对于一个接在循环命令方块后面——不停地循环执行——的MP过程,它就可以被看作是不停地判断实体,判断成功时执行操作。
在计算机程序界中(在其他地方也有可能),对于一个不停判断条件,一旦条件满足就进行动作的东西,我们可以说它属于一种“事件监听器”;不停判断条件就是在“监听”;判断的条件即为“事件”;而进行的动作,名字就多了,比如“回调”“响应函数”“钩子函数”等等。于是我们可以说,当这个东西“监听”到“事件”发生时,才执行“响应函数”;若“事件”没有发生,就不执行“响应函数”。十分的形象。
仿照计算机程序界(其他地方或许也有)的概念,我们可以管一个不停检测条件的MP过程叫做“事件监听器”,于是我们便成功把MP过程套到了一个简单易懂的“事件”模式中。类似的,我们可以把这种事件模式称为MP事件模式或简称MP事件。当我们的MP过程“监听”到“有特定条件的实体出现”这一“事件”发生后,便执行“响应指令”;否则就不执行。
MP事件的实现
听起来这个概念貌似还挺容易理解的,那么如何实现呢?我们跟着上面走,搭一个MP过程的命令方块串试试。
首先这个要不停地检测来“监听”,那么最好是接在一个循环命令方块上面。
好,接下来“监听”是判断事件有没有发生。我们讲过条件判断有两种形式,那就是选择器或者有条件命令方块。任选一个,比如就选择器吧,最简单的实现就是给符合条件的人打标签,就是MP过程的标记。那么这个命令方块串的第一个指令就是给人打标签。
然后监听完了,就是执行响应指令。这里我们发现一个问题,如果我们直接把无条件连锁命令方块的指令接在命令方块串后面,那么不论检测事件发生没发生,指令都会执行。那怎么办?我们发现有两个办法,一种是execute判断一下,如果有人有MP过程的标记,那就执行。一种是改成有条件连锁命令方块。
响应指令一般不止一个啊,我们还得继续加指令是吧。再加无条件连锁命令方块,发现又出现了上一次的问题:要不然用execute判断,要不然改成有条件命令方块。于是有条件命令方块的缺点在此显露了:这时的有条件并不是“第一个命令执行成功”,而是“第一个命令执行成功后,第二个命令执行,并且成功”。也就是说若第二个命令没成功,即使第一个命令成功了,事件发生了,响应也不会继续执行。所以我们一般使用execute判断。
其他的响应指令都一样了,最后我们别忘了MP事件是特殊的MP过程,还要把标记给去掉,结束这个MP过程,防止干扰下一次检测。
1 | # 例子 |
MP事件的简单用途
最后我们发现这个所谓MP事件的响应指令前面全都有一个execute!你可能会说这“一点也不优雅,‘简单’个鸡毛!”。
是的,我的世界不存在真正的事件模型,这只是我们通过奇技淫巧搞出来的类似事件的东西。本系列所讲述的与其说是设计模式,倒不如说是设计技巧。有的时候这些技巧不仅不会让编写指令变简单,还会增加指令的复杂度。
那么什么时候这些技巧会让指令变简单呢?对于MP事件,比较简单的用法是将相同的条件判断串到一串上。这样子不仅提高了可维护性,还减少了命令方块串的数量,是简单的指令复用。
1 | # 例子 |
这样子就不需要分别为重工业玉米和其他不是帅比的人分别弄个抬头检测了。要是之后还有需要抬头的指令,直接在中间加命令方块就可以了。如果条件和指令又变得更多,可以在这个MP事件里再串一个MP过程,进行条件判断的复用。
第 3 节
本节最后修改于 2022 / 11 / 12
MP事件的主动触发
我们上一节说了MP事件。事件不止有玩家扔雪球或者抬头这一类,我们还可以用指令主动触发MP事件来执行响应。
1 | # 例子 |
上面这个例子我们可以发现,监听器1所监听的事件是“有人的playTime
积分为999999
”,于是我们可以通过指令设定玩家的积分项目来触发此事件——上面例子中就是一个每秒加分的指令会触发此事件。
MP事件实现的异步
我们讲可以用指令来主动触发MP事件,那么MP事件能不能在响应指令里触发MP事件呢?当然也是可以的。于是我们可以借此搞出异步指令。
1 | # 例子 |
上面的例子中,对于一个夸奖完了的帅比,我们是不知道他什么时候会抬头的,所以抬头之后的动作算作抬头这个“异步操作”的“回调指令”。通过MP事件之间的相互触发,我们能够把回调指令放到MP事件里,可以叫做“回调事件”,当异步操作完成后触发回调事件完成回调指令。
有些眼尖的读者可能会发现例子中的回调指令不需要放到回调事件的命令方块串中,直接放进抬头事件的命令方块串里也可以。这是没错的。不过异步操作不止玩家抬头,还可以是某时刻的红石信号、某个不知名指令操作后的结果等。这里是出于演示目的,将回调指令单独放到回调事件里。虽然是这么说,但是建议实际编写指令时也把回调指令单独放到回调事件里,方便指令的维护。
第 4 节
本节最后修改于 2022 / 11 / 12
MP模块
上面我们说MP事件能使用指令来主动触发,这意味着我们可以从不同地方通过触发事件的方式执行同一个响应指令。这就是以MP事件为基础的指令复用。
1 | # 例子 |
上面这个例子中,监听器1
中的响应指令同时被老玩家提示功能和重工业玉米的大喇叭功能通过事件触发的方式使用。这个时候我们可以把监听器1
单独叫做一个MP模块,比如可以叫提示老玩家模块
,那么激活监听器1
来执行响应指令就可以称作调用MP模块。
如果想再提高模块的内聚性,可以像这样。
1 | # 例子 |
于是不仅playTime=999999
可以触发事件,只要想的话,任何条件都可以作为触发事件的方式。因为我们将监听器1
作为MP过程的第一步——打标签——的那个命令方块从监听器1
移动到了其他触发这个事件的命令方块串上,相当于由其他命令方块串来激活监听器1
这个MP过程。所打的这个标签可以被称作MP模块的入口标签。
带参数调用MP模块
调用MP模块一般来说就是给实体加标签或者设置积分什么的来激活MP事件,这就是说我们调用MP模块实际上是给出了一个实体,MP模块能够通过选择器获取到这个实体。所以我们如果想给MP模块传递参数,我们可以在这个实体上增加标签、计分板分数等。
1 | # 例子 |
上面的例子中话编号
计分项目是给一句话模块
的参数入口。改变实体的话编号
项目,调用给一句话模块
后得到的结果也不一样。但是和真正的函数与参数的关系不同,这里的所谓“参数”类似于“绑定在全局作用域”;而模块也不像真的在接受参数,更像是在访问特定的“全局变量”来获得执行信息。我们可以管MP模块的参数叫MP模块参数。
第 5 节
本节最后修改于 2022 / 11 / 12
MP模块的递归
上节中我们讲了带参数调用MP模块。有人立马会想到:可调用 + 带参数 = 能递归。
——递归是什么?其实就是自己调用自己。那不就成死循环了吗?其实不是,因为递归到了一定程度会停止继续递归,这个程度叫边界条件。之所以在某种程度上可以说:可调用 + 带参数 = 能递归,是因为“可调用”意味着能自己调用自己;“带参数”意味着能判断边界条件来防止变成死循环——
调用MP模块实际上就是满足某个条件,而MP模块会循环检查这个条件,根据其是否满足来决定是否执行命令。所以MP模块的递归其实就是在执行完一次命令后,条件仍然满足自己再执行一次。直到MP模块遇到边界条件才会修改实体使其不满足激活条件。
但是由于MP模块的参数是“全局”的,而基岩版我的世界中又没有二维的计分板没法实现计分板堆栈,MP模块调用自己会丢失现在的计分板参数,所以只能实现尾递归或者用一些奇技淫巧。
定量给东西
定量给东西可以说是最简单的递归形式了。它不需要返回计分板值,而且貌似很有用,可以说是非常经典了。
1 | # 例子 |
定量给东西有二分法的实现,感兴趣的读者可以了解一下,在此我们就不对其深入研究了。
等差数列
除了给鸡蛋这种简单的没有返回值的MP模块,修改计分板这种有返回值的MP模块更复杂一些。例如获取数列特定项的MP模块。
等差数列意思就是一个数列,任意相邻两项的差相同。例如 $1,3,5,7,9$ 或者 $5,4,3,2,1$ 。求等差数列是很好的一个尾递归的例子。
求数列第 $n$ 项,一般来说有两种方法。
一种是把 $n$ 代入到通项公式里。例如 $1,3,5,7,9$ 的通项公式为 $a_n=2n-1$ ,求第 $10$ 项就是 $2\times 10-1=19$ 。
但是世界上有很多数列并没有通项公式,这时可以使用第二种方法叫递推公式。递推公式就是根据前面项得出后面项的公式。例如 $5,4,3,2,1$ 的递推公式就是 $a_n=a_{n-1}-1$ ,求第 $10$ 项就得求第 $9$ 项,就得求第 $8$ 项……直到我们发现第 $1$ 项是 $5$ ,那么第 $2$ 项就是 $4$ ,第 $3$ 项就是 $3$ ……最终得到第 $10$ 项就是 $-4$ 。
这就是一种递归,我们在不断的调用递推公式,只不过每次的参数都不一样:第一次我们代入的 $n=10$ ,发现 $a_{n-1}$ 也就是 $a_9$ 的值不知道!于是我们调用第二次,这时 $n=9$ ,发现 $a_8$ 的值又不知道;于是第三次, $n=8$ ……云云。直到 $n=1$ 时,就是边界条件,我们知道 $a_1$ 就是 $5$ ,所以没有不知道的值了,就不用再调用什么东西了。这时别忘了,之前调用的公式还等着我们的值呢!于是我们开始返回结果。第一次返回 $a_1$ 的值是 $5$ ,于是 $a_2=a_1-1=4$ ,继而 $a_3=a_2-1=3$ ……直到 $a_10=a_9-1=-4$ 。
我们要练习MP模块的递归,于是我们就假装不知道等差数列的通项公式吧。
1 | # 例子 |
这个模块的调用方法:给实体设置AP_n
分数。获取返回值方法:当实体AP_n=0
时,获取实体AP_value
的分数并重置AP_value
和AP_n
。
斐波那契数列
用点奇技淫巧也可以做出不是尾递归的递归。
众所周知,斐波那契数列 $\{\ Fi_n\ \}$ 是一个数列,每一项等于前两项之和,而且 $Fi_0=Fi_1=1$ 。例如前 $7$ 项就是 $1,1,2,3,5,8,13$ 。想要知道第 $n$ 项就必须得知道第 $n-1$ 和第 $n-2$ 项。这里我们用MP模块的递归(with奇技淫巧)来实现。
不难发现,斐波那契数列不能只通过上一项来求出这一项,所以无法简单地写成尾递归的形式。这里我们采用的奇技淫巧是使用两个计分板,一个存储上一项的值,一个存储上上一项的值。我们将这一项求出来后,将这一项的值覆盖到存储上上一项的计分板上。这样子对于下一次调用模块来说,这两个计分板仍然是一个存储上一项,一个存储上上一项。根据调用次数的奇偶,两个计分板,哪个是上一项哪个是上上一项也不同。这是使用有限的计分板把结果存储了起来。
1 | # 例子 |
这个模块的调用方法:给实体设置Fi_n
分数。获取返回值方法:当实体Fi_n=0
时,获取实体Fi_value
的分数并重置Fi_value
和Fi_n
。
第 6 节
本节最后修改于 2022 / 11 / 12
MP模块的无限制递归猜想
我们上一节讲过,
但是由于MP模块的参数是“全局”的,而基岩版我的世界中又没有二维的计分板没法实现计分板堆栈,MP模块调用自己会丢失现在的计分板参数,所以只能实现尾递归或者用一些奇技淫巧。
现在我表示,这段话不一定正确,因为最近我找到了计分板堆栈可能的实现方法。
计分板堆栈是什么,不就是一大堆计分板吗?
这是原来的计分板结构:
1 | |计分板A|计分板B|计分板C ... |
这是我们希望的计分板结构:
1 | |计分板A|计分板B|计分板C ... |
我们可以发现什么?
首先,我们希望的大概是不可能实现的,因为基岩版中计分板值不可能是个数组——以及Java版我没了解过。
但是,我们还可以发现,其实我们需要的不是一个数组,而是一个“方格”!
没错,实际上我们需要的是已知横坐标(计分板)和纵坐标(实体)就可得到的唯一一个数组。数组的形式不重要,重要的是我们能根据一个计分板和一个实体确定这一个数组。
到这里就明了了。我们只需要创造一大堆额外的实体,然后让这堆实体都和“实体1”、“实体2”等已有的实体联系起来,就可以了。我们就能根据一个确定的已有实体(也就是实体1、2、3)得到一大堆与其联系的实体。这个时候再来一个确定的计分板,我们就能得到已知实体的分数,及其联系着的实体们的分数。这就是传说中的根据已有实体和计分板,得到一大堆分数,就能达到目的,实现计分板堆栈。
主要的技术难点就在于,如何把一大堆实体同一个确定的已知实体联系起来。
第 7 节
本节最后修改于 2022 / 11 / 12
搞个雪球菜单
之前几节讲了条件判断、MP过程、MP事件、MP模块等设计模式。只是讲的话可能没有什么感受,而一个雪球菜单基本上能把简单条件判断、MP过程、MP事件和MP模块都应用到,于是我们可以试着从雪球菜单入手,试一试这些设计模式。
作为演示,这个雪球菜单的功能可以是:
- 扔出雪球显示菜单,再扔雪球切换选项。抬头确认,低头返回上一级菜单或者取消;
- 菜单有三个选项:切换创造/生存、索要雪球、夸赞玉米菜单;
- 选中切换创造/生存后,创造变成生存,生存变成创造;
- 选中索要雪球后,得到 $1$ 个雪球。再选中就给 $2$ 个。选中的第 $n$ 次给 $n$ 个雪球;
- 夸赞玉米菜单又有三个选项:玉米好帅、玉米真帅、玉米很帅。选项被选中后,在聊天栏提示对应选项。
架构
在编写指令前,先简单思考一下雪球菜单的架构。
- 雪球菜单通常有三个操作:扔雪球、抬头、低头。这意味着我们可能需要搞三个MP事件监听器来对用户的操作进行响应。
- 雪球菜单通常有显示在actionbar的title作为界面。这告诉我们可能需要搞一个MP模块用以显示界面。这里我们考虑一套菜单有多少选项就有多少title指令,还有不止一套菜单,就是所谓“多菜单”。不难发现对于不同选项或者不同菜单,实际上都是执行不同的title,所以可将菜单也看作是一种选项,这样子就是“单菜单”了。这个MP模块当然不能给谁都显示一样的东西,所以至少还需要一个MP模块参数用来控制显示的界面,这里可以选择计分板。
- 雪球菜单通常有各种功能。我们可以把每个功能都做成MP模块,到时候只要让雪球菜单调用就可以了。
- 雪球菜单通常有各种提示。比如“使用功能成功”、“您没有权限”、“已关闭菜单”等等。这些大部分是在使用完功能再提示。然而我们发现这是异步操作,而且那么多功能MP模块总不能给每个都写一个提示。所以可以把提示抽离为MP模块,再用一个计分板作为MP模块参数控制提示内容。
思考完这些之后,我们发现这个雪球菜单大体可以分成四部分:用户事件监听器
、界面模块
、各种功能MP模块和提示模块
。还有两个作为MP模块参数的计分板,一个是界面模块
的,一个是提示模块
的——为了方便可以合二为一。
变量命名
为了方便维护,计分板和标签应该有一套命名规范。
因为这是雪球菜单,所以我们可以在所有标签和计分板前面加个标识SB_
表示这是Snow Ball menu相关标签和计分板。类似的,CM_
表示Change Mode、GS_
表示Give Snowball,HS_
表示yǜ mǐ Hǎo Shuài。
然后可以在MP过程标签最后增加-ing
用以区分,表示这个标签不可用在MP过程之外。
对于MP过程内的MP过程标签,可以在尾部使用_
追加信息来命名。例如SB_A-ing_B-ing
表示这是过程A
里的过程B
的标签。
有读者可能会好奇,有的MP模块也相当于一个MP过程,它们的入口标签就相当于MP过程的过程标签,为何末尾却不带-ing
呢?带-ing
主要是为了让人注意到这是个封闭标签,来避免在MP过程之外使用,保证MP过程的封闭性。但是一般来说调用MP模块的命令并不在MP模块命令方块串上,这还意味着在添加标签到执行MP模块之间一般会有一块空档,入口标签是暴露出来的。入口标签本来就没有封闭性,也就不需要加后缀。
计分板
由于这个计分板控制着提示和界面,我们可以命名这个计分板为SB_ui
。
我们知道这个计分板需要控制界面和提示,所以可以规定一下计分板分数的范围。 $[0,9]$ 显示主菜单的界面、 $[10,19]$ 显示夸赞玉米菜单的界面、 $[-10,-1]$ 显示提示,没有分数值的人就不显示。虽然不是每个界面都有十个选项,但是给每个界面都分配十个分数可以方便以后给菜单再添加界面。
各种功能MP模块
在编写雪球菜单前,我们可以将雪球菜单需要的各种功能MP模块先编写完。雪球菜单有切换模式模块
、索要雪球模块
和夸赞玉米模块
三个主要模块。如果指令不太能看懂,可以只看代码块下方介绍的调用方法。在功能执行完之后,需要再反过来通过既是MP模块参数同时也是入口计分板的SB_ui
来调用雪球菜单的提示模块
,告诉玩家执行成功。
1 | # 切换模式模块 入口标签:“CM_do” |
这个MP模块用来给人切换模式。如果一个人有入口标签CM_do
,那么如果他是创造,他就会变成生存;如果他是生存,他就会变成创造。
1 | # 索要雪球模块 入口标签:“GS_do” |
这个MP模块用来索要雪球。如果一个人有入口标签GS_do
,那么这是他第几次有这个标签,他就会得到几个雪球。
1 | # 夸赞玉米模块 入口计分板:“HS_num” |
这个MP模块用来夸赞玉米。如果一个人的入口计分板HS_num
为1
,那就提示“玉米好帅”;如果为2
就提示“玉米真帅”;为3
就提示“玉米很帅”。
这样子,我们就把所有需要的功能MP模块都实现了。
第 7.5 节
本节最后修改于 2022 / 11 / 12
搞个雪球菜单·续
一节写太多读起来不太舒服,就分了一节,我们接着来搞。
上一节是弄完了除了主体指令之外的所有东西,那么这一节我们就来弄主体指令。
用户MP事件监听器
我们知道一共有三个操作:扔雪球、抬头、低头。那不用多说先来三组监听器!
1 | # 扔雪球监听器 |
监听器框架有了,然后我们可以开始一个一个往里填指令了。
首先是扔雪球监听器。这个雪球菜单的要求是扔一次菜单就出现,再仍一次切换选项。在最后一个选项切换的时候返回开头。
1 | # 扔雪球MP过程内的指令 |
理论上我们的扔雪球监听器就这么简单的写完了。
扔雪球后面的MP事件是抬头。我们知道在不同时候抬头会有不同的效果,例如在选中索要雪球选项时抬头会调用索要雪球模块
、在选中夸赞玉米菜单时抬头会进入夸赞玉米菜单等等。
1 | # 抬头MP过程内的指令 |
最后还差一个低头的监听器。低头主要就是取消或者返回上一页菜单,那就比较简单了。
1 | # 低头MP过程内的指令 |
提示MP模块
我们从上面写到下面,发现好像只需要两种提示,一种是成功提示,一种是取消提示。
这倒是好事,至少写的指令挺少的。
1 | # 雪球菜单提示模块 入口计分板:“SB_ui” |
界面MP模块
于是到了现在,万事俱备只欠东风。我们已经把所有逻辑都写完了,就差提供一个界面了。
由于我们的逻辑和界面是分开的几个MP模块,所以我们只需要给SB_ui
不同的人显示不同的界面就可以,不需要再关心逻辑。
1 | # 雪球菜单界面模块 入口计分板:“SB_ui” |
全部指令
1 | # 切换模式模块 入口标签:“CM_do” |
1 | # 索要雪球模块 入口标签:“GS_do” |
1 | # 夸赞玉米模块 入口计分板:“HS_num” |
1 | # 扔雪球监听器 |
1 | # 雪球菜单提示模块 入口计分板:“SB_ui” |
1 | # 雪球菜单界面模块 入口计分板:“SB_ui” |
虽然我不是特别了解雪球菜单,不知道这个命令量算多还是算少。但是可以知道的一点是,这个雪球菜单对维护和拓展开发非常友好。这是设计模式的价值。
这就写完了?其实不是。如果你搞出来了,你会发现在选择夸赞玉米菜单试着抬头后,还没等你反应过来,就已把玉米夸赞了!
究其原因是没有菜单之间的缓冲。解决方法是让一个人抬头后两秒之内不再计算抬头事件。如果读者有兴趣的话可以尝试自己编写出来,碍于篇幅这里就不详述了。
第 8 节
本节最后修改于 2023 / 03 / 28
条件的逻辑运算
我们第 2 节讲了简单的条件判断。那么众所周知,如果同时有多个条件的话,相互之间一般会存在逻辑。说到逻辑那主要就是与( $\mathrm{and}$ )、非( $\mathrm{not}$ )和或( $\mathrm{or}$ )了,除此之外还有与非( $\mathrm{nand}$ )、或非( $\mathrm{nor}$ )、异或( $\mathrm{xor}$ )和同或( $\mathrm{xnor}$ )。
逻辑运算的种类
与
与就是“若 A、B 都为真,则 A 与 B 为真;否则 A 与 B 为假”,也就是“……而且……”的意思。符号为 $\mathrm{and}$ 或者
&
。如果我又“吃饭”又“喝水”,那么“我吃饭而且喝水”这件事就发生了;如果“吃饭”、“喝水”至少一个没发生,那么“我吃饭而且喝水”就不能说是发生了。
非
非就是“若 A 为真,则非 A 为假;否则非 A 为真”,也就是“没有……”的意思。符号为 $\mathrm{not}$ 或者
!
。如果“我吃饭”这件事没发生,那么“我没有吃饭”这件事就发生了;如果我“吃饭”了,那么“我没有吃饭”这件事就没发生。
或
或就是“若 A、B 都为假,则 A 或 B 为假;否则 A 或 B 为真”,也就是“……或者……”的意思。符号为 $\mathrm{or}$ 或者
|
。如果我既没“吃饭”也没“喝水”,那么我就没有“吃饭或者喝水”;如果我“吃饭”和“喝水”这俩件事中至少干了一件事,那么就可以说我“吃饭或者喝水”。
与非
与非就是“若 A、B 都为真,则 A 与非 B 为假;否则 A 与非 B 为真”,也就是“没有……又……”的意思。符号为 $\mathrm{nand}$ 。
如果我又“吃饭”又“喝水”,那么“我没有吃饭又喝水”就没发生;如果“吃饭”、“喝水”至少一个没发生,那么“我没有吃饭又喝水”。
可以看出
$$A \ \mathrm{nand} \ B=\mathrm{not}\ (A\ \mathrm{and}\ B)$$或非
或非就是“若A、B都为假,则A或非B为真;否则A或非B为假”,也就是“没有……也没有……”的意思。符号为 $\mathrm{nor}$ 。
如果我既没“吃饭”也没“喝水”,那么我就“没有吃饭也没有喝水”;如果我“吃饭”和“喝水”这俩件事中至少干了一件事,那么我就不是“没有吃饭也没有喝水”。
可以看出
$$A\ \mathrm{nor}\ B=\mathrm{nor}\ (A\ \mathrm{or}\ B)$$异或
异或就是“若A、B都为真或都为假,则A异或B为假;否则A异或B为真”,也就是 “……和……不同”。符号为 $\mathrm{xor}$ 。
如果我“吃饭”,但不“喝水”;或者没“吃饭”,但“喝水”了,那我“吃饭和喝水不同”这件事就发生了;如果我“吃饭”又“喝水”,或者两个都没干,我就不是“吃饭和喝水不同”。
可以看出
$$ \begin{aligned} & \quad \ A \ \mathrm{xor} \ B \newline &=(A\ \mathrm{nand}\ B)\ \mathrm{and}\ (A\ \mathrm{or}\ B)\newline &=(\mathrm{not}\ (A\ \mathrm{and}\ B))\ \mathrm{and}\ (A\ \mathrm{or}\ B) \end{aligned} $$
同或
同或就是“若A、B都为真或都为假,则A同或B为真;否则A同或B为假”,也就是 “……像……一样”。符号为 $\mathrm{xnor}$ 。
如果我“吃饭”又“喝水”,或者两个都没干,就可以说我“吃饭像喝水一样”如果我“吃饭”,但不“喝水”;或者没“吃饭”,但“喝水”了,那就并非“吃饭像喝水一样”。
可以看出
$$\begin{aligned} & \quad \ A\ \mathrm{xnor}\ B\newline &=\mathrm{not}\ (A\ \mathrm{xor}\ B)\newline &=(A\ \mathrm{and}\ B)\ \mathrm{or}\ (A\ \mathrm{nor}\ B)\newline &=(A\ \mathrm{and}\ B)\ \mathrm{or}\ (\mathrm{not}\ (A\ \mathrm{or}\ B))\newline \end{aligned}$$
由此可以看出,与、非、或、与非、或非、异或和同或这七种逻辑,最后都可以用与、非和或这三种逻辑组合得到。
只要实现了与、非和或这三种逻辑,我们就能实现全部七种逻辑。
那么我的世界中能不能实现与、非和或呢?
我的世界实现逻辑运算
与
命令方块中的与是最容易实现的。在第一节中我们讲条件判断的形式一般是有条件命令方块和选择器。有条件命令方块的实现很简单,只需要把两个有条件命令方块接在一起就可以了。
1
2
3
4
5
6# 例子
# 当重工业玉米的背包里同时有tnt和床时,提示“重工业玉米好帅!!”
[~,~,~,~] clear @a[name=重工业玉米] tnt -1 0
[+,L,+,0] clear @a[name=重工业玉米] bed -1 0
[+,L,+,0] say 重工业玉米好帅!!选择器的实现就更简单了,在方括号中使用逗号把两个条件连接起来就可以。
1
2
3
4# 例子
# 当有一个人叫重工业玉米且有熊孩子标签时,提示“重工业玉米好帅!!”
[~,~,~,~] execute @a[name=重工业玉米,tag=熊孩子] ~~~ say 重工业玉米好帅!!上面是两个条件都为有条件命令方块或都为选择器时的与。
那如果一个条件是有条件命令方块,另一个条件是选择器呢?貌似这两种形式的条件没法通过简单的方式组合到一起,但是他们之间可以相互转化。我们可以将其都转化为选择器:
1
2
3
4
5
6
7# 例子
# 当重工业玉米有熊孩子标签,背包里还有tnt时,提示“重工业玉米好帅!!”
[~,~,~,~] clear @a[name=重工业玉米] tnt -1 0
[+,L,+,0] tag @a[name=重工业玉米] add tntWith //将有条件命令方块转化为标签
[+,L,-,0] execute @a[name=重工业玉米,tag=熊孩子,tag=tntWith] ~~~ say 重工业玉米好帅!!
[+,L,-,0] tag @a[tag=tntWith] remove tntWith也可以都转化为有条件命令方块:
1
2
3
4
5
6# 例子
# 当重工业玉米有熊孩子标签,背包里还有tnt时,提示“重工业玉米好帅!!”
[~,~,~,~] clear @a[name=重工业玉米] tnt -1 0
[+,L,+,0] testfor @a[name=重工业玉米,tag=熊孩子] //将标签转化为有条件命令方块
[+,L,+,0] say 重工业玉米好帅!!看起来都转化为有条件命令方块可以少用一个命令方块?别忘了我们第1节时提到,可以用
execute
代替testfor
:1
2
3
4
5# 例子
# 当重工业玉米有熊孩子标签,背包里还有tnt时,提示“重工业玉米好帅!!”
[~,~,~,~] clear @a[name=重工业玉米] tnt -1 0
[+,L,+,0] execute @a[name=重工业玉米,tag=熊孩子] ~~~ say 重工业玉米好帅!!这样子就又可以少用一个命令方块。于是我们可以说找到了两个条件类型不同时的与。
至此,全部的与我们都找到了实现方法。
非
命令方块中的非一般通过选择器来取反——也就是取补集——来得到。
1
2
3
4# 例子
# 当有一个人叫重工业玉米且没有熊孩子标签时,提示“重工业玉米太帅了!!”
[~,~,~,~] execute @a[name=重工业玉米,tag=!熊孩子] ~~~ say 重工业玉米太帅了!!对于有条件命令方块的非,可以通过设置一个临时标签来使用选择器取反
1
2
3
4
5
6
7# 例子
# 当重工业玉米的背包里没有tnt时,提示“重工业玉米太帅了!!”
[~,~,~,~] clear @a[name=重工业玉米] tnt -1 0
[+,L,+,0] tag @a[name=重工业玉米] add 熊孩子
[+,L,-,0] execute @a[name=重工业玉米,tag=!熊孩子] ~~~ say 重工业玉米太帅了!!
[+,L,-,0] tag @a[tag=熊孩子] remove 熊孩子或
我的世界中没有直接的或。想要实现或,一般是给两个条件分别搞个命令方块,使满足任意条件就满足第三个条件。再判断第三个条件来实现或。
一般来说,有条件命令方块只能被自己的上一个命令方块满足;而选择器条件由于操作的是实体,可以被任意多个命令方块满足。因为上文“第三个条件”需要能被两个命令方块满足,所以我们的第三个条件只能使用选择器的形式。
1
2
3
4
5
6
7
8# 例子
# 当重工业玉米有熊孩子标签或者背包里有tnt时,提示“重工业玉米怎么这么帅!!”
[~,~,~,~] tag @a[name=重工业玉米,tag=熊孩子] add needSay
[+,L,-,0] clear @a[name=重工业玉米] tnt -1 0
[+,L,+,0] tag @a[name=重工业玉米] add needSay
[+,L,-,0] execute @a[tag=needSay] ~~~ say 重工业玉米怎么这么帅!!
[+,L,-,0] tag @a[tag=needSay] remove needSay上面例子中,两个条件分别是“有熊孩子标签”和“背包里有tnt”,而这两个条件满足时都会给重工业玉米增加
needSay
标签来满足第三个条件“有人带needSay
标签”。通过检测第三个条件有没有被满足,可以实现或。
第 9 节
本节最后修改于 2023 / 07 / 25
条件的进阶逻辑运算
上节讲了与、非和或的实现,我们还知道世界上有与非、或非、异或和同或等逻辑,那么能不能在我的世界中通过与、非和或组合出这些呢?
与非
$$A \ \mathrm{nand} \ B=\mathrm{not}\ (A\ \mathrm{and}\ B)$$
1 | # 例子 |
看起来我们就轻松地实现了与非。
或非
$$A\ \mathrm{nor}\ B=\mathrm{nor}\ (A\ \mathrm{or}\ B)$$
1 | # 例子 |
或非的实现也比较简单。
异或
$$
A \ \mathrm{xor} \ B =(\mathrm{not}\ (A\ \mathrm{and}\ B))\ \mathrm{and}\ (A\ \mathrm{or}\ B)
$$
1 | # 例子 |
这就是异或的实现。
发现在指令编写到一定程度时,有条件命令方块基本不会再出现了,我们会一直以选择器的形式进行条件运算。选择器一般的用法是标签。那么对于着繁多的标签和运算,怎么才能防止自己犯迷糊呢?可以使用表达式作为标签的名称,来表示含义,这样可以使运算的“操作数”和“返回值”都变得非常清晰,甚至让我在计算中有一丝享受的感觉。
原始的思想是:使用大写字母,例如A
、B
、C
、D
来表示条件;使用and
、or
和not
来表示运算符。这样子如果有一个人带标签AandB
,我们就能清晰的知道这个人的身份——既满足 $A$ 条件,也满足 $B$ 条件。那么既不满足 $A$ 条件,也不满足 $B$ 条件的人所应该带的标签就应该是not(AorB)
。
以此类推,(非(A与B))与(A或B)的人应该就是(not(AandB))and(AorB)
……真的吗?我们发现这个更简单的写法是AxorB
。对于不同的表达式,结果却相同,那我们还不如用结果来命名。
我们知道对于 $A$ 和 $B$ ,他们只可能为两个值。我们可以称其 $真$ 或 $假$ ,也可以说是 $1$ 或 $0$ 。既然如此,对于 $A$ 和 $B$ 的运算的结果也一定是有限的。 $A\ \mathrm{and}\ B$ 随机搭配,可能有 $1\ \mathrm{and}\ 1$ 、 $1\ \mathrm{and}\ 0$ 、 $0\ \mathrm{and}\ 1$ 、 $0\ \mathrm{and}\ 0$ 四种可能,结果就是 $1$ 、 $0$ 、 $0$ 、 $0$ 四种可能。我们就可以说 $A=[1,1,0,0]$ , $B=[1,0,1,0]$ , $A\ \mathrm{and}\ B=[1,0,0,0]$ 。那么对于结果相同的标签AandB
、(AxorB)andA
等等就都可以改写成1000
。
这种并列计算手工算有点麻烦了,我为此搞了一个在线并列计算器。点击左下角“导入基础配置”后将列宽设置为4,在右边表达式栏里即可输入表达式。比如A and B
或者~not (A or B)
等。点击计算即可得到结果。
1000
有点长,我们就可以用十六进制表示为8
。于是标签就可以叫T_8
,仅仅三个字符的标签就可表示任意一类人群。
在命令方块串的最后,我们别忘了把这些临时标签都去掉。
逻辑运算对照表
读者可自己尝试将标签T_0
~`T_F`都用指令实现出来,相信对上面段所介绍的逻辑运算会有更深的理解。
下方表格显示了标签T_0
~`T_F`分别包含和排除了哪些情况的实体,还有各个标签的表达式。
标签 | 运算结果 | 全部符合 | 只符合 A | 只符合 B | 全都不符 | 最简表达式 |
---|---|---|---|---|---|---|
T_0 | 0000 | 排除 | 排除 | 排除 | 排除 | 0 |
T_1 | 0001 | 排除 | 排除 | 排除 | 包含 | 非(A或B) |
T_2 | 0010 | 排除 | 排除 | 包含 | 排除 | (非A)与B |
T_3 | 0011 | 排除 | 排除 | 包含 | 包含 | 非A |
T_4 | 0100 | 排除 | 包含 | 排除 | 排除 | A与(非B) |
T_5 | 0101 | 排除 | 包含 | 排除 | 包含 | 非B |
T_6 | 0110 | 排除 | 包含 | 包含 | 排除 | (A或B)与(非(A与B)) |
T_7 | 0111 | 排除 | 包含 | 包含 | 包含 | 非(A与B) |
T_8 | 1000 | 包含 | 排除 | 排除 | 排除 | A与B |
T_9 | 1001 | 包含 | 排除 | 排除 | 包含 | (非(A或B))或(A与B) |
T_A | 1010 | 包含 | 排除 | 包含 | 排除 | B |
T_B | 1011 | 包含 | 排除 | 包含 | 包含 | (非A)或B |
T_C | 1100 | 包含 | 包含 | 排除 | 排除 | A |
T_D | 1101 | 包含 | 包含 | 排除 | 包含 | A或(非B) |
T_E | 1110 | 包含 | 包含 | 包含 | 排除 | A或B |
T_F | 1111 | 包含 | 包含 | 包含 | 包含 | 1 |
可以发现这个表具有上下对称性,这是因为对于任意十六进制数 $n$ ,都有
$$n =\ \sim (0\mathrm{x}F - n)$$
也就是说,对于任意 $n$ 来说, $15-n$ 就是对 $n$ 进行取反的得数。
我们可以验证一下,比如选择 $0\mathrm{x}3$ 来验证:
$$\begin{aligned} & \quad \ (\sim\ (0\mathrm{x}F-0\mathrm{x}3)) \newline &= (\sim 0\mathrm{x}C) \newline &= (\sim 0\mathrm{b}1100) \newline &=0\mathrm{b}0011 \newline &=0\mathrm{x}3 \newline \end{aligned}$$
可以发现是成立的。所以如果我们已实现了T_ $n$ ,那我们就不需从头实现T_ $(0xF - n)$ ,只要对前者进行非运算就能得到后者。
类似的情况还有很多。实现一个标签后就可用于计算另一个标签。使用上文中提到的并列计算工具或者直接手动进行演算,找到运算最少的方法,可以让标签的实现更加简单。
到现在为止我们一直在研究两个条件之间的运算。那么三个以上的条件怎么办呢?其实多个条件的运算就是多来几次两条件运算。只要弄懂了两条件运算,就能套到其他多个条件的运算中,一样解决。
第 10 节
本节最后修改于 2023 / 04 / 02
基于设计模式的指令生成
我们讨论了很多指令设计模式相关。
与计算机领域相似的,一旦我们得到了一个较为普遍的原理,我们便可以写入程序,大规模地使用。
于是,一个野心勃勃的计划被提出来了:我们可以使用更简洁的语言来描述指令,最终使用某种解析器搭配指令设计模式对其中的细节进行补全。
这个计划更通俗的描述,就是“指令生成”。
这个计划中的“更简洁的语言”学名叫做领域限定语言(DSL),为了方便,我们可以管它叫“代指令”。
由于我的世界指令语法十分拉跨,没有编程语言应有的大部分特征,甚至其本身就是一种通用性较弱的 DSL,所以我们不可能利用指令写一个解析器来实现代指令,而需借助外部的力量。
考虑到我们最终需要搞一个解析器,这种“外部力量”与实现解析器的编程语言就有很大关系了。
从灵活性的角度来讲,使用 C/C++ 作为代指令是不错的选择,甚至可能还有接轨 LLVM(一种适用于各种编程语言的黑魔法“解析器”)翻译成其他语言的光明前景。
但是从跨平台性以及本人的代码水平方面来讲,我觉得 JavaScript 是一个更好的选择。
代指令
由于本人十分抵触复杂的编译原理,代指令可以是由各种 API 构成的 JS 的一种内部 DSL。
听起来比较抽象,意思就是说代指令实际上不能算一种新的语言,而只是一种有特殊代码风格的 JS。
举个例子,比如我们如果想判断现在有没有实体,如果有的话就说“玉米真的好帅”,否则就“玉米实在太帅了”。
1 | // 如果代指令是一种基于 JS 的新的语言 |
1 | // 如果代指令是一种由 API 实现的 JS 的内部 DSL |
不难发现,上面的第二个示例中——也就是我们的做法——相较于第一个示例,更加的复杂、繁琐,也没那么易懂。
但是第二个示例可以直接作为 JS 运行,也就是说我们可以通过直接运行这个有特殊代码风格的 JS 来对其进行解析,而不是对其进行各种繁琐的各种静态分析。
于是我们就这样绕过了编译原理……实际上没有。
由于代指令的结构较为复杂,我们实际上还是要由代指令对 API 的调用生成抽象语法树(AST),再通过对 AST 的操作实现指令的生成。
相当于我们是只绕过了词法分析和语法分析。
解析器
知道了代指令的形式之后,我们可以着眼于另一重点——解析器。
上面已经提到,我们使用 JS 编写解析器。
实际上本人赶潮流,最近 TypeScript 很流行,我是用的 TS 写的解析器。
这些都不重要,重要的是解析器的基本架构。
运行代指令
这一部分帮助我们跳过了词法分析和文法分析,充当了对代指令进行分析的角色,所以这部分也可以叫做“分析”。
代指令中包含对 API 的调用,我们需要使用一些奇技淫巧完成这一步骤。
就拿本文中的MP模块做示例,每个MP模块实际上都可以看作是一个函数:有输入值,有返回值。
这意味着为了使结构更加清晰,我们也需要让代指令在声明函数后可以直接调用。
例如声明一个夸赞玉米MP模块,再调用它:1
2
3
4
5
6// 本人猜想,仅供参考
const func = Fn(() => {
Command.say('玉米太帅');
});
func();一个很显而易见的事实是,我们一定要让
Fn
这个 API 返回一个真正的函数,要不然我们func();
就会在运行时报错。对于 API 的设计,这里就不展开讲了。
实际开发中,发现最终得到指令的一个 AST 更方便接下来的工作。
所以 API 本质上是一堆对 AST 进行增删改查等工作的函数。“补全指令细节”
在得到指令的 AST 后,树上的各种条件分支、条件运算、函数声明等节点让人心惊胆战,我们根本没法通过这么花里胡哨多彩多样的 AST 直接生成对应的指令。
于是我们不得不对 AST 进行转译——这个步骤的正式名称叫做转译。我们在其中插入更多“平平无奇的命令节点”来代替各种花里胡哨的其他节点,最终把整颗花里胡哨的树压成两层的数组来表示每个命令方块组中的每个指令。
如何使用普通的命令节点代替这些结构性节点呢?
这便是本文中指令设计模式发挥作用的时候。
就像条件分支,本文中使用MP过程这一基于标签或计分板的封闭系统,而MP过程的特征是普遍的。
就如“套路”、“模板”一样,我们只需要根据对应的模式,把对应的东西套上去就可以了。除了本文所描述的各种设计模式,还可以提供些额外服务。
这里举个例子:有些条件分支的条件是一个命令是否成功运行,相对应的,条件也可以是一个函数是否运行成功。
根据我们的MP模块相关知识,这个条件实际上是一串命令方块是否全部运行成功。
一个简单的方法是将这串命令方块全弄成有条件命令方块,然后在这串命令方块最后插入一个给任意实体打标签的连锁命令方块。
检测到是否任意实体有这个标签,可以判断最后这个命令方块是否成功运行,也就是整串命令方块是否成功运行。
如此,我们便将一个条件分支节点转化为了一个打标签命令和一个检测标签的命令。但是,如果这串命令方块中已经有命令方块是有条件命令方块,我们又要怎么判断是否成功运行呢?
这种情况下,一个比较合理的解决方法是在有条件命令方块的前一个命令方块前面把命令方块串劈成两半,视作两个命令方块组,分别检测是否成功运行。
最终判断两个命令方块是否都成功运行,若是则整条命令方块串都成功运行。这虽然不属于本文所介绍的设计模式之一,但确实可以应用到程序中。
转译便是通过上述这些“指令设计套路”来对 AST 进行处理。
输出指令
当我们有了一个结构简单的转译过的 AST 后,实际上我们就得到了我们生成的指令。
我们唯一需要做的是把这个 AST 弄为人类理解的样式。虽然这个步骤不是最困难的,但这个步骤是最困难的。
让人理解命令方块是一个比较困难的东西,现在貌似也没有普遍的描述命令方块的标准。
图片等可视化的也是一种选择,但是太麻烦了。
实际上本人还有一个项目是命令方块图片生成器,如果能直接套用这个项目就很好,但是目前这个项目进度缓慢,估计是指望不了了。当然,还有一种方法是直接生成 OOC,但是目前基岩版还不能这么搞。
宣传
作为一个惊天地泣鬼神前无古人后无来者的看起来挺有用的玩意,却没有人关注,无疑是十分蛋疼的。
于是宣传便成为了比较重要的一个环节。
其实可以说我现在就是用一个章节对这个东西进行宣传——当然还是希望本章节确实对诸读者有一定启发性——所以大概意思就是这玩意我确实搞起来了。
项目是 GPL-3.0 协议开源的,大家可以访问代码仓库,一起来搞这个项目。
相对于手动使用设计模式进行重复性劳动,将设计模式套入程序中生成指令无疑是生产力的大进步。
在这个项目完成后,我希望可以借助这个项目代替我编写指令,我的注意力便可从“使用设计模式”移到“发现更多设计模式并应用到这个项目”。
我也希望诸读者可以试着采取我的这种方法。
如果大家都把使用设计模式的精力放到发现设计模式上,就像共产主义摆脱了低级劳动,那一定是《MC设计模式》的新世纪。