Rez v1.5.0 — open source engine for choice-based HTML games

I’ve released v1.3.1 of Rez which is a QoL release and proof of life (yes I am still working but veeeerrrrrrryyyyyyy slowly).

The main changes are:

Using a @keybinding no longer disrupts input in <input> or <textarea> inputs.

For example:

@keybinding c :show_character_sheet

on_show_character_sheet: () => {
  let resp;
  if($game.current_scene.id === "s_char_sheet") {
    resp = RezEvent.sceneResume();
  } else {
    resp = RezEvent.sceneInterlude("s_char_sheet");
  }
  return resp;
}

Uses a keybinding to either interrupt the current scene with the character sheet scene, or resume the previous scene if we’re in the character sheet scene. But it would also capture the ‘c’ key if typing in an input. That no longer happens.

Asset handling is improved, well put more simply it works again. So we can declare and use assets in templates:

@filter icon {
  name: "icon"
  impl: (icon_id) => {
    const asset = $(icon_id);
    return asset.imageTag(16, 16);
  }
}

@asset icon_reload {
  file_name: "refresh_24dp_E8EAED_FILL0_wght400_GRAD0_opsz24.png"
  width: "64"
  height: "64"
}

Means we can now write:

${"icon_reload" | icon}

In a template to get a properly formatted <img> tag for the icon asset.

2 Likes

In my previous post I had an example where I want to turn an asset-id into an <img> tag. What I end up with is something like this:

<button class="button is-small" data-event="reload">${"icon_reload" | icon}</button>

The icon @filter transforms the asset-id into an <img> tag for that asset.

There is nothing especially bad about ${"icon_reload" | icon} but having worked with Elixir/Phoenix functional components a lot I got a yen for being able to write something like:

<.icon type="reload" />

instead. To my eyes it’s a cleaner syntax and the intent is clearer.

In Rez 1.3.2 you will be able to write a user macro:

@macro icon (bindings, assigns) => {
  const asset_id = assigns["type"];
  const asset = $t(`icon_${asset_id}`, "asset", true);
  return asset.imageTag(16, 16);
}

that can then be used within templates.

Should all uses of ${…} be replaced with user-macros? No. But it can clean up some common cases where the use of an interpolation is clumsy or the intent is unclear (for example, in the above case were generating an actual tag, rather than a string representing content within a tag).

The implementation I have allows for arbitrary attributes but, unlike Phoenix functional components, does not support a “slot”, i.e. you can’t compose macros. So couldn’t write an event button macro and then do:

<.event_button event="reload"><.icon type="reload" /></.event_button>

I’m not sure how much effort this would be. It’s not vastly different to parsing conditional expressions ($if supports nested template exoressions) so perhaps relatively doable. If I come across a situation where I really need this I will see just how hard slots would be in the current parser.

Just re-reading and seeing something different today. Your invocation of “pure choice” which I guess is about the difference between static choices (defined at author time) and dynamic choices (inferred at run-time, i.e. from the model). In your definition of purity corresponds to the % of static choices?

Twine is great for mostly static choices with a sprinkling of dynamic choices. The first thing I started wanting to do when using Twine was gate/hide choices. For example is a vendor available?

Yes
Not right now
You don’t know they exist

RezDecision is based on some of the oldest code I wrote while using Twine which was supporting conditional links.

By this definition Rez is certainly not a pure-choice system and caters very much to dynamic choices.

Hi Matt. Yes indeed, choices that depend on world state. Obviously there needs to be a world model for this to be a thing. And as i understand it, Rez has this.

To give another perspective, in Strand, i have what i call “choice elevation”. These are things you can do in the world, that could be actual parser input in some cases, that can be elevated to choice. In other words dynamic choices dependent on the state of the world.

A simple example. Say you have the “gold chalice” which is one of your game treasures. When this is laying about, it might be good to have an elevated choice for “get chalice”. So if you get the chalice and leave it somewhere else, in that place will be an additional choice to get the gold chalice.

I can already see people poised to mention that it might not initially be appropriate to automatically suggest “get chalice” when it’s formerly guarded by the vicious, killer Vark of Spondor. Totally true. So you’d need a condition.

