【TIS-100 攻略】第 14~17 關(guān):測試圖 1、測試圖 2、曝光遮罩查看器、直方圖查看器

本文首發(fā)于 B 站《TIS-100》文集(https://www.bilibili.com/read/readlist/rl626023)。原創(chuàng)不易,轉(zhuǎn)載請注明出處。
第 14 關(guān)《測試圖?1》(Image Test Pattern 1)關(guān)卡展示

本關(guān)我們需要接觸一個(gè)新的模塊——畫圖模塊。打開數(shù)據(jù)手冊,翻到最后一頁,這里介紹了畫圖模塊的使用方法:

翻譯一下:畫布的大小是 30x18 像素。你首先要指定畫筆的起點(diǎn)坐標(biāo),分別發(fā)送 X 坐標(biāo)和 Y 坐標(biāo),接下來指定一或多個(gè)顏色值用于畫圖,畫筆會(huì)從你指定的起點(diǎn)位置開始不斷水平向右移動(dòng),但不會(huì)自動(dòng)換行。繪制完畢后,發(fā)送一個(gè) -1 信號(hào)。你可以指定這些顏色值:
0:黑
1:暗灰
2:亮灰
3:白
4:紅
舉例說明:
0, 0, 3, -1:在左上角 (0, 0) 點(diǎn)畫一個(gè)白色的點(diǎn);
0, 0, 4, 4, 4, 4, 4, -1:從左上角 (0, 0) 點(diǎn)開始,畫一條長度為 5 的紅色水平線。
本關(guān)需要把整個(gè) 30x18 的畫布都涂上白色,那么我們自然是依次發(fā)送下面這樣的指令:
0, 0, 3, 3, 3, ..., 3(30 個(gè) 3), -1:從 (0, 0) 點(diǎn)開始,畫一條長度為 30 的白色水平線;
0, 1, 3, 3, 3, ..., 3(30 個(gè) 3), -1:從 (0, 1) 點(diǎn)開始,畫一條長度為 30 的白色水平線;
0, 2, 3, 3, 3, ..., 3(30 個(gè) 3), -1:從 (0, 2) 點(diǎn)開始,畫一條長度為 30 的白色水平線;
……
0, 17, 3, 3, 3, ..., 3(30 個(gè) 3), -1:從 (0, 17) 點(diǎn)開始,畫一條長度為 30 的白色水平線。
這很明顯是一個(gè)雙重循環(huán):y 的坐標(biāo)從 0~17 依次遞增,每到達(dá)一個(gè)新的 y,都要畫一條新的水平線。用 C 語言描述的話,大概長這樣:
將以上 C 語言代碼改寫成 TIS-100 的匯編代碼,如下:


本關(guān)僅用一個(gè)節(jié)點(diǎn)就能完成任務(wù)。這個(gè)節(jié)點(diǎn)的 acc 用于存儲(chǔ) y,而 bak 用于存儲(chǔ) pixels。
首先我們自然是向下發(fā)送一個(gè) 0(mov 0 down)
和當(dāng)前的 y 坐標(biāo)(mov acc down),
然后使用 swp 指令將 pixels 切換出來(swp)。此時(shí)的 acc 為 pixels,而 bak 為 y。
每到達(dá)一個(gè)新的 y,我們都要給 pixels 賦上 30 的初值(mov 30 acc)。
每畫一個(gè)白色的點(diǎn)(mov 3 down),
我們就令 pixels 減去 1(sub 1),然后判斷 pixels 是否減到了 0。
尚未減到 0 時(shí),跳回到第 5 行,繼續(xù)畫點(diǎn)(jnz 5)。
直到 pixels 減到 0 為止,向下發(fā)送一個(gè) -1 的結(jié)束信號(hào)(mov -1 down)。
本行繪制完畢后,再使用一次切換指令將 y 切換出來(swp)。于是該節(jié)點(diǎn)又回到了初始的 acc 為 y,bak 為 pixels 的狀態(tài)。
此時(shí)我們令 y 加上 1(add 1),并回到開頭,繼續(xù)畫下一行,直到將整個(gè)畫布都畫滿。
點(diǎn)擊左下角的【RUN】,稍等片刻,便會(huì)彈出結(jié)算界面:


第 15 關(guān)《測試圖 2》(Image Test Pattern 2)關(guān)卡展示

這次要畫一個(gè)國際象棋的棋盤了。本關(guān)只需要在上一關(guān)的基礎(chǔ)上做一點(diǎn)微小的改動(dòng)即可完成。上一關(guān)的顏色是固定的 3(白色),但是本關(guān)的顏色是 3(白色)0(黑色)相間,所以我們可以用另外一個(gè)節(jié)點(diǎn)來提供 3、0 相間的無限數(shù)據(jù)流,畫圖的核心節(jié)點(diǎn)不斷從這個(gè)節(jié)點(diǎn)處取得顏色值發(fā)給下方的畫圖模塊。本關(guān)的代碼如下:


本關(guān)至少需要兩個(gè)節(jié)點(diǎn)來完成。右邊的鄰居節(jié)點(diǎn)是一個(gè)提供 3、0 相間數(shù)字的無限數(shù)據(jù)流,左邊的節(jié)點(diǎn)每次都要從右邊的節(jié)點(diǎn)處取得當(dāng)前像素的顏色。
左邊節(jié)點(diǎn)的循環(huán)里,每行 pixels 的初值由 30 變成了 31。如果你還是像之前一樣畫 30 個(gè)像素的話,會(huì)出現(xiàn)一個(gè)問題:由于每行都畫了偶數(shù)個(gè)像素,所以不論到達(dá)哪一行,都會(huì)是先畫 3(白色),再畫 0(黑色),這樣你畫出來的圖就不是國際象棋棋盤了,而是 15?條豎線:

而如果我們每行畫奇數(shù)個(gè)像素的話,就能確保起始顏色和終止顏色一致:比如第 1 行以 3(白色)起始,畫 31 個(gè)像素后,本行仍然以 3(白色)終止,那么到達(dá)下一行后,我們從無限數(shù)據(jù)流里取到的第一個(gè)顏色就是 0(黑色),下一行就變成了同時(shí)以黑色開始和停止。這樣我們就成功地畫出了國際象棋的棋盤。
點(diǎn)擊左下角的【RUN】,稍等片刻,便會(huì)彈出結(jié)算界面:


第 16 關(guān)《曝光遮罩查看器》(Exposure Mask Viewer)關(guān)卡展示

本關(guān)的 IN 會(huì)不斷地給你一些數(shù)字。你需要以四個(gè)一組來讀取這些數(shù)字,并在畫板上根據(jù)這些數(shù)字畫出對應(yīng)的矩形。這四個(gè)數(shù)字分別代表:矩形的左上角 X 坐標(biāo)、矩形的左上角 Y 坐標(biāo)、矩形的寬度、矩形的高度。比如,當(dāng)你收到 [23, 10, 4, 5] 這一組數(shù)字后,你需要畫一個(gè)左上角在 (23, 10) 的,大小為 4 x 5 的矩形。
設(shè)一組數(shù)據(jù)里的四個(gè)數(shù)字分別為 X、Y、W、H,那么我們需要給畫圖模塊發(fā)送以下指令來繪制矩形:
X, Y, 3, 3, ..., 3(W 個(gè) 3), -1:以 (X, Y) 為起點(diǎn),畫一條長度為 W 的水平線;
X, Y+1, 3, 3, ..., 3(W 個(gè) 3), -1:以 (X, Y+1) 為起點(diǎn),畫一條長度為 W 的水平線;
X, Y+2, 3, 3, ..., 3(W 個(gè) 3), -1:以 (X, Y+2) 為起點(diǎn),畫一條長度為 W 的水平線;
……
X, Y+H-1, 3, 3, ..., 3(W 個(gè) 3), -1:以 (X, Y+H-1) 為起點(diǎn),畫一條長度為 W 的水平線。
以上的畫法中,我們一共畫了 H 條長度為 W 的水平線,起點(diǎn)坐標(biāo)從 (X, Y) 一直增加到 (X, Y+H-1)。最終得到的就會(huì)是左上角坐標(biāo)在 (X, Y) 的,寬度為 W 的,高度為 H 的矩形。我們先嘗試將以上繪畫過程改寫成 C 語言代碼:
我們發(fā)現(xiàn)畫矩形的時(shí)候,需要讀取的數(shù)字只有 X、Y、W 這三者,H 是用來判定當(dāng)前矩形有沒有畫結(jié)束的。所以,畫任何一條水平線之前,我們都要想辦法把 X、Y、W 這三個(gè)量傳給最終的畫圖節(jié)點(diǎn)。本關(guān)的 TIS-100 代碼如下:

上方節(jié)點(diǎn)純粹向下傳話(mov up down)。
中央靠左的節(jié)點(diǎn)用來接收上方節(jié)點(diǎn)發(fā)來的輸入量,但不是所有輸入量都由自己保留:
我們收到的第一個(gè)量是左上角的?X 坐標(biāo),我們將這個(gè)量丟給右邊的節(jié)點(diǎn)(mov up right);
第二個(gè)量是左上角的 Y 坐標(biāo),我們將這個(gè)量存入自己的 acc 里(mov up acc);
第三個(gè)量是矩形的寬度,我們將這個(gè)量丟給右邊的節(jié)點(diǎn)(mov up right);
第四個(gè)量是矩形的高度,我們將這個(gè)量存入自己的 bak 里(swp)
(mov up acc)
(swp)中央靠左的節(jié)點(diǎn)就是不斷給畫圖的節(jié)點(diǎn)傳實(shí)時(shí)的 Y 坐標(biāo),每傳一個(gè) Y 就令 Y 加上 1,同時(shí) H 減去 1,如此反復(fù),直到 H 減到 0 為止,才從上方接收一組新的數(shù)據(jù)。但是,這里有一個(gè)要注意的問題:當(dāng)前這個(gè)節(jié)點(diǎn)可以通過【H 是否減到了 0】來決定是否要接收新的 Y 和 H,但是右邊的節(jié)點(diǎn)沒有 H 這個(gè)量,它自己是無法判斷什么時(shí)候接收新的 X 和 W 的。這時(shí)候我們就得使出 jro 大法了,由左邊的節(jié)點(diǎn)控制右邊的節(jié)點(diǎn),保留還是丟棄手上的 X 和 W。我在右邊節(jié)點(diǎn)的 jro 指令上方寫上了注釋:收到 1 時(shí)表示保留當(dāng)前的 X 和 W,并將 X 和 W 往下傳;收到 -4 時(shí)丟棄當(dāng)前的 X 和 W,并從左邊重新接收新的 X 和 W。
畫第 1 行水平線的時(shí)候,當(dāng)然是保留當(dāng)前的 X 和 W,所以左邊節(jié)點(diǎn)給右邊節(jié)點(diǎn)發(fā) 1(mov 1 right),
同時(shí)我們將當(dāng)前的 Y 發(fā)給下面(mov acc right)。
發(fā)完后,我們令 Y 加上 1(add 1),
同時(shí)令 H 減去 1(swp)
(sub 1)
此時(shí)判斷 H 是否減到了 0。尚未減到 0 時(shí),跳回第 6 行(jnz 6)繼續(xù)畫第 2 行水平線、第 3 行水平線……
直到 H 減到 0 為止,向右邊發(fā)送 -4(mov -4 right),丟棄掉當(dāng)前的 X 和 W,等待接收新的 X 和 W。
現(xiàn)在我們來看右邊的節(jié)點(diǎn):
首先我們會(huì)從左邊收來 X 和 W 這兩個(gè)數(shù),我們將 X 放到 acc 里(mov left acc),
將 W 放到 bak 里(swp)
(mov left acc)
(swp)
接下來我們聽從左邊節(jié)點(diǎn)的命令(jro left):
左邊節(jié)點(diǎn)發(fā)來 -4 時(shí),我們丟棄掉當(dāng)前的 X 和 W,跳回第一行,從左邊接收新的 X 和 W;左邊節(jié)點(diǎn)發(fā)來 1 時(shí),我們需要保留當(dāng)前的 X 和 W,將當(dāng)前的 X 和 W 發(fā)給下方的畫圖節(jié)點(diǎn)(mov acc down)
(swp)
(mov acc down)
(jmp 4)
左下角節(jié)點(diǎn)只是給最終的畫圖節(jié)點(diǎn)傳話的(mov up right)。
現(xiàn)在終于到了最終的畫圖節(jié)點(diǎn)。它會(huì)從上方收到 X 和 W,從左方收到 Y。根據(jù)繪圖模塊的使用規(guī)則,我們首先需要指定左上角的 X 和 Y 坐標(biāo),因此:
我們依次從上方和左方接收 X 和 Y,并將它們發(fā)給下方(mov up down)
(mov left down)
接下來,我們要從上方接收 W,然后以給定的 (X, Y) 為起點(diǎn),畫一條長度為 W 的水平線。我們將寬度 W 放入 acc 中(mov up acc),
然后每畫一個(gè)點(diǎn)(mov 3 down),
就令 acc 減去 1(sub 1)并判斷 acc 是否減到了 0。
尚未減到 0 時(shí),跳回到第 4 行繼續(xù)畫點(diǎn)(jnz 4),
直到 acc 減到 0 后,我們就成功畫出了一條起點(diǎn)在 (X, Y) 的,長度為 W 的水平線。此時(shí)我們發(fā)送 -1 結(jié)束本條線的繪制(mov -1 down),然后跳回第一行,從鄰居節(jié)點(diǎn)接收新的一組 X、Y、W,準(zhǔn)備畫下一條水平線。待所有的水平線都畫完后,我們便畫完了所有要求的矩形。
點(diǎn)擊左下角的【RUN】,稍等片刻,便會(huì)彈出結(jié)算界面:


第 17 關(guān)《直方圖查看器》(Histogram Viewer)關(guān)卡展示

出現(xiàn)了,在直方圖游戲里畫直方圖?。ń固淄蓿?/p>
本關(guān)的 IN 會(huì)提供一系列的數(shù)字,每一個(gè)數(shù)字都代表直方圖中一個(gè)豎條的高度。你需要根據(jù)這些數(shù)字畫出對應(yīng)的直方圖。
前面三關(guān)我們畫的都是水平線,但是本關(guān)我們要畫垂直線了。但是,數(shù)據(jù)手冊里只提供了快速畫水平線的指令,沒有提供快速畫垂直線的指令。所以我們只能一個(gè)點(diǎn)一個(gè)點(diǎn)的畫。
設(shè)收到的高度數(shù)字為 H,當(dāng)前所在的橫坐標(biāo)為 X。由于畫布的高度為 18,所以很明顯,Y?坐標(biāo)的有效范圍是?18 - H ~ 17。我們需要畫一條從 (X, 18 - H)?~?(X, 17) 的垂直線。因此,我們需要依次發(fā)送這些指令給畫圖模塊:
X,?18 - H,?3,?-1:在 (X, 18 - H)?處畫一個(gè)白色的點(diǎn);
X, 17 - H,?3, -1:在 (X, 17 - H)?處畫一個(gè)白色的點(diǎn);
X, 16 - H,?3,?-1:在 (X, 16 - H) 處畫一個(gè)白色的點(diǎn);
……
X,?17,?3, -1:在 (X, 17)?處畫一個(gè)白色的點(diǎn)。
本題我們把 Y 存在畫圖節(jié)點(diǎn)的 acc 寄存器里,把 X 存在畫圖節(jié)點(diǎn)的 bak 寄存器里,然后分別維護(hù)即可。代碼如下:

左側(cè)的三個(gè)節(jié)點(diǎn)純粹傳話(mov up down, mov up down, mov up right)。
所有的活全部是右邊的畫圖節(jié)點(diǎn)在干:
從左邊收到的是 H,將其轉(zhuǎn)換成 18 - H(sub left)
(add 18),得到 Y 的起始值。
接下來,我們按照計(jì)劃,依次向下發(fā)送 bak 里的 X 坐標(biāo)(swp)
(mov acc down)
(swp)、
acc 里的 Y 坐標(biāo)(mov acc down)、
3 的顏色值(mov 3 down)
和 -1 的結(jié)束信號(hào)(mov -1 down)。
發(fā)送完畢后,我們需要檢查 Y 是否到達(dá)了 17,即 Y - 17 是否為 0。我們將 Y 減去 17(sub 17),然后判斷是否為 0。
不為 0 時(shí),說明沒有畫到最后一行,此時(shí)跳轉(zhuǎn)到第 2?行令 acc 加上 18(jnz?2),此時(shí) acc?變成了 Y - 17 + 18,即 Y?+?1,相當(dāng)于光標(biāo)向下移動(dòng)了一行。然后我們繼續(xù)畫點(diǎn),直到畫到最后一行為止。
畫到最后一行后,我們將 bak 里存的 X 坐標(biāo)加上 1,準(zhǔn)備畫下一列的垂直線(swp)
(add 1)
(swp)。執(zhí)行完畢后,程序會(huì)自動(dòng)跳回第 1 行。由于此時(shí) acc 已經(jīng)是 0,所以不需要額外清零,執(zhí)行 sub left, add 18 得到的結(jié)果就是下一列的 18 - H 的值。如此反復(fù),直到畫滿整個(gè)畫布。
點(diǎn)擊左下角的【RUN】,稍等片刻,便會(huì)彈出結(jié)算界面:
