惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
小众软件
小众软件
博客园_首页
博客园 - 聂微东
V
V2EX
WordPress大学
WordPress大学
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
罗磊的独立博客
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 司徒正美
博客园 - 三生石上(FineUI控件)
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
S
SegmentFault 最新的问题
J
Java Code Geeks
Last Week in AI
Last Week in AI
The Cloudflare Blog
月光博客
月光博客
雷峰网
雷峰网
宝玉的分享
宝玉的分享
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Hugging Face - Blog
Hugging Face - Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
有赞技术团队
有赞技术团队
人人都是产品经理
人人都是产品经理
博客园 - Franky
腾讯CDC
Jina AI
Jina AI
博客园 - 叶小钗
大猫的无限游戏
大猫的无限游戏
阮一峰的网络日志
阮一峰的网络日志
量子位
爱范儿
爱范儿
美团技术团队
T
Tailwind CSS Blog
博客园 - 【当耐特】
D
Docker
IT之家
IT之家
V
Visual Studio Blog
P
Proofpoint News Feed
L
LangChain Blog
Engineering at Meta
Engineering at Meta
C
Check Point Blog
G
Google Developers Blog
Google DeepMind News
Google DeepMind News
云风的 BLOG
云风的 BLOG
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
Recorded Future
Recorded Future

Whitebeard's Realm

Unequal, and hungry for it There's a STAR man, waiting in the sky Introducing Uruk, a language for tabletop games The diving watch I never knew I wanted Introducing Planedrift > ASK THE VM WHERE IS THE PLAYER Building a Z-Machine in the worst possible language Playing Zork with a gen alpha AI Building a better crossword page for my daily cryptic hit What AI is doing for me, in a difficult situation It's Your Turn — a prompt deck for new roleplayers Seven reasons that Trump is a bad GM. Very bad You've Been Pawned — Now With a Snazzy New Look Six months on startplaying.games An online tool to make playable paper pawns Campaign report: Dragon of Icespire Peak: session #6 Campaign report: Rime of the Frostmaiden: session #4 Campaign Report: Dragon of Icespire Peak: Session #5 A few months on StartPlaying.Games Campaign report: Rime of the Frostmaiden: session #3 Character Quirks for easier roleplay How I use Avrae An Owlbear/Discord/DndBeyond checklist for new campaigns About me Projects Talks Videos
Make your own interactive fiction client in Elm
whitebeard · 2026-04-21 · via Whitebeard's Realm

I’m proud of Planedrift.app — my interactive fiction app. The initial feedback has been really good and one of the most common questions has been: “Ben, can you extract an Elm library from Planedrift so I can easily make my own interactive fiction client with innovative features?”

Okay, to be honest, no one asked for that. Four people did ask for dark mode. Dark mode can wait.

elm-zmachine is my Z-Machine interpreter — low-level, all bits and opcodes. Sitting above that is the library I’ve extracted from Planedrift, elm-zengine. It has a set of functionality that every online Z-Machine client might need — session management, queued input, transcript management, turn history, save/restore, and an event stream.

It lets you build a very minimal but usable in-browser player in around 100 lines of Elm. I’ve made a minimal client app in the demos folder of the repo and we’ll walk through some of that code below.

Alternatively, you could build something like I did — with a library, to-dos, dialogs, fonts, colors and all the other shiny bits you need for a full client. Planedrift uses this library itself to run everything game-related.

Or you could, and this is my real hope, build something truly extraordinary. I plan to use it to experiment with new features for Planedrift and odd things like if-pal. If you do make something fun, please let me know, I’d love to see it lead to some interesting stuff.

The rest of this post is a bit dry, but it does walk you through some of the code.

You’ll need

  • Elm 0.19.1
  • A Z-Machine v3 or v5 story file (.z3 / .z5). Infocom’s early catalogue is the canonical source; several stories are freely distributed for personal use.

You might need some familiarity with Elm and The Elm Architecture.

Using it in a new elm project

$ mkdir if-browser && cd if-browser
$ elm init
$ elm install techbelly/elm-zengine

The model

The engine keeps all game state inside an opaque Session value (“opaque” meaning the library doesn’t let you poke inside it — you just hold on to it and hand it back to ZEngine when it wants). A very simple model might be something like:

type alias Model =
    { session : Maybe Session
    , input : String
    , error : Maybe String
    }

