Creating some Loaded Dice to make our board game fairer 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:
    &quot;&quot;&quot;
    Simulation of 2 Dice being rolled.
    Weights the random roll to try and tend to an even distribution
    &quot;&quot;&quot;

    def __init__(self, sides: int = 6) -&gt; None:
        self.sides = sides
        self.history: TCounter[int] = Counter(range(sides ** 2))

    def roll(self) -&gt; 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) -&gt; 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 &lt;= running_total:
                return result_index

    def roll_value_from_index(self, index: int) -&gt; 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) -&gt; 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) -&gt; 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:)
Performance
If you intend on using this for (e.g.) large-scale Monte-Carlo simulation, consider moving to vectorized Numpy rather than base Python.
Direct generators
There's no need for yield here:
    for v in self.history.values():
        yield 1 / v

Instead,
return (1/v for v in self.history.values())

Counter initialization
roll_history can be reduced, I think, to
return Counter(
    (
        sum(self.roll_value_from_index(roll_index)),
        count,
    )
    for roll_index, count in self.history.items()
)

In other words, Counter accepts an iterable of key-count tuples. If the above generator expression seems unwieldy to you, write a separate method to yield roll-count pairs:
    for roll_index, count in self.history.items():
        roll_1, roll_2 = self.roll_value_from_index(roll_index)
        yield roll_1 + roll_2, count