肉丁土豆园地

静谧编程花园 - The Secret Garden
追求性能的我玩《图灵完备》的攻略记录

《图灵完备》是一个很有意思的游戏。
我最近在研究布尔运算相关的问题,看到这个游戏是“从逻辑门开始手搓一个电脑”,我立马从某俄国盗版网站上搞了一个就开始玩。
果然好玩,直接耗掉了我四天时间玩这个游戏。不过这个游戏并没有让我学到多少布尔运算相关的知识,更多的是计算机底层知识就是了。

我看马上也通关了,就来做个类似攻略的东西记录一下吧。
如果你想看特定关卡的答案,可以用浏览器的查找当前页面功能(同时按键盘的 ctrl 和 F 就能切出来)进行搜索。把鼠标放到标题上就能看到标题末尾出现了一个小图标,点击还可以打开目录导航。

基础逻辑电路

原力觉醒

img

这关的名字倒是很有意思。

与非门(NAND)

img

没想到一开始学的是 nand ,不知道这个对应着现实中的什么东西?

非门(NOT)

img

与门(AND)

img

之前有朋友问我为什么与非门叫与非门,我觉得是因为就是先与一下后非一下,所以叫与非门吧。——这么看为什么不叫非与门?毕竟与非也是非的与。

或门(OR)

img

为什么把两个操作数非一下就从与非变成或了?我觉得是因为“吃饭或喝水”的意思就是“不能不吃饭也不喝水”。
游戏里是说如果把操作数都非了一下,那么结果的真值表会左右翻转。

或非门(NOR)

img

高电平

img

第二刻

img

这个可以理解为“输入 1 为真且输入 2 为假”为真,也就是 \(A \land \lnot B\)

异或门(XOR)

img

这个能用很多方法做出来,化成最简就是图中这样。

三路或门

略。

三路与门

略。

同或门(XNOR)

img

你问能不能用异或门非一下?我一开始就是这么干的,但是玩到后面会解锁门数量和总延迟两个性能指标,如果你像我一样追求性能,图中的方法是最好的。

算术运算

往下开始先写算术运算区域的关卡。

二进制速算

挺有意思的小游戏,略。

成对的麻烦

img

如果把四个输入分别命名为 \(A,B,C,D\) ,不难发现题目要我们求的就是

\[\begin{aligned} &(A\land B) \lor(A\land C) \lor(A\land D) \lor(B\land C) \lor(B\land D) \lor(C\land D) \\ =&((A\lor B)\land C) \lor((A\lor B)\land D)\lor (A\land B) \lor(C\land D) \\ =&((A\lor B)\land (C\lor D)) \lor((A\land B) \lor (C\land D)) \\ \end{aligned}\]

奇数个信号

img

连着异或就行。

信号计数

img

当成四个信号相加,和下面那关半加器是一样的。

半加器

img

分别看两个输出的真值表就能得出这个解决方法,比较常规。

然而如果你像我一样追求性能,你应该可以发现异或可以使用与门的结果,这样子能少用一个逻辑门。

img

加倍

img

就是左移一位。

全加器

img

两个半加器弄一块,两个进位或一下就行,比较常规。

然而如果你像我一样追求性能,跟半加器那关一样,还可以少两个逻辑门。

img

8 位或

布线太乱,略。

8 位非

略。

8 位加法器

img

串在一起。

负数

有点考验反应速度的小游戏,略。

相反数

img

相反数就是和原数相加为 \(0\) 的数,也就是取反后 \(+1\) ,比较常规。

然而如果你像我一样追求性能,不知道你有没有发现,由于我们只 \(+1\) ——每一位的运算只需要一个半加器——这里我们可以不用整个加法器。
为此,我特地又设计了一个使用“ \(+1\) 器”(就是分线器右边,集线器左边的那一大坨电路)的解决方法。

img

进一步,你可能还会发现可以像游戏里说的什么德摩根定律一样把输入的 \(8\) 位非门融入 \(+1\) 器里,这就能让门数量和总延迟更少,最终得到的是一个“取相反数器”。

img

如果你自己做出过 \(-1\) 器,你还可以发现其实这个“取相反数器”就是把 \(-1\) 器的结果非一下。这是因为

\[\begin{aligned} \lnot A +1 &= -A \\ \lnot A &= -A - 1 \\ \lnot (A - 1) &= -(A - 1) - 1= -A \end{aligned}\]

1 位解码器

img

3 位解码器

img

图中在门数量和总延迟两方面应该都是开销最少的方法。

逻辑引擎

img