in Strand it would go a bit like this:

CHALICE@ GETTABLE
* name
the gold chalice
* x it
Wow!

Marking it GETTABLE will do everything normally to handle get/drop etc. Then i add my elevation;

CHALICE@ GETTABLE
* name
the gold chalice
* x it
Wow
*= get it
> get it

I’ve added a behaviour to get it which is marked by “=” as elevated. when called, this punts to > get it to handle the usual stuff (calls parent get).

Let’s say you have to vanquish the Vark before you want to elevate to choice and this depends on VARKDEAD;

CHALICE@ GETTABLE
* name
the gold chalice
* x it
Wow
*=?VARKDEAD get it
> get it

Choice elevation is a powerful idea that straddles both text input and choice, but it does need to be controlled by the game.

1 Like

Makes sense. In the Rez case I would lean towards handling such situations with a behaviour tree although I can’t mentally picture your example in action to translate it.

Answering my own curiousity it turns out this is a good deal harder than I guessed. I enjoy the challenge so I might figure it out anyway (I am damned by being unable to leave a loose thread unpulled) but I had a quick go y’day and was mildly surprised at just how hard it is matching nested, dynamically specified, delimeters.

I think the easiest way would be to modify my Ergo parser combinator library to allow returning an AST value from a lookahead expression. But that’s probably more detail than anyone here is up for :slight_smile:

1 Like

So… being unable to leave a thread un-pulled… the v1.3.2 release of Rez will include container macros. I’ve had to change the syntax from the Phoenix syntax of <.foo> to <foo*> to pull this off.

%% component elements are the implementation for HTML components
%% which are used as <component_name*> the '*' indicates a component
%% bindings are the bound variables available at the component site,
%% assigns are the attributes defined on the component
%% content, for container components, is what goes inside 
@component event_button (bindings, assigns, content) => {
  return `<button class="button is-small" data-event="${assigns["event"]}">${content}</button>`;
}

@component icon (bindings, assigns, content) => {
  return $(assigns["icon"]).imageTag(16, 16);
}

@card test {
  content: ```
    <event_button* event="reload"><icon* icon="reload"/></event_button*>
  ```
}

I kind of implemented these on a whim, as I wasn’t liking the ergonomics of using filters to generate tags. On a purely rational basis this probably makes no sense, there were a couple of quite tricky parsing issues I had to solve to make container based components work (eventually leading me to have to update my Parser Combinator library, Ergo, for the first time in two years).

But I like the end result and how clean it can make my templates, hiding away implementation details.

I guess at some level I have to accept that Rez is kind of my interactive fiction :slight_smile: :grinning:

1 Like

To show what this does, what I used to write was:

<button class="button is-small" data-event="reload">${"reload" | icon_tag}</button>

What I would now write is:

<event_button* event="reload"><icon* icon="reload" /></event_button*>

I hope that it would be relatively easy to identify that event_button and icon are components because of the * suffix and then go look them up if necessary. But the intent of what is going on seems much clearer to me.

1 Like

I’ve just released v1.4.4 which is mainly a QoL release as I continue sanding down rough edges.

2 Likes

I guess I should also mark that I broke ground on Rez in Oct '22. So I have been working on this thing on and off for over 2 years now. I console myself that I probably wouldn’t have produced a game in Twine in that time either and I’ve had a lot of satisfaction from building this thing.

6 Likes

I’m experimenting with using storylets. It turns out this is pretty easy in Rez which is kind of “QBN by default” (I think I have that right).

I’ve created a component <.storylets /> that dynamically discovers and presents the available storylets as links. I just drop that component into any card where I want to offer storylet based choices.

A storylet is a @card that defines storylet: true and has an available: function() {…} that returns either [] (not available) or an array of ‘descriptor’ objects used to render a link to activate the storylet.

For example:

[
  {
    storylet_id: this.id,
    action: "Cast fireball",
    params: {spell_id: "sp_fire_ball"}
  },
  {
    storylet_id: this.id,
    action: "Cast magic missle",
    params: {spell_id, "sp_magic_missile"}
  }
]

