Summary:
- Rolling and discarding averages fewer than 2 d20 per any other die, even d12. This is good for time and the stated criterion of number of rolls.
- Divide and round up is easier than actually working through your formula, and maybe easier than doing the modulo-like operation that's equivalent to your formula, except for d10 where you just take the trailing digit. This is more ideal for time, same number of rolls.
- idea from @superb rain in comments, not yet incorporated into this answer except for d4: often 1 player needs to roll multiple d6s or d8s (or d12s) in the same turn. A single d20 can give us a d8 and a d2 result without correlation, for example, letting us chain rolls by giving us a d8 output and some leftover entropy. With clever math, we can even reduce the number of d20 rolls per d8 below 1 when rolling multiple d8s. If someone wants to write this up as a new answer, I'll link to it here.
Discarding random samples is the standard way of avoiding bias when creating a uniform distribution from an RNG that generates a larger range than what you want, and the range isn't an exact multiple1. It's simple, easy to remember and understand, and probably quick to do in practice.
But how many rolls will we need on average with the simple re-roll method, e.g. discarding 19 and 20 when trying to roll a d6? Expected number of rerolls for simple sampling, discarding and rerolling 2 out of 20 numbers for a d6 (20 mod 6=2), 4 out of 20 for a d8 (20%8=4), 8 out of 20 for a d12 (20%12=8).
- d4: 1.0
- d6:
sum((2/20)^n, n=0..inf)
= 1.11111...
- d8:
sum((4/20)^n, n=0..inf)
= 1.25
- d10: 1.0
- d12:
sum((8/20)^n, n=0..inf)
= 1.6666...
These are all less than 2, even for d12. Any method that always rolls 2 dice is on average more rolls than rerolling. In the worst case for rerolls (d12), you don't have to do any math (just take the 1..12 result from the d20) so the total time spent to get a number is still not bad.
The chance of rolling 19 or 20 is 1 in 10. The chance of doing it again is 1/100, etc. The series 1/10 + 1/100 + 1/1000 + ...
converges to 0.1111...
extra d20 rolls per roll d6 roll, if I have the logic correct.
@Rayllum suggests in comments that instead of just discarding, switch strategy. e.g. for d8, on 17..20 use that as a d4, then the next roll determines whether to add 4 or not. i.e. the 2-roll d4 + 4*d2
strategy suggested in another answer. This reduces the average number of rolls for a d8 to 1.2, and sets a hard upper bound of 2. Similar tricks are possible for other dice.
But at least you don't have to do any math when you reroll, only on the one that's eventually in the right range. Rerolls are "cheaper" than other multiple-roll strategies because they can be done quickly with little mental effort, and uses the die "as intended" with no need to judge angles, just read the face. You do still need to do math at the end on the one die result, but that can be simpler than a formula that involves 2 inputs and larger intermediate values.
A rare long string of high d20 rolls might be amusing itself, or might just be frustrating ("where were these rolls for my attacks / saving throws / checks?")
Possibly some players who haven't overcome the gambler's fallacy would be frustrated that they're "wasting" all their high rolls by having to discard them. Discarding low rolls and shifting the mapping would make the math slightly less simple (1 extra subtraction step at the start), but might make some players happier (than discarding high rolls).
Frame challenge: time, not just number of rolls, is probably the real concern
Clever outside-the-box answers have suggested using other properties of dice / angles, but some of them might make each roll take longer (e.g. judging the angle and/or consulting a lookup table of vertices).
Minimizing the average time to result is probably a better real-world goal than minimizing number of rolls. This part is a frame-challenge based on your post-apocalypse premise. The actual math question of finding minimum-rolls is also interesting.
With time in mind, let me suggest a simpler formula for mapping d20 rolls to smaller ranges. Your formula based on floor $$X - B \times \left \lfloor{\frac{X-1}{B}}\right \rfloor$$
is equivalent to 1 + (x-1)%B
, a remainder from 1..B instead of 0..B-1. e.g. for B=4, it goes 1,2,3,4, 1,2,3,4, etc. I think it would be easier to directly use quotient rounded up ("ceiling").
$$\left \lceil{\frac{X}{5, 3, \tt or\ 2}}\right \rceil$$
This has the benefit for humans that higher numbers on the d20 are higher numbers on the d4 / d6 / d8 / d10 / d12.
e.g. for a d4, it's fairly intuitive to see where your number falls in the four ranges 1-5, 6-10, 11-15, 16-20. This works out nicely because 5 is a factor of 10, the number base we're used to using. Or just think of it as d20/5
round up.
Unfortunately we can't do it both ways on the same d20 to get 2d4 from one roll. (Unless you're rolling a lot of d4s and don't mind the correlation). 1 on the ceil(x/5)
makes 1
on the 1+(x-1)%4
twice as likely as other results. Same with other numbers. (A different mapping could give opposite correlation, making rolls more likely to tend towards the average instead of away). You could do d20/4
and d20%4
for 2 independent d4 results, or just 1 if the roll was 17..20. With 2 players looking at the same d20 roll, one can total the the 1 + (x-1)%4
in their head, and the other can total the ceil(x/4)
, saving time for a player to roll magic missile damage or a healing potion.
For a d6, d20/3
round up is probably not as intuitive for most people, but I expect it's something you can quickly get good at. (Unless 5e mechanics of rounding down for damage resistance leads to frequent mistakes...) You could write down a table which is easy to visually search because d20 input and d6 output are both in ascending order.
For a d8, divide by 2 round up (and discard rolls 17..20). Or use your formula, which amounts to subtracting 8 or not for the high half of the acceptable range. That's also fairly simple and avoids "divide by 2 round up" which is very close to, but different from, standard 5e divide by 2 round down damage resistance. It probably comes down to experience which is mentally easier overall. Or discard 1 and 18..20, then divide by 2 round down.
For a d10, your way amounts to ignoring the leading digit (treating 0 as 10) which should be very quick and intuitive (and less risk of error) than dividing by 2 round up. Also avoids conflicting number-memory for damage resistance = divide by 2 round down.
For a d12, it's trivial math ("divide by 1"). The only cost is in a larger number of expected/average rerolls.
Footnote 1: In computer programming, rand() % 12
has only a tiny bias and can be "good enough" when rand()
can produce numbers up to 2^32-1, but a huge bias when it produces numbers from 0..19. That would produce a random number from 0..11, but not uniformly distributed. The numbers 0..7 would come up twice as often as 8..11
Similarly, rand() / (RAND_MAX/12)
has bad bias for small numbers like 12 because the range isn't evenly divisible into 12 equal-sized chunks.
I mention this only to point out that this problem is basically similar to one that's been studied extensively for computing. Doing it by hand, and with small ranges, makes the tradeoffs different.