这回是用或门和非门凑出另外几个。

存储器

循环依赖

略。

延迟线

略。

奇变偶不变

img

根本没必要管输入,只要弄个会震荡的电路就行了。

1 位开关

img

开关跟与门的作用差不多。区别是多个开关连在一起的时候可以省下一些或门,但是有的时候开关会使游戏不能立即找到一些隐藏的循环依赖。

1 位取反器

感觉智商被侮辱了,略。

数据选择器

img

总线

img

优雅存储

这个问题有两个解法,一个是延迟一刻输出的逻辑门锁存器。

img

还有一个是根据是否写入,判断这一刻要写到延迟线里的值是待写入值还是上一刻延迟线的值。

img

可以看出来第二个方法不论是门数量还是总延迟都比第一个方法好,那为什么我还要贴第一个方法呢?是因为我发现手册里介绍了逻辑门锁存器。然而玩完游戏之后,我感觉唯一可能用到逻辑门锁存器的关卡只有这个。所以就试着用了用锁存器。

存储一字节

img

小盒子

img

刚刚好装满盒子,原理跟数据选择器差不多。

计数器

img

根据是否写入,判断这一刻要写到寄存器里的值是待写入值还是上一刻寄存器 \(+1\) 的值。比较常规。

然而如果你像我一样追求性能,你会发现这里跟相反数一样只需要 \(+1\) ,所以我选择把那个 \(+1\) 器搬过来。

img

处理器架构

算数引擎

img

看起来不是很优雅,不知道是哪里的问题。

寄存器之间

到这里我们开始组装自己的 OVERTRUE 电脑了。
我是个完美主义者,而我之前弄的那台 OVERTRUE 从各种方面来讲都过于搞笑了。刚好趁着写这篇记录,我打算再搞一台!

指令分叉器

OVERTRUE 的一条指令八个字节分为两部分,分别是前两字节表示操作模式,后六字节表示具体参数。
其中,复制模式的后六字节分别表示源地址和目标地址。
我来试着拆分出两个地址来。

img

解码器

虽然游戏里给了三位解码器,但是用起来还是不舒服,毕竟“码”大多是直接一条 8 位线给出的。

img

寄存器

因为 OVERTRUE 架构一刻只会写入一个值,也就意味着整个计算机在一刻里只有一个值在流动,我就用一根线把所有寄存器的输入输出都给连起来了,这根线可以叫值线。

img

把解码器和寄存器连起来,然后再加上个程序和计数器,这关就过了。

img

指令解码器

img

这是第几个解码器了?

计算单元

这一关里我们所有的寄存器都变成了寄存器 Plus ,也就是多一个无视开关直接读取的引脚。这就可以一次性读取两个寄存器的值。

img

之后当指令的前两位所代表的含义不是复制时,我们要禁用写入的功能。我这里不是在除复制之外的模式时禁用解码器,而是给每个寄存器的写入引脚前面加入一个只有模式为复制时才开启的开关。
同时这里我们要实现的是计算功能,所以寄存器 3 号这时要被写入。

img

之后摆上计算单元,连接指令和 1 号还有 2 号寄存器,并且在计算模式时接入值线。

img

这一关就过了。

条件判断

img

那一大堆或门用来判断 \(=0\) ,单独拉出的 \(-128\) 线用来判断 \(<0\) ,最后和指令的第三位异或一下来在第三位为真时反转结果。
由于一个数不可能既 \(<0\)\(=0\) ,所以这里可以用开关来代替与门,就能少用一个或门。

程序

我发现这个时候电脑里才有程序元件,好吧不管怎样这关过了。

立即数

这关也很简单,只要在立即数模式时写入 0 号寄存器并把指令线接到值线就行。

img

图灵完备

用条件判断器读取指令线来判断 3 号寄存器。

img

之后把计数器和 0 号寄存器接一起,当判断为真且处于跳转模式时写入计数器。

img

于是这个 OVERTRUE 计算机应该是设计完了。

编程

加 5 等于几

想要输入 \(+5\) 后输出,我们应该先一个立即数 5 到 0 号寄存器,再把 0 号寄存器的 5 复制到 2 号寄存器,之后复制输入到 1 号寄存器,相加,最后复制 3 号寄存器到输出。

代码如下:

00000101
10000010
10110001
01000100
10011110

激光炮直瞄

这关开始就能用汇编了。
为了方便用汇编,我先来添加一些汇编别名。

