Rez 1.1.0 — open source engine for choice-based HTML games

You might have heard of this guy; he’s one of my favourite minds. Dieter Rams.

10 Design Principles ← ( click to expand )
  1. is innovative – The possibilities for progression are not, by any means, exhausted. Technological development is always offering new opportunities for original designs. But imaginative design always develops in tandem with improving technology, and can never be an end in itself.
  2. makes a product useful – A product is bought to be used. It has to satisfy not only functional, but also psychological and aesthetic criteria. Good design emphasizes the usefulness of a product whilst disregarding anything that could detract from it.
  3. is aesthetic – The aesthetic quality of a product is integral to its usefulness because products are used every day and have an effect on people and their well-being. Only well-executed objects can be beautiful.
  4. makes a product understandable – It clarifies the product’s structure. Better still, it can make the product clearly express its function by making use of the user’s intuition. At best, it is self-explanatory.
  5. is unobtrusive – Products fulfilling a purpose are like tools. They are neither decorative objects nor works of art. Their design should therefore be both neutral and restrained, to leave room for the user’s self-expression.
  6. is honest – It does not make a product appear more innovative, powerful or valuable than it really is. It does not attempt to manipulate the consumer with promises that cannot be kept.
  7. is long-lasting – It avoids being fashionable and therefore never appears antiquated. Unlike fashionable design, it lasts many years – even in today’s throwaway society.
  8. is thorough down to the last detail – Nothing must be arbitrary or left to chance. Care and accuracy in the design process show respect towards the consumer.
  9. is environmentally friendly – Design makes an important contribution to the preservation of the environment. It conserves resources and minimizes physical and visual pollution throughout the lifecycle of the product.
  10. is as little design as possible – Less, but better. Simple as possible but not simpler. Good design elevates the essential functions of a product.

You can apply every aspect of his design principles to code bases too. Even #9 can apply in that you’re contributing to the health of the IF landscape and reducing the effort required for authors to construct relational game systems. The aesthetics details can apply to your syntax, etc. :slight_smile:

Edit: Keep in mind that these design principles (in respect to Rez) are for the programmer, not the player of the IF product the programmer is building. Rez is like a hammer. So when things like “leave room for the user’s self-expression” are mentioned, it means leave room for the programmer’s self-expression. Anyway, I didn’t realize how much Apple adhered to Dieter Rams’ principles. They worship him, like a god!

I read through the documentation and got utterly lost with the bindings and rendering section, which is a shame because it seems important. I am a developer (albeit a jr. one) who’s worked with javascript, typescript, etc. It seems very in the weeds and I think boiling it down to how bindings can make the story people want would be good.

In general the documentation seems very thorough at explaining what the language does and how from a…well, programmer’s documenting mindset. But an IF author mindset is a little different, more focused on “what stories can I tell with this?”

In that respect, the reason I eagerly opened the documentation was because you mentioned NPC behavioral tree support and I didn’t see it anywhere.

There are examples of bindings in Matt’s sample dungeon crawler ( source code ). Tinker with the game and check out the source code; it may help.

Hi Aster.

That’s valuable feedback, thank you.

Bindings are a part of the template system that I am not entirely happy with yet, although conceptually they are relatively simple. A binding is a “starting point” for referring to a value.

So for example you might have an @actor player element and want to get it’s name. So you’d want to bind a variable to the element to do so.

