传家宝软件:将过去作为探险

作者: Eric S. Raymond

多年来,我花费了在某些人看来可能过多的时间来清理和保存古老的软件。我的 怀旧计算博物馆 页面存档了许多可能显得完全过时的计算机语言和游戏。

我保存这些材料是因为我认为关注它们有非常充分的理由。有时,这些古老的设计揭示出意想不到的艺术性,令人惊讶的方法可以帮助我们摆脱我们不知道自己背负的假设和限制。

但同样重要的是,文化通过其历史和文物来理解自身,编程文化也不例外。如果您是一位计算机黑客,那么传家宝软件的伟大作品就是您的遗产,就像古代大师的画作是视觉艺术家的遗产一样;了解它们可以丰富您的知识,并有助于巩固您与您的技艺的关系。

为了完全重现历史计算体验,没有什么比在主机硬件的软件模拟器上运行原始二进制可执行文件更好的了。有一些小但蓬勃发展的重现主义者团体为数十种不同的历史计算机做这类事情。

但这并不是我今天在这里要写的,因为我发现那种博物馆化没什么意思。它通常不会对旧代码或其设计者的思维产生深刻的见解。为了实现这一点——为了获得与充分欣赏古代大师画作平行的体验——您不仅需要一个正在运行的程序,还需要您可以阅读的源代码。

因此,我一直对向前移植传家宝源代码更感兴趣,以便它可以在现代环境中运行和研究。我甚至不一定认为保留原始实现语言至关重要;在我看来,重要的目标是:1) 以一种可以研究该设计作为工艺和艺术作品的方式来保存原始设计,以及 2) 尽可能地复制原始的用户界面,以便对深入研究源代码不感兴趣的休闲探索者至少可以感受到其原始用户的体验。

现在我将具体地谈谈 Colossal Cave Adventure

这款游戏,仍然被许多粉丝称为 ADVENT,因为它是在一个最多支持六个字符长的单字符文件名的操作系统上编写的,是早期软件的伟大经典之一。它于 1976-77 年编写,是第一个文本冒险游戏。它也是每个 rogue-like 地牢模拟的直接祖先,并通过这些地牢模拟,间接成为当今编写的相当大比例的游戏的祖先。

如果您到了某个年龄,以下开场序列将带回一些美好的回忆


Welcome to Adventure!!  Would you like instructions?

> n

You are standing at the end of a road before a small brick building.
Around you is a forest.  A small stream flows out of the building and
down a gully.

> in


You are inside a building, a well house for a large spring.

There are some keys on the ground here.

There is a shiny brass lamp nearby.

There is food here.

There is a bottle of water here.

>

从这个开始,游戏以一种嘲讽、古怪、幽默和略带超现实主义的风格发展——这种模式强烈影响了计算机黑客的民间文化,这种文化后来演变成今天的开源运动。

作为一种流派的开山之作,ADVENT 的风格在回顾时显得惊人地成熟。作者们并没有在为一种后来会被更自信的后来的艺术家大大改进的习语而摸索;相反,他们实现了一种始终如一(并且在当时是独一无二的)的风格,这种风格几乎被所有在文本冒险中追随他们的人所效仿,并且作为风格并没有得到太多改进,即使游戏引擎的技术突飞猛进,主题范围也大大拓宽了。

ADVENT 在艺术上具有创新性——并且其架构也超前于时代。尽管早在十年前的研究语言(尤其是 LISP)中就已瞥见过这种可能性,但 ADVENT 仍然是最早幸存下来的程序之一,它被组织成一个复杂的、声明式指定的数据结构,由一个简单得多的状态机驱动。即使在今天,这种设计风格也未得到充分利用。

另一方面,ADVENT 实际具体源代码的持续相关性则完全是另一回事。与架构、游戏或其散文相比,实现的老化速度更快——而且更糟糕。

ADVENT 最初是在 TOPS-10 下编写的,TOPS-10 是 DEC PDP-10 小型计算机的早已过时的操作系统。原始版本的源代码仍然存在(您可以在 互动小说档案馆 中找到它和其他相关资源),但它往往会击败尝试将其作为编程艺术作品来欣赏的尝试,因为它是在一种古老的 FORTRAN 方言中编写的,其中(根据实际计数)在其 2.4KLOC 的源代码中包含超过 350 个 goto。