10000000 copy
00001000 from
00000110 in
00000110 out
11000000 jump
00000001 if_equ0
00000010 if_lss0
00000011 if_leq0
00000100 always
00000101 if_neq0
00000110 if_geq0
00000111 if_gtr0
01000000 or
01000001 nand
01000010 nor
01000011 and
01000100 add
01000101 sub

题目要求我们求 \(2\pi r\) ,其中 \(\pi=3\) ,实际上就是 \(6r\)
由于 OVERTRUE 架构的循环很麻烦,我这里就手工复制代码了。

copy | from*in | 1
copy | from*in | 2
add
copy | from*3 | 1
add
copy | from*3 | 1
add
copy | from*3 | 1
add
copy | from*3 | 1
add
copy | from*3 | out

img

太空入侵者

这关我是一行一行手动操作,太麻烦了!

点我展开程序
const LEFT 0
const GO 1
const RIGHT 2
const EMM 3
const ACT 4
const SHOOT 5

LEFT copy|out
GO copy|out
GO copy|out
RIGHT copy|out
GO copy|out
GO copy|out
GO copy|out
GO copy|out
GO copy|out
RIGHT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
RIGHT copy|out
RIGHT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
RIGHT copy|out
RIGHT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
RIGHT copy|out
RIGHT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out
SHOOT copy|out

密码锁

这关我也是很暴力,枚举过去的。

1 copy|1 copy|2
label begin
add
copy | from*3 | out
copy | from*3 | 1
begin jump|always

img

时间掩码

这关要用 \(3\) 按位与输入,就是 \(输入 \mod 4\)

copy | from*in | 1
3 copy|2
and
copy | from*3 | out

迷宫

这是我程序的伪代码。

while (true)
{ // loop
	robot_left();
	if (get_facing() == WALL)
	{ // if0w
		robot_right();
		if (get_facing() == WALL)
		{ // if1w
			robot_right();
		}
		// if1end
		if (get_facing() == DOOR)
		{ // is_door
			robot_act();
		}
		robot_go();
	}
	else
	{
		robot_go();
	}
	// if0end
}

这段代码看起来层次很诡异,因为我是先两眼一黑写的汇编,再根据汇编翻译出的伪代码。
不过我发现层次正常的代码反而没有这段代码的性能好。

const COIN 8
const WALL 1
const AIR 0
const DOOR 3
const RIGHT 2
const LEFT 0
const GO 1
const ACT 4

label loop
LEFT copy|out
copy | from*in | 1
WALL copy|2
sub
if0w jump|if_equ0
GO copy|out
label if0end
loop jump|always

label if0w
RIGHT copy|out
copy | from*in | 1
WALL copy|2
sub
if1w jump|if_equ0
label if1end
copy | from*in | 1
DOOR copy|2
sub
is_door jump|if_equ0
GO copy|out
if0end jump|always

label if1w
RIGHT copy|out
if1end jump|always

label is_door
ACT copy|out

img

处理器架构 2

异或

上面我们已经做过异或门了。

copy | from*in | 1
copy | from*in | 2
or
copy | from*3 | 4
nand
copy | from*3 | 1
copy | from*4 | 2
and
copy | from*3 | out

8 位常数

img

8 位异或

img

相等

img

还有一种方法是相减看是否为 \(0\) ,但是开销要大得多。

无符号小于

img

图中的电路是 \(\neg A + B\) 。因为 \(\neg A = 255 - A\) ,所以 \(\neg A + B = B - A + 255\) 。如果 \(A < B\) ,那么 \(B - A + 255 > 255\) ,图中的加法器会进位;反之则不会。比较常规。

然而如果你像我一样追求性能,还有一种方法是一位一位地比较。

img

我的想法是每一位的比较都视作一个“单元”。每个单元都会获得当前位是否 \(B>A\) 和上一单元有没有比出大小两个信息。若上一单元已比出大小则直接将其结果传递到下一单元,否则将当前位比较的结果传递到下一单元。

有符号小于

img

这个方法是我从网上找到的,大概意思就是把 \(8\) 位数变成 \(9\) 位,这样子就方便做减法来判断大小了。

img

或者还有一种方法是把无符号小于那关的电路拿过来用,只是因为最高位变成了负的,所以最高位的非门从第一个输入挪到了第二个输入。

反正,如果你像我一样追求性能,这关也可以一位一位比较。

img

宽指令

img

这关也可以用寄存器,总延迟与图中方法相同。但是图中门数量比寄存器方法少了 \(24\) 个。

一把线,像挂面

到这里我们开始组装自己的 LEG 电脑了。
前略,总之我要再从头搞一个。

\(3\) 位解码开关

