【TIS-100 攻略】第 6~7 關(guān):序列生成器、序列計數(shù)器

本文首發(fā)于 B 站《TIS-100》文集(https://www.bilibili.com/read/readlist/rl626023)。原創(chuàng)不易,轉(zhuǎn)載請注明出處。
第 6 關(guān)《序列生成器》(Sequence Generator)關(guān)卡展示

本關(guān)要求從 IN.A 和 IN.B 各讀入一個數(shù)字,然后先輸出其中較小的數(shù)字,再輸出其中較大的數(shù)字,最后輸出一個 0。
那么,我們的想法自然是:計算 B - A?的值,若該值 >?0,說明 B 比 A 大,需要依次輸出 A、B、0,否則依次輸出 B、A、0。
本關(guān)的難點在于:A 和 B 這兩個輸入量都是要使用兩次的,一次用于計算差值,一次用于輸出。而從 IN.A 和 IN.B 這兩個輸入口讀到的數(shù)據(jù)都是一次性的,沒法反復(fù)讀。那么這一關(guān)里,我們必須要借助額外的寄存器保存從輸入口讀到的值,以便將來可以多次使用。
第一篇攻略里,我提到了這一點:
每個節(jié)點中還有一個 bak 寄存器,但是在游戲設(shè)定里,bak 寄存器不能通過 mov 指令讀取或?qū)懭胫?,而且接下來要用到的各種算術(shù)指令也不能直接讀取 bak 中的值。關(guān)于 bak 寄存器的使用方法,以后的攻略里我們會說到。 作者:ココアお姉ちゃん https://www.bilibili.com/read/cv19164674?出處:bilibili
那么,我們該如何往 bak 寄存器里存值,以及讀取 bak 寄存器里的值呢?TIS-100 里提供了兩條用于操作 bak 寄存器的指令:
保存指令 sav,它的作用是將 acc 里的值復(fù)制一份到 bak 中;
交換指令 swp,它的作用是將 acc 和 bak 寄存器互換。互換后再對 acc 寄存器進行讀寫操作,操作完畢后再執(zhí)行一次 swp 換回來,就相當(dāng)于對 bak 寄存器進行相應(yīng)的讀寫操作了。
任何時候,我們都只能同時對?acc 和 bak 中的一個進行讀寫,而不能同時對兩個寄存器進行讀寫。比如你想令 acc 加上 bak 的值,這需要你在讀取 bak?的同時,將讀到的值作為增量累加到 acc?里。即使有了 sav 和 swp 指令,這樣的事情也是做不到的。
現(xiàn)在我們回到題目。接收 IN.A 的節(jié)點需要把值匯總到右邊,我們利用 acc 可反復(fù)讀寫的特性,收到值后先放到 acc 里,再將 acc 里的值向右發(fā)兩遍,這樣就達到了多次發(fā)送同一個值的目的。
而接收 IN.B 的節(jié)點稍微有點麻煩,它的 acc 寄存器需要計算 B - A 的值,不像左邊節(jié)點那么悠哉游哉。那么原始的 B 值就只能使用 bak 寄存器來存儲了。
本題的代碼如下:

IN.A 下方的節(jié)點要做的事情很簡單,首先從 IN.A 讀入 A 的值存入 acc(mov up acc),然后將 A 向右邊發(fā)兩遍(mov acc right, mov acc right),因為右邊要兩次使用這個量。
IN.B 下方的節(jié)點首先讀入 B 的值并存入 bak 備用(mov up acc, sav),接下來減去左邊發(fā)來的 A 值(sub left)。至此,該節(jié)點的 acc 寄存器里放的是 B - A 的值,而 bak 寄存器里放的是 B 的值。那么我們按照計劃,判斷 B - A 的值是否大于 0。若大于 0,則跳到第 9 行去執(zhí)行(jgz 9)。
第 5~8 行是 B - A <= 0 時執(zhí)行的代碼。B - A <= 0 時,說明 B <= A,此時我們需要依次輸出 B、A、0。這時候我們首先執(zhí)行交換指令,將 bak 里保存著的 B 值換到 acc 里(swp),把換出來的 B 值往下送(mov acc down),緊接著把左邊第二次發(fā)來的 A 值往下送(mov left down),送完后,強制跳到第 12 行(jmp c)把 0 往下送(mov 0 down)。
第 9~12?行是 B - A > 0 時執(zhí)行的代碼。B - A > 0 時,說明 B > A,此時我們需要依次輸出 A、B、0。那么這里,我們改為先往下送 A(mov left down),再往下送 B(swp, mov acc down),最后往下送 0(mov 0 down)。
下面的兩個節(jié)點都是純傳話用的,不解釋(mov up down)。
點擊左下角的【RUN】,稍等片刻,便會彈出結(jié)算界面:


第 7 關(guān)《序列計數(shù)器》(Sequence Counter)關(guān)卡展示

本關(guān)的 IN 口會源源不斷地給你提供一些以 0 結(jié)尾的序列。每當(dāng)你完整地收到一個序列,就要向 OUT.S 端口輸出這個序列的總和,同時向 OUT.L 端口輸出這個序列的長度。
本關(guān)的難點在于:節(jié)點需要配置多種功能:收到非 0 數(shù)字時,OUT.S 上方的節(jié)點需要在 acc 寄存器里加上該值,OUT.L 上方的節(jié)點需要令 acc +1;收到 0 數(shù)字時,兩個節(jié)點都需要把各自的 acc 發(fā)給輸出端口并重置。本關(guān)需要用到在上一篇攻略里最后提到的 jro 指令來操作,也就是根據(jù)外部輸入來決定要執(zhí)行哪個代碼塊。本關(guān)的代碼如下:

最上方的節(jié)點純粹往下傳話,不解釋(mov up down)。
中間的節(jié)點將 IN 發(fā)來的數(shù)字存入 acc(mov up acc),并判斷它是 0 還是非 0。若是非 0,則跳轉(zhuǎn)到第 5?行去執(zhí)行(jnz 5)。這就導(dǎo)致了:收到非 0 時,執(zhí)行的是第 5~6 行代碼,向下發(fā)送一個 5,和當(dāng)前的這個數(shù)字(mov 5 down, mov acc down);收到 0 時,執(zhí)行的是第 3~4 行代碼,向下發(fā)送一個 1 然后跳回開頭(mov 1 down, jmp 1)。其實向下發(fā)送的 5 和 1 都是偏移量,對應(yīng)著下方節(jié)點的兩個執(zhí)行不同任務(wù)的代碼塊。
然后我們看下方的和 OUT.S 通訊的節(jié)點。第一行用于等待上方的指示,看上方是叫我往下跳 5 行還是 1 行執(zhí)行代碼(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,準(zhǔn)備迎接下一次累加任務(wù)(sub acc, jmp 1)。
我們注意到這個節(jié)點也在收到上方的指示后,也給右邊的節(jié)點傳了個 3 或 1。其實這兩個數(shù)字是用作右邊的?jro 指令的參數(shù)的,跟上方傳給自己的 5 或 1 類似。這個節(jié)點也在用同樣的方式控制著右邊節(jié)點的行動?,F(xiàn)在我們來看右邊節(jié)點的代碼。
首先也是等待左邊節(jié)點的控制信號(jro left)。如果左邊傳的是 3,說明本輪從 IN 里收到的是非 0 值,我們往下跳 3 行,令序列長度 +1(add 1);如果左邊傳的是 1,說明本輪從 IN 里收到的是 0 值,我們往下跳 1 行,向 OUT.L 口輸出當(dāng)前序列的長度(mov acc down),并清空 acc,準(zhǔn)備下一次計數(shù)任務(wù)(mov -1 acc, add 1)。這里我們沒有使用 sub acc, jmp 1 這樣的寫法,而是先把 acc 置為 -1,然后再加上 1。這樣我們就成功復(fù)用了收到非 0 值時執(zhí)行的 add 1 這一行代碼,省去了一行強制跳轉(zhuǎn)到開頭的代碼。
點擊左下角的【RUN】,稍等片刻,便會彈出結(jié)算界面:

優(yōu)化運行速度
以上方案還有改進空間。我們注意到 IN?發(fā)來非 0 值時,中央節(jié)點會給下方節(jié)點發(fā)送兩個值,一個是 5 的地址偏移,一個是當(dāng)前收到的需要累加上的數(shù)字。對應(yīng)的下方節(jié)點每次也都需要收兩個數(shù)字,效率上有損失。能不能每次只傳一個值呢?答案是可以的。首先,下方節(jié)點將 acc 初始化為一個足夠小的負數(shù),比如 -999。然后,平時收到非 0 值時,中央節(jié)點直接將該值往下傳;而當(dāng)收到 0 值時,中央節(jié)點改為向下方發(fā)送 +999,清除掉下方節(jié)點原先的 -999 偏置量。這樣下方節(jié)點在累加的過程中,就會發(fā)現(xiàn)自己的 acc 突然變成正數(shù)了,那么就說明這個序列結(jié)束了。此時我們得到的就是沒有 -999 偏置的正確答案,我們直接將該答案輸出給下方的?OUT.S 即可。這樣我們就做到了任何時候都只用一個數(shù)字進行通訊,無形間提升了效率。代碼如下:

中央節(jié)點由 6 行代碼縮減成了 4 行代碼,因為少傳了一個值,還少了一次跳轉(zhuǎn)。中央節(jié)點的邏輯是,收到非 0 值時,將對應(yīng)的值傳給下方(mov up acc, jnz 4, mov acc down);收到 0 值時,改為給下方發(fā)送 999(add 999, mov acc down)。
下方節(jié)點的邏輯改動較大。首先將 acc 設(shè)置上 -999 的偏置(mov -999 acc),然后跳過第 3?行代碼,直接到達第 4 行,從上方接收數(shù)值(jmp 4)。接下來,不論上方發(fā)了什么值,我都無腦加到 acc 里(add up)。如果 acc 仍是負數(shù),則跳回到第 3 行給右邊傳個 3(jlz 3, mov 3 right),然后繼續(xù)等待上方的累加信號,直到 acc 突然變成正數(shù)為止。acc 突然變成正數(shù),就說明上方發(fā)送了最終的 +999 偏置修正量。此時我們給右邊傳個 1(mov 1 right),然后自己將本次正確答案發(fā)往下方的輸出口(mov acc down)。
右邊節(jié)點的代碼和上一版方案完全一樣,沒有做任何改動。
點擊左下角的【RUN】,稍等片刻,便會彈出結(jié)算界面:

運行時長由原先的 248 周期減少到了 214 周期。而且三項指標(biāo)都到了直方圖的最左端。所以我們又得到了一個十全十美的方案。
解鎖成就 NO BACKUP
該成就的說明是 Solve SEQUENCE COUNTER without using the SWP instruction,要求不使用 swp 指令完成第 7 關(guān)。以上兩版方案里都沒有用到 swp 指令,所以你已經(jīng)解鎖這個成就了,不需要額外的攻略了。