写于 2021 年 8 月 13 日.

写在前面

关于游戏标题

游戏名 Recursed 是 recurse (递归) 的一般过去时态,译作 “递归” 应该是没有问题的。《递归》里面的递归显然就是递归算法里面的递归。另一方面,游戏标题界面无限播放的动画似乎在将 Recursed 分割成 Re(重复)和 cursed (诅咒)两部分,倒也很契合游戏的氛围。

cursed

cursed 诅咒

从程序设计的角度理解《递归》

游戏的中的物品可以分成两类,一类是不涉及递归机制的寻常物品,如:石头、钥匙、锁、水,仅仅是横版解谜游戏中的一般要素。另外一类则是引入新机制的物品。

红色箱子(Chest):

首先出现的不一样的要素是红色的箱子。通过红色箱子进入一个新的房间,对应了一个红色的出口,通过红色出口返回上一层,再次进入房间被重置。

Alt text
Alt text

游戏教学

1.1 函数的角度

很自然的联想就是将红色箱子视为一个函数(function),从函数的角度可以很好地解释游戏中很多情况。玩家拿着物品进入箱子相当于参数传递和调用函数。拿起房间里面的物品,通过红色的出口返回。相当于函数返回(return)一个值。函数的调用结束,该作用域内的变量就会被清除,所以下次再进入房间,一切都会重置。

由于玩家只能拿起一个物品,所以函数的返回值只能有一个。相当一部分的编程语言的函数都只支持返回一个值,有的时候实际需求需要返回多个数值,游戏里面很多时候也面临相同的问题。

函数内支持调用另外的函数如:

1
2
3
4
fun1 (parms){
//other operations
fun2 ()
}

游戏中的箱子里面可以包含另外一个箱子,对应了这种情况。游戏中的箱子里面也可以包含箱子自己本身,相当于在函数中调用函数本身,即递归

1
2
3
4
 fun1 (parms) {    
if (condition) return // 必须在某种条件下返回,否则会不停执行下去
fun1 (parms)
}

1.2 面向对象(OOP)的角度

另一种有用的观点是将红色箱子视为对象变量,不同的箱子引用了不同的对象,玩家则穿梭于内存的迷宫之中,通过对象引用的指向顺腾摸瓜解决谜题。可以认为玩家进入箱子的过程才是类实例化的过程,才分配新的内存(Chest chest1 = new Chest () ),而进入房间之前,对象并不存在。在箱子内玩家可以操纵封装在类里面的数据,当玩家退出房间的时候,该类的实例被删除(析构函数调用),内存被回收。

函数内可以调用其他函数,类的成员也可以是其他类,当然也可以包含本身,比如链表的实现

1
2
3
4
class Node {         
T data;
Node nextNode; // 下一个节点
}

对于游戏里面的箱子,有类似的结构

1
2
3
4
5
6
class Chest {
Key key1, key2;
Lock lock;
Rock rock;
Chest chest
}

2 水

下水道(Sewer)关卡引入的新的要素是水,浸没在水中的箱子内部也会充满水。从函数的角度来看,可以认为水是一个参数,相当于调用函数
fun (Parm,....,Water = true)
从 OOP 的角度来看,箱子这个类实现可以被水浸没的结构( interface )

1
class Chest implements Soakable

后面可以看到罐子(Jar) 指向的空间是不会受水的影响的,所以可以认为罐子没有实现这个接口。

3 神秘的绿色能量 ——static 修饰符

从地牢关卡(Dungeon)开始,有的物品散发着神秘绿光。戒指中留存的信息显示,被绿色能量包围的物体十分难闻,有杏仁的味道,还担心它们是否有毒(某些氰化物的味道就是杏仁味)。发绿光的物体不受游戏的核心规则的影响:离开房间之后并不会被销毁,即再次进入房间,其状态和上次离开房间的时候相同。不知道是不是巧合,《时空幻境》中同样有不受主角时间倒流能力影响的物品,恰好也是发着出光。

