dice-box icon indicating copy to clipboard operation
dice-box copied to clipboard

Deterministic Rolls

Open frankieali opened this issue 3 years ago • 22 comments

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.

frankieali avatar Jun 17 '22 15:06 frankieali

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.

cthos avatar Jul 22 '22 02:07 cthos

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.

arrisar avatar Jul 23 '22 07:07 arrisar

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.

cthos avatar Jul 23 '22 18:07 cthos

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.

aseigo avatar Jul 23 '22 21:07 aseigo

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.

cthos avatar Jul 23 '22 23:07 cthos

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.

arrisar avatar Jul 24 '22 00:07 arrisar

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.

cthos avatar Jul 24 '22 00:07 cthos

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.

frankieali avatar Jul 24 '22 13:07 frankieali

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.

fromi avatar Jul 24 '22 13:07 fromi

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.

frankieali avatar Jul 24 '22 13:07 frankieali

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.

cthos avatar Jul 24 '22 17:07 cthos

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.

frankieali avatar Sep 19 '22 17:09 frankieali

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.

itlackey avatar May 26 '23 23:05 itlackey

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

Vonadise avatar Jul 19 '23 21:07 Vonadise

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.

frankieali avatar Jul 24 '23 15:07 frankieali

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!

itlackey avatar Jul 24 '23 15:07 itlackey

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

Heilemann avatar Sep 05 '23 11:09 Heilemann

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.

FaithLilley avatar Jan 25 '24 22:01 FaithLilley

Any updates on this feature? I need all users in a room to see the same dice roll.

Dragg-io avatar Oct 12 '24 04:10 Dragg-io

@Dragg-io check out this instead: https://github.com/MajorVictory/3DDiceRoller

FaithLilley avatar Oct 12 '24 17:10 FaithLilley

Somehow there are "strongly deterministic" dice rolls, i.e. the dice roll can be forced to give a certain result, and "weakly deterministic" dice rolls, i.e. the dice roll can be repeated and all the randomness is contained in a seed. This is talking about strong determinisim. If the plan is to do that by rolling the dice quickly offscreen, recording the trajectories, reorienting the faces as desired, and then replaying the trajectories, then weak determinism could be helpful. It wouldn't be necessary to record the whole trajectoies anymore as they can simple be re-simulated with the same initial condition and the reoriented faces. This is a good way to live-stream a dice roll to other clients for example. Furthermore achieving weak determinism means the physics are kind of reversible which could also be useful for this kind of thing. I need weak determinism myself and I've figured out a number of changes that achieve it after a lot of tinkering. Basically, I added a cache clearing function to AmmoJS, I fixed time step sizes and I feed the dice into queue instead of rolling them directly when the message arrives in the physics.worker.js so that 1 die is added each simulation step. If someone wants to collaborate with me on a PR to add weak determinism conditional on a seed, hit me up. My fork is @Robert-Wegner/dice-box-deterministic. I don't have the overview to cleanly integrate my changes. I've just hacked things to work for diceBox.roll and I am unsure about things like rerolling dice, removing them mid simulation etc. Given this, strong determinism would only require computing and applying a rotation to each die's initial condition.

Robert-Wegner avatar Nov 29 '24 15:11 Robert-Wegner