【TIS-100 攻略】TIS-NET 第 15 关:字符终端

本文首发于 B 站《TIS-100》文集(https://www.bilibili.com/read/readlist/rl626023)。原创不易,转载请注明出处。
TIS-NET 第 15 关《字符终端》(Character Terminal)关卡展示

本关要求将读入的数字映射成一个 2x2 的字符画打印在屏幕上。要求读到 0 时强制换行,读到 1~4 时按照下面的规则在画布上画出一个字符画:

按照上一关的思路,本关我们很容易想到一种将输入量映射成四个颜色的值的打表法。但问题在于,本关的每个输入量都对应着 4 个输出量,简单粗暴地打表的话,需要 4x4=16 行代码,这还没有算上额外的无条件跳转带来的开销,一张表根本写不下。因此,本关必须要通过找规律的方法减少表的大小。我们注意到,这四个字符画有这样的规律:
仅当输入量为 3 时,左上角的像素才为 0(黑色),其余时候都为 3(白色);
仅当输入量大于 2 时,右上角的像素才为 3(白色),其余时候都为 0(黑色);
仅当输入量为 2 时,下方的两个像素才为 0、3(黑色、白色),其余时候都为 3、0(白色、黑色)。
我们按照这样的规律,依次提供对应字符画的四个像素的颜色,同时再特殊处理一下换行,本题就完成了。代码如下:

1 号节点把收到的 in 发给 3 号节点(mov up down)。然后我们来观察 3 号节点:
3 号节点收到 in 后(mov up acc),检查收到的数字是否为 0,
若不为 0 则跳到第 5 行执行(jnz 5)。
收到的数字为 0 时,要做的事情非常简单,给 4 号节点传一个 9 信号,强制让 4 号节点换行就结束了(mov 9 right)
(jmp 1)
收到的数字不为 0 时,首先给右边发送一个 1 信号(mov 1 right),然后根据以上规律计算出字符画左上角的颜色。
这里判断 in-3(sub 3)是否为 0,
成立则按顺序执行,不成立则跳到第 10 行执行(jnz a)。
判断成立时,按照规律,左上角的颜色是 0(黑色),我们将这个 0 传给 5 号节点(mov 0 down)
(jmp b)
判断不成立时,按照规律,左上角的颜色是 3(白色),我们将这个 3 传给 5 号节点(mov 3 down)。
最后,将 in-3 的值传给下方,由下方继续计算剩余 3 个像素的颜色(mov acc down),
并给 4 号节点再发送一个 1 信号(mov 1 right)。
然后看 3 号节点下方的 5 号节点:
5 号节点收到了 3 号节点发来的左上角颜色后,将其传给 6 号节点(mov up right)。
接下来计算剩下 3 个像素的颜色。由于剩下 3 个颜色都是和 2 这个输入量比大小的,所以我们将传来的 in-3 改为 in-2(mov 1 acc)
(add up)
首先是右上角的颜色:当输入量大于 2 时,跳到第 7 行执行(jgz 7)。
当输入量小于等于 2 时,右上角的颜色是 0(黑色)(mov 0 right)
(jmp 8)
当输入量大于等于 2 时,右上角的颜色是 3(白色)(mov 3 right)。
然后是下方两个像素的颜色:当输入量不为 2 时,跳到第 12 行执行(jnz c)。
当输入量为 2 时,下方的两个像素依次为 0、3(黑色、白色)(mov 0 right)
(mov 3 right)
(jmp 1)
当输入量不为 2 时,下方的两个像素依次为 3、0(白色、黑色)(mov 3 right)
(mov 0 right)至此,我们就将一个字符画里四个像素的颜色全部传给了右边的 6 号节点。
然后我们看一下 4 号节点。4 号节点是用来控制画笔的 (x, y) 坐标的。它的 acc 用来存储实时的 x 坐标,bak 用来存储实时的 y 坐标,初始值均为 0。
它首先会监听 3 号节点的信号(jro left)。
3 号节点发来 1 时,表示正常绘制。而且因为一个字符画有两行,所以每画一个字符画,3 号节点都会发来两个 1。此时我们将当前的 x(mov acc down)
和 y 坐标(swp)
发给下方的 6 号节点(mov acc down),
然后从上方的 2 号节点接收增量 Δy 和 Δx,加到 y 和 x 中(add up)
(swp)
(add up)这里的 2 号节点用了上一关的思想:用增量 Δy 和 Δx 来控制画笔坐标的移动。由于每个字符画都是 2x2 大小,因此画完第一行后,画笔向下移动一行,(Δx, Δy) = (0, 1);画完第二行后,画笔向上移动一行,向右移动三格,(Δx, Δy) = (3, -1)。如下图所示:




2 号节点正是提供这两组增量的无限流。画笔切换到当前字符画的第二行时,Δy = 1,Δx = 0(mov 1 down, mov 0 down);画笔切换到下一个字符画的位置时,Δy = -1,Δx = 3(mov -1 down, mov 3 down)。4 号节点也正是每画完一行就从 2 号无限流中取两个增量值加到 x, y 中,得到新的 x, y 值。
8. 4 号节点在改变了 x, y 值后,需要判断是否触发了换行。这里判断 x 是否等于 30,即 x - 30(sub 30)是否等于 0。
9. 不等于 0 时,说明 x 还没有到达行尾的 30 处,跳到第 14 行,加回一个 30,将 x 还原,准备再次落笔(jnz e, add 30);
10. 若 x - 30 等于 0,说明 x 到达了行尾的 30 处,此时要触发换行:将 y 加上 3(swp)
11. (add 3)
12. (swp)
13. 将 x 清零(mov -30 acc)
14. (add 30)这里之所以使用 -30 + 30 这样迂回的方式清零,是为了未触发换行时能够经由第 9 行的 jnz e 复用这行 add 30 的代码。
另外要注意一下,3 号节点在收到 0 时,会给我们发一个 9,让我们强制换行。4 号节点里的第 10~14 行代码正是用于换行的,正好位于第一条 jro 指令下方 9 行的位置,所以 3 号节点发送 9 让 4 号节点强制换行。
最后是 6 号画图节点:
首先从 4 号节点收取 x, y 坐标(mov up down)
(mov up down)
然后从 5 号节点收取当前字符画的当前行的两个像素的颜色(mov left down)
(mov left down)
将这些数字依次发给 image 后,发送一个 -1 结束本次绘制(mov -1 down)。如此循环,直到把画布画满要求的字符画为止。
点击左下角的【RUN】,稍等片刻,便会弹出结算界面:
