欢迎光临散文网 会员登陆 & 注册

【TIS-100 攻略】第 14~17 关:测试图 1、测试图 2、曝光遮罩查看器、直方图查看器

2022-10-25 20:20 作者:ココアお姉ちゃん  | 我要投稿

本文首发于 B 站《TIS-100》文集(https://www.bilibili.com/read/readlist/rl626023)。原创不易,转载请注明出处。

第 14 关《测试图 1》(Image Test Pattern 1)关卡展示

本关我们需要接触一个新的模块——画图模块。打开数据手册,翻到最后一页,这里介绍了画图模块的使用方法:

翻译一下:画布的大小是 30x18 像素。你首先要指定画笔的起点坐标,分别发送 X 坐标和 Y 坐标,接下来指定一或多个颜色值用于画图,画笔会从你指定的起点位置开始不断水平向右移动,但不会自动换行。绘制完毕后,发送一个 -1 信号。你可以指定这些颜色值:

  • 0:黑

  • 1:暗灰

  • 2:亮灰

  • 3:白

  • 4:红

举例说明:

  • 0, 0, 3, -1:在左上角 (0, 0) 点画一个白色的点;

  • 0, 0, 4, 4, 4, 4, 4, -1:从左上角 (0, 0) 点开始,画一条长度为 5 的红色水平线。

本关需要把整个 30x18 的画布都涂上白色,那么我们自然是依次发送下面这样的指令:

  • 0, 0, 3, 3, 3, ..., 3(30 个 3), -1:从 (0, 0) 点开始,画一条长度为 30 的白色水平线;

  • 0, 1, 3, 3, 3, ..., 3(30 个 3), -1:从 (0, 1) 点开始,画一条长度为 30 的白色水平线;

  • 0, 2, 3, 3, 3, ..., 3(30 个 3), -1:从 (0, 2) 点开始,画一条长度为 30 的白色水平线;

  • ……

  • 0, 17, 3, 3, 3, ..., 3(30 个 3), -1:从 (0, 17) 点开始,画一条长度为 30 的白色水平线。

这很明显是一个双重循环:y 的坐标从 0~17 依次递增,每到达一个新的 y,都要画一条新的水平线。用 C 语言描述的话,大概长这样:

将以上 C 语言代码改写成 TIS-100 的汇编代码,如下:

本关仅用一个节点就能完成任务。这个节点的 acc 用于存储 y,而 bak 用于存储 pixels。

  1. 首先我们自然是向下发送一个 0(mov 0 down)

  2. 和当前的 y 坐标(mov acc down),

  3. 然后使用 swp 指令将 pixels 切换出来(swp)。此时的 acc 为 pixels,而 bak 为 y。

  4. 每到达一个新的 y,我们都要给 pixels 赋上 30 的初值(mov 30 acc)。

  5. 每画一个白色的点(mov 3 down),

  6. 我们就令 pixels 减去 1(sub 1),然后判断 pixels 是否减到了 0。

  7. 尚未减到 0 时,跳回到第 5 行,继续画点(jnz 5)。

  8. 直到 pixels 减到 0 为止,向下发送一个 -1 的结束信号(mov -1 down)。

  9. 本行绘制完毕后,再使用一次切换指令将 y 切换出来(swp)。于是该节点又回到了初始的 acc 为 y,bak 为 pixels 的状态。

  10. 此时我们令 y 加上 1(add 1),并回到开头,继续画下一行,直到将整个画布都画满。

点击左下角的【RUN】,稍等片刻,便会弹出结算界面:

第 15 关《测试图 2》(Image Test Pattern 2)关卡展示

这次要画一个国际象棋的棋盘了。本关只需要在上一关的基础上做一点微小的改动即可完成。上一关的颜色是固定的 3(白色),但是本关的颜色是 3(白色)0(黑色)相间,所以我们可以用另外一个节点来提供 3、0 相间的无限数据流,画图的核心节点不断从这个节点处取得颜色值发给下方的画图模块。本关的代码如下:

本关至少需要两个节点来完成。右边的邻居节点是一个提供 3、0 相间数字的无限数据流,左边的节点每次都要从右边的节点处取得当前像素的颜色。

左边节点的循环里,每行 pixels 的初值由 30 变成了 31。如果你还是像之前一样画 30 个像素的话,会出现一个问题:由于每行都画了偶数个像素,所以不论到达哪一行,都会是先画 3(白色),再画 0(黑色),这样你画出来的图就不是国际象棋棋盘了,而是 15 条竖线:

而如果我们每行画奇数个像素的话,就能确保起始颜色和终止颜色一致:比如第 1 行以 3(白色)起始,画 31 个像素后,本行仍然以 3(白色)终止,那么到达下一行后,我们从无限数据流里取到的第一个颜色就是 0(黑色),下一行就变成了同时以黑色开始和停止。这样我们就成功地画出了国际象棋的棋盘。

点击左下角的【RUN】,稍等片刻,便会弹出结算界面:

第 16 关《曝光遮罩查看器》(Exposure Mask Viewer)关卡展示

本关的 IN 会不断地给你一些数字。你需要以四个一组来读取这些数字,并在画板上根据这些数字画出对应的矩形。这四个数字分别代表:矩形的左上角 X 坐标、矩形的左上角 Y 坐标、矩形的宽度、矩形的高度。比如,当你收到 [23, 10, 4, 5] 这一组数字后,你需要画一个左上角在 (23, 10) 的,大小为 4 x 5 的矩形。

设一组数据里的四个数字分别为 X、Y、W、H,那么我们需要给画图模块发送以下指令来绘制矩形:

  • X, Y, 3, 3, ..., 3(W 个 3), -1:以 (X, Y) 为起点,画一条长度为 W 的水平线;

  • X, Y+1, 3, 3, ..., 3(W 个 3), -1:以 (X, Y+1) 为起点,画一条长度为 W 的水平线;

  • X, Y+2, 3, 3, ..., 3(W 个 3), -1:以 (X, Y+2) 为起点,画一条长度为 W 的水平线;

  • ……

  • X, Y+H-1, 3, 3, ..., 3(W 个 3), -1:以 (X, Y+H-1) 为起点,画一条长度为 W 的水平线。

以上的画法中,我们一共画了 H 条长度为 W 的水平线,起点坐标从 (X, Y) 一直增加到 (X, Y+H-1)。最终得到的就会是左上角坐标在 (X, Y) 的,宽度为 W 的,高度为 H 的矩形。我们先尝试将以上绘画过程改写成 C 语言代码:

我们发现画矩形的时候,需要读取的数字只有 X、Y、W 这三者,H 是用来判定当前矩形有没有画结束的。所以,画任何一条水平线之前,我们都要想办法把 X、Y、W 这三个量传给最终的画图节点。本关的 TIS-100 代码如下:

上方节点纯粹向下传话(mov up down)。

中央靠左的节点用来接收上方节点发来的输入量,但不是所有输入量都由自己保留:

  1. 我们收到的第一个量是左上角的 X 坐标,我们将这个量丢给右边的节点(mov up right);

  2. 第二个量是左上角的 Y 坐标,我们将这个量存入自己的 acc 里(mov up acc);

  3. 第三个量是矩形的宽度,我们将这个量丢给右边的节点(mov up right);

  4. 第四个量是矩形的高度,我们将这个量存入自己的 bak 里(swp)

  5. (mov up acc)

  6. (swp)中央靠左的节点就是不断给画图的节点传实时的 Y 坐标,每传一个 Y 就令 Y 加上 1,同时 H 减去 1,如此反复,直到 H 减到 0 为止,才从上方接收一组新的数据。但是,这里有一个要注意的问题:当前这个节点可以通过【H 是否减到了 0】来决定是否要接收新的 Y 和 H,但是右边的节点没有 H 这个量,它自己是无法判断什么时候接收新的 X 和 W 的。这时候我们就得使出 jro 大法了,由左边的节点控制右边的节点,保留还是丢弃手上的 X 和 W。我在右边节点的 jro 指令上方写上了注释:收到 1 时表示保留当前的 X 和 W,并将 X 和 W 往下传;收到 -4 时丢弃当前的 X 和 W,并从左边重新接收新的 X 和 W。

  7. 画第 1 行水平线的时候,当然是保留当前的 X 和 W,所以左边节点给右边节点发 1(mov 1 right),

  8. 同时我们将当前的 Y 发给下面(mov acc right)。

  9. 发完后,我们令 Y 加上 1(add 1),

  10. 同时令 H 减去 1(swp)

  11. (sub 1)

  12. 此时判断 H 是否减到了 0。尚未减到 0 时,跳回第 6 行(jnz 6)继续画第 2 行水平线、第 3 行水平线……

  13. 直到 H 减到 0 为止,向右边发送 -4(mov -4 right),丢弃掉当前的 X 和 W,等待接收新的 X 和 W。

现在我们来看右边的节点:

  1. 首先我们会从左边收来 X 和 W 这两个数,我们将 X 放到 acc 里(mov left acc),

  2. 将 W 放到 bak 里(swp)

  3. (mov left acc)

  4. (swp)

  5. 接下来我们听从左边节点的命令(jro left):

  6. 左边节点发来 -4 时,我们丢弃掉当前的 X 和 W,跳回第一行,从左边接收新的 X 和 W;左边节点发来 1 时,我们需要保留当前的 X 和 W,将当前的 X 和 W 发给下方的画图节点(mov acc down)

  7. (swp)

  8. (mov acc down)

  9. (jmp 4)

左下角节点只是给最终的画图节点传话的(mov up right)。

现在终于到了最终的画图节点。它会从上方收到 X 和 W,从左方收到 Y。根据绘图模块的使用规则,我们首先需要指定左上角的 X 和 Y 坐标,因此:

  1. 我们依次从上方和左方接收 X 和 Y,并将它们发给下方(mov up down)

  2. (mov left down)

  3. 接下来,我们要从上方接收 W,然后以给定的 (X, Y) 为起点,画一条长度为 W 的水平线。我们将宽度 W 放入 acc 中(mov up acc),

  4. 然后每画一个点(mov 3 down),

  5. 就令 acc 减去 1(sub 1)并判断 acc 是否减到了 0。

  6. 尚未减到 0 时,跳回到第 4 行继续画点(jnz 4),

  7. 直到 acc 减到 0 后,我们就成功画出了一条起点在 (X, Y) 的,长度为 W 的水平线。此时我们发送 -1 结束本条线的绘制(mov -1 down),然后跳回第一行,从邻居节点接收新的一组 X、Y、W,准备画下一条水平线。待所有的水平线都画完后,我们便画完了所有要求的矩形。

点击左下角的【RUN】,稍等片刻,便会弹出结算界面:

第 17 关《直方图查看器》(Histogram Viewer)关卡展示

出现了,在直方图游戏里画直方图!(禁止套娃)

本关的 IN 会提供一系列的数字,每一个数字都代表直方图中一个竖条的高度。你需要根据这些数字画出对应的直方图。

前面三关我们画的都是水平线,但是本关我们要画垂直线了。但是,数据手册里只提供了快速画水平线的指令,没有提供快速画垂直线的指令。所以我们只能一个点一个点的画。

设收到的高度数字为 H,当前所在的横坐标为 X。由于画布的高度为 18,所以很明显,Y 坐标的有效范围是 18 - H ~ 17。我们需要画一条从 (X, 18 - H) ~ (X, 17) 的垂直线。因此,我们需要依次发送这些指令给画图模块:

  • X, 18 - H, 3, -1:在 (X, 18 - H) 处画一个白色的点;

  • X, 17 - H, 3, -1:在 (X, 17 - H) 处画一个白色的点;

  • X, 16 - H, 3, -1:在 (X, 16 - H) 处画一个白色的点;

  • ……

  • X, 17, 3, -1:在 (X, 17) 处画一个白色的点。

本题我们把 Y 存在画图节点的 acc 寄存器里,把 X 存在画图节点的 bak 寄存器里,然后分别维护即可。代码如下:

左侧的三个节点纯粹传话(mov up down, mov up down, mov up right)。

所有的活全部是右边的画图节点在干:

  1. 从左边收到的是 H,将其转换成 18 - H(sub left)

  2. (add 18),得到 Y 的起始值。

  3. 接下来,我们按照计划,依次向下发送 bak 里的 X 坐标(swp)

  4. (mov acc down)

  5. (swp)、

  6. acc 里的 Y 坐标(mov acc down)、

  7. 3 的颜色值(mov 3 down)

  8. 和 -1 的结束信号(mov -1 down)。

  9. 发送完毕后,我们需要检查 Y 是否到达了 17,即 Y - 17 是否为 0。我们将 Y 减去 17(sub 17),然后判断是否为 0。

  10. 不为 0 时,说明没有画到最后一行,此时跳转到第 2 行令 acc 加上 18(jnz 2),此时 acc 变成了 Y - 17 + 18,即 Y + 1,相当于光标向下移动了一行。然后我们继续画点,直到画到最后一行为止。

  11. 画到最后一行后,我们将 bak 里存的 X 坐标加上 1,准备画下一列的垂直线(swp)

  12. (add 1)

  13. (swp)。执行完毕后,程序会自动跳回第 1 行。由于此时 acc 已经是 0,所以不需要额外清零,执行 sub left, add 18 得到的结果就是下一列的 18 - H 的值。如此反复,直到画满整个画布。

点击左下角的【RUN】,稍等片刻,便会弹出结算界面:


【TIS-100 攻略】第 14~17 关:测试图 1、测试图 2、曝光遮罩查看器、直方图查看器的评论 (共 条)

分享到微博请遵守国家法律