【深圳 IO 攻略】番外篇:巧用 ROM 打表提高运行效率

本文首发于 B 站《深圳 IO》文集(https://www.bilibili.com/read/readlist/rl569860)。原创不易,转载请注明出处。
ROM 的作用有两个,一是像第 16 关《幽灵娃娃》那样存储需要连续读取的大量数据,二是建立大量的地址→数值的映射对(俗称【打表】)。以往的攻略里,我只在第 30 关《航空鸡尾酒调酒器》和阿瓦隆城第 8 关《脑机接口》里使用了 ROM 打表。其实还有很多很多关卡可以使用打表的方式提高运行效率,这里举几例:
示例 1:第 13 关《古钱币付款终端》
这一关在之前的 【深圳 IO 攻略】番外篇:调整代码执行顺序以节省行数 里已经做了一次优化。当时的电路板是这样的:

现在,我们使用 ROM,去掉三态判定,电路板变成了下面的样子:

线路板右边没有变化,左边的变化比较大:

【价格】常数芯片本应接到 x 口上,但是加了一块 ROM 后,上方 MC6000 的 x 口不够用了:ROM 用掉两个,DX-300 用掉一个,和右侧芯片通讯用掉一个,四个 x 口全用完。所以左下方的 MC4000 只做了一件事:将【价格】常数芯片转成 p 口信号,供上方的 MC6000 使用(@ mov x1 p1)。
ROM 中存的是 DX-300 的三位数与金额增量间的映射。当用户投入 1 元硬币时,DX-300 = 1,所以 ROM 的 1 号地址里写的增量值是 1;当用户投入 5 元硬币时,DX-300 = 10,所以 ROM 的 10 号地址里写的增量值是 5;当用户投入 12 元硬币时,DX-300 = 100,100 mod 14 = 2,所以 ROM 的 2 号地址里写的增量值是 12。其余位置的值可以无视。
最后是上方的 MC6000。首先检查 DX-300 的值是否为 0(tcp x2 0),若为 0,则本轮没有任何金额增量,关闭所有 + - 号指令,直接跳到最后睡眠。本轮有金额增量时,将 DX-300 的值置为 ROM 的地址,然后读取一格 ROM,得到映射的金额增量(+ mov x2 x1, + add x0)。此时判定用户投入的金额是否是否【不小于】价格(+ tlt acc p0)。若【仍小于】价格,则 + 号激活,跳过所有 - 号前缀的代码,直奔最后休眠;若【不小于】价格,则将 acc 减去价格,将得到找零金额发给右边(- sub p0, - mov acc x3);做完这些后,响铃 4 秒,然后清空 acc,迎接下一次任务(- gen p1 4 2, - mov 0 acc, slp 1)。
点击左下角的【模拟】,稍等片刻,便会弹出结算界面:

上一版方案耗电量为 284,代码行数为 21 行;本方案改为 ROM 打表后,耗电量减少到了 255,代码行数减少到了 19 行。只是,成本上,多了一块 ROM,多了一块用于转换 p/x 信号的 MC4000,共计贵了 5 块钱。
示例 2:第 15 关《卡宾枪瞄准照明器》
原版攻略:【深圳 IO 攻略】第 15 关:卡宾枪瞄准照明器


上一版方案里,我们特意从 5 开始计数,同时每秒计 5 个数,就是为了将 1~6 换算成 10~35,然后对十位数做“三态判定”,把六种状态压缩成三种状态,以此来设置最后的点灯状态。但如果你使用 ROM 打表的话,直接莽就完事了,根本不需要这些花里胡哨的技巧:


由于 1~6 秒的每种时间间隔都对应着【激光】和【泛光】两个数字(也就是每个自变量 x 对应两个因变量),所以我们在获得映射值时,要先把 x 的值乘以 2 并设为地址值,然后连续读两格数据。我们观察这个 ROM,不难发现:
地址 2 和 4 开头的两格空间存的是时间间隔为 1 或 2 秒时的激光和泛光信号:激光 0,泛光 60,右侧的 DX-300 激活 p0 口,三位数为 001。所以两格数据依次是 0、1;
地址 6 和 8 开头的两格空间存的是时间间隔为 3 或 4 秒时的激光和泛光信号:激光 50,泛光 20,右侧的 DX-300 激活 p2 口,三位数为 100。所以两格数据依次是 50、100;
地址 10 和 12 开头的两格空间存的是时间间隔为 5 或 6 秒时的激光和泛光信号:激光 100,泛光 0,右侧的 DX-300 不激活任何 p 口,三位数为 000。所以两格数据依次是 100、0。
那么代码就很容易解读了:由于读取 ROM 时,需要把地址设为经过的时间 ×2,所以我们在计时的时候,每秒钟需要计两个数(slp 1, add 2)。然后,我们计算【雷达输入】和【雷达输出】信号的差值(tcp p0 x3)。差值为负时,【雷达输出】信号激活,重置计时器(- sub acc);差值为正时,【雷达输入】信号激活,从 ROM 中读取根据当前时间差计算出的【激光】和【泛光】映射值。由于 acc 记录的是经过的时间 ×2 的值,所以我们直接将该值设为 ROM 的地址(+ mov acc x1)。接下来,依次读取两格 ROM,将读到的值依次发送给【激光】和经由 DX-300 转接的【泛光】口(+ mov x0 p1, + mov x0 x2)。
点击左下角的【模拟】,稍等片刻,便会弹出结算界面:

上一版方案耗电量为 183,代码行数为 13;本方案改为 ROM 打表后,耗电量减少到了 160,代码行数减少到了 7 行。ROM:我们的目标是,摈弃一切花里胡哨。
示例 3:第 31 关《安全网追踪徽章》

上一版方案里,当“脉冲雷达”发生信号时,左侧的芯片使用了大量的篇幅计算用户当前所在的区域代码。其实与其费劲地找规律,各种套数学公式,还不如打表来得爽快。我们打出如下的表:

脉冲雷达是 80 以上时,无需查表,直接短路返回 100。
脉冲雷达是 51~79 时,同步雷达和区域码的映射关系是:0→600,20(mod 14 = 6)→700,40(mod 14 = 12)→700,60(mod 14 = 4)→700,80(mod 14 = 10)→700,100(mod 14 = 2)→700,因此我们在 ROM 的第 0 格里填入 600,在 ROM 的第 6、12、4、10、2 格内填入 700。
脉冲雷达是 1~50 时,我们无法在以上格子内填入映射值,但是可以填在其后的格子里。即:第 1 格填入 600,第 7 格填入 200,第 13 格填入 201,第 5 格填入 202,第 11 格填入 203,第 3 格填入 204。
还有第 8 和第 9 格没填,我们可以将这些格子填入满足第一个条件时短路返回的 100。

右边的芯片没有任何变化,重点是这块加了 ROM 的左边芯片:

首先等待右侧芯片的唤醒信号(slx x3)。当【脉冲雷达】的值 >79 时,直接将 ROM 的地址指针定位到第 8 格(tgt x3 79, + mov 8 x1)。当【脉冲雷达】的值 ≤79 时,先将 ROM 地址置为同步雷达的值 mod 14(- mov p1 x1),然后检查脉冲雷达是在 1~50 范围还是 51~79 范围。若在 1~50 范围,则空读一次数据口,令地址指针向后偏移一格(- tcp p0 51, - mov x0 null)。ROM 指针定位完毕后,读一次数据口,将读到的映射值发送给右边的芯片,即完成任务(mov x0 x3)。
点击左下角的【模拟】,稍等片刻,便会弹出结算界面:

电量由 366 降低到了 344,代码行数也由 28 行减少到了 21 行。