因此,保存原始 FORTRAN 对于建立出处(正如历史学家对这些事物的看法)很有好处,但对于我提出的将这些文物保留下来的文化目的并没有多大帮助。为此,忠实地翻译成更现代的语言会更有用。

碰巧的是,Don Woods 在 1977 年的 ADVENT 版本在编写后不到两年就被翻译成了 C 语言。您仍然可以玩它——并阅读代码——作为 BSD Games 包的一部分。唉,虽然该翻译对于构建和运行程序很有用,但对于阅读来说却不是那么好。它比 FORTRAN 更容易理解,但没有完全转移到地道的 C 语言中,并且在现代人看来有点奇怪。(公平地说,对于翻译人员来说,C 语言在 1977 年仍处于起步阶段,其现代习语尚未完全发展起来。)

因此,BSD 翻译中仍然存在令人望而生畏的 goto 数量。大量信息通过共享全局变量传递,这在 FORTRAN 中很典型,但即使在当时在 C 语言中也是有问题的风格。BSD C 代码充满了从祖先 FORTRAN 源代码继承而来的神秘常量。并且围绕原始 FORTRAN 和 BSD C 版本都使用的自定义文本数据库存在严重的理解性问题——我将在本文后面回到这个问题。

在 1970 年代后期和 1980 年代初期,很多人编写了 ADVENT 的扩展,添加了更多的房间和宝藏。这些变体的历史复杂且难以追踪。几乎被喧嚣淹没的是,原始作者——Will Crowther 和 Don Woods——继续自己修改他们的游戏。最后一个主线版本——Don Woods 的最后一个版本——是 1995 年的 Adventure 2.5

我在 2016 年底在互动小说档案馆中找到了 Adventure 2.5。关于它有两件事引起了我的注意。首先,我以前不知道 Crowther 和 Woods 自己 已经发布了一个从著名的原始版本扩展而来的版本。其次——与早期的 BSD 端口不同——没有任何类似于我们期望在现代源代码版本中看到的与裸代码和 Makefile 配套的东西。没有手册页。没有许可声明。

此外,2.5 代码非常难看。它是 C 语言,但情况比 BSD 端口更糟糕。注释实际上包括 Don Woods 的道歉,解释说它是通过他自己设计的自制翻译器从 FORTRAN 机械地提取出来的——并为糟糕的风格道歉。

尽管如此,我看到了可能性——我写信给 Don,征求他的许可,以真正的开源许可证发布清理后的版本。回复来得有些晚,但 Don 不仅代表他自己和 Will Crowther 授予了许可,他还积极鼓励我做这件事。

现在提醒一下我对遗产保护目标的看法:我认为至关重要的是,清理后的版本在任何时候都不应破坏与我们从 Woods 和 Crowther 那里获得的版本的功能兼容性。因此,在获得传家宝源代码以进行干净构建之后,我做的第一件事是添加了捕获命令日志以进行回归测试的功能。

当您进行这样的修复时,仅仅尽最大努力保持原始行为是不够的。您应该能够证明您已经做到了。因此,最佳实践是从构建一套真正全面的回归测试开始。这就是我所做的。

应该说,是我们一起做的。该项目很快吸引了合作者——最值得注意的是 Jason Ninneman。Jason 的几个好主意中的第一个是使用覆盖率分析工具来识别测试套件中的差距。后来,Petr Vorpaev、Peje Nilsson 和 Aaron Traas 也加入了进来。从开始大约一个月后,我们可以展示超过 95% 的测试覆盖率。而且,当然,我们使用最新版本的测试套件对我们可以读取日志的最早版本进行了回顾性测试。

这种真正良好的测试覆盖率解放了您的双手。它使我们能够在另一个主要目标上取得快速进展,即将我们开始使用的模糊源代码变成一部可读的艺术作品,充分揭示原始设计的意图和惊人的聪明才智。

因此,所有神秘的幻数都必须去掉。goto 缠身的意大利面条式代码必须重组为 Don Woods 在 2017 年不会觉得需要道歉的东西。总的来说,我们旨在将源代码转换为我们相信 Crowther 和 Woods——他们那个时代最聪明的黑客中的两位——如果在 1977 年拥有 2017 年的工具和最佳实践,他们会写出的东西。