LEG 需要一次读取两个值,为了方便,我就采取一刻不停读取每个寄存器并从得到的值里挑两路的方法。这就需要这个解码开关。

img

最底下那个双向接头其实是个输出,表示路线 7 是否开启。关卡输入走的是 7 路线,有最底下这个输出就方便来激活关卡输入,双向接头是为了避免循环依赖。

寄存器

之后放六个寄存器,用一条值线(白色)连接所有写入数据引脚,再用刚才的解码开关连接所有的读取数据引脚,把解码开关接到对应指令线(玫红色)。

img

之后用解码器连接写入开关,高电平连接读取开关。

img

最后来实现加法操作。

img

这一关就过了。

操作码

img

题目这个指令集如果把加减法而不是按位非和按位异或放到 5 号 6 号指令上,这个计算单元还能再少 \(4\) 个总延迟。

用这个计算单元替换掉上一关的加法器,这一关就过了。

img

立即数

img

用指令中的立即数位来选择是使用指令线还是读取线来当计算单元的输入。

条件判断 II

img

放弃了,这关卡给的指令集太搞笑了,不想优化了。

把上面这个条件单元连接到两个读取线上,当处于跳转模式时断开计算单元,当判断为真且处于跳转模式时写入计数器并把第四根指令线接入值线里,当判断为假且处于跳转模式时断开写入解码器。

img

然后这个 LEG 计算机终于是弄完了。

函数

十六进制速算

这一关诀窍是十六进制的一位对应二进制的四位,略。

移位

img

这个答案也是我从网上找的,真是非常聪明的方法!

内存

\(4\) 位解码器

从这一关开始我们要给电脑加各种存储器。理论上可以通过移除之前的那些寄存器来腾出空间加内存。但是实际上这么玩会发现到最后那几关的时候没有足够的寄存器用,只能使用内存。最后由于内存需要的指令太多,程序在 \(256\) 条里写不完,也跑不起来。

所以最好的方法就是再加一堆寄存器。就需要先做出 \(4\) 位解码器。

img

\(4\) 位解码开关

img

\(4\) 位的解码器和解码开关足够再支撑 \(8\) 个寄存器,装到 LEG 电脑上,用条件判断 II 那一关测试一下。

img

虽然新加的 \(8\) 个寄存器没有测试到,不过至少说明这个大改造没破坏之前的功能。

内存

之后我们用 8 号寄存器(就是新加的那 \(8\) 个中的第一个)来存储地址,再把 9 号寄存器替换为内存元件。

img

内存元件太大了。

汇编

这一关要自己编程,我先来添加一些汇编别名。

10000000 num1
01000000 num2
11000000 num
00000000 add
00000001 sub
00000010 and
00000011 or
00000100 not
00000101 xor
00000000 _
10000000 copy
00000110 steper
00000111 in
00000111 out
00001000 addr
00001001 mem
00100000 jump
00000000 if_equ
00000001 if_neq
00000010 if_lss
00000011 if_leq
00000100 if_gtr
00000101 if_geq

之后来写程序。

label read
copy _ in mem
add|num2 addr 1 addr
jump|if_neq|num2 addr 32 read

copy|num2 _ 0 addr
label write
copy _ mem in
add|num2 addr 1 addr
jump|if_neq|num2 addr 32 write

img

一直读取的寄存器

然而如果你像我一样追求性能,就不难发现一直读取的寄存器不需要使用三态输出,能省个 \(8\) 位开关。

img

这里是为了使预览尽可能小,才用了这么复杂的布线。反正就是个三态输出换成普通输出的寄存器。
为什么之前我一直没提,因为我发现之前的关卡要求电脑上至少有 \(6\) 个“ \(8\) 位寄存器”元件。

然后来替换电脑里的寄存器。

img

延迟内存

延迟内存的功能是在内存没操作完的时候就进入下一刻,所以我打算把内存改为延迟内存(并且容量改小一点,我才发现容量是能改的)。
这就使得内存变成了像关卡输入一样需要读取的时候才能激活的元件,所以我又简单修改了一下解码开关,在 9 号路线的后面加了个双向端子。

img

但是我发现如果只是写入的话倒还简单,每次写入间隔一些时间就行,真正复杂的是读取。由于还没操作完就进入了下一刻,导致读取的那一刻根本什么也读不到,必须要等操作完的那一刻读取才行。先不说能不能准确地找到这一刻,就算成功读取,又会激活内存,使得内存再花好几刻来读现在的地址,严重影响执行下一步的速度。

