-
Notifications
You must be signed in to change notification settings - Fork 107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New prng #552
base: master
Are you sure you want to change the base?
New prng #552
Conversation
Aaargh I’m sorry, don’t merge this! While testing this new PRNG a bit further, I saw other cases where the generation was bad, such as when using 10 elements. I’m pretty sure it’s related to the reseeding of the generator at every step. Are you opposed to adding a |
Related to #31. @y-lohse I confirm there's a severe bias in the PRNG. I checked with 200 throws across a hundred reseeds and the bias is very stable. The PRNG only outputs 1s and 8s. Thanks catching and fixing @PiOverFour!
A compelling reason to replace it anyway.
All good, they are generated from TypeScript. Updating the TypeScript tests is enough (thanks for doing so by the way). The linter is complaining though, but I'm not certain it's related to this PR.
Let me summon @joethephish, he'll know more about it.
Since the PRNGs are different between inkjs and ink anyway, I think it would be okay. |
Yep, good catch!
The thing is, right now the differences are scope to the PRNG.js file. We can divert from upstream in Story and StoryState, but it seems we are talking about changes that would benefit upstream as well. @joethephish can you give some context on the Ideally this would be the occasion where both implementations converge to the same PRNG and we end up with no differences at all ! But if that doesn't work out, we can definitely consider some divergences here :) |
I pushed a new version to add to the discussion (here is a backup of the previous proposition). It changes the logic for number generation, by storing the PRNG instance in the StoryState. I didn’t realise that yesterday, but this logic is actually unrelated to the shuffles’ error, since their states are determined by their hash and not by the other previous numbers. I still feel it’s a better practice though, but I haven’t found anything wrong with the current implementation, so feel free to disregard that change. It also fixes a problem I noticed with my previous proposition: xoroshiro64** uses two state variables, which complexifies the StoryState and may make saves from previous versions impossible to load. The new version uses Mulberry32 instead, as it has only one variable of state. I still don’t know how it performs, but it’s also a recommended algorithm, and one which better fits the requirements. This probably needs further testing for quality probability distributions but I don’t know too well how to… |
cdf9f4f
to
0559167
Compare
It looks like I forgot to follow up on this, apologies 😬 I'm all for replacing the faulty PRNG, but there are some constraints:
|
Hello @y-lohse, no worry, I’m not using Ink enough these days to be in a hurry :D
|
Yes! Ideally there should be no change in StoryState :) Cool if that can be achieved easily!
No it's all good, I was mentioning this in case the previous point was too much of an issue and we wanted to introduce a dependency rather than embedding an implementation :) |
Oh! right, thanks. |
I worked a bit on this and I believe it can be done without changes to the
This is completely untested, but I wanted your opinion on the direction it’s taking before proceeding. This is also why I haven’t updated the tests. |
35f5b34
to
08bd60e
Compare
I tested the code on the test case from the original post and it works fine. I just rewrote the commit messages to better describe what is happening. The only reservation I have is that Also, I’m not sure this qualifies as a change of API, but it still introduces significant changes from upstream… I couldn’t find a way to avoid this while using the PRNG properly. |
Thank you for the update! I'm afraid I'm going to be difficult again 😭
I think this is a problem 😬 Two playthroughs with the same seed should result in the same story. If the story seed is ignored I think that's not the case anymore? If so. maybe we can tweak things a little? I think the other changes are fine (the nextSeed function and the storing of a seed i previousRandom rather than a result value.) What happens if we change |
Oh no, actually the date is used to seed the generator, same as before. The storySeed (which was and is computed from the initial date) is still used in seeding the PRNG, but then only the previous state is needed.
So two playthroughs will be different if they are not seeded with the same number, but it was already the case. It’s just that now, the initial seed is not kept: the state of the generator depends only on the previous state, as most PRNGs do. Actually, I think it’s easier to get a predictable run now than before, because in order to replay from a branch, you only need to know the previous state at this particular branch, instead of the previous state plus the initial state (storySeed). EDIT I realise that this may sound condescending. It is not my intention, it's just that as far as I understand, modifying the state at each step by adding a constant number to it — the StorySeed — gives no benefit, and may even result in a poorer number distribution. |
No problem 😁 I think you might have missed this bit: https://github.com/y-lohse/inkjs/blob/master/src/engine/Story.ts#L1215 which is explained here https://github.com/inkle/ink/blob/master/Documentation/WritingWithInk.md#seed_random . Basically, yes the storySeed is still initialized using a date, but it can be overriden from within the ink script itself. And unless I've missed something, doing that would have no effect at all with the current PR. Unless I'm the one missing something? |
Damn, you’re right, I missed this bit! Makes sense. But this: this.state.storySeed = seed.value;
this.state.previousRandom = 0; can be easily replaced by: this.state.previousRandom = seed.value; And the seed becomes the new initial state for the generator. Thus, the state can even be restored from a saved seed/state. I’ll update the PR as soon as possible. |
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.
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.
I implemented that and upon testing and re-reading the code, noticed I'd forgotten to modify the shuffle altogether. Now I understand better why the storySeed is used at each generation, and in the case of the shuffle, I don't see a way to change it without getting too far from upstream. Anyway, I tested again on various small tests, and it seems to behave well. |
Checklist
npm test
).npm run-script lint
).Description
Hello, while writing a story, I stumbled upon a weird edge case with the PRNG. When evaluating a shuffle with 14 elements (or, I suspect, any multiple of 7, as I confirmed by using 7 and 21 elements), the results are very poor. Here is my test case:
And here is the result:
As you can see, the shuffle works as expected when running a single shuffle multiple times, but breaks when running new shuffles many times. This can be useful to get simple variation in different story runs, but in this case they will always have the same (n/7) values.
After a bit of investigation, I went to the gist whence the PRNG comes from, and it’s been recently updated to warn against using it*. User bryc has a page recommanding alternatives. Now my math skills are far from sufficient to evaluate the reliability of these options, so I just trust that user’s judgement…
I replaced the PRNG with xoroshiro64**. I don’t know how to test it for speed or probability distribution, but at least it appears to give good results in my test case.
I updated the tests to match the new PRNG, but didn’t understand what I was supposed to do with the javascript tests, which don’t seem to be version-controlled…
The other modification isn’t entirely related, but I think it is an improvement. It simplifies seed initialisation, so that the timestamp doesn’t go through the PRNG and through a modulo(100), which limits the number of possible seeds. Now maybe there is a reason I’m not seeing for this, but as far as I understand it only reduces the number of possible playthroughs…
* I also suspect there is something wrong with the way we reseed the generator at every iteration with
instead of just keeping its state and using
next()
, but since that’s the way ink does it, I guess it’s simpler to keep that…