In the interests of contributing even when not directly asking/answering a question, here’s some code I wrote because I needed an instanceable PRNG for the game I’m working on.
Basically the problem, if you want to call it that, was that the normal, built-in PRNG is global across all rand()
calls (directly or indirectly) in the entire game. I needed the ability to have PRNGs that were “local” to individual things exhibiting random behavior. Think of a simple game where the player can either roll a single six-sided die or flip a single coin. The player rolls the die, gets a 5, rolls again and gets 2, rolls again and gets 6. Now the player flips the coin, gets heads. Then there’s a time warp that sends the player back to the start of the scene.
What I needed is for the behavior of the “random” bits to be the same. So the first time the player flips the coin it will come up heads again, the next three rolls of the die will come up 5, 2, and 6, and so on.
But even if you reset the global PRNG seed to the value it was at the start, if the player rolls the die (getting a 5), and then they flip the coin next (instead of rolling the die again), the PRNG will produce the same value…but instead of using it to generate a die roll, it’s going to be used for flipping the coin. Which can come up tails. Because instead of being generated by the 4th PRNG value after seeding, it gets the second. Then the second die roll is no longer necessarily 2, because it’s now using the third PRNG value instead of the second, and so on.
You can kludge this by a lot of elaborate juggling (saving and restoring) the global PRNG seed, but that’s a mess. So instead, here’s a very simple instanceable PRNG, and everything that needs to be “independently” (but deterministically) generated can get their own.
I’m putting all this stuff into a library because it involves a lot of other RNG/procgen stuff, but here’s just the “basic” PRNG stuff in a simple sample “game”:
#charset "us-ascii"
#include <adv3.h>
#include <en_us.h>
// Include BigNumber support. This is a part of TADS3, but isn't enabled
// by default. It's only needed for the randomBigNumber() and
// randomBigNumberNormal() functions, so if you don't need them you can
// get rid of them and not have to include bignum.h.
#include <bignum.h>
// Simple PRNG class.
// This is a TADS3 implementation of Marsaglia's xorshift algorithm. We
// make a couple of simplifying assumptions/kludges to work around the fact
// that TADS3 doesn't have any unsigned integer types.
// NOTE: NOT SAFE FOR CRYPTOGRAPHIC USE.
class PRNG: object
_seed = nil // current seed
_max = 2147483647 // max integer we can handle
construct(seed?) { _seed = (seed ? seed : rand(_max)); }
getSeed() { return(_seed); }
setSeed(v) { _seed = v; }
toRange(v, min, max) {
min = (min ? min : 0);
max = (max ? max : _max);
// KLUDGE: this produces not-quite-uniform results, but
// it's much, much faster than any of our alternatives
return((v % (max - min + 1)) + min);
}
random(min?, max?, seed?) { return(toRange(nextVal(seed), min, max)); }
nextVal(seed?) {
local v;
if(seed != nil) v = seed;
else v = _seed;
if(v == 0) v = 1;
v ^= v << 13;
v ^= v >> 17;
v ^= v << 5;
// KLUDGE: if we overflow, we just take the negation
if(v < 0) v = ~v;
if(seed == nil) _seed = v;
return(v);
}
;
// Returns a number in the given range, inclusive.
// Example: randomInt(0, 4) will return 0, 1, 2, 3, or 4 with equal
// probability.
// A PRNG instance can optionally be specified. If none is given, tads-gen's
// rand() will be used instead.
randomInt(min, max, prng?) {
if(prng != nil) return(prng.random(min, max));
return(rand(max - min + 1) + min);
}
// Roll n d-sided dice.
// Example: randomDice(2, 6) is 2d6, or the results of two six-sided dice
// added together for a number between 2 and 12.
randomDice(n, d, prng?) {
local i, v;
for(i = 0, v = 0; i < n; i++) v += randomInt(1, d, prng);
return(v);
}
// Return a decimal number in the given range. If no range is given, 0 and 1
// are assumed.
// Example: randomBigNumber(0.5, 0.75) will return a decimal number between
// 0.5 and 0.75. randomBigNumber() will return a decimal number between 0
// and 1.0.
randomBigNumber(min?, max?, prng?) {
local v;
v = new BigNumber(randomInt(0, 2147483647, prng)
/ new BigNumber(2147483647));
if((min == nil) || (max == nil)) return(v);
min = new BigNumber(min);
max = new BigNumber(max);
return((v * (max - min)) + min);
}
// Return random values which match the given normal distribution.
// This means that, for example, ~68.2% of the returned values will
// be the given mean plus or minus sigma (the standard deviation);
// 95.4% will be the mean plus or minux two sigma; 99.6% will be the
// mean plus or minus three sigma, and so on.
randomBigNumberNormal(mean, sigma, prng?) {
local one, u, v, x;
mean = new BigNumber(mean);
sigma = new BigNumber(sigma);
one = new BigNumber(1.0);
u = one - randomBigNumber(0, 1, prng);
v = one - randomBigNumber(0, 1, prng);
x = (new BigNumber(-2.0) * u.log10()).sqrt()
* (new BigNumber(2.0) * BigNumber.getPi(10) * v).cosine();
return(mean + (x * sigma));
}
// Returns a shuffled copy of the passed List.
// Uses Fischer/Yates to do the shuffling.
randomShuffle(lst, prng?) {
local i, k, r, tmp;
if((lst == nil) || !lst.ofKind(List)) return([]);
r = lst.sublist(1, lst.length);
for(i = r.length; i >= 2; i--) {
k = randomInt(1, i, prng);
tmp = r[i];
r[i] = r[k];
r[k] = tmp;
}
return(r);
}
versionInfo: GameID
name = 'sample'
byline = 'nobody'
authorEmail = 'nobody <foo@bar.com>'
desc = '[This space intentionally left blank]'
version = '1.0'
IFID = '12345'
;
gameMain: GameMainDef
newGame() {
local i, prng, seed;
prng = new PRNG;
seed = prng.getSeed();
"Seed = <<toString(seed)>>\n ";
for(i = 0; i < 5; i++) {
"val 0/<<toString(i)>>: <<randomInt(1, 10, prng)>>\n ";
}
prng.setSeed(seed);
"<.p> ";
for(i = 0; i < 5; i++) {
"val 1/<<toString(i)>>: <<randomInt(1, 10, prng)>>\n ";
}
"<.p>Native rand():\n ";
for(i = 0; i < 5; i++) {
"rand() <<toString(i)>>: <<randomInt(1, 10)>>\n ";
}
}
showGoodbye() {}
;
Just offering it in case it’s useful to anyone else.