为此,我用 10 号寄存器来存储延迟内存的输出。不过问题又出现了——最容易想到的方法是当延迟内存等待结束时就对 10 号寄存器进行写入,然而等待结束的时候多了去了,却只有等待刚结束的那一刻内存才有有效输出。所以我又搞了个挂档器。

img

这个挂档器只会在上一刻输入为假且这一刻输入为真时在这一刻输出真。

img

最后终于是装上了这个延迟内存。我又添加了一些汇编别名。

00001010 memtemp
10000000 wait

代码则是这个样子。

label read
copy _ in mem
add|num2 addr 1 addr
wait _ _ _
wait _ _ _
wait _ _ _
wait _ _ _
wait _ _ _
jump|if_neq|num2 addr 32 read

copy|num2 _ 0 addr
label write
copy _ mem memtemp
add|num2 addr 1 addr
wait _ _ _
wait _ _ _
wait _ _ _
wait _ _ _
wait _ _ _
wait _ _ _
wait _ _ _
copy _ memtemp out
jump|if_neq|num2 addr 32 write

img

优化的指令集

如果你像我一样追求性能,你应该还会对之前关卡里糟糕的指令集耿耿于怀。

img

把计算单元稍微优化了一下,修改这些汇编别名

00000100 and
00000101 or
00000110 not
00000111 xor

img

条件单元也优化,修改这些汇编别名

00000001 if_lss
00000010 if_equ
00000100 always
00000110 if_neq
00000111 if_gtr

img

最终结果是这样。

延迟量

img

半字节乘法

img

与移位那关的思想一样。

呃,这个手工肯定是能优化,等我有时间吧(其实就是懒了)。

img

我一看这个背景心里就咯噔一下,知道这又要保存在元件工坊里了,所以塞的这么满。

我的基本想法是压栈后储存地址的寄存器 \(+1\) ,弹栈时 \(-1\) (也就是 \(+255\) )。
为了减少门数量,我还把这个内存的容量限制在了 \(64\) 字节。

如果忽略空间,最好是使用一个“批判性 \(+1\) 器”和“批判性 \(-1\) 器”来代替两个加法器。
批判性 \(+1\) 器其实就是可以选择是否 \(+1\)\(+1\) 器。批判性 \(-1\) 器也一样。由于这关不能用元件工坊压缩空间,下图仅作性能演示用途。

img

除法

一开始天真的我尝试用 OVERTRUE 计算机来解决这个问题,结果怎么都解决不了。之后我才回过味来, OVERTRUE 是有符号 \(8\) 位计算机,但题目给的是无符号 \(8\) 位数。

总之我是按照以下伪代码写的程序。

function div(a: number, b: number) {
	let c = 0;
	a = a + b;
	do {
		a = a - b;
		c = c + 1;
	} while (a >= b);
	return {
		quotient: c - 1,
		mod: a,
	};
}
const a 0
const b 1
const c 2

copy _ in a
copy _ in b
add a b a

label loop
sub a b a
add|num2 c 1 c
jump|if_geq a b loop

sub|num2 c 1 out
copy _ a out

img

压栈与弹栈

更好的栈

因为栈那关不能用元件工坊,现在能了,所以我用元件工坊改一下。

img

具体是改用批判性 \(+1\) 器和批判性 \(-1\) 器,就是图中那两个大块块元件。同时我还把三态输出改成了普通的输出,寄存器改成一直读取的寄存器。

加到计算机上

因为出栈需要一个信号来激活,就跟关卡输入一样,所以在解码开关的 11 号路线上加一个双向端子。

img

然后就可以把 11 号寄存器替换为栈了。

img

添加汇编别名

00001011 stack

之后程序是这样

const temp 0

label loop
copy _ in temp
jump|if_equ|num2 temp 0 pop
copy _ temp stack
jump|always _ _ loop

label pop
copy _ stack out
jump|always _ _ loop

包装内存

不知道是因为接下来的函数那关太复杂引起了我的逆反心理,还是我突然有了一些美学感悟,总之我发现上图这个内存太大了,好丑,应该像栈一样包装起来才好看。

img

把挂档器改成了只有上一刻为真且这一刻为假时才输出真,跟之前的条件是反过来的。

img

内存和 10 号寄存器就可以打包成上图这个样子。

img

在此期间我发现这个包装是有问题的。当计算机的总延迟过高(比如加了个用普通内存的栈)以至于延迟内存并没有延迟到下一刻才输出的时候,不会有等待响应的刻,也就挂不上档,导致 10 号寄存器没有输出。

