Lack of randomness sometimes when compiling for Glulx

Individual games deciding to roll their own RNG and storing the RNG state is certainly a possibility for future games, but I think it’s an interesting question what behavior is most useful for individual terps, across all existing games, even if it’s not portable.

Implementing that non-portably, for a single terp isn’t very difficult conceptually: if the terp uses a PRNG with some internal state it can access, then that state can be dumped when saving the game and read back when restoring. (An opaque, unseedable RNG as Glulxe and Git have recently learned to use is of course incompatible with the whole enterprise.) This is efficient regardless of how many random numbers were generated before, and can be added to Quetzal files without spec changes by putting it in a non-standard chunk type. Unrecognized chunks ought to be (are they?) ignored by terps that don’t recognize them. It would be best to keep the new chunk type open to extension and evolution, e.g., by including an unambiguous name for the RNG algorithm, and of course several terps could choose to interoperate without or before any changes to specifications.

But what exactly is the best behavior for such a modified terp? I assume the behavior should be consistent between autosave/autorestore, Quetzal saves, and undo states. Still, there’s at least two distinct options (I’d be happy to hear about others I missed), and I’m not sure which is preferable:

  1. The RNG state is always part of the saved game state: if you save the same or an undo state, you’ll get the same sequence of RNG outputs any time you restore from that save / undo state. This is a player- and author-visible difference from all existing terps, and one that may be controversial. It means, among other things, that one can’t just re-roll the dice on a random outcome by repeatedly reloading and retrying an action.
  2. There is a special “reproducible RNG” mode for the terp: the RNG state is only saved then this mode is turned on at the time, other saved games don’t include a PRNG state and will behave as the Glulx spec says when restored from. This means predictable behavior is opt-in and won’t affect the behavior most people will see. However, creating a saved game that reproduces some RNG-dependent bug requires extra knowledge and work to do turn on that mode and then reproduce the bug, even if one happens to be using the “right” terp when encountering a bug.

There’s a somewhat related question, which I have been mulling after the previous discussions: how should @setrandom 0 (i.e., asking for “unpredictable” RNG) interact with a terp-specific “predictable RNG” mode? That is, if a game repeats “@setrandom 0 and then generate a random number” twice in a “predictable RNG” terp, which of the below should be true about the generated numbers?

  1. Both numbers will be be unpredictable “because the game explicitly asked for unpredictable RNG.” This is the implementation in Glulxe and Git right now: the initial RNG state is predictable, but @setrandom has the same effect as usual. I can imagine a couple of reasons for doing this, but it also means any game can intentionally or accidentally thwart attempts to get “deterministic RNG behavior”.
  2. Both numbers are the same, because “predictable means overriding @setrandom 0 to behave basically like @setrandom SEED for some fixed seed hard-coded in the terp.” However, this means a game that repeatedly uses @setrandom 0 for whatever reason (e.g., an otherwise harmless bug) would repeatedly get the same sequence of RNG outputs, which seems undesirable in general and qualitatively different from normal terp behavior.
  3. Both numbers are predictable, but not the same. One way to arrive at this conclusion would be to say: the terp has a single RNG, which can be seeded repeatedly by the game (@setrandom n for non-zero n) or from an external source of randomness (on startup and on @setrandom 0). This external source of randomness is usually unpredictable (e.g., current time or OS-provided randomness), but a “predictable RNG” mode replaces it with some pseudorandom stream of numbers (i.e., a separate RNG with a hardcoded seed). This means every @setrandom 0 truly changes the RNG state, but RNG outputs are still deterministic with respect to the overall number of @setrandom 0s executed.

All of these options, but especially the third one, interact with the issue of save/restore raised by @Angstsmurf. You may get predictable behavior if you shut down and restart the terp with a certain flag, but if you try to reproduce things in a single session (e.g., using undo states), you can’t necessarily go back to the RNG state you had on the previous turn. This seems undesirable for interactive debugging. It can be addressed in the same way as the RNG state proper (e.g., saving and restoring all relevant terp state, not just the RNG state in a narrow sense). But it adds another dimension to the “what do we actually want to happen?” question that I’m not quite sure how to answer.

1 Like

But I can’t change the interpreters to do it. This falls under the heading of “sensible commanders don’t give commands they know will be disobeyed.”

