ANN: Rez — a language, compiler, and runtime for creating HTML-based interactive fiction games

Hi.

I’ve just released (under GPLv3) Rez, a system I have been building for creating interactive fiction HTML games. I started it in response to the feeling that Twine was hindering, rather than helping, me to build the game I had in mind.

In terms of complexity, Rez is aimed at the gap between Twine and something like Inform or TADS. It’s primarily declarative, with embedded Javascript callbacks to handle events and a standard library that includes items, inventories, actors and a save/load system that round-trips through JSON. Anyone with basic Javascript skills should be able to create something that pleases them. The Rez compiler outputs a dist folder containing the HTML/JS/CSS and any assets included in the game.

Rez uses a scene/card concept to keep complexity under control. A scene represents a situation for the player and cards represent content & actions that can be played as part of a scene. Scenes have a layout and can display a card at a time or a sequence of cards. Cards have content that is wrapped in the scene layout. Both have many event hooks to customise them.

Here’s an incredibly simple coin-toss example game to give you an idea of what writing in Rez looks like:

@game begin
  name: "Test Game"
  author_name: "Matt Mower"
  author_email: "self@mattmower.com"
  IFID: "D3C31250-53B4-11ED-9A26-3AF9D3B0DD88"
  archive_format: 1
  initial_scene: #play_game
  layout: """
  {{{scene}}}
  """

  @actor player begin
    score: 0
  end

  @scene play_game begin
    initial_card: #play_round
    blocks: [#sidebar]
    layout_mode: :single

    layout: """
    <div class="columns">
      <div class="column">{{{sidebar}}}</div>
      <div class="column">{{{content}}}</div>
    </div>
    """
  end

  @card sidebar begin
    bindings: {player: "player"}
    content: """
    Score: {{$ player "score"}}
    """
  end

  @card play_round begin
    on_start: (card, event) => {
      card.setAttribute("coin", coin_flip());
    }
    content: """
    Do you [[Choose heads]] or [[Choose tails]]?
    """
  end

  @alias result_card = card # begin
    bindings: {player: "player"}
    content: """
    {{$ card "response"}}
    [[Play again|play_round]]
    """
  end

  @result_card choose_heads begin
    on_start: (card, event) => {
      const flip = card.$("play_round").getAttribute("coin");
      won_or_lost(card, flip == "heads");
    }
  end

  @result_card choose_tails begin
    on_start: (card, event) => {
      const flip = card.$("play_round").getAttribute("coin");
      won_or_lost(card, flip == "tails");
    }
  end

  @script begin
    function won_or_lost(card, winner) {
      const player = card.$("player");

      if(winner) {
        card.setAttribute("response", "Congratulations");
        player.incAttribute("score", 2);
      } else {
        card.setAttribute("response", "Bad luck");
        player.decAttribute("score");
      }
    }

    function coin_flip() {
      return (Math.random() < 0.5) ? "heads" : "tails";
    }
  end
end

For more, see the language guide.

They say that if you aren’t embarrassed by your first release, you waited too long. No fear of that here. Rez is probably mature enough to build a game, although many bits are placeholders for future functionality, including the beginnings of support for the procedural generation of content.

I’m posting now to see if I can find some early adopters to check it out and give me feedback.

Matt

13 Likes

Is there a runnable version of this example game up?

The ideal Rez user right now is probably creating a game that is ambitious for Twine, is probably using a Twee compiler rather than the Twine visual editor, and is not afraid of writing some Javascript code.
rez/README.md

I’m probably close to your ideal user. I’ve worked in JavaScript professionally for years and my first comp game was written in VSCode with Snowman and Tweego; not because Twine wasn’t adequate for what I wanted to build, but because it’s where I was most comfortable as a developer. I’ll consider giving this a shot next time I’m ready to build something.

This is neat! I love how extensive the docs are already. I’d like to see more complete examples in the cookbook to help me build up a sense of what a Rez project can look like.

Something that gives me pause right now is that the small complete examples (the one you posted here and this one in the docs) seem a little verbose, both in terms of requiring several entities and attributes (game, scene, card) and just in some of the syntax (""" strings, {{{ macros; but I’ve never been a fan of Handlebars). I bet that becomes less of an issue as projects scale up (one game, a few scenes, many cards) but it means as a beginner it can take me a while to parse the examples.

Features I’m curious about (but others might not be):

  • Including other JavaScript libraries. What an interesting opportunity to, say, integrate OpenStreetMap into a game.
  • Hooks for automated testing. If you’re hoping folks will build more complex behaviors, helping them add test coverage seems good. It might be sufficient to let the author write plain JS files, test them separately, and import them with your standard import syntax.

Excellent point. I’ve just uploaded it.

The compiler outputs the game into a dist folder. I just uploaded that directly to the WordPress site.

It’s worth noting that, as of today, Rez doesn’t do anything about minifying or compressing either its own JS or anything else you include. I’d rather not implement this myself since there are plenty of solutions. I’m just not sure how to integrate them.

Hi Brad.

Thanks for writing and I do hope you’ll give Rez a whirl at some point.

Like you, I was writing my game with Twee & Snowman. I implemented a bunch of JS functionality and found Twine was getting in the way. I snapped and started a “weekend hobby project” that got out of hand.

Thanks for the comments about the docs; I have made some effort to make them useful, although I readily concede there are far too few examples. I’ll address that as I go forward.

Rez indeed introduces a little more verbosity in the simple case. The tradeoff is that it makes far more complex things more straightforward.

In the example above, I could have reduced the verbosity a little. For example, the @alias is not required (though it reduces duplication in the @cards), and I could have stored the score in the @scene to eliminate the @actor. Still, I wanted to introduce a few concepts that might stoke the imagination about what becomes possible.

I agree about the Handlebars syntax (& Handlebars generally). I’d be happy to use an alternative; please feel free to offer a suggestion. Something I could implement natively in Elixir would be best as that would reduce a dependency and a need to shell out to Node.

It may be that the Heredoc syntax is unfamiliar. But I needed a way to mix arbitrary strings in with the rest of the definitions, and it was the syntax that seemed to do it best. Again I am open to alternative suggestions.

Now on to your points. The only way to include arbitrary Javascript is with a @script tag or by modifying the generated index.html file. That is not a great answer. In a future version, you will be able to include it just like any other asset.

So, right now, you can include images as assets:

@asset img_01 begin
  file_name: "foo.png"
end

This does two things. It makes automatically finds & copies the referenced file into dist/assets and makes including it as simple as putting {{r_asset "img_01"}} in your content somewhere (syntax sugar in a future version). The pathing gets figured out automatically.

I can very easily extend this to handle .css/.jss files. I could also see syntax like:

@resource leaflet_js begin
  link: "https://unpkg.com/leaflet@1.9.2/dist/leaflet.css"
  script: "https://unpkg.com/leaflet@1.9.2/dist/leaflet.js"
end

Which could then do the right thing to include a library.

You raise a very good point about testing; I don’t have a great answer. With my Twine game, I had a script that mined out the javascript code and ran it through a linter to check nothing was broken. I’m open to suggestions about what would help.

It’s perhaps worth noting that while I have been writing Javascript since it arrived in 1995, it’s not a language I particularly like or use often. But if you want to be in the browser, it’s the only game in town. My JS knowledge was very out of date when I started this project, but I’ve done my best to learn from the Modern Javascript Tutorial. I’m not very au fait with the language and its eco-system. This is to say that I am open to suggestions about how to improve the JS and for libraries that I should be aware of.

M

1 Like

A more straightforward solution occurred to me. You can now write:

@game my_game begin
  links: [
    "https://unpkg.com/leaflet@1.9.2/dist/leaflet.css"
  ]
  scripts: [
    "https://unpkg.com/leaflet@1.9.2/dist/leaflet.js"
  ]
end

This allows you to include arbitrary external CSS & JS files in your game.

1 Like

Awesome, that unlocks a ton of options.

Like you, I was writing my game with Twee & Snowman. I implemented a bunch of JS functionality and found Twine was getting in the way. I snapped and started a “weekend hobby project” that got out of hand.

:sweat_smile: I’ve got a few of those.

I definitely worked around Twine on a few things rather than going with the grain, so I can see how you started down this path. One feature that I considered for Esther’s was a two-page view that could set the illustration on the facing page from the text (like so) and maybe a page-turn animation. I dropped it because it felt too far off the beaten path for Snowman in the time I’d set aside. I can see how it’d be much easier to build with your scene layout and cards system.

1 Like

I haven’t spent any time with Elixir so I’m afraid I’m unlikely to be much help! My first thought is ECMAScript template literals

`You have ${countdown} turns left.`

…but mostly because they’re familiar and your users will already be writing JS. Handlebars does have the benefit that a bunch of folks already know it and/or can look it up elsewhere.

I haven’t looked at them in great detail, but I am not happy with what I have seen. The tagged literals, which I guess I would need to implement the helper functions I am using, are especially strange and unintuitive to my eyes.

I did think about implementing something custom, I don’t need all the complexity that Handlebars offers, but it wasn’t a priority.

1 Like

Congratulations on developing your IF platform! I wish you the best of luck.

I have a standard piece of advice that I share with developers of new IF authoring systems.

Most people choose an IF platform by playing a great game and saying, “I really like this game, and I would like to make another game just like it. How did the author(s) make it?”

So, when IF platforms successfully take off, they require an admirable story (not just a technology demo) to attract new authors. Historically, the first “admirable” story for each now-successful IF platform was typically either written by the platform authors themselves, or directly funded by them. (Twine’s first admirable story by Anna Anthropy is the only exception I’m aware of.) Admirers don’t seem to directly care about any of the details of the system, except that if it’s too hard for them to learn the system and finish a game, that’s a major factor in achieving true popularity.

I think you’ll either need to write something great or hire a great writer (preferably paid in advance) to launch your platform effectively. (If your system doesn’t even have one full game written in it yet, then I think it doesn’t make sense to post a link to Github with an example demo and hope/expect that an early adopter will show up and do it for you.)

Writing the first good game yourself is also important because there are already competitive choice-based IF platforms out there, including Twine, Ink, Adventuron, and our platform, ChoiceScript.

Your platform is also in competition with authors who just want to write their own platform, like you did. Developing a work of choice-based IF is often a novice programmer’s second program, literally right after “hello world.” It’s one of the recommended projects in JavaScript for Kids for Dummies. (Chapter 16: Choose Your Own Adventure)

All in all, Rez looks great, and I believe you can build a community around it if you’re willing to put in the work/money required to write a great game.

3 Likes

Hi Dan.

Thanks for the insight; everything you say makes sense, with the caveat that our perspectives may differ; I built a tool primarily for my satisfaction. I’m not starting a games company. If I am the only Rez user, well, it’s still served its purpose.

Like most creators, I hope my creation may be helpful to and find favour with others. But the rationale for Rez is quite specific. I don’t imagine someone considering using Choicescript would also consider Rez.

I agree a “good game” is an encouragement to people to try something. Whether mine will be good or even finished is an open question at this point. I guess we’ll see.

Thanks again.

Matt

2 Likes

A few other people are giving unsolicited advice about how to help this get off the ground and I will too!

Seeing your coin example, I see that the default style / html is: “completely unstyled”.

I think if you had a default html/css template and very simple look-and-feel (like Twine does) it would help people get off the ground getting a story working and then they could easily circle back to refining that look-and-feel.

1 Like

Fantastic job on your IF system. I’m always interested in new systems. How else will IF evolve? You should add more media support in your system; pictures, sounds, animation, etc.
best of luck.

1 Like

Thanks jkj.

Essential support is there for pictures. I need to fill in the tag generation for audio & movies. At that point, I guess it’s “what more than making it easy to include them”. My needs are pretty simple so what I have at that point is probably enough for me. If anyone else were doing something more complex, I’d be open to hearing what they needed.

You make a good point about evolution. There are already a lot of systems, but none that I felt were well-suited to the hybrid IF simulation/RPG space I want to play in and will grow Rez towards (for example I already have some support for procedural generation and want to improve that).

Who knows if it will turn into anything but it’s fun to try :smile:

Hi Daniel.

Feel free to; I appreciate the feedback.

You’re right about the demo, I threw it together really fast, and it looks like it! I’m expanding on it and will try to style it better.

I’m using Bulma as the CSS library. It’s not a library that I am very familiar with, but it was easy to include and seems pretty clean. It might be as simple as linking to an available free theme. Worth a try, anyway!

Thanks again for your thoughts.

M.

Interesting. What do you see as the distinction between IF and RPG. Are we talking combat systems or something else?

So I updated the sample cointoss game and sprinkled a few Bulma CSS classes.

@game begin
  name: "Test Game"
  IFID: "D3C31250-53B4-11ED-9A26-3AF9D3B0DD88"
  archive_format: 1
  initial_scene: #intro
  links: [
    "https://jenil.github.io/bulmaswatch/journal/bulmaswatch.min.css"
    "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"
  ]
  layout: """
  {{{scene}}}
  """

  @scene intro begin
    initial_card: #get_name
    layout_mode: :single
    layout: """
    <div class="container">
    <h1 class="title">SuperCoinToss</h1>
    <h2 class="subtitle">A game written with Rez</h2>
    {{{content}}}
    </div>
    """
  end

  @card get_name begin
    content: """
    Before you play:

    <form class="form" name="name_form" rez-live>
      <div class="field">
        <label class="label" for="name">Name</label>
        <div class="control has-icons-left has-icons-right">
          <input class="input is-success" type="text" name="name" placeholder="What is your name?"></input>
          <span class="icon is-small is-left"><i class="fas fa-user"></i></span>
          <span class="icon is-small is-right"><i class="fas fa-check"></i></span>
        </div>
      </div>
      <div class="field">
        <label class="field" for="target_score">Target Score</label>
        <div class="control">
          <select class="select" name="target_score">
            <option value="5">5</option>
            <option value="10">10</option>
            <option value="15">15</option>
            <option value="20">20</option>
          </select>
        </div>
      </div>
    </form>
    """
    on_name_form: (card, event) => {
      const name_field = event.form.elements["name"];
      const name = name_field.value;

      const score_field = event.form.elements["target_score"];
      const score = score_field.value;

      if(name != "") {
        const player = card.$("player");
        player.setAttribute("name", name);
        player.setAttribute("target_score", score);
        card.game.setCurrentScene("play_game");
      } else {
        name_field.className = "important";
      }
    }
  end

  @actor player begin
    score: 0
  end

  @scene play_game begin
    initial_card: #play_round
    blocks: [#sidebar]
    layout_mode: :single

    layout: """
    <div class="container">
      <h1 class="title">SuperCoinToss</h1>
      <h2 class="subtitle">A game written with Rez</h2>
      <div class="columns">
        <div class="column is-one-quarter">{{{sidebar}}}</div>
        <div class="column">{{{content}}}</div>
      </div>
    </div>
    """
  end

  @card sidebar begin
    bindings: {player: "player"}
    content: """
    <p class="title is-3">{{$ player "name"}}</p>
    <p class="subtitle is-5">Score: {{$ player "score"}}</p>
    """
  end

  @card play_round begin
    on_start: (card, event) => {
      card.setAttribute("coin", coin_flip());
    }
    content: """
    Do you [[Choose heads]] or [[Choose tails]]?
    """
  end

  @alias result_card = card # begin
    bindings: {player: "player"}
    content: """
    {{$ card "response"}}
    [[Play again|play_round]]
    """
  end

  @result_card choose_heads begin
    on_start: (card, event) => {
      const flip = card.$("play_round").getAttribute("coin");
      won_or_lost(card, flip == "heads");
    }
  end

  @result_card choose_tails begin
    on_start: (card, event) => {
      const flip = card.$("play_round").getAttribute("coin");
      won_or_lost(card, flip == "tails");
    }
  end

  @card winner begin
    content: """
    You are a winner!
    """
  end

  @card loser begin
    content: """
    Sorry, you lost!
    """
  end

  @script begin
    function won_or_lost(card, winner) {
      const player = card.$("player");
      const game = card.game;

      if(winner) {
        card.setAttribute("response", "Congratulations");
        player.incAttribute("score", 2);
        if(player.getAttributeValue("score") >= player.getAttributeValue("target_score")) {
          game.getCurrentScene().playCardWithId("winner");
        }
      } else {
        card.setAttribute("response", "Bad luck");
        player.decAttribute("score");
        if(player.getAttributeValue("score") <= -player.getAttributeValue("target_score")) {
          game.getCurrentScene().playCardWithId("loser");
        }
      }
    }

    function coin_flip() {
      return (Math.random() < 0.5) ? "heads" : "tails";
    }
  end

  @style begin
  .important {
    border-style:solid;
    border-width:2px;
    border-color:red;
  }
  end
end

Ah, that was a bit more throwaway than a distinction that would hold up in a “theory of games” sense. You’re right that systems are a part of it (also the role of narrative and puzzles). It would take me a little longer than I have right now to articulate what I am thinking in a meaningful way.

Cool, I actually think that’s a major improvement since people could start with your example and then just work on implementation until they were ready to pretty it up. Completely unstyled I feel like I’d be distracted by that immediately.

1 Like

I just realised that as a side effect of the library I wrote to create “logical files” (to handle includes), you can do this with string content. So you could re-write:

layout: """
<div class="container">
  <h1 class="title">SuperCoinToss</h1>
  <h2 class="subtitle">A game written with Rez</h2>
  <div class="columns">
    <div class="column is-one-quarter">{{{sidebar}}}</div>
    <div class="column">{{{content}}}</div>
  </div>
</div>
"""
layout: """
%(play_game_layout.html)
"""

And then, move the string into a file play_game_layout.html, and it continues to work. I’d never considered moving HTML/Markdown out of the main file.

The upside is that it looks a little cleaner, and the HTML can be syntax appropriately highlighted by your editor. The downside is that you can’t just see it, you have to open another file (but this is the case for any include-type macro).

Is it better? I’m not sure. But it is an option.

2 Likes