This is the same storylet-based @card presented as two options with a different link title and param for which spell is being cast. Using the component I built this would end up rendering as:

<li><a data-event="card" data-target="st_cast_spell" data-spell-id="sp-fire-ball">Cast fireball</a></li>
<li><a data-event="card" data-target="st_cast_spell" data-spell-id="sp-magic-missle">Cast magic missile</a></li>

Although there might be other options like “Run away” or “Swing sword” that get defined by other available storylet @cards.

Most of the work is happening in the available: function that determines whether the storylet should be presented given the current game state. In this example, available: would determine if we are in a spell casting context but could also filter the spells available by how much mana the player has remaining.

I’m also using tags on scenes to scope certain actions (for example merchant related actions happen in a ‘merchant’ scene). Otherwise using things like plot stage, scene & player attributes, inventory contents and so on. It’s pretty simple if you are comfortable writing a little Javascript (always a big if I guess).

Lastly, I have a mechanism allowing the system to force storylets onto the player (this is, if I read it right, somewhat akin to what Emily Short calls the ‘salience’ appoach) with different priority levels.

Within each storylet I use the on_start: and on_finish: event handlers to change the game state, changing which storylets will subsequently presented.

I’m not very well versed in storylet lore (although I am going back and reading past posts around the topic) so at the moment I am mixing and matching. Some @scene’s use plotted content and some scenes use storylets.

This feels a bit clunky at the edges. Could I convert everything into a storylet? I think I could but I wonder about the implicitness and how easy it is to keep track of, and debug, everything if you end up with hundreds of these things.

What are other peoples experiences of building storylet based works?

My current Twine games have had from 0 to 100% Storylets. (Zenith was 100%, The Green 0%, A Mouse Speaks to Death about 70%, others just dabble in them). It’s definitely the case that the more you rely on storylet self-selection for narrative, the harder it can be to guide it as an author, because you can’t easily visualise those links or see the probable ordering without actually running the game. In the right story that doesn’t matter so much, in others, there’s definitely a need for some overarching structure to link and partially order those storylets.

What I am doing in my most recent game is having more than one pool of storylets, so different phases of the game can separate out the sets of available ones. You could do that with a single master-phase variable that every storylet checks, but MQBN lets me just have multiple stories, so I do it that way.

Something else that you’ve not mentioned yet is the “stickyness” of storylets — whether they can be selected more than once or not. In general it’s easier to have the selection system track this than leave each storylet to check for itself whether it has been picked before. The standard QBN approach is a sticky flag, which is what I use in MQBN.

One other thing to consider is where you may want to get a list of open storylets without displaying their links. In MQBN I have ways to directly goto a storylet, or to include it, or just to check what’s available, as well as the link-presenting approach. You might want to think about those things too.

Thanks, David.

My game has a number of scenes. In Rez a @scene sets a context for part of the game/story and uses @cards (roughly analogous to Twine passages) to present its content. So the current scene can be a filter for storylets and in fact this seems quite natural as I break the game up into scenes that interconnect.

In this case the storylet available: function would look like:

