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)
Feedback
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.
Results
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:)
1 / v ** 0
? You realize this will always evaluate to 1? \$\endgroup\$