diff --git a/diceware/random_sources.py b/diceware/random_sources.py index 67264855b241cd103d5cefd96addb44f46ce130f..4b95ab8eb6cafd09f7012156ea88323b146ca06e 100644 --- a/diceware/random_sources.py +++ b/diceware/random_sources.py @@ -131,9 +131,8 @@ class RealDiceRandomSource(object): We make sure that `num_rolls`, the number of rolls, is in an acceptable range and issue an hint about the procedure. """ - if num_rolls < 1: - raise ValueError( - "Must provide at least %s items" % self.dice_sides) + if num_rolls == 0: + raise(ValueError) if (self.dice_sides ** num_rolls) < len(sequence): print( "Warning: entropy is reduced! Using only first %s of %s " @@ -150,6 +149,22 @@ class RealDiceRandomSource(object): """Pick one item out of `sequence`. """ num_rolls = int(math.log(len(sequence), self.dice_sides)) + use_modulo = False + if num_rolls < 1: + # If this happens, there are less values in the sequence to + # choose from than there are dice sides. + # First check whehter the length is 1. Then we don't have + # to do anything else + if len(sequence) == 1: + # Check whether len(sequence) is a factor of dice.sides + return sequence[0] + if self.dice_sides % len(sequence) == 0: + use_modulo = True + num_rolls = 1 + else: + # otherwise We will perform one extra roll and apply modulo + use_modulo = True + num_rolls = 2 self.pre_check(num_rolls, sequence) result = 0 for i in range(num_rolls, 0, -1): @@ -159,4 +174,6 @@ class RealDiceRandomSource(object): rolled = input_func( "What number shows dice number %s? " % (num_rolls - i + 1)) result += ((self.dice_sides ** (i - 1)) * (int(rolled) - 1)) + if use_modulo: + result = result % len(sequence) return sequence[result] diff --git a/tests/test_random_sources.py b/tests/test_random_sources.py index 2c828a941b61f41618a11ea8f0523f3e18a7dab9..c101bceb0df11ebef1bc17dd488f03e3d17e89d7 100644 --- a/tests/test_random_sources.py +++ b/tests/test_random_sources.py @@ -219,13 +219,6 @@ class TestRealDiceRandomSource(object): src = RealDiceRandomSource(None) assert src.choice([1, 2, 3, 4, 5, 6]) == 1 - def test_choice_len_too_short(self, monkeypatch): - # We raise an exception if choice gets less than 6 elements. - self.fake_input_values(["1"], monkeypatch) - src = RealDiceRandomSource(None) - with pytest.raises(ValueError): - assert src.choice([1, 2, 3, 4, 5]) # list len < 6 - def test_choice_input_lower_value_borders(self, monkeypatch): # choice() does not accept "0" but it accepts "1" self.fake_input_values(["0", "1"], monkeypatch) @@ -263,3 +256,29 @@ class TestRealDiceRandomSource(object): src.pre_check(5, ['doesntmatter']) out, err = capsys.readouterr() assert "Please roll 5 dice (or a single dice 5 times)." in out + + def test_sequence_less_than_dice_sides(self, capsys, monkeypatch): + # Test to see whether we can use a n-sided die to choose from + # a sequence with less than n items + src = RealDiceRandomSource(None) + src.dice_sides = 6 + # A length of 1 requires no rolls + self.fake_input_values(["1"], monkeypatch) + picked = src.choice([1]) + out, err = capsys.readouterr() + assert "roll" not in out + assert picked == 1 + # A length of 2,3 only requires 1 roll + for choice_length in (2, 3): + self.fake_input_values(["1"], monkeypatch) + picked = src.choice(range(1, choice_length + 1)) + out, err = capsys.readouterr() + assert "roll 1 dice" in out + assert picked == 1 + # A length of 4,5 requires 2 rolls + for choice_length in (4, 5): + self.fake_input_values(["1", "1"], monkeypatch) + picked = src.choice(range(1, choice_length + 1)) + out, err = capsys.readouterr() + assert "roll 2 dice" in out + assert picked == 1