不排除受到 《时空幻境》的影响,但绿光本身和静态(static)修饰符高度相似。被 static 修饰的域,该类的所有对象都共享同一份拷贝,这个域叫做静态域,或者叫类域。考虑有一个箱子类,其内部成员是两把钥匙,其中一把钥匙散发着绿光

1
2
3
4
 class Chest {         
static Key static_key;
Key key;
}

对于这个类的所有对象, static_key 对应的是同一把钥匙。假设房间 1 和房间 2 都是这个类的实例,在房间 1 中改变 static_key 和 key 两个变量的位置,离开房间 1 后进入房间 2 中,static_key 的位置和房间 1 的位置相同,而 key 的位置被重置。

悖论空间:Bug or Feature?

在 JAVA 中对静态域而言,即使类的实例不存在,静态域依然存在。比如通过 Math.PI 就可以直接调用值为 pi 的静态常量,并不需要创建类的实例。所以游戏里发绿光的物品似乎也可以认为是一直存在,并不会消失。然而游戏中还是存在不止一种方式,可以销毁绿色物品。尤其是绿光作用于红色箱子时,红色箱子既是出口,又是入口。如果红色箱子被销毁,就会无法返回上一层的空间。这样的恶性 bug 游戏中称之为悖论(paradox),为其设置了新的关卡。

4 罐子(Jar)

某些箱子内有一个绿色的出口,从绿色出口离开,该空间内的状态不会被销毁,在上层空间内生成一个罐子,通过罐子可以再次访问这片区域。与之相似的概念是指针(pointer),可以认为我们通过绿色出口,创建了一个指向该空间的指针。罐子所指向的空间本身就存在,并不是进入罐子才创建的,所以也可以解释当其罐子泡在水中的时候,这片空间并不会受到影响。

野指针(wild pointer)

野指针是指向位置不明的指针,如果对它解引用或者利用它对指向未知的内存进行操作,可能造成严重的后果。未对指针进行绑定,或者指针指向的对象被销毁,这两种情况下指针指向的空间都是不明的。

一般情况下利用罐子访问的操作只能进行一次,在罐子里只能通过红色出口离开,这样保存在罐子里面的空间将被销毁。所以为了防止罐子成为 “野罐子”,相应的罐子也会被销毁,对游戏和对玩家而言,都是安全和合理的机制。但是如果通过某种方式可以获得一个本该销毁的罐子的一份拷贝,就得到了一个 “野罐子”。通过这个罐子访问未知的区域的危险操作对应了成就故障(Glitch),似乎存在不同的 Glitch 空间,但是似乎并没有针对这种情况设计新的谜题。

5 DLC1 青色六边形

DLC 1 中的青色六边形可以拷贝任意的物品。程序中的拷贝操作有时候让人迷惑。

以 python 为例,a 是一个整数,用 b 将它的数值拷贝一份。我们确实得到了 a 的一份拷贝,就像一张白纸在复印机里面的复印了一份文件,如果修改复印文件,原来的文件不会受到影响。这样的复制符合对复制的一般预期。

1
2
3
4
5
6
7
>>> a = 42
>>> b = a
>>> a,b
(42, 42)
>>> b = 24
>>> a,b
(42, 24)

但是,如果 a 是 python 一个列表(list),还用原来的方法(=)实现预想的拷贝,如果修改原始文件的那份拷贝 Chest2,原始文件 Chest1 也跟着改变了,这种情况就好比在复印机复印了一份文档,在上面涂画的时候发现原来的文件也出现了相同的痕迹。

1
2
3
4
5
6
7
>>> Chest1 = ["key","rock","rock"]
>>> Chest2 = Chest1
>>> Chest2 [0] = "rock"
>>> Chest1
['rock', 'rock', 'rock']
>>> Chest2
['rock', 'rock', 'rock'] # Chest1 和 Chest2 指向相同的列表

在游戏中,是哪一种拷贝方式,要视情况而定。

6 DLC2 火炉

DLC1 和 DLC2 没有交叉,不是很喜欢在这个机制,只通关了第一页谜题。

待续