bindings: {player: #player}

so that in a template you could refer to:

${player.name}

Where things get more complicated is when you want to bind to something that isn’t a plain element. Or, for example, a dynamically chosen element. That’s something you might do with a function binding, for example:

bindings: {monster: () => {return $("room").monster;}

You can bind to pretty much anything this way. There are other types of binding that are a bit more specific and I won’t go into here. But that’s the gist of it, a starting point for referring to a value.

Your observation about the documentation being more of a programmers mindset than an IF authors mindset is, I guess, kind of fair. I’ve been trying to document how it works because I figure Rez will probably appeal first to people with more programming experience. But that’s not where I want to leave it.

The cookbook documentation is meant, over time, to be more about how to use Rez to achieve different ends, rather than the main docs which are describing capabilities.

The behaviour tree support is a little nascent. You can define behaviour trees as attributes, you can define query/action tasks (as well as Rez defining a set of common tasks), and you can run them. I’m still working out how to integrate this more completely.

It looks like the documentation for the behaviour tree attributes got lost when I rewrote the docs. I’ll have to add that back in.

It’s worth mentioning perhaps that the reason for bindings existing is that I need a way to make things in-scope for the rendering process.

If you want to know more take a look at rez_view.js where the renderer is implemented.

I am not going to claim this is the easiest code to grok, it’s a little bit tricky in parts. But I’m happy to answer questions.

What it boils down to is templates get compiled to a function that takes a set of bindings as its input and returns a string containing rendered HTML.

Those functions are generated in template_compiler.ex which is Elixir code and, again, probably not the easiest to follow, mainly because I’m code generating JS functions.

I don’t like delving in there too much as it’s messy and largely untested. But it works, mostly… :slight_smile:

It’s probably also worth mentioning a few things that may not be obvious.

In the first place where Twine comes from the perspective of “narrative first”, Rez comes from the perspective of “world model first” and narrative is a component. In Twine the passage has primacy and other aspects fit around that. In Rez the model comes first.

Why might this be? Well the kind of games I am primarily interested in making are what I would call a hybrid of simulation/rpg/emerging narrative games.

An author who mostly wants to tell a story (with some dynamic elements) might fight Twine a better tool as it mostly gets out of the way of that purpose. On the other hand someone who wants to build games like I describe could find Rez a better fit because it has a world model and a bunch of systems that can do a bunch of the heavy lifting.

To be clear: you can do either in both systems. Rez could serve as a perfectly functional replacement for Twine. And people do build complex games in Twine. But I’d argue that both have a different sweet spot.

The kind of games I want to make involve procedural generation.

While I am still developing this aspect Rez has already has some useful support for this.

In the first place most Rez elements support copyWithAutoId() which is useful with template elements that also use the various forms of dynamic attribute. This makes it relatively easy to create many customised variants of a set of base templates.

Next we have the @list element which is, more or less, exactly what you might guess:

@list female_names {
    values: [
      "Arata"
      "Asa"
      "Fujioka"
      "Fujiwara"
      "Fukaya"
      "Funai"
      "Furuse"
      ...
  ]
}

@list names begin
  values: [
    "Takahashi"
    "Konishi"
    "Yashiro"
    "Nakazono"
    "Seki"
    "Ishikawa"
    ...
  ]
}

At run-time these become instances of RezList which is a wrapper around an array providing an API with functions I’ve found useful.

The most obvious is randomElement() which is just inherited from Array nothing special there. But there are others…

The next is nextForCycle(cycle_name). This treats the values as a cycle and will go around and around them continuously in order. At which point a new walk is started.

Then there is randomWalk(walk_name). This is a bit like randomElement() only it guarantees that you won’t receive the same value twice until all values have been exhausted.

Lastly there is randomRemaining(bag_name) which treats the list like a bag of objects. When you take one out, it can’t be taken again. The difference between this and the walk is that you can empty the bag.

Note that, in the case of nextForCycle(), randomWalk(), and randomRemaining() you can run multiple simultaneous instances against the same list. A small point but useful in some circumstances.

I’ll give a passing nod to two other capabilities that can be useful here, those are the Tracery grammar and probability table attributes. The former allows you to embed and run Tracery grammars and Rez comes with tracery.js included. The second allows you to do easy probability-based result generation:

eye_color: |:brown 48 :blue 29 :green 14 :grey 9|
loot_quality: |:poor 20 :okay 10 :great 5 :amazing 1|
meet_on_the_road: |#ranger 15 #wizard 10 #traveller 45 #evil 30|

The relative probability of each outcome is calculated and then sampled to return a result as the attribute value.

2 Likes

Hi,

Just for comparison, i have similar list orderings in Strand.

This would generate a fruit at random:

ALIST
* apple
* banana
* cherry
* durian
* elderberry
* fig
* grape

You can add “indicators” on terms to alter the list. Random is the same as ~. Then there is shuffle &

This will randomly choose without repeats. Same as your nextForCycle.

ALIST&
* apple
* banana
* cherry
* durian
* elderberry
* fig
* grape

A few extras i have are “Nonrandom”, # and sequence < and what to do thereafter.

so LIST< will choose them in sequence, LIST# is a “non-random” list (explained later).

What do to thereafter indicates how to deal with the list after it has played out. This is done with a follow-on indicator, eg

&& will shuffle, then re-shuffle after (and forever after that).

ALIST&&
* apple
* banana
* cherry
* durian
* elderberry
* fig
* grape

And you can mix them, so ALIST&~ means shuffle, then after all played, it will randomise. And ALIST&< will shuffle, then play out in order.

The one you don’t have is non-random. If find this incredibly useful for games and hardly ever use pure random ~ in my game code. Trouble is pure random tends to repeat too much. For example, people will get three bananas from the original random list annoying a lot of the time.

So non-random is a randomisation with starvation restriction. For example, you might spend ages before you get grape just by bad luck. Non-random avoids this.

I could post some code, but it might be easier to explain;

Consider a 6 sided die (for discussion). You maintain an array of 6 slots (1 through 6), each containing the number of throws since that number has come up. So slot 1 contains how many throws since the last 1 was emitted. and so on.

You have a max history value. say 8, which is used to prevent starvation.

pseudo-code:

initialise stats[] to zero.

To throw a non-random of 6:

  1. find j, where stats[j] is the most.
  2. if stats[j] + 1 >= max-history, then choice=j
  3. else choice = random value 1-6.
  4. increment stats[i], 1 through 6.
  5. set stats[choice] = 0
  6. emit choice

I find a good choice for games for max-history is the non-random size is: n+(n+2)/3. So 6 gives 8 and 10 gives 14 etc. Also “warm-up” the stats array in your “init” for a bit to populate them.

2 Likes

Very clever. :slight_smile:

I’m curious, what reason is there to re-shuffle a list that is already in a random order?
How does doing that make that list “more random” than the outcome of the first shuffle?

If I understand right, this means it re-shuffles whenever it runs out.

So if you had four playing cards, “shuffle then shuffle” would give you 1, 4, 2, 3, 2, 4, 3, 1, 2, 3, 1, 4…

While “shuffle then sequence” would give you 1, 4, 2, 3, 1, 4, 2, 3, 1, 4, 2, 3…

Yes, it reshuffles after it’s played out.

eg

GENFRUITS
GENFRUIT4 GENFRUIT4
GENFRUIT4 GENFRUIT4
GENFRUIT4 GENFRUIT4

GENFRUIT4
\
ALIST ALIST ALIST ALIST

ALIST&&
* apple
* banana
* cherry
* durian

emitted,

cherry apple durian banana
banana cherry durian apple
apple banana cherry durian
durian banana apple cherry
durian cherry banana apple
apple durian cherry banana
1 Like

Can you check my homework here?

function next_non_random(list) {
	let stats = list.stats ?? Array.from({length: list.length}, () => 0);
	const len = stats.length;
	let max = Math.floor(len + (len+2)/3);
	let choice = stats.findIndex((element) => element+1 >= max);
	
	if(choice == -1) {
		choice = Math.floor(Math.random() * stats.length);
	}
	
	stats = stats.map((element) => element+1);
	stats[choice] = 0;
	
	list.stats = stats;
	return list[choice]
}

It’s an interesting idea, thanks for sharing.

I also think it would be helpful to have a different name to ‘non-random’ because this is certainly a random process and non-random doesn’t express the protection from starvation that is at the heart of the method:

Something like randomWithoutStarvation suggests itself although it’s a bit of a mouthful.

I’m also curious how you ended up at n+(n+2)/3?

From your code, it looks like the right algorithm.

Some considerations:

You will need to prepare the stats list before using it. So you can’t just call this method cold. What I do is initialise the stats list to zeros, then call the non-random method for, say 100, times to initialise it. After that, I start using the output.

Secondly, if you’re going to simultaneously have multiple list lengths, you need to manage those lists. Say you need a non-random-6 and a non-random-10. You need separate lists for this.

Some background:

This (and other ideas) came out of some thinking about that i call “non-random” numbers. Let’s say you have a random sequence. The notion is to limit the things that are sufficiently improbable. Say I’m flipping a coin. Then I flip “heads” 6 times in a row. This is improbable - but it happens all the time in real life.

Now the notion of “improbable” is vague because any given sequence is just as likely as any other (for the same length). 6 heads is just as likely as what appears to be a “random” sequence of 6 heads and tails.

I also realised there is no definition of what is “random”. Some people say it’s something that can’t be predicted or having no pattern, but many unpredictable things are not random - by most people’s understanding.

Example sequence: 1,1,1,1,1,1

Does this have a pattern? Does it have a pattern when i tell you the next number in the sequence is 7. After that, the sequence goes; 1,1,1,1,2.

It doesn’t look very random, but it has no pattern and is not predictable.

This leads to the idea of washing patterns out of sequences to make then “more random” (whatever that means). But it actually has the opposite effect and this is what I call a non-random sequence - A random sequence with patterns washed out. By “random” here, i mean the output of another supposedly random sequence generator.

So;

First I tried removing repeats, then runs of numbers then absent numbers (ie starvation). These methods became complicated and I eventually discovered removing starvation does as good job as all the others put together - because starvation also breaks repeats and runs.

Finally; n+(n+2)/3 is chosen because i found adding about 50% extra works quite well. So for 6 dice set list length to 9.

I wanted to tweak this back a bit for games because you still get a lot of repeats with 50% - although it does look quite random.

For use in games, i wanted to constrain a bit more. So increase your list length to become more “random” and decrease it to become more “non-random”.

If you set list length to n (eg 6 for a 6-dice), you get a shuffle sequence that repeats. I actually use this idea in my shuffle generator rather than having two different bits of code.

2 Likes

I wonder if we, like max can determine the number of rounds of warming based on length. Presumably we’d need more rounds for longer lists. I have some lists with 50+ items in them

Interesting. On a different, but similar, tack, the Rez stdlib adds Math.clrand_int that takes advantage of the central limit theory to return normally distributed random numbers. I often use this in preference to uniform random.

I think there’s lots of scope for interesting takes on randomness, for example Perlin noise. Which (IIRC) has applications in city/landscape generation.

You can just run n warmup rounds where n is your list length.

Okay so I have implemented

$if
  (cond1) -> {% if_body1 %}
  (cond2) -> {% else_if body2 %}
  () -> {% else_body %}

which compiles to

if(cond1) {
  …
} else if(cond2) {
  …
} else if(true) {
  …
}

An example could be:

$if(player.wounded)->{% your wounds are slowing you down. %}

$if(player.wounds>0)->{%
  You are
  $if
    (player.wounds>5)->{% seriously %}
    (player.wounds>2)->{% badly %}
    ()-> {% mildly %}
  wounded.
%}

Although this latter example is a touch contrived and could be cleaner using a filter, esp if it were going to be used in several places:

$if(player.wounds>0)->{% You are ${player.wounds | wound_level} wounded. %}

@filter wound_level {
  name: "wound_level"
  impl: function(l) {return l > 5 ? "seriously" : l > 2 ? "badly" : l > 0 ? "mildly" : "not";}
}

If you omit the () case you get "" returned by default. I dropped the , between clauses as, in this syntax, each condition-body is discrete.

I’m not 100% sold, but this will be the $if syntax in v1.1.7 and I’ll play with it myself and see how I feel in practice.

1 Like

I released v1.1.7 which includes the new $if syntax, an option to warn the user before leaving or reloading the page, and @list/RezList gains a new randomWithoutStarvation() method based on @jkj_yuio approach earlier in the thread.

2 Likes

I believe this is often called a “random with pity timer” or “perceptual random” because people expect a random output to actually give all results evenly, which isn’t right but feels right

1 Like