我们最(嗯哼)冒险的举动是放弃 Crowther 和 Woods 用于描述游戏词汇表和 Colossal Cave 拓扑结构的自定义文本数据库格式。

这——我之前提到的“复杂的、声明式指定的数据结构”——是设计中最聪明的功能,它可以追溯到 Crowther 的第一个版本。地牢的拓扑结构通过一种伪代码来表达,这种伪代码大致类似于在许多处理器架构下找到的微代码;移动包括调度到与当前房间相对应的操作码序列,并根据用户输入的移动动词以及伪代码中的条件来确定要触发哪个操作码,这些条件可以测试对象的存在或不存在及其状态。

祝您从我们开始使用的 2.5 代码中理解这一点一切顺利。以下是最初出现在 adventure.text 中的前两条规则,共包含十个操作码


3
1       2       2       44      29
1       3       3       12      19      43
1       4       5       13      14      46      30
1       145     6       45
1       8       63
2       1       12      43
2       5       44
2       164     45
2       157     46      6
2       580     30

以下是这些规则的样子,已转换为我们的修复版本 Open Adventure 现在使用的 YAML 标记


- LOC_START:
    travel: [
      {verbs: [ROAD, WEST, UPWAR], action: [goto, LOC_HILL]},
      {verbs: [ENTER, BUILD, INWAR, EAST], action:
       ↪[goto, LOC_BUILDING]},
      {verbs: [DOWNS, GULLY, STREA, SOUTH, DOWN], action:
       ↪[goto, LOC_VALLEY]},
      {verbs: [FORES, NORTH], action: [goto, LOC_FOREST1]},
      {verbs: [DEPRE], action: [goto, LOC_GRATE]},
    ]
- LOC_HILL:
    travel: [
      {verbs: [BUILD, EAST], action: [goto, LOC_START]},
      {verbs: [WEST], action: [goto, LOC_ROADEND]},
      {verbs: [NORTH], action: [goto, LOC_FOREST20]},
      {verbs: [SOUTH, FORES], action: [goto, LOC_FOREST13]},
      {verbs: [DOWN], action: [speak, WHICH_WAY]},
    ]

Adventure 2.5 编写时,使用 Python 辅助程序将这样的声明式标记编译为 C 源代码以链接到游戏的其余部分的概念可能只是勉强可以想象的。YAML 在六年之后才出现。

但是……设计师的意图。在 YAML 版本中,这比在它所取代的版本中更容易看到。因此,考虑到传家宝修复的目的,YAML 更好。就像从伦勃朗的画作上去除变暗的清漆一样——如果您习惯了遮蔽性的覆盖层并认为它是权威的,那么下面的鲜艳色彩可能会让您感到惊讶,但它们才是作品的真相。

考虑到我们对可以更改的内容的选择如此受限,您可能会认为修复工作是苦差事,但事实并非如此。它更像是抛光一颗粗糙的钻石——逐渐看到光彩从不起眼的表面下显现出来。粗糙之处在很大程度上——虽然并非完全是——是 Crowther 和 Woods 手头工具的局限性的结果。当我们清理干净后,我们发现天才之作只有少许错误。

我的开发团队当然修复了这些错误。我们是黑客;这意味着我们认为传家宝软件是一种活生生的遗产,应该加以改进,而不是应该崇拜的偶像。例如,我们当然不认为 Don Woods 打算使用动词“熄灭”来使装满油的未点燃的瓮中的油消失。

Petr Vorpaev 在审阅本文草稿时观察到“有时,我们也剥离了一些天才之处。因为那是用来解决不再存在的局限性的天才之处。”他想到的是 2.5 代码的一个非常奇怪的特性——它通过以每字符 6 位的编码方式将五个字符打包到一个 32 位字中,从而解决了旧 FORTRAN 中缺少字符串类型的问题。当然,这在 C 语言中是一件疯狂的事情,我们早期就将其作为移除目标。

我们还添加了一些次要功能。例如,Open Adventure 允许一些在今天的文本冒险游戏中是标准的,但在原始 ADVENT 中不支持的命令缩写。默认情况下,我们的版本发出 > 命令提示符,这也已经使用了几十年。而且,您可以使用 Emacs 键击来编辑您的命令输入。

但是,这是至关重要的,所有新功能都通过“oldstyle”选项被抑制。如果您选择该选项,您将获得一种用户体验,即使是主题专家也很难或不可能将其与 1995 年和 1976-1977 年的原始版本区分开来。

