Some friends and I were playing a board game and decided that the random dice rolls just weren't fair! So we came up with a scheme for making the rolls slightly more evenly distributed, and I implemented it in python. The class represents 2 six sided dice and makes the probability of a given combination coming up (there are 36 options for 2 dice) inversely proportional to the number of times it has come up previously - thereby ensuring the distribution tends to be more even than randomness allows:
from collections import Counter from typing import Tuple, Counter as TCounter import random class LoadedDice: """ Simulation of 2 Dice being rolled. Weights the random roll to try and tend to an even distribution """ def __init__(self, sides: int = 6) -> None: self.sides = sides self.history: TCounter[int] = Counter(range(sides ** 2)) def roll(self) -> Tuple[int, int]: result_index = self.get_weighted_random_roll_index() self.history[result_index] += 1 return self.roll_value_from_index(result_index) def get_weighted_random_roll_index(self) -> int: roll_threshold = random.random() reciprocals_total = sum(self.reciprocals()) running_total = 0 for result_index, value in enumerate(self.reciprocals()): running_total += value / reciprocals_total if roll_threshold <= running_total: return result_index def roll_value_from_index(self, index: int) -> Tuple[int, int]: mod, remainder = divmod(index, self.sides) roll_1 = mod + 1 roll_2 = remainder + 1 return roll_1, roll_2 def reciprocals(self): for v in self.history.values(): yield 1 / v @property def roll_history(self) -> TCounter[Tuple[int, int]]: result: TCounter[Tuple[int, int]] = Counter() for roll_index, count in self.history.items(): result[self.roll_value_from_index(roll_index)] += count return result @property def result_history(self) -> TCounter[int]: result: TCounter[int] = Counter() for roll_index, count in self.history.items(): result[sum(self.roll_value_from_index(roll_index))] += count return result def __repr__(self): return repr(self.roll_history)
... which can be used like this:
especially_fair_dice = LoadedDice(6) # simulate 2 6-sided dice especially_fair_dice.roll() # returns the dice values results like (1,5)
I'd like some review on types, as I've never done them before, efficiency of my algorithm; I feel like it's a bit overly complicated, anything I've missed about pseudo-random numbers that make my approach undesirable etc.
I plotted some of the results, for 100 rolls, the loaded die approach tends to perform better than random at keeping to the expected distribution, while still feeling random (that's the goal, interpret it as you wish, I don't know how to quantify it either:)