@storylet st_bargain {
  available: function() {
    if($game.current_scene.id != "s_dialog") {
      return [];
    else {
      // return descriptors
    }
  }
}

At the moment I am just using one big filter function but later it might be more convenient to do something like:

@storylet st_bargain {
  scene_filter: #s_dialog
}

and combine together more specific filters (with a default available: function that combines them). This is probably more friendly but I’m not sure what all those filters would be yet.

It’s easy to use any combination of attributes of the various cards, scenes, game, or actors (e.g. player) to determine whether a scene is available or not. A simple example would be:

@storylet st_bargain {
  available: function() {
    if($game.current_scene.merchant.bargained_with) {
      return [];
    } else {
      // return descriptor
    }
  }

  on_start: (storylet) => {
    $game.current_scene.merchant.bargained_with = true;
  }
}

This would allow you to bargain with each merchant exactly once.

That’s an interesting point. You mean for debugging?

In principle this is simple enough. The <.storylets /> component is already iterating them and I could have a different component that doesn’t generate links but just displays a list.

Do you maybe have a screenshot you could share? I’m not familiar with MQBN.

Thanks.

Having looked at MQBN a little I now see what you are getting at.

A direct analogy is:

$game.filterObjects((o) => o.game_object_type === "card" && o.storylet)

That is I’ve implemented storylets in my game as Rez @card elements with storylet: and available: attributes, i.e. they aren’t a special type and linking to, or invoking, one would be no different to invoking any other card.

<a data-event="card" data-target="st_bargain">Bargain</a>

or 

$game.current_scene.playCard("st_bargain")

In fact I can simplify this and combine storylet: and available: attributes into a storylets: attribute (since available can return multiple ways of invoking the same storylet).

1 Like

Released v1.4.5 with some QoL fixes and supporting writing user components using the:

<.component />

rather than the previous

<component* />

syntax. It’s a small point but Phoenix components use the .-prefix which is more familiar to me.

Also a few additions to the stdlib.

1 Like

Depending on how you are using QBN this is either entirely reasonable, or a huge chore :slight_smile: If you look at most storylet systems, you’ll see that stickyness is the default. i.e. all storylets can be chosen once only unless the author overrides that. Requiring every single storylet to track its own state like you suggested is a big burden on the author if they are expecting every storylet to be once only.

Of course, if they are not expecting that, then your approach would seem a lot less irksome.

I get you.

In my case, adopting storylets as a wholesale approach requires their availability be driven by dynamic world state, rather than use-once. Since I am the only user of Rez right now this works fine for me. Should that change then supporting single-use by default is probably feasible.

In fact this wouldn’t have been so easy to implement except that I have now added a @defaults directive to Rez 1.4.6.

In fact I had started to think about adding this anyway as I realising I needed a way to set defaults across all cards and using an alias & template object was clunky.

So now the implementation of “sticky” storylets would be:

@defaults card {
  used_storylet: false

  available: ^p{return true;}

  storylets: function() {
    if(!this.used_storylet && this.available) {
      return this.storylet();
    } else {
      return [];
    }
  }

  on_start: (storylet) => {
    storylet.used_storylet = true;
  }
}

%% With an example sticky storylet

@storylet st_bargain {
  available: ^p{
    return $game.current_scene.has_vendor;
  }

  storylet: function() {
    const actor = $game.current_scene.actor;
    return new Storylet(this.id, "Bargain with ${actor.name}");
  }

  content: ```
  <p>You bargain with ${scene.actor.name}</p>
  <.storylets />
  ```
}

Thanks for helping me think this through.

Something didn’t sit right with me about my implementation of defaults. This is setting a default on all cards, whether they are storylets or not. It’s not harmful exactly but it’s unintended and bulks data unnecessarily.

It occurred to me that it wouldn’t be hard to extend defaults to aliases. In my game I use:

@object T_storylet {
  $template: true
  storylets: function() {
    return [];
  }
}

@alias storylet = card<T_storylet>

More than anything this is just a syntax sugar so I can write:

@storylet st_bargain {…}

rather than

@card st_bargain {…}

However extending defaults in this way neatly removes the need for the template object. Now I can write:

@defaults storylet {
  storylets: function() {
    return [];
  }
}

@alias storylet = card

In this case these defaults only apply to cards defined using the storylet alias. I think this will remove quite a few cases where I use an alias and a template object as a shorthand.

Adapting the first example, and changing @defaults card {…} to @defaults storylet {…} would give a default sticky storylet implementation but only for storylet cards, other cards would be unaffected.

Attribute defaults are layered. So you get the defaults for @card then the defaults for @storylet. So you could, for example, do something like:

@defaults card {
  storylet: false
}

@defaults storylet {
  storylet: true
}

Regular cards would have storylet: false while storylet cards would have storylet: true.

This is probably more detail than anyone except me would be interested in…

3 Likes

Okay I really need to create a simple game to demonstrate this thing.

1 Like

Okay I really need to create a simple game to demonstrate this thing.

:smiley: