【深圳 IO 攻略】最終 BOSS 關(guān):水下收割機器人

本文首發(fā)于 B 站《深圳 IO》文集(https://www.bilibili.com/read/readlist/rl569860)。原創(chuàng)不易,轉(zhuǎn)載請注明出處。

此文章已過時。請?zhí)D(zhuǎn)至?【深圳 IO 攻略】最終 BOSS 關(guān)《水下收割機器人》的全新解決方案(空間換時間) 繼續(xù)閱讀。

點擊主頁上的【控制面板】,在【謎題檔案】里輸入數(shù)字 3113,打開隱藏關(guān)第 3 關(guān)《水下收割機器人》。這也是整個游戲(截至 2022 年 7 月)的最終 BOSS 關(guān)卡。

本關(guān)是阿瓦隆城第 5 關(guān)《海藻收割機器人》的升級關(guān)卡,尚未完成阿瓦隆城第 5 關(guān)的同學(xué)建議先閱讀阿瓦隆城第 5 關(guān)的攻略,完成后再來挑戰(zhàn)本關(guān):【深圳 IO 攻略】阿瓦隆城第 5 關(guān):海藻收割機器人
和阿瓦隆城第 5 關(guān)相比,本關(guān)雖然少了【收割】這一路輸出,但是難度卻陡然增大。因為本關(guān)的要求是就近收割,而不是按照先來后到的順序收割。也就是說,每走一步都需要掃描一遍區(qū)域里有哪些海藻,然后往最近的海藻方向奔去。因為是就近收割,所以不存在像第 5 關(guān)那樣往遠處跑的過程中“順手牽羊”的情形,自然也就不需要【收割】這一路輸出信號了。
另外,為了簡化問題,計算距離時,對角線方向上 1 格的距離也是 1,不是根號 2。類似于國際象棋里的國王,既可以橫/豎走一步,也可以斜著走一步。國王在 a 點,到達棋子 b 處至少需要走多少步,在這道題里 a 點和 b 點的距離就是多少。
那么,相比于阿瓦隆城第 5 關(guān)來說,本關(guān)的思路要做這么幾點改變:
因為是就近收割,不是按先來后到的順序收割,所以我們向 RAM 里添加新的海藻坐標時,要使用第 1 關(guān)的模式:遍歷 RAM,找到空位后就放進去。如果仍然使用隊列模式的話,存在“早期海藻因為距離過遠遲遲未被收割,致使新出現(xiàn)的海藻坐標覆蓋掉早期海藻”的可能性。
因為每一步都要搜索距離自己最近的海藻在哪,而電機和海藻間的距離是隨著電機的移動而變化的,不是一成不變的。所以控制電機的芯片每秒鐘都要對外公布自己所在的 (x, y) 坐標,以方便其余芯片計算此時此刻電機和各海藻間的距離。
這道題我花了整整一天時間才做出來。我的設(shè)計方案里,一共花了 31 塊錢(MC6000×4?+ MC4000(X)×3 +?RAM×1),寫了 74 行代碼,且走了背線。我這個方案肯定不是最優(yōu)秀的,世界排行榜上用?20 多塊錢,50 多行代碼做出來的大神比比皆是。好在我這個方案的電量只有 6.4K,屬于第一梯隊。電路圖和代碼如下:


整塊電路板堆得滿滿的,一股濃濃的窒息感。
以上電路圖中,我用黃色字體標注出了 7 塊芯片的編號。我來說明一下各個芯片的分工:
1 號芯片負責(zé)接收 C2S-RF901 發(fā)來的海藻坐標,并放入 RAM 的空位中。
2 號芯片遍歷 RAM 中的所有海藻坐標,并委托 3 號芯片依次計算電機與這些海藻的距離。遇到距離為 0 的海藻時,說明當前位置的腳下有海藻,委托 4 號芯片將 RAM 中的相應(yīng)數(shù)據(jù)抹除;遇到距離大于 0 的海藻時,找到距離最近的海藻坐標,并發(fā)送給 4 號芯片。
3 號芯片在收到 2 號芯片發(fā)來的海藻坐標后,計算電機與該海藻的距離并反饋給 2 號芯片。
4 號芯片收到 2 號芯片發(fā)來的抹除信號后,抹除數(shù)據(jù)庫中的對應(yīng)數(shù)據(jù);收到海藻坐標信號后,將目標位置發(fā)送給 5 號芯片。
5 號芯片將目標位置的 x、y 坐標依次傳送給 6、7 號芯片。
6、7 號芯片在收到 5 號芯片發(fā)來的目標位置后,判定本輪是否應(yīng)該移動一格,以及向什么方向移動。移動完畢后,向 3 號芯片報告電機的最新 x、y 坐標,方便它去計算電機與各海藻間的距離。
我們依次來分析。首先是 1?號芯片:

C2S-RF901 元件有一個隱藏特性:第一秒鐘一定沒有數(shù)據(jù)。因此和 C2S-RF901 相接的 1 號芯片可以在第 1 秒時直接休眠,避免做無用功(slp 1)。這個特性在以往的關(guān)卡里也一樣存在,但提升不明顯??杀娟P(guān)不一樣,本關(guān)里,每秒鐘例行的掃描數(shù)據(jù)庫任務(wù)是一個高耗能操作,如果第一秒休眠的話,相比于不休眠,能節(jié)省幾百格電量。
從第 2 秒起,1 號芯片就要從 C2S-RF901 的 rx 口讀入數(shù)據(jù)了(mov x2 acc)。如果讀入的是 -999(tcp acc -999),那么關(guān)閉一切 + - 號指令,直接跳到最后喚醒?2 號芯片。讀入的不是 -999 時,我們需要將新的海藻坐標存入 RAM。我們剛才讀到的首數(shù)字是 x 坐標,同阿瓦隆城第 5 關(guān)一樣,現(xiàn)在我們再讀入 y 坐標并放在十位數(shù)上,形成一個由 y 和 x 構(gòu)成的兩位數(shù)(+ dst 1 x2)。此時開始尋找 RAM 中的空位,即值為 0 的格子(+ mov x1 dat, + tgt x0 0, + jmp 5)。找到后,我們定位到該空位,并將記錄海藻坐標的 acc 的值存入該格(- mov dat x1, - mov acc x0)。做完這些后,給 2 號芯片發(fā)送 999 喚醒它(mov 999 x3),此時我們就可以安心睡眠了(slp 1)。
注意一下 1 號芯片的 x3 口。我們再按住 TAB 鍵,觀察一下透視圖:

發(fā)現(xiàn) 1 號芯片的 x3 口和其余芯片的很多端口都有連接。首先是和 C2S-RF901 的 tx 口相連接,但本關(guān)并不需要向 tx 口發(fā)送數(shù)據(jù),tx 口一直處于高阻狀態(tài),所以數(shù)據(jù)并不會流到 tx 中。這里的 tx 口僅僅起到了一個“借過”的作用。
除 tx 外,1 號芯片的 x3 口還和 2 號芯片的 x3 口、3 號芯片的 x0 口和 4 號芯片的 x2 口相連接。后面我們會說到,2、3 號芯片通過各自的 x2 和 x1 口通訊,3 號芯片的 x0 口是處于高阻態(tài),不接收數(shù)據(jù)的,因此 3 號芯片的 x0 口也只起到了一個“借過”的作用。
因此,這根導(dǎo)線上的數(shù)據(jù)實際只會在【1 號芯片的 x3 口】、【2 號芯片的 x3 口】和【4 號芯片的 x2 口】間流動。1 號芯片給 x3 發(fā)送 999 時,會同步喚醒 2 號和 4 號芯片。時序上要確保 2 號芯片“快人一步”收掉這個 999,4 號芯片需要接收的只是將來 2 號芯片發(fā)來的數(shù)據(jù)。因為 1 號芯片發(fā)完 999 后,這一秒就睡過去了,因此 2 號芯片在本秒內(nèi)發(fā)送的數(shù)據(jù)都只會流向 4 號芯片,不會回流到 1 號芯片。
為什么要接這么復(fù)雜的線,還要各種借過?實在是因為電路板面積有限啊,君不見本關(guān)的電路板已經(jīng)塞得滿滿的了,不借過的話就無路可走了。
現(xiàn)在我們來看 2 號芯片:

2 號芯片的第 1 秒鐘里跟?1 號芯片一樣睡眠(slp 1),從第 2 秒鐘開始工作。首先趁著 4 號芯片沒醒,趕緊把 1 號芯片發(fā)來的 999 給收了(mov x3 dat)。至于這個 999 有什么用,我們接下來會說。
2 號芯片每秒鐘都需要掃描整個 RAM,檢查有哪些待收割的海藻,以及最近的海藻在哪。首先我們從 RAM 中讀一格數(shù)字(mov x0 acc),檢查它是否大于 0(tcp acc 0)。如果讀到了 0 的格子,說明這個格子是空的,直接關(guān)閉所有 + - 號指令,跳到第 12 行的循環(huán)末尾,準備讀下一格(tcp x1 0, + jmp 3)。如果讀到了非 0 的格子,我們將從該格讀到的坐標值發(fā)送給 3 號芯片兩次(+ mov acc x2, + mov acc x2),并從 3 號芯片接收算好的距離值,將它放到百位上(+ dst 2 x2)。此時判斷帶上了距離值的 acc 是否小于 100(+ tlt acc 100)。小于 100 時,百位為 0,說明剛才讀到的這個海藻和我們電機的距離為 0,說明腳下的這個格子是有海藻的,說明我們需要把這個海藻收割掉。此時我們將 RAM 地址(當前海藻所在的位置?+1)發(fā)給上方的 4 號芯片,委托它將地址 -1 處的格子置零(+ mov x1 x3)。而如果 acc 大于 100,那說明我們找到了一個等待收割的海藻。但是怎么判斷它是不是所有海藻中最近的那一個呢?
我們在上面說了,原先記錄海藻 (x, y) 坐標值的 acc,在經(jīng)過 3 號芯片的計算后,會變成一個三位數(shù),它的百位用于記錄電機和這個海藻的距離。例如,當電機在 (5, 5),海藻在 (3, 2) 時,acc 初始值為 23。然后由于兩者距離為 3,acc 在經(jīng)過 3 號芯片的計算后會變成 323。我們在比較兩個數(shù)的大小的時候,是從高位開始比的,這樣一來,問題就變成了“找最小值問題”。我們需要將 RAM 中記錄的所有【兩位數(shù)】都變成【三位數(shù)】,并找出【最小的三位數(shù)】。
這里的 dat 記錄的是“迄今為止找到的最小三位數(shù)”,初值為本游戲能容納的最大值 999。如果當前的 acc 比 dat 中記錄的三位數(shù)小(- tcp acc dat),那么就說明 acc 的百位是小于等于 dat 的百位的。題目又明確說明了不會出現(xiàn)多個等距的海藻,也就是說各個三位數(shù)間,百位一定互不相等。那就只可能 acc 的百位比 dat 的百位小,說明找到了比之前最小的三位數(shù)還要小的三位數(shù)。此時我們需要把當前的 acc 覆蓋到 dat 里(- mov acc dat),這樣一圈下來,我們的 dat 里存儲的就是最小的三位數(shù)了。
循環(huán)的末尾,我們檢查 RAM 的地址口是否繞了一圈回到了 0(tcp x1 0)。尚未回到 0 時,說明還沒遍歷完整個 RAM,跳回到第 3 行繼續(xù)遍歷(+ jmp 3),直到遍歷完所有的格子為止,將 dat 中存儲的【最小的三位數(shù)】發(fā)給 4 號芯片(mov dat x3)。
現(xiàn)在我們來看專門計算電機和海藻的距離的 3 號芯片:

該芯片可以從 p1 口獲得電機當前的 x 坐標(由 6 號芯片每秒刷新),從 p0 口獲得當前電機的 y 坐標(由 7 號芯片每秒刷新)。首先等待 2 號芯片發(fā)來記錄著海藻坐標的兩位數(shù)(slx x1)。我們首先取出這個兩位數(shù)的個位(x 坐標),然后計算和電機 x 坐標的橫向距離(dst 0 x1, sub p1, dst 1 0),計算完畢后放入 dat 中暫存(mov acc dat)。
值得注意的是第 4 行的置位指令(dst 1 0),它的作用是【取絕對值】。我在龍騰第 15 關(guān)《卡賓槍瞄準照明器》的攻略里提到過:
置位指令:dst I1/R1/P1 I2/R2/P2,將 acc 寄存器中的某一位置為特定的值。位數(shù)由第一個操作數(shù)決定,0~2 分別表示個位/十位/百位,若在此范圍外,則不執(zhí)行任何操作。具體設(shè)置的值由第二個操作數(shù)決定,會只看最低位,忽略最高位,同時會將數(shù)字的符號設(shè)置為和該數(shù)一致。 作者:ココアお姉ちゃん https://www.bilibili.com/read/cv16919367?出處:bilibili
注意這句【同時會將數(shù)字的符號設(shè)置為和該數(shù)一致】。在本游戲里,0 是視為正數(shù)的。因此當我們執(zhí)行了 dst 1 0,將十位強行置為 0 這樣的操作后,會強行將 acc 置為正數(shù)。10×10 的網(wǎng)格里,任意兩點間的距離最多為 9,所以計算出距離后,十位、百位是肯定為 0 的。這里的置位指令并不會修改十位數(shù)字,唯一的作用就是取絕對值。
接下來的 6~9 行代碼,我們再從 2 號芯片處獲得一份一模一樣的兩位數(shù)(mov x1 acc),取出它的十位(y 坐標),然后計算和電機 y 坐標的縱向距離(dgt 1, sub p0, dst 1 0)。至此,dat 中記錄的是橫向距離,acc 中記錄的是縱向距離,兩者中的較大值即為電機和當前海藻的距離。我們將兩者中的較大值反饋給 2 號芯片(tgt acc dat, + mov acc x1, - mov dat x1)。
現(xiàn)在是 4 號芯片,它有兩個任務(wù):收到地址值時,將 RAM 中對應(yīng)地址處的海藻坐標抹除;收到三位數(shù)時,將其中記錄的【最近的海藻坐標】告知接下來的芯片。我們來看它的代碼:

首先等待 2 號芯片的喚醒信號(slx x2),并將它發(fā)來的數(shù)存入 acc(mov x2 acc)。此時檢查這個數(shù)是不是三位數(shù)(tlt acc 100)。不是三位數(shù)時,說明是一個地址值。我們按照 2 號芯片里的囑托,將 RAM 中地址值 -1 處的格子清零(+ sub 1, + mov acc x0, + mov 0 x1, + jmp 1)。而如果發(fā)來的是三位數(shù)的話,需要分情況討論:如果不是 999(- tlt acc 999),那皆大歡喜,直接把這個三位數(shù)發(fā)給 5 號芯片發(fā)兩份就完事了(+ mov acc x3, + mov acc x3)。而如果傳來的是 999,則有兩種可能性:①區(qū)域里沒有待收割的海藻,三位數(shù)一直保持著最開始的 999;②區(qū)域里只有唯一一個位于 (9, 9) 點的,距離為 9 的海藻。收到 999 時,我們需要遍歷一遍 RAM,檢查有沒有位于 (9, 9) 位置的海藻。僅當 RAM 中有位于 (9, 9) 位置的海藻時,我們才能將這個三位數(shù)發(fā)給 5 號芯片(- mov x0 dat, - teq x1 99, + mov acc x3, + mov acc x3, - teq x0 dat, - jmp a)。如果把 RAM 遍歷了一圈都沒有發(fā)現(xiàn)位于 (9, 9) 位置的海藻,那么就什么都不做,直接跳回第 1 行等待下一次任務(wù)。
最后來看控制電機的 5~7 號芯片的代碼:

5 號芯片做的事比較簡單。5 號芯片會從 4 號芯片處收到兩份同樣的三位數(shù),將它的個位(目標點的 x 坐標)提取出來通過 x2 發(fā)給 6 號芯片(slx x0, dst 0 x0, mov acc x2),再將它的十位(目標點的 y 坐標)提取出來通過 x1 發(fā)給 7 號芯片(mov x0 acc, dgt 1, mov acc x1),即完成任務(wù)。
6、7 號芯片的 acc 寄存器用來記錄電機的實時?(x, y) 坐標。初始狀態(tài)下,兩塊芯片都給各自的輸出端口賦上 50 的電平值(mov 50 p1),然后等待 5 號芯片發(fā)來的目標 x/y 信號(slx x1/x0)。發(fā)來后,將目標位置和當前位置做差值運算,檢查差值的正負號(tcp x1/x0 acc)。差值為 0 時,讓電平信號保持為 50 即可,同樣也不需要修改 acc 的值。差值為負時,說明目標點在當前點的左/下方,需要令 acc -1,并給輸出口賦上 0 的電平值(- sub 1, - mov 0 p1);差值為負時,說明目標點在當前點的右/上方,需要令 acc +1,并給輸出口賦上 100 的電平值(+ add 1, + mov 100 p1)。做完后,休眠一秒令電平生效(slp 1)。一秒過后,立刻將新的電機?(x, y) 坐標發(fā)往 p0 口,供 3 號芯片使用(mov acc p0)。
點擊左下角的【模擬】,稍等片刻,便會彈出結(jié)算界面:

至此,深圳 IO 的所有主線攻略就全部寫完了。如果這份攻略里出現(xiàn)了筆誤,亦或讀者在某些關(guān)卡里有比我更好的設(shè)計方案的話,歡迎和我探討。也希望各位讀者能喜歡這個游戲作品。