3d-dice / dice-box

3D Game Dice for any JavaScript App
https://fantasticdice.games/
MIT License
112 stars 26 forks source link

Deterministic Rolls #47

Open frankieali opened 2 years ago

frankieali commented 2 years ago

It has been requested that dice rolls be made with a pre-determined outcome. This way, numbers can be generated securely server side and the visual effect of the roll could then be triggered based on numbers passed over from the server.

I've looked into how this could be done by investigating Teal Dice, Dice so Nice and this thread on Reddit. Basically, the physics emulation is run once using preset vector inputs. The resulting faces are stored, then the dice are rotated to the desired start position and the 3d engine renders the dice using the same vector inputs again for the physics engine.

I was surprised at how quickly the physics engine can resolve the simulation when the time step is unrestricted. I think I'm able to speed up the AmmoJS simulation by increasing the stepSimulation method's substeps.

Overall this would be a pretty heavy lift because I would have to change out a lot of the randomness of the core system. Currently, random values are used for the dice start positions, the dice starting rotation, and the forces used to push the dice out into the box. I think I would have to forgo using an impulse to give the dice a linear velocity. I also have a sleepTimeout for the physics bodies (calculated in real time) which would have to be accounted for (time dilated) in the emulation. I also allow a config option to space out individual die generation (also calculated in real time), which is kind of a silly option. I had to add it early on so the dice were not generated right on top of each other which would cause them to forcefully explode away from one another.

Another big difference between my system and something like Teal, is that this library allows for different dice models with different textures. The faces of the models are variable. I would have to record the rotation of each dice face from the "up" position in order to calculate the desired starting rotation of a die.

In short, this is a big feature request, but probably doable with time.

cthos commented 2 years ago

How do you feel about allowing a method to set the seed for the roll?

~I've only skimmed the code but if you can have the server set the seed for the initial positions/vectors/etc you'd at least be able to get the same value out for that particular seed (or multiple seeds if you're seeding webcrypto separately for each of the aforementioned areas).~

Edit: Scratch that, you can't set the seed on getRandomValues. I was misremembering what webcrypto actually exposed.

Double Edit: I suppose you could also have a mode where you use a PRNG lib that accepts a seed and potentially seed that with getRandomValues ... or some combination of using the existing algorithm on "clean" rolls and a seeded algorithm on deterministic rolls.

arrisar commented 2 years ago

How is this progressing? Is there anything particular you would like a hand with?

This would be a fantastic addition, and I'd be happy to assist however I can.

cthos commented 2 years ago

So I did a little digging into this code to follow up on the seeded random idea, and it looks like basically everything is doing a Math.random() in the Physics worker - it should be relatively straightforward to just use a drop-in replacement for that which is seeded.

As far as the interface goes, I'm thinking something like:

const seededRandom = new SeededPRNG(); // This would be a class that conforms to an interface that provides a `.random()` method - either via a drop in replacement package or a 

seededRandom.setSeed(newSeed); // get this from the server however it wants to generate the seed.

const box = new DiceBox({
  rng: seededRandom,
  // Other initialization config
});

(await box.init()).roll('1d6');

Any time you need a new seed you could just do a .setSeed() on the new RNG path to reseed (or provide an interface to change the rng class on the fly, though I think that'd require more changes to the physics engine). Though because the physics engine is a web worker it'd require some trickery to pass a dynamic class to it, so it might need to be a fixed PRNG class and passing the seed via .postMessage

I'll probably tinker with this over the next couple of days, just to get it out of my head.

aseigo commented 2 years ago

How would the server calculate a seed such that e.g. "4d6+2d10" results in given roll, e.g. the d6's coming up 4,4,2,5 and the d10s 6 and 8?

Or is your solution assuming that it doesn't matter what the results are, as long as everyone gets the same results? Because that's a slightly different use case to "predictable results". Also, in that case: is the expectation that one client would report back the result to the server for e.g. durable storage in cases where is needed?

Also, does the size of the dicebox matter for the physics? Because if so, then different sizes would give different results even with the same seed, which isn't great either.

cthos commented 2 years ago

Or is your solution assuming that it doesn't matter what the results are, as long as everyone gets the same results? Because that's a slightly different use case to "predictable results".

Yep, it's this one. In most of my use-cases, the server doesn't so much care what the results of the rolls were so long as every client is seeing the same thing. Notably, I'm not trying to protect against a client reporting to the server that "I got a nat 20" every time - I just want the server to say "here's the thing to start from, all of you play the same physics simulation". So yeah, true - there's the element of "I want the server to be the source of truth for the numerical result" missing from my proposed implementation.

Also, in that case: is the expectation that one client would report back the result to the server for e.g. durable storage in cases where is needed?

Yeah, I think you'd need something either designated as a "host", or have all of the clients report back and decide what the consensus mechanism is if you're worried about clients manipulating the results. I suppose you could also run the simulation on the server and have it store those results.

Also, does the size of the dicebox matter for the physics? Because if so, then different sizes would give different results even with the same seed, which isn't great either.

That's a good question, I've not dug into the code enough to know how the world's size is being generated - my working assumption is "yes" which would necessitate pinning the size of the canvas element to absolute values rather than allowing it to fill the entire screen. Should be pretty easy to test. I'm also not 100% sure if Babylon / Ammo do different things if the resolution is different.

As an aside, the other way I'd thought about solving "I want everyone to see the same thing" problem is by doing canvas.captureStream() and then streaming that from a host to the other clients via WebRTC ....which has its own downsides.

arrisar commented 2 years ago

While an interesting route to explore, each theory is seemingly being faced by more issues and doesn't seem to further the goal of this issue which is externally determined outcomes.

It's probably worth splitting off your exploration into a different issue or even a PR if the drop in replacements prove fruitful.

That said, if we can solve for pre-determined outcomes, then each person seeing the same thing is mostly irrelevant, as they can see the same outcomes and have their own physics get them there.


Overall, the suggestion mentioned in OP seems most sound. When I get a moment I'll have a look and see if I can find any wins to that end.

Another option that comes to mind is to capture the movement through the world in 3D space the first time using physics more like a 3D animation. Play that back to the canvas using the same rotated textures concept.

Theoretically that would also allow for your use-case @cthos, ensuring players see the same thing and the same outcome.

cthos commented 2 years ago

It's probably worth splitting off your exploration into a different issue or even a PR if the drop in replacements prove fruitful.

Yep, I'm doing some tinkering over in a fork, and initial results an hour in are pretty good - simply using seededrandom and having it replace Math.random() in the physics worker does indeed produce the same dice roll every time (for the same viewport size etc, I've not checked the other cases mentioned above).

Next up will be to futz with the various bits of the canvas and whatnot and see if the canvas size matters ... but looking through the code it looks like the actual world size / camera distance / etc are static so maybe it'll be alright.

I'll keep y'all posted, but won't clutter this issue any further since I do agree that this is a different but relatedish line of thinking.

frankieali commented 2 years ago

Hey guys, Sorry I've been unavailable. My family all got Covid and then we had a summer vacation that I was not allowed to bring my computer on (my wife's ruling). I've continued to consider this issue. While @cthos approach would work to duplicate a roll, it does not necessarily solve the issue of asking the module to "Roll me a 5 on a d6" regardless of the box's size (based on the user's device). I still think the original approach is the way to go. Generate the inputs for the physics engine, run the unrestricted simulation virtually, collect the die face results, set the starting rotation as necessary to produce the desired outcome, then run the fully rendered scene. To deal with the time dilation issues mentioned, I think I'll change those values from being milliseconds on a setTimeout to being integers for the "# of steps" in the simulation. Then the time will scale with the simulation speed as desired. While in "deterministic" mode, I'll have to lock down any configuration options that could change the simulation while it's running.

fromi commented 2 years ago

I plan to use the feature on Game Park for board games adaptations. For my use case, the server do really care for what the result is.

frankieali commented 2 years ago

As a side note, I dug into Teal dice a bit more and found that they handle this a little differently. Their dice models use plain color textures with a text node mapped to the 3D objects' faces. After running the unrestricted simulation and seeing what number is facing up, the module then just swaps the text numbers on the 3D model itself. They do not have to calculate starting rotations. I've considered the idea of just re-writing teal dice to expose similar config options that I have for this project. Others have already done this, but not with the goal of making it an npm module. However, I see the need to have deterministic rolls in this project. Not just for server generated results, but also to ensure that multiple user see the same outcomes when connected on a platform. Enabling this feature is my top priority. I'm going to be learning a lot about quaternion mathematics.

cthos commented 2 years ago

Sorry I've been unavailable. My family all got Covid and then we had a summer vacation that I was not allowed to bring my computer on (my wife's ruling).

