Z-machine 是 1980 年代初由 Infocom 開發的虛擬機,目的是讓他們能夠一次編譯文字冒險遊戲,並在多種硬體架構上運行。這是一個很棒的技巧:如果你有 10 款遊戲需要在 10 種不同電腦架構上運行(啊,那些美好的舊時光!),你可以將工作量從 10 x 10 的編譯噩夢減少到 10 個 Z-machine 加上 10 次編譯噩夢。隨著架構和遊戲數量增加,這種數學關係會成倍擴大。現代有許多 Z-machine 實作,既是軟體考古,也是對文字冒險黃金時代的致敬。
我一直想自己做一個,現在終於做到了!
我選擇的語言是 Elm。但……Z-machine 是一台直接記憶體存取且有獨立堆疊的機器,設計用於非常簡單的硬體。最糟糕的實作語言,莫過於所有資料結構皆不可變且函數無副作用的純函數式語言。像 Elm 就是如此。但事情就是這樣。
舉個例子說明這想法有多瘋狂:假設你用一個位元組陣列來存放記憶體。在非純函數語言中,寫入記憶體的函數會接受記憶體、地址和數值,然後直接修改記憶體,可能回傳成功或錯誤代碼。但 Elm 做不到這點。
取而代之的是,你會寫一個函數,接受相同參數,回傳一個新的記憶體陣列,該陣列在指定位址的位元組已被更改,原本的記憶體不受影響。對於 Z-machine 模擬器來說,這看起來像是大量資料複製,效能和記憶體使用將是災難——只為了改一個位元組。起初我以為沒有複雜資料結構的輔助,這根本不實用。但事實上,Elm 已經幫忙了。陣列底層是持久化資料結構——一種 RRB trie 變體,能在切片和附加時提供更好的效能。初步測試證明了這點,所以我繼續努力,告訴自己沒那麼瘋狂。
幾週後——寫測試、研究 Z-machine 規格(光是文字編碼就花了幾天理解)、尋找不可變模式來支持可變操作——我有了一個能運行 .z3 Infocom 遊戲(版本 3,最常見版本)的工作中 Z-machine,並通過捷克的 Z-machine 合規測試。
有些程式碼不太漂亮,但它能運作且效能足夠,成為在瀏覽器或其他環境中打造優秀互動小說播放器的可行方案。
我主要想用它做一些有趣的客戶端實驗(像是 if-pal)。為了方便,我盡力讓這個函式庫擁有乾淨的介面來執行步進和事件處理。來點 Elm 代碼吧,抱歉。
只要一行就能載入 .z3 檔案並回傳一個 ZMachine:
Elm 沒有無限迴圈,因此我們讓機器執行最多指定指令數,並期待它告訴我們是否尚未完成。回傳的是更新後的機器和一個 StepResult:
StepResult 會告訴你步進結果、輸出事件清單和新的機器狀態:
OutputEvent 可能是以下幾種,主要要處理的是 PrintText、NewLine 和 ShowStatusLine:
我認為這樣的介面非常簡單,功能足夠用來打造優秀的客戶端。
更多細節可見 GitHub 頁面,倉庫中還有一個 node.js/elm 範例應用,展示如何搭配 Zork1 使用。如果你曾想打造自己的 Infocom 客戶端——說真的,誰沒想過呢?——這個專案會帶你走很長一段路。