The messages

In the demo app, we have messages to support the app and one for the engine bookkeeping:

type Msg
    = OpenClicked
    | FileChosen File
    | BytesLoaded Bytes
    | InputChanged String
    | Submit
    | EngineMsg ZEngine.Msg

Update

OpenClicked ->
    ( model, Select.file [] FileChosen )

FileChosen file ->
    ( model, Task.perform BytesLoaded (File.toBytes file) )

-- `ZEngine.new` is how you make a new session. --
BytesLoaded bytes ->
    ZEngine.new EngineMsg ZEngine.defaultConfig bytes
        |> ZEngine.apply mergeEngine { model | error = Nothing }

InputChanged s ->
    ( { model | input = s }, Cmd.none )

-- ZEngine.sendLine queues a line of input ---
Submit ->
    if String.isEmpty (String.trim model.input) then
        ( model, Cmd.none )
    else
        ZEngine.sendLine EngineMsg model.input model.session
            |> ZEngine.apply mergeEngine { model | input = "" }

-- ZEngine.update handles any messages that need to go to the library ---
EngineMsg engineMsg ->
    ZEngine.update EngineMsg engineMsg model.session
        |> ZEngine.apply mergeEngine model

A couple of things to note — ZEngine.new takes a Config type. For now, we use ZEngine.defaultConfig — it tells the engine to auto-handle things like “press any key” and ignore “save” and “restore.”

Every session-advancing call in ZEngine — new, update, sendLine, and the more specialized sendChar, sendSaveResult, sendRestore, restoreFrom, and resumeFrom — returns the same thing:

type alias Step msg =
    { session : Maybe Session
    , events : List Event
    , cmd : Cmd msg
    , error : Maybe Error
    }

Of note is the events list. These are things the engine noticed during this step (a prompt was issued, the game ended, the status line changed). A minimal client can ignore these; a fancier one listens for GameOver or PromptIssued to play sounds, scroll, auto-save, etc.

ZEngine.apply is a helper that turns a Step into a ( Model, Cmd Msg ) tuple that you can just return from update, given a merge function of your own:

mergeEngine : Maybe ZEngine.Session -> Maybe ZEngine.Error -> Model -> Model
mergeEngine session error model =
    { model
        | session = session
        , error = Maybe.map ZEngine.errorMessage error
    }

The merge function is the one piece of boilerplate you write per app because ZEngine can’t know what your Model looks like.

Rendering the transcript

The session holds a List Frame that represents the game transcript. Each frame is either output the game produced or a command the player entered:

viewTranscript : Model -> Html msg
viewTranscript model =
    case model.session of
        Just session ->
            pre [ style "white-space" "pre-wrap", style "line-height" "1.4" ]
                (List.map viewFrame (ZEngine.transcript session))

        Nothing ->
            p [] [ text "Load a .z3 story to begin." ]

viewFrame : Frame -> Html msg
viewFrame frame =
    case frame of
        OutputFrame data ->
            text data.text

        InputFrame data ->
            span [ style "color" "#06c" ] [ text ("\n> " ++ data.command ++ "\n") ]

Because the transcript lives on the session and every step returns a new session, you never have to accumulate output yourself. Re-render from ZEngine.transcript session on every view call and you’re done. In practice, the Elm DOM diffing will make that plenty performant.

Other properties

In the demo app, you’ll see we also grab other properties from the Session — like the statusLine and the story title. See the docs for more.

What we skipped

The minimal client ignores several other ZEngine features:

  • events: Listening to PromptIssued, GameOver, TurnCompleted, StatusLineChanged, and TitleDetected. Use ZEngine.foldEvents to thread a (Event -> Model -> ( Model, Cmd Msg )) handler through the step’s event list.
  • sendChar / sendSaveResult / sendRestore: The engine surfaces three other prompt types (single character input, in-game save, in-game restore). defaultConfig auto-handles them; a real client can receive them instead by customizing Config.
  • Persistence: ZEngine.snapshot, encodeSnapshot, snapshotDecoder, restoreFrom let you serialize a session to JSON, store it in localStorage or a file, and restore later. Skipped here.
  • turnHistory / resumeFrom: the engine records a per-turn checkpoint. You can offer “rewind to an earlier turn” with resumeFrom.

If you’re planning to make a version of Planedrift.app that supports Dark mode, you’ll probably need all these bits too.