From e2d9cae42320e87c2c3321a56fbc1314efce0e76 Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Thu, 11 Jun 2020 15:03:26 +0200 Subject: [PATCH 1/3] Change PRNG to Mulberry32 instead of Lehmer LCG --- src/engine/PRNG.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/engine/PRNG.ts b/src/engine/PRNG.ts index 2fcf8733..3137f70f 100644 --- a/src/engine/PRNG.ts +++ b/src/engine/PRNG.ts @@ -1,16 +1,26 @@ -// Taken from https://gist.github.com/blixt/f17b47c62508be59987b +// Taken from https://github.com/bryc/code/blob/master/jshash/PRNGs.md // Ink uses a seedable PRNG of which there is none in native javascript. export class PRNG { private seed: number; constructor(seed: number) { - this.seed = seed % 2147483647; - if (this.seed <= 0) this.seed += 2147483646; + this.seed = seed; + } + + public nextSeed(): number { + let a = this.seed; + a |= 0; + a = (a + 0x6d2b79f5) | 0; + this.seed = a; + return a; } public next(): number { - return (this.seed = (this.seed * 16807) % 2147483647); + let a = this.seed; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return (t ^ (t >>> 14)) >>> 0; } public nextFloat(): number { - return (this.next() - 1) / 2147483646; + return this.next() / 4294967296; } } From cfc70bb71321279e993185df6e31b0f8a1c009c5 Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Fri, 12 Jun 2020 03:08:08 +0200 Subject: [PATCH 2/3] Update PRNG logic The new algorithm, Mulberry32, doesn't return its internal state as the generated random number. This means that previousRandom, the value we keep between generations, should not be the actual previous random number generated, but the previous seed normally kept inside the generator. To do that, a new nextSeed() method is added. Also, the storySeed is no longer fed into the PRNG, but only used on first initialization. --- src/engine/Story.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/engine/Story.ts b/src/engine/Story.ts index b06d1419..f03d0420 100644 --- a/src/engine/Story.ts +++ b/src/engine/Story.ts @@ -1189,7 +1189,7 @@ export class Story extends InkObject { ". The maximum must be larger" ); - let resultSeed = this.state.storySeed + this.state.previousRandom; + let resultSeed = this.state.previousRandom; let random = new PRNG(resultSeed); let nextRandom = random.next(); @@ -1197,7 +1197,7 @@ export class Story extends InkObject { this.state.PushEvaluationStack(new IntValue(chosenValue)); // Next random number (rather than keeping the Random object around) - this.state.previousRandom = nextRandom; + this.state.previousRandom = random.nextSeed(); break; } @@ -1212,8 +1212,7 @@ export class Story extends InkObject { return throwNullException("minInt.value"); } - this.state.storySeed = seed.value; - this.state.previousRandom = 0; + this.state.previousRandom = seed.value; this.state.PushEvaluationStack(new Void()); break; @@ -1349,7 +1348,7 @@ export class Story extends InkObject { newList = new InkList(); } else { // Generate a random index for the element to take - let resultSeed = this.state.storySeed + this.state.previousRandom; + let resultSeed = this.state.previousRandom; let random = new PRNG(resultSeed); let nextRandom = random.next(); @@ -1377,7 +1376,7 @@ export class Story extends InkObject { newList = new InkList(randomItem.Key.originName, this); newList.Add(randomItem.Key, randomItem.Value); - this.state.previousRandom = nextRandom; + this.state.previousRandom = random.nextSeed(); } this.state.PushEvaluationStack(new ListValue(newList)); @@ -2092,7 +2091,9 @@ export class Story extends InkObject { } for (let i = 0; i <= iterationIndex; ++i) { - let chosen = random.next() % unpickedIndices.length; + let nextRandom = random.next(); + let chosen = nextRandom % unpickedIndices.length; + random.nextSeed(); let chosenIndex = unpickedIndices[chosen]; unpickedIndices.splice(chosen, 1); From 6b48bbfec97ea696bb4b177c16a51b17d5bfb590 Mon Sep 17 00:00:00 2001 From: Damien Picard Date: Sat, 13 Jun 2020 21:17:08 +0200 Subject: [PATCH 3/3] Improve storySeed initialisation The storySeed now takes the timestamp directly and the initial seed is stored as the storySeed. It also no longer uses modulo 100, which should give a lot more variety. --- src/engine/StoryState.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/engine/StoryState.ts b/src/engine/StoryState.ts index 9a0588c1..35d7d07d 100644 --- a/src/engine/StoryState.ts +++ b/src/engine/StoryState.ts @@ -350,9 +350,9 @@ export class StoryState { this._turnIndices = new Map(); this.currentTurnIndex = -1; - let timeSeed = new Date().getTime(); - this.storySeed = new PRNG(timeSeed).next() % 100; - this.previousRandom = 0; + let timeSeed = Date.now(); + this.storySeed = new PRNG(timeSeed).nextSeed(); + this.previousRandom = this.storySeed; this._currentChoices = [];