This post builds on Rolling the Dice, using the code from there.
Sometimes, as a DM/GM, I want to be able to roll “biased” dice. Sometimes biased heavier, sometimes biased lighter.
How do we go about making a weighted die programmatically?
I had to do some research on that topic myself. The short answer is to use powers and roots. And roots are just inverse powers.
Let’s just say that we want a biased coin, a 2-sided die. Let’s just square the number of sides, so now we are rolling 1 to 4. What do we do when we get the results? We take the square root, but we need to round that. Up or down? Well, are we trying to bias to big numbers or small ones? floor
the result to bias down, ceil
to bias up.
First let’s add the options to the console command:
14 15 16 17 18 19 20 21 22 23 |
protected function configure() { $this->setName("Roll") ->setDescription("Rolls fair dice.") ->addArgument('Dice String', InputArgument::REQUIRED, 'e.g.: 2d6, 1d20+4') ->addOption('h', null, InputOption::VALUE_NONE, 'Roll the dice with a bias towards higher numbers') ->addOption('l', null, InputOption::VALUE_NONE, 'Roll the dice with a bias towards lower numbers') ->addOption('f', null, InputOption::VALUE_REQUIRED, 'The factor of the weight', 2) ; } |
Alright, but now we need to add those functions to the Dice class:
22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public static function rollLow($number, $sides, $factor = 2) { $sides = (int)floor(pow($sides, $factor)); $rolls = []; for ($i = 0; $i < $number; $i++) { $rolls[] = (int)floor(pow(self::rollOne($sides), 1 / $factor)); } return $rolls; } public static function rollHigh(int $number, int $sides, float $factor = 2) { $sides = (int)floor(pow($sides, $factor)); $rolls = []; for ($i = 0; $i < $number; $i++) { $rolls[] = (int)ceil(pow(self::rollOne($sides), 1 / $factor)); } return $rolls; } |
I’ve highlighted the important lines here. To take the root, we do another power operation, but we invert the power. After that, we will either ceil
or floor
the result.
Okay, but we still need to call the functions.
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
protected function execute(InputInterface $input, OutputInterface $output) { $dice = new Dice(); $matches = []; preg_match('/(\d+)d(\d+)\+?(\d)?/i', $input->getArgument('Dice String'), $matches); $modifier = empty($matches[3]) ? 0 : (int)$matches[3]; if ($input->getOption('h')) { $roll = $dice::rollHigh((int)$matches[1], (int)$matches[2], (float)$input->getOption('f')); } elseif ($input->getOption('l')) { $roll = $dice::rollLow((int)$matches[1], (int)$matches[2], (float)$input->getOption('f')); } else { $roll = $dice::roll((int)$matches[1], (int)$matches[2]); } $output->writeln('Your roll without modifier was ' . array_sum($roll) . ' [' . implode('|', $roll) . '] + ' . $modifier . ' for a total of ' . (array_sum($roll) + $modifier)); } |
Sweet, let’s test that out.
1 2 3 4 5 6 7 |
$ php dice Roll 20d2 --l Your roll without modifier was 25 [2|1|1|1|1|1|1|1|1|1|2|1|1|2|1|2|1|1|1|2] + 0 for a total of 25 $ php dice Roll 20d2 --h Your roll without modifier was 36 [2|2|1|2|2|1|2|2|2|2|2|2|2|2|2|1|2|2|1|2] + 0 for a total of 36 |
Using our 2-sided die, or rather 20 of them, we can see that the bias is working. How about testing the factor option we have set up. For that, we will need to roll something with more sides.
1 2 3 4 5 6 7 |
$ php dice Roll 20d20 --l --f=10 Your roll without modifier was 355 [18|18|18|18|18|19|15|19|19|17|18|16|17|17|16|19|16|19|19|19] + 0 for a total of 355 $ php dice Roll 20d20 --h --f=10 Your roll without modifier was 375 [18|19|18|20|18|16|20|20|20|19|19|20|19|20|19|19|18|17|19|17] + 0 for a total of 375 |
Hmmm. That didn’t seem to work. The low rolling is rolling high.
Let’s take a look at the numbers
When we take the square root of 1 to 4 we get 1, 1.41, 1.73, 2. When we floor these we do get 3x 1’s, and 1x 4’s.
But let’s take that up one more, to a 3-sided die. We now have 3x 1’s, 5x 2’s, and 1×3’s. The weights are all wrong. If we do a 4-sided die, the number of 3’s goes up to 7, and there’s 1 of the 4’s. Okay. This solution will not work for us.
We need to change the low function:
22 23 24 25 26 27 28 29 30 |
public static function rollLow($number, $sides, $factor = 2) { $rolls = self::rollHigh($number, $sides, $factor); foreach ($rolls as $num => $roll) { $rolls[$num] = $sides + 1 - $roll; } return $rolls; } |
Let’s see how that works now!
1 2 3 4 5 6 7 |
$ php dice Roll 20d20 --h --f=10 Your roll without modifier was 364 [17|15|20|13|20|15|18|20|20|16|20|20|20|19|16|18|18|20|20|19] + 0 for a total of 364 $ php dice Roll 20d20 --l --f=10 Your roll without modifier was 38 [1|2|3|4|2|1|2|3|1|3|2|2|2|3|1|1|1|2|1|1] + 0 for a total of 38 |
Nice!
The repository for this post can be found on GitHub here .
The repository for this post can be found on GitLab here .
The repository for this post can be found on Lupe Code’s GitLab mirror here .