您们中的一些人可能仍然会在此时皱起眉头,想知道“YAML?Emacs 键击?即使作为选项?哎呀……这真的还是 Colossal Cave Adventure 吗?”

这是一个由来已久的问题。哲学家们谈到“忒修斯之船”的思想实验;如果忒修斯离开雅典,在他的漫长航行中,船的每一块木板、绳索和桅杆都逐渐被更换,直到当他返回雅典时,没有一块原始木材的碎片留下,它还是同一艘船吗?

正如任何普通语义学的学生都可以告诉您的那样,答案是“您所说的‘相同’是什么意思?”身份不是一个明确定义的谓词;它会根据您使用语言来解决的预测性问题而改变。源代码中相同的位排列?相同的用户界面?在比用户界面更深的层次上相同的行为?

真的没有一个正确的答案。那些倾向于回答“相同”的人可能会争辩说“嘿,它通过了相同的回归测试。”只是,也许现在没有通过。记住,我们修复了一些错误。另一方面……如果忒修斯之船在完全重建后仍然是“相同的”,那么如果我们得知其部件之一的替代品没有复制原始部件中的隐藏缺陷,或者如果在航行过程中添加了一些原始计划中没有的改进,它是否就不再是同一艘船了?

事实上,Adventure 已经通过了一次完整的语言翻译——从 FORTRAN 到 C——其“身份”(以黑客和其他人通常认为的这些事物的方式)完好无损。我认为我可以明天将其翻译成 Go 语言,它仍然是同一个游戏,即使它与相同的位排列相去甚远。

此外,我可以向您展示船的日志。如果您转到 项目存储库,您可以查看 Adventure 2.5Open Adventure tip 版本之间代码的每一次小规模转换。

只要我们的目标仅限于对 Colossal Cave Adventure 进行高质量的修复,那么在这个特定项目上可能没有太多工作要做了。它们几乎肯定会是这样;如果我们想在这种游戏中做一些实质性的事情,明智的方法不是编写自定义 C 代码,而是使用一种专门用于实现它们的语言,例如 Muddle (aka MDL) 或 Adventure Definition Language。

我希望一些更大的教训显而易见。尽管我认为 Colossal Cave Adventure 作为一个个案本身就很有趣,但我写这篇文章的真正目的是提出建设性的方法来思考围绕修复传家宝软件的一般问题——您可能为什么要这样做,您会发现哪些挑战和回报,以及什么是最佳实践。

以下是我可以确定的最佳实践

  • 要牢记的目标是:1) 使原始代码的设计意图可供研究,以及 2) 充分保留 oldstyle 模式的用户界面以欺骗原始用户。

  • 首先构建您的回归测试套件。您需要能够证明您的修复是忠实的,而不仅仅是断言它。

  • 使用覆盖率工具来验证您的回归测试是否足够好以构成证明。

  • 一旦您有了测试,就不要担心更改工具、语言、实现策略或文档格式。这些都是短暂的;良好的设计才是永恒的。

  • 始终有一个 oldstyle 选项。通过承诺并保持对 oldstyle 模式下的原始行为的忠实性,获得改进的自由。

  • 确实要修复错误。这可能与完美回归测试的目标相冲突,但您是工程师,而不是防腐师。根据需要解决该冲突。

  • 展示您的工作。您的产品不仅是修复后的软件,还是它所来自的存储库。该存储库中的历史记录需要持续证明对代码的原始设计意图的良好判断力和敏感性。

  • 记录您更改的内容,包括错误修复。最佳实践是包含维护者注释,详细描述您的修复过程。

  • 当您不确定是否要添加功能时,既不要过于渴望在代码上留下您的印记,也不要成为其过去的奴隶。相反,问问“什么是好的品味?”

当您做所有这一切时,不要忘记玩得开心。最伟大的传家宝作品,如 Colossal Cave Adventure,通常是在高度玩乐的精神下编写的。如果您以同样的精神来修复它们,您将更忠实于它们的意图。

Eric S. Raymond 是一位游走的文化人类学家和惹麻烦的哲学家。他也以写几行代码而闻名。实际上,如果“ESR”标签对您来说毫无意义,那么您在读这本杂志做什么?

加载 Disqus 评论