img

所以我让它在开始读取的下一刻和挂挡后都对 10 号寄存器进行一次写入。虽然这样就没法在这次的读取开始后读取上次的内容,但是我觉得现在没遇到这个问题就不用管了。

函数

实现思路

函数其实不是一个新的指令,因为函数的调用和返回都可以通过已有的指令实现,所以实现调用和返回其实就是包装指令。

根据关卡的提示,我们可以知道用已有指令实现调用和返回其实就是下面这个样子。

const reg0 0

copy|num2 _ 1 reg0

add|num1 8 steper stack #保存程序现在执行到的位置
jump|always _ _ ori #调用函数
add|num1 8 steper stack
jump|always _ _ ori
add|num1 8 steper stack
jump|always _ _ ori
add|num1 8 steper stack
jump|always _ _ ori

jump|always _ _ progend #跳过函数定义区域

label ori #开始定义函数
add|num2 reg0 1 reg0
copy _ stack steper #回到被调用位置

label progend
copy _ reg0 out

这段代码在 reg0 里存入 \(1\) 后调用了 \(4\) 次用来给 reg0 \(+1\) 的函数 ori ,最后输出了 reg0 中的内容。

由此我们可知,调用函数就是给计数器加上一定数后存入栈中,再开启跳转模式;从函数返回就是把栈中的数字写入计数器。

用指令从高往低数第 \(4\) 位来作为调用指令的标识,第 \(5\) 位来作为返回指令的标识。
当调用时,开启跳转模式并通过 \(+4\) 器直接运算并压栈。
当返回时接管第二三四指令线,直接执行 copy _ stack steper

\(+4\)

img

就是个缩短版的 \(+1\) 器。

调用

img

返回

img

汇编

添加汇编别名

10010000 call
01001000 return

上面那个程序就能改写成

const reg0 0

copy|num2 _ 1 reg0

call _ _ ori
call _ _ ori
call _ _ ori
call _ _ ori

jump|always _ _ progend

label ori
add|num2 reg0 1 reg0
return _ _ _

label progend
copy _ reg0 out

这个 LEG 电脑差不多就这样了。总觉得性能还是不好,可能是因为栈的延迟太高了,唉知足常乐。
最后合个影

img

汇编挑战

AI 打牌

这一关说谁最后拿牌谁就输,我们可以来推导一下。

如果桌子上只有 \(1\) 张牌,那么我们必输。
如果有 \(2,3,4\) 张牌,我们可以分别拿 \(1,2,3\) 张牌,我们能赢。
如果有 \(5\) 张牌,那么我们不论拿几张牌,都能使对方进入 \(2,3,4\) 张牌的情况,我们必输。

由此可见,只要每次让桌子上剩 \(4n+1\) 张牌,对方就必输。
这关开始给了 \(12\) 张牌,那我们只要第一次使桌子上剩 \(9\) 张,第二次剩 \(5\) 张,第三次剩 \(1\) 张就行。

sub|num2 in 9 out
sub|num2 in 5 out
sub|num2 in 1 out

img

机器赛跑

这关我懒得优化代码了。

点我查看长代码
const r 0
const d 1
const l 2
const u 3

call _ _ ur
call _ _ dr
num _ r out
call _ _ ul
call _ _ ur
call _ _ ul
num _ l out
call _ _ dl
num _ u out
call _ _ ur

call _ _ fn0

num _ r out
num _ r out

call _ _ fn0

num _ d out
call _ _ dl
call _ _ ul
num _ l out
call _ _ dr
call _ _ dl
call _ _ dr
num _ r out
call _ _ ur
num _ d out


label ur
num _ u out
num _ r out
return _ _ _
label dr
num _ d out
num _ r out
return _ _ _
label ul
num _ u out
num _ l out
return _ _ _
label dl
num _ d out
num _ l out
return _ _ _

label fn0
call _ _ ul
num _ u out
call _ _ ur
call _ _ dr
call _ _ ur
num _ d out
call _ _ dl
call _ _ dr
return _ _ _

img

新品上市

用水果作地址来读内存,如果读到了之前放的内容就是重复了。

const LEFT 0
const GO 1
const RIGHT 2
const EMM 3
const ACT 4
const BELT 92
const EXIST 1

num _ RIGHT out
num _ GO out
num _ RIGHT out
num _ GO out
num _ GO out
num _ GO out
num _ GO out
num _ RIGHT out
num _ GO out
num _ LEFT out
num _ GO out