It's important to unplug!

While @cthos approach would work to duplicate a roll, it does not necessarily solve the issue of asking the module to "Roll me a 5 on a d6" regardless of the box's size (based on the user's device).

And just to be clear, I agree with y'all, I'm just tinkering around with "can I get to repeatable rolls with minimal effort", and I 100% support the proposed approach here. (I can't help with that because I'd need to learn a lot more about babylon + ammo).

Though, a seeded PRNG might also be useful if you wanted to do unit tests against the version where the physics engine is doing the rolling, though there's something happening that causes drift even when the inputs are predictable.

frankieali commented 1 year ago

So, I've had trouble getting deterministic rolling done in this project, but I forked Major Victory's repo to make this feature available. It may not be wise of me to have two dice-box projects, but I wanted to get something out there people could use now until I have more availability to work it out here. It also comes with dice sounds which is nice. Repo: https://github.com/3d-dice/dice-box-threejs Demo: https://codesandbox.io/s/dice-box-threejs-j79h35

It's still a work in progress.

itlackey commented 1 year ago

Hey @frankieali thanks for moving this forward. Any updates on your fork? Needing this feature too and evaluating options. This library is fantastic but this feature is much needed and I'm having perf issues on iOS with this one currently.

Vonadise commented 1 year ago

How are things going and is it worth waiting for updates on this issue in the near future @frankieali ?

frankieali commented 1 year ago

Hi @Vonadise. I am working on a version 2.0 that has a number of new features. Most importantly, I'm switching the physics engine from AmmoJS to RapierJS which makes an explicit effort to implement deterministic simulations. However, progress is slow. Mostly due to lack of time availability. Sorry that things have slowed down. I'm also trying to convert this project to a plug-in architecture to support more customizations. I'm moving more dice characteristics over to the 3D files and adding support for .gltf imports. I want the new system to support any sort of bizarre dice set from Legend of the Five Rings to Left Right Center. The current timeline is pointing to a release perhaps this Winter.

itlackey commented 1 year ago

I am excited to see v2 when it's ready 👀

For anyone needing this feature now, I have been using the 3d-box threejs package and it works great! https://github.com/3d-dice/dice-box-threejs

Thanks @frankieali for all the work on these libraries!

Heilemann commented 10 months ago

Just chiming in with a voice of support. Super excited that you’re working on this!

FaithLilley commented 6 months ago

Awesome to see - this is a great library and tool for TTRPG projects and I'm excited for the direction. Adding deterministic rolling is really key to collaborative projects, so other users in a session can see the same roll as the person making it.