用最糟糕的语言构建 Z 机器 – 白胡子领域
Building a Z-Machine in the worst possible language – Whitebeard's Realm

原始链接: https://whitebeard.blog/posts/building-a-z-machine-in-elm/

本文详细介绍了使用 Elm 编程语言创建一个 Z 机器模拟器的过程。Z 机器是由 Infocom 在 1980 年代开发的虚拟机,用于在不同的计算机系统上运行文字冒险游戏。尽管 Elm 是一种纯函数式语言,不适合 Z 机器的直接内存访问架构,但作者成功构建了一个可用的模拟器。 挑战在于在 Elm 的不可变框架内复制可变操作(如写入内存)。然而,Elm 底层持久化数据结构,特别是 RRB 树,在处理这些操作时,无需过度复制数据,表现出令人惊讶的效率。 该库可在 GitHub 上获取,允许开发者加载 `.z3` 游戏文件并逐步执行,处理输入、保存和输出事件,例如文本显示。它为构建交互小说玩家提供了一个简洁的接口,即使在浏览器环境中也是如此,并包含一个带有 Zork1 的示例应用程序来演示其功能。作者希望该库能够促进对交互小说客户端功能进行实验。

黑客新闻 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 用最糟糕的语言构建 Z-Machine – 白胡子领域 (whitebeard.blog) 4 分,来自 techbelly 1 小时前 | 隐藏 | 过去 | 收藏 | 讨论 帮助 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

The Z-machine is an early 1980s virtual machine made by Infocom to allow them to compile their text adventure games once and have them run on multiple architectures. It’s a good trick: if you have 10 games that need to run on 10 different computer architectures (ah, the good old days!) you can reduce the work from 10 x 10 compilation nightmares to 10 Z-machines + 10 compilation nightmares. And that maths compounds as you add more architectures and more games. There are many modern Z-machines that are part software archeology and part love letters to this age of text adventures.

I’ve always wanted to make one of my own and now I have!

My language of choice is Elm. But… the Z-machine is a direct memory access machine with a separate stack, designed to run on a very modest machine. The worst language to implement this in would be one in which all data structures are immutable and no functions have side-effects, a pure language. Like Elm. But there we are.

As an example of how silly an idea this is: imagine that you’re storing the memory as an Array of bytes. To write a bit to the memory in a non-pure language you might have a function that takes the memory, an address and a value. It would then write the value into the memory, perhaps returning a success or error code. We can’t do that in Elm.

Instead you’ll have a function that takes the same parameters but then returns a new memory with the byte changed. The original parameter remains unaffected. For a Z-machine emulator that looks like a lot of data copying and a performance/memory disaster - just to set one byte. Initially I thought it wouldn’t be practical without a lot of clever data structures. But actually, it turns out that Elm was already there. Arrays are backed by a persistent data structure - a RRB trie variant - that allows much better performance when slicing and appending. An initial test seemed to prove this and so I pushed on telling myself I wasn’t as crazy as I suspected.

A couple of weeks later - writing tests, wrestling with the Z-machine spec (the text encoding alone took a few days to understand), trying to find immutable patterns to support mutable operations - and I have a working Z-machine that can run a .z3 infocom game (version 3, the most common version) and that passes the Czech compliance test for Z-machines.

Some of it is not very pretty, but it works and it’s performant enough to be a viable way to build a good interactive fiction player - in the browser or elsewhere.

elm-zmachine running Zork in a terminal — status line showing “West of House | Score: 0 Turns: 2”, the welcome text and a few opening moves into Forest Path

The main thing I want from this is to do some interesting client experiments (like if-pal). To make that easier I’ve done my best to give the library a clean interface for stepping and handling events. Time for a bit of elm code. I’m sorry.

It’s one line to load a .z3 file and get back a ZMachine:

ZMachine.load : Bytes -> Result String ZMachine

There are no infinite loops in elm so instead we run the machine for a maximum number of instructions and expect it to tell us if it hasn’t finished. We get back an updated machine and a StepResult:

ZMachine.runSteps : Int -> ZMachine -> StepResult

A StepResult gets you the result of a step/steps, a list of output events and a new machine:

type StepResult
    = Continue (List OutputEvent) ZMachine
        -- we ran the steps, but there are more to run
    | NeedInput InputRequest (List OutputEvent) ZMachine
        -- we need to get some input from the user
        -- once we have it call ZMachine.provideInput
    | NeedSave Snapshot (List OutputEvent) ZMachine
        -- the user's asked to save this snapshot
        -- once we've done it call ZMachine.provideSaveResult
    | NeedRestore (List OutputEvent) ZMachine
        -- the user wants to load a snapshot
        -- once we've done it call ZMachine.provideRestoreResult
    | Halted (List OutputEvent) ZMachine
        -- we're done
    | Error ZMachineError (List OutputEvent) ZMachine
        -- oops

An OutputEvent can be one of the following - the main ones to handle are PrintText, NewLine and ShowStatusLine:

type OutputEvent
    = PrintText String
    | NewLine
    | ShowStatusLine StatusLine
    | SplitWindow Int
    | SetWindow Window
    | EraseWindow Int
    | SetCursor Int Int
    | SetBufferMode Bool
    | PlaySound Int

I think that makes for a very simple surface with enough functionality to build a good client.

There are more details on the github page and in the repository there’s an example node.js/elm app that shows an example of using it with a copy of Zork1. If you’ve ever wanted to build your own infocom client - and really, who hasn’t? - this should get you a long way there.

联系我们 contact @ memedata.com