label loop
num _ EMM out
jump|if_equ|num2 in BELT loop
copy _ in addr
copy _ mem memtemp
num _ EMM out
jump|if_equ|num2 memtemp EXIST repeat
num _ EXIST mem
jump|always _ _ loop

label repeat
num _ RIGHT out
num _ ACT out

美味排序

我比较懒,就写了个冒泡排序。

function sort(mem: number[]) {
	let sorting = mem.length - 1;
	do {
		let addr = 0;
		do {
			let temp0 = mem[addr];
			addr++;
			let temp1 = mem[addr];
			if (temp0 < temp1) continue;
			mem[addr - 1] = temp1;
			mem[addr] = temp0;
		} while (addr != sorting);
		sorting--;
	} while (sorting != 0);
}

之后就能得到对应的程序

const LENGTH 15
const sorting 0
const temp0 1
const temp1 2

label read
copy _ in mem
add|num2 addr 1 addr
jump|if_neq|num2 addr LENGTH read

sub|num LENGTH 1 sorting
label sort_one
num _ 0 addr
label contrast
copy _ mem memtemp
add|num2 addr 1 addr
copy _ memtemp temp0
copy _ mem memtemp
wait _ _ _
jump|if_leq temp0 memtemp ok
copy _ memtemp temp1
sub|num2 addr 1 addr
copy _ temp1 mem
add|num2 addr 1 addr
copy _ temp0 mem
label ok
jump|if_neq addr sorting contrast
sub|num2 sorting 1 sorting
jump|if_neq|num2 sorting 0 sort_one

num _ 0 addr
label write
copy _ mem memtemp
add|num2 addr 1 addr
copy _ memtemp out
jump|if_neq|num2 addr LENGTH write

img

跳舞机器

这关需要左移右移。左移 \(\times 2\) 就行,右移的指令我怎么也没想出来,真是气死我了,只好再给电脑装一个右移器。

img

添加汇编别名

00000010 shr

之后程序这么写

const seed 0
const temp1 1
const temp2 2
const temp3 3

copy _ in seed

label loop
shr|num2 seed 1 temp3
xor seed temp3 temp1
add temp1 temp1 temp3
xor temp1 temp3 temp2
shr|num2 temp2 2 temp3
xor temp2 temp3 seed
and|num2 seed 3 out
jump|always _ _ loop

img

核金汉诺塔

根据题目描述写就行

const GRAB 5
const disk_nr 0
const source 1
const dest 2
const spare 3
const temp 4

copy _ in disk_nr
copy _ in source
copy _ in dest
copy _ in spare

call _ _ move

label move
jump|if_equ|num2 disk_nr 0 is0
copy _ disk_nr stack
copy _ source stack
copy _ dest stack
copy _ spare stack
sub|num2 disk_nr 1 disk_nr
copy _ dest temp
copy _ spare dest
copy _ temp spare
call _ _ move
copy _ stack spare
copy _ stack dest
copy _ stack source
copy _ stack disk_nr

call _ _ outfn

copy _ disk_nr stack
copy _ source stack
copy _ dest stack
copy _ spare stack
sub|num2 disk_nr 1 disk_nr
copy _ source temp
copy _ spare source
copy _ temp spare
call _ _ move
copy _ stack spare
copy _ stack dest
copy _ stack source
copy _ stack disk_nr
return _ _ _
label is0
call _ _ outfn
return _ _ _

label outfn
copy _ source out
num _ GRAB out
copy _ dest out
num _ GRAB out
return _ _ _

img

行星之名

根据题意可以写出以下伪代码

bool flag = true;
while (true) {
	char c = get_in();
	if (flag) {
		put_out(c - 32);
		continue;
	}
	put_out(c);
	if (c == 32) {
		flag = true;
	} else {
		flag = false;
	}
}

翻译成程序

const flag 0
const c 1
const TRUE 0
const FLASE 1

label loop
copy _ in c
jump|if_equ|num2 flag TRUE upper
copy _ c out
jump|if_equ|num2 c 32 is_space
num _ FLASE flag
jump|always _ _ loop
label is_space
num _ TRUE flag
jump|always _ _ loop
label upper
sub|num2 c 32 out
num _ FLASE flag
jump|always _ _ loop

img

水世界

这关有几种解法。

水漫金山法

从最矮的海拔开始往上一层一层放水。如果现在海平面放到了 \(n\) ,就从左往右检查有没有高度 \(h \geq n\) 的列。如果现在检查到了从左往右数第 \(x\) 列,够高,且之前检查到的距离最近的第 \(y\) 列也够高,他们之间就能存水 \(x-y-1\) 。如果这一层只能检查到 \(\leq 1\) 列够高的,就说明放的水全漏光了,停止检查并输出之前的水量总数。