The short answer is, neither Z-code nor Glulx can do this. (Keep a deterministic RNG sequence running through SAVE/RESTORE.) Don’t try. Deterministic RNG is meant for testing and debugging anyhow. If your game really needs such a sequence for regular play, implement your own RNG.

(However, Quixe does save the RNG state in the autosave sequence, which is a nonstandard save that happens when you close the browser window.)

2 Likes

I can confirm that on rudimentary evaluation this works well on the 10.1.2 build of the Windows IDE (thanks David +++).

I understand that it may well not work on other builds of the IDE (you’ll probably just get a blank story pane after compilation, like I did with the previous attachment which was targeted for the (as yet unreleased) Version 10.2 IDE build).

Looking at Quixe, it seems to do this by saving and restoring an array named xo_table containing four “seed” values (and without any repeated calling of the random function after autorestore.) Would this method work for the new Glulxe code as well?

Zarf just made an issue for that

1 Like

It should work, but it will only affect numbers generated from the xoshiro RNG. If you want it to affect all random numbers, you need to convince the code in osdepend.c to always use the xoshiro RNG (instead of opaque, unseedable RNGs like arc4random or Windows rand_s) even on startup without --rngseed and after @setrandom 0. Either you modify that code directly, or compile Glulxe with an OS_* define (which may have other side effects, I’m not sure) to get into one of the existing code paths that do this.

1 Like

That change will just bring Glulxe in line with Quixe: the deterministic RNG will carry over autosaves (not regular saves or undo).

The nondeterministic RNG does not need to be saved because, see name.

2 Likes

Thanks, all. To clarify, what I want to achieve is making the new Glulxe code work as well as the old one in Spatterlight, so that when deterministic mode is on, “random” numbers will be the same whether running the game normally or autosaving and autorestoring. That is all.

EDIT: It seems to be working now.

Right, that’s the goal of the https://github.com/erkyrath/glulxe/issues/37 task.

1 Like

Are you sure these negative random() calls do that in Glulx?

For random(-1), shouldn’t it call @random -1 r, which would return a random number in the range “the range (L1+1) to 0”, ie, 0 to 0? It then adds one, so random(-1) should return 1 non-randomly. A call like random(-2) would result in @random range of -1 to 0, turning into a random() range of 0 to 1 once once is added. In general, random(N) with negative N should give a result in the range of (N + 2) to 1.

But I haven’t tested it, that’s just from me reading the I6 source code.

Edit: I wrote a test, and it’s working as I thought, not as you described.

random test.ulx (504.3 KB)

Source Code
"Glulx Random Test"

The Lab is a room.

To decide which number is random N (N - number): (- random({N}) -).

Every turn:
	say "random(-1) = [random N -1][line break]
	random(-2) = [random N -2][line break]
	random(-10) = [random N -10]";
1 Like

I’ve made a proposal document for what Inform 10 should do for its random implementation:

The main question remaining is how to seed the RNG:

Even though the interpreter’s RNG may be flawed, one call with a high enough range is probably much safer than calling it with a range of only 4 or 8. It has not yet been determined whether a full 32-bit @random call is safe in affected interpreters, or instead whether it should make a ranged call using a high prime number, or if any priming is necessary.

I’ve also made an issue suggesting the I6 compiler fix its Glulx random implementation, because no one would want random(-10) to return a number between -8 and 1.

1 Like

Yes, you’re right of course. I’d clearly got muddled. I’ve edited post accordingly

1 Like

I’m on board with the I6 change. I’ll try to make sure that a constant positive argument like random(100) produces the same assembly output as before.

Unfortunately this will still result in a binary change for most Glulx games, as parserm.h has the expression tab-->(random(tab-->0)). This is only a nuisance for my test apparatus, however.

I think I disagree with the I7 change proposal. I’ll write up my thoughts on github.

1 Like

I thought I read that the arguments must be constant, but I guess that’s only a rule for the multi-arg form. Variables does make it more complex. Maybe it should just be turned into a full veneer function? But that would change the output when it is a single constant arg…

I think Inform 7 should do something to insulate against old terps, so I’ll be interested what alternative you’d recommend instead.

So with no one else really in favour of changing Inform to embed xoroshiro directly, I’ve closed my proposal.

But I’ve also made a pull request to at least improve the priming Inform does, which in the old interpreters will change the number of sequences random(4) would have from 4 to 388.

When that’s merged I think we can call this saga over.

1 Like