【TIS-100 攻略】第 6~7 关:序列生成器、序列计数器

本文首发于 B 站《TIS-100》文集(https://www.bilibili.com/read/readlist/rl626023)。原创不易,转载请注明出处。
第 6 关《序列生成器》(Sequence Generator)关卡展示

本关要求从 IN.A 和 IN.B 各读入一个数字,然后先输出其中较小的数字,再输出其中较大的数字,最后输出一个 0。
那么,我们的想法自然是:计算 B - A 的值,若该值 > 0,说明 B 比 A 大,需要依次输出 A、B、0,否则依次输出 B、A、0。
本关的难点在于:A 和 B 这两个输入量都是要使用两次的,一次用于计算差值,一次用于输出。而从 IN.A 和 IN.B 这两个输入口读到的数据都是一次性的,没法反复读。那么这一关里,我们必须要借助额外的寄存器保存从输入口读到的值,以便将来可以多次使用。
第一篇攻略里,我提到了这一点:
每个节点中还有一个 bak 寄存器,但是在游戏设定里,bak 寄存器不能通过 mov 指令读取或写入值,而且接下来要用到的各种算术指令也不能直接读取 bak 中的值。关于 bak 寄存器的使用方法,以后的攻略里我们会说到。 作者:ココアお姉ちゃん https://www.bilibili.com/read/cv19164674 出处:bilibili
那么,我们该如何往 bak 寄存器里存值,以及读取 bak 寄存器里的值呢?TIS-100 里提供了两条用于操作 bak 寄存器的指令:
保存指令 sav,它的作用是将 acc 里的值复制一份到 bak 中;
交换指令 swp,它的作用是将 acc 和 bak 寄存器互换。互换后再对 acc 寄存器进行读写操作,操作完毕后再执行一次 swp 换回来,就相当于对 bak 寄存器进行相应的读写操作了。
任何时候,我们都只能同时对 acc 和 bak 中的一个进行读写,而不能同时对两个寄存器进行读写。比如你想令 acc 加上 bak 的值,这需要你在读取 bak 的同时,将读到的值作为增量累加到 acc 里。即使有了 sav 和 swp 指令,这样的事情也是做不到的。
现在我们回到题目。接收 IN.A 的节点需要把值汇总到右边,我们利用 acc 可反复读写的特性,收到值后先放到 acc 里,再将 acc 里的值向右发两遍,这样就达到了多次发送同一个值的目的。
而接收 IN.B 的节点稍微有点麻烦,它的 acc 寄存器需要计算 B - A 的值,不像左边节点那么悠哉游哉。那么原始的 B 值就只能使用 bak 寄存器来存储了。
本题的代码如下:

IN.A 下方的节点要做的事情很简单,首先从 IN.A 读入 A 的值存入 acc(mov up acc),然后将 A 向右边发两遍(mov acc right, mov acc right),因为右边要两次使用这个量。
IN.B 下方的节点首先读入 B 的值并存入 bak 备用(mov up acc, sav),接下来减去左边发来的 A 值(sub left)。至此,该节点的 acc 寄存器里放的是 B - A 的值,而 bak 寄存器里放的是 B 的值。那么我们按照计划,判断 B - A 的值是否大于 0。若大于 0,则跳到第 9 行去执行(jgz 9)。
第 5~8 行是 B - A <= 0 时执行的代码。B - A <= 0 时,说明 B <= A,此时我们需要依次输出 B、A、0。这时候我们首先执行交换指令,将 bak 里保存着的 B 值换到 acc 里(swp),把换出来的 B 值往下送(mov acc down),紧接着把左边第二次发来的 A 值往下送(mov left down),送完后,强制跳到第 12 行(jmp c)把 0 往下送(mov 0 down)。
第 9~12 行是 B - A > 0 时执行的代码。B - A > 0 时,说明 B > A,此时我们需要依次输出 A、B、0。那么这里,我们改为先往下送 A(mov left down),再往下送 B(swp, mov acc down),最后往下送 0(mov 0 down)。
下面的两个节点都是纯传话用的,不解释(mov up down)。
点击左下角的【RUN】,稍等片刻,便会弹出结算界面:


第 7 关《序列计数器》(Sequence Counter)关卡展示

本关的 IN 口会源源不断地给你提供一些以 0 结尾的序列。每当你完整地收到一个序列,就要向 OUT.S 端口输出这个序列的总和,同时向 OUT.L 端口输出这个序列的长度。
本关的难点在于:节点需要配置多种功能:收到非 0 数字时,OUT.S 上方的节点需要在 acc 寄存器里加上该值,OUT.L 上方的节点需要令 acc +1;收到 0 数字时,两个节点都需要把各自的 acc 发给输出端口并重置。本关需要用到在上一篇攻略里最后提到的 jro 指令来操作,也就是根据外部输入来决定要执行哪个代码块。本关的代码如下:

最上方的节点纯粹往下传话,不解释(mov up down)。
中间的节点将 IN 发来的数字存入 acc(mov up acc),并判断它是 0 还是非 0。若是非 0,则跳转到第 5 行去执行(jnz 5)。这就导致了:收到非 0 时,执行的是第 5~6 行代码,向下发送一个 5,和当前的这个数字(mov 5 down, mov acc down);收到 0 时,执行的是第 3~4 行代码,向下发送一个 1 然后跳回开头(mov 1 down, jmp 1)。其实向下发送的 5 和 1 都是偏移量,对应着下方节点的两个执行不同任务的代码块。
然后我们看下方的和 OUT.S 通讯的节点。第一行用于等待上方的指示,看上方是叫我往下跳 5 行还是 1 行执行代码(jro up)。如果上方传的是 5,说明本轮从 IN 里收到的是非 0 值,我们往下跳 5 行,把这个值累加到 acc 里(add up),然后给右边传个 3(mov 3 right);如果上方传的是 1,说明本轮从 IN 里收到的是 0 值,我们往下跳 1 行,给右边传个 1(mov 1 right),然后把 acc 里的累加值向下传到 OUT.S(mov acc down)并清空 acc,准备迎接下一次累加任务(sub acc, jmp 1)。
我们注意到这个节点也在收到上方的指示后,也给右边的节点传了个 3 或 1。其实这两个数字是用作右边的 jro 指令的参数的,跟上方传给自己的 5 或 1 类似。这个节点也在用同样的方式控制着右边节点的行动。现在我们来看右边节点的代码。
首先也是等待左边节点的控制信号(jro left)。如果左边传的是 3,说明本轮从 IN 里收到的是非 0 值,我们往下跳 3 行,令序列长度 +1(add 1);如果左边传的是 1,说明本轮从 IN 里收到的是 0 值,我们往下跳 1 行,向 OUT.L 口输出当前序列的长度(mov acc down),并清空 acc,准备下一次计数任务(mov -1 acc, add 1)。这里我们没有使用 sub acc, jmp 1 这样的写法,而是先把 acc 置为 -1,然后再加上 1。这样我们就成功复用了收到非 0 值时执行的 add 1 这一行代码,省去了一行强制跳转到开头的代码。
点击左下角的【RUN】,稍等片刻,便会弹出结算界面:

优化运行速度
以上方案还有改进空间。我们注意到 IN 发来非 0 值时,中央节点会给下方节点发送两个值,一个是 5 的地址偏移,一个是当前收到的需要累加上的数字。对应的下方节点每次也都需要收两个数字,效率上有损失。能不能每次只传一个值呢?答案是可以的。首先,下方节点将 acc 初始化为一个足够小的负数,比如 -999。然后,平时收到非 0 值时,中央节点直接将该值往下传;而当收到 0 值时,中央节点改为向下方发送 +999,清除掉下方节点原先的 -999 偏置量。这样下方节点在累加的过程中,就会发现自己的 acc 突然变成正数了,那么就说明这个序列结束了。此时我们得到的就是没有 -999 偏置的正确答案,我们直接将该答案输出给下方的 OUT.S 即可。这样我们就做到了任何时候都只用一个数字进行通讯,无形间提升了效率。代码如下:

中央节点由 6 行代码缩减成了 4 行代码,因为少传了一个值,还少了一次跳转。中央节点的逻辑是,收到非 0 值时,将对应的值传给下方(mov up acc, jnz 4, mov acc down);收到 0 值时,改为给下方发送 999(add 999, mov acc down)。
下方节点的逻辑改动较大。首先将 acc 设置上 -999 的偏置(mov -999 acc),然后跳过第 3 行代码,直接到达第 4 行,从上方接收数值(jmp 4)。接下来,不论上方发了什么值,我都无脑加到 acc 里(add up)。如果 acc 仍是负数,则跳回到第 3 行给右边传个 3(jlz 3, mov 3 right),然后继续等待上方的累加信号,直到 acc 突然变成正数为止。acc 突然变成正数,就说明上方发送了最终的 +999 偏置修正量。此时我们给右边传个 1(mov 1 right),然后自己将本次正确答案发往下方的输出口(mov acc down)。
右边节点的代码和上一版方案完全一样,没有做任何改动。
点击左下角的【RUN】,稍等片刻,便会弹出结算界面:

运行时长由原先的 248 周期减少到了 214 周期。而且三项指标都到了直方图的最左端。所以我们又得到了一个十全十美的方案。
解锁成就 NO BACKUP
该成就的说明是 Solve SEQUENCE COUNTER without using the SWP instruction,要求不使用 swp 指令完成第 7 关。以上两版方案里都没有用到 swp 指令,所以你已经解锁这个成就了,不需要额外的攻略了。