fn get_water(terrain: &[usize]) -> usize {
    let mut water = 0;
    let mut level = *terrain.iter().min().unwrap() + 1;
    loop {
        let mut last_peak = 0;
        let mut peak_num = 0;
        for i in 0..16 {
            if terrain[i] >= level {
                if peak_num > 0 {
                    water += i - last_peak - 1;
                }
                peak_num += 1;
                last_peak = i;
            }
        }
        if peak_num <= 1 {
            break;
        }
        level += 1;
    }
    water
}

翻译成程序

const water 0
const level 1
const last_peak 2
const peak_num 3
const temp 4

sub|num2 level 1 level

label read
copy _ in temp
copy _ temp mem
jump|if_geq temp level not_min
copy _ temp level
label not_min
add|num2 addr 1 addr
jump|if_neq|num2 addr 16 read

label loop
add|num2 level 1 level
num _ 0 addr
copy _ mem memtemp
num _ 0 last_peak
num _ 0 peak_num
label check
jump|if_lss memtemp level lower
jump|if_equ|num2 peak_num 0 first_peak
add water addr water
sub water last_peak water
sub|num2 water 1 water
label first_peak
add|num2 peak_num 1 peak_num
copy _ addr last_peak
label lower
add|num2 addr 1 addr
copy _ mem memtemp
jump|if_neq|num2 addr 16 check
jump|if_gtr|num2 peak_num 1 loop

copy _ water out

img

找水池两边法

这个解法是我搜到的。
用一个水池的中心把地图分为左右两部分,则水池的左右两边分别为左右两部分的最高列,且水面与这两列中较矮的那个齐平。对水池的不是中心的其他列来说也是这样。求这个水池的水量就是用水面高度减每列高度。从左往右开始寻找最高海拔,找到第 \(i\) 列时的最高海拔就是第 \(i\) 列左边的最高海拔。反之,从右往左寻找最高海拔,找到第 \(i\) 列时的最高海拔就是第 \(i\) 列右边的最高海拔。

fn get_water(terrain: &[usize]) -> usize {
    let mut left = [0; 17];
    let mut right = [0; 17];
    let mut water = 0;
    for addr in 0..16 {
        left[addr + 1] = left[addr].max(terrain[addr]);
    }
    for addr in (0..16).rev() {
        right[addr] = right[addr + 1].max(terrain[addr]);
    }
    for addr in 0..16 {
        water += left[addr + 1].min(right[addr]) - terrain[addr];
    }
    water
}

翻译成程序

const temp0 0
const temp1 1
const LEFT 18
const RIGHT 37
const water 2

num _ 0 addr
label left
copy _ in temp0
copy _ temp0 mem
add|num2 addr LEFT addr
copy _ mem memtemp
add|num2 addr 1 addr
copy _ memtemp temp1
jump|if_geq temp0 temp1 left_gtr
copy _ temp1 temp0
label left_gtr
copy _ temp0 mem
sub|num2 addr LEFT addr
jump|if_neq|num2 addr 16 left

num _ 15 addr
label right
copy _ mem memtemp
add|num2 addr RIGHT addr
copy _ memtemp temp0
copy _ mem memtemp
sub|num2 addr 1 addr
copy _ memtemp temp1
jump|if_geq temp0 temp1 right_gtr
copy _ temp1 temp0
label right_gtr
copy _ temp0 mem
sub|num2 addr RIGHT addr
jump|if_neq|num2 addr 255 right

num _ 0 addr
label calc
add|num2 addr 19 addr # LEFT+1
copy _ mem memtemp
add|num2 addr 17 addr # (RIGHT-1)-(LEFT+1)
copy _ memtemp temp0
copy _ mem memtemp
sub|num2 addr 36 addr # RIGHT-1
copy _ memtemp temp1
copy _ mem memtemp
jump|if_leq temp0 temp1 calc_u0
copy _ temp1 temp0
label calc_u0
add water temp0 water
sub water memtemp water
add|num2 addr 1 addr
jump|if_neq|num2 addr 16 calc

copy _ water out

img

于是就通关了。
写这个攻略比我玩通关的时间还长,可能是因为写攻略和再玩一遍没太有区别。这么想的话一个游戏能玩两遍我还赚了,不过本来玩盗版也没花钱就是了。

posted on 2025-01-28 14:54  肉丁土豆表  阅读(3006)  评论(3)    收藏  举报