What is best way to create random numbers for dice roller?

What is best way to create random number for dice roller? I’m planning to create LiveView app to help me and my friends remotely play pen & paper RPG using Savage Worlds system.

1 Like

Define ‘best’… what are the requirements?

Requirements are that user clicks dice of choice on screen then I need to generate random from it based on a chosen dice.

Never done this before so should I created random seed based on time so it will be randomized based on when used clicked? I also don’t know much about different random algorithms so what would be best for D4, D6, D8, D10 and D12 dices?

I did find this

Erlang -- rand
For all these generators except exro928ss and exsss the lowest bit(s) has got a slightly less random behaviour than all other bits. 1 bit for exrop (and exsp), and 3 bits for exs1024s. See for example the explanation in the Xoroshiro128+ generator source code:

So based on that if I will use division remainder to create random for dice I should be using exro928ss or exsss (this seems to be default).

To get stronger and less predictable randomness the recommended API is :crypto.strong_rand_bytes.

5 Likes

Just beware, that changing output of that function to be uniform dice roll isn’t obvious.

@wanton7

def roll(sides, number) do
  Enum.take_random(1..sides, number)
end
2 Likes

This produces unexpected results… For instance, rolling 20d6: Enum.take_random(1..6, 20) will take only up to the max available numbers on the list, sample output: [5, 1, 2, 6, 4, 3].

I think that perhaps the :rand module is the easiest option for simple use cases. It’s also what the rollex lib uses internally:

def roll(number, sides) do
  for _ <- 1..number, do: :rand.uniform(sides)
end

PS.: Now I’m also curious about an implementation that uses what @dimitarvp suggested and I’m kind of intrigued by @hauleth’s comment.

@thiagomajesk yeah you’re right, I should have read the docs more carefully.

a replacement that should actually work is something like

def roll(number, sides) do
  Stream.repeatedly(fn -> :rand.uniform(sides) end) |> Enum.take(number)
end
1 Like

How could you do that @hauleth ?

<<byte>> = :crypto.strong_rand_bytes(1)
trunc(byte * faces / 256) + 1

edit: fixed off-by-one error (x2 :sweat_smile: )

edit3: Actually this is still not uniform. I give up. :smile:

3 Likes

I highly recommend using :rand instead of :crypto.

You aren’t aiming to make the next number unpredictable (this is what :crypto is built for), you’re aiming to make the dice be fair; 1/n even chance for each face.

You’ll have a hard enough time convincing your players that the computer dice aren’t rigged without also having to admit “I wrote my own random number generator”.

For example, the trunc approach suggested above has an unexpected bug in it (even after an edit) and will slightly favor 1, 3 and 5 over 2, 4 and 6:

faces = 6
0..255
|> Enum.map(fn x -> trunc(x * faces / 255) + 1 end)
|> Enum.frequencies()

%{1 => 43, 2 => 42, 3 => 43, 4 => 42, 5 => 43, 6 => 42, 7 => 1}

I show this out in detail because I too have gone down this path of ‘I’m going to roll my own RNG that’s using strong crypto’ when I should have just trusted non-crypto-strength randomness for games.

tl;dr Use :rand.uniform(sides) not :crypto.

7 Likes

I actually use this currently. I can’t remember where I got idea how to check if roll is fair

defmodule SavageWorlds.DiceRoller do
  def roll(sides) when is_integer(sides) and sides >= 2 and sides <= 100, do: do_roll(sides)

  defp do_roll(sides) do
    <<roll::unsigned-integer-8>> = :crypto.strong_rand_bytes(1)

    case is_fair_roll(roll, sides) do
      true -> rem(roll, sides) + 1
      false -> do_roll(sides)
    end
  end

  def exploding_roll(sides), do: do_exploding_roll(sides, []) |> Enum.reverse()

  def do_exploding_roll(sides, rolls) do
    new_roll = roll(sides)
    rolls = [new_roll | rolls]

    cond do
      new_roll == sides && Enum.sum(rolls) < 1000 -> do_exploding_roll(sides, rolls)
      true -> rolls
    end
  end

  defp is_fair_roll(roll, sides), do: roll < sides * div(255, sides)
end

Any problems with this or should I use rand.uniform(sides) instead?

This is the right approach to take: you grab values from :crypto until the value falls within a multiple of the number of sides. n = 100 is maybe the easiest to see it. div(255, 100) = 2, so there are two full 100s in 255, so you’re considering it a non fair_roll if it falls outside of 0-199.

however:

  1. the fancy properties of :crypto being unpredictable may be broken by discarding values that are ‘too large’, so you may have removed the singular gain of using :crypto and now you’re just using a slower source of randomness.
  2. I think this is what erlang does under the hood! (in some cases at least, uniform_range / uniform_s employ a number of strategies to get a uniform integer, but I see uniform_range is calling itself if something falls outside of a range). It’ll be faster at it than is_fair_roll
  3. I think you do have a bug – I think you want roll < sides * div(256, sides). if the die has 64 sides, you should be able to use the whole byte. (this bug is one of wasted cpu cycles, however, and not one of unfairness)