From 43f6b61a727fecd6e675ec9473960142446b0247 Mon Sep 17 00:00:00 2001 From: Gergely Polonkai Date: Tue, 3 Jun 2025 19:03:19 +0200 Subject: [PATCH] feat: Create a helper function for the d666 special die --- gm_assistant/dice.py | 24 ++++++++++++++++++++++++ tests/test_dice.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/gm_assistant/dice.py b/gm_assistant/dice.py index f5f3945..0104700 100644 --- a/gm_assistant/dice.py +++ b/gm_assistant/dice.py @@ -8,6 +8,7 @@ import random import re DICE_DESCRIPTOR_PATTERN = re.compile(r"^([0-9]*d[0-9]+)([+-]([0-9]+|[0-9]*d[0-9]+))*$") +D666_COMBINATIONS = [(0, 1, 2), (0, 2, 1), (1, 0, 2), (1, 2, 0), (2, 0, 1), (2, 1, 0)] class Die: @@ -96,3 +97,26 @@ class Die: result += self.modifier return result + + +def d666(strict_order: bool = False) -> list[int]: + """Roll three six sided dice and use them as separate digits + + “d666” is a special die type often used in random tables; here you don’t roll one 666 sided die, but three 6-sided + ones, then use the three values of the three digits of the final result. + + :param strict_order: if ``True``, use the dics in strict order. Otherwise, return all possible combinations. + :returns: the list of all valid combinations + """ + + roll_values = [random.randint(1, 6) for _ in range(3)] + results = [] + + combinations = [(0, 1, 2)] if strict_order else D666_COMBINATIONS + + for a, b, c in combinations: + result = roll_values[a] * 100 + roll_values[b] * 10 + roll_values[c] + if result not in results: + results.append(result) + + return results diff --git a/tests/test_dice.py b/tests/test_dice.py index 984c51a..40c9374 100644 --- a/tests/test_dice.py +++ b/tests/test_dice.py @@ -7,7 +7,7 @@ import pytest from pytest_mock import MockerFixture -from gm_assistant.dice import Die +from gm_assistant.dice import Die, d666 def test_parse_empty() -> None: @@ -143,3 +143,45 @@ def test_roll_complex(mocker: MockerFixture) -> None: mocker.call(1, 4), ] ) + + +def test_d666_strict_order(mocker: MockerFixture) -> None: + """Test ``d666()`` with strict dice order""" + + mocked_randint = mocker.patch("gm_assistant.dice.random.randint", side_effect=[2, 4, 6]) + + assert d666(strict_order=True) == [246] + assert mocked_randint.call_count == 3 + mocked_randint.assert_has_calls([mocker.call(1, 6), mocker.call(1, 6), mocker.call(1, 6)]) + + +def test_d666_any_order(mocker: MockerFixture) -> None: + """Test ``d666()`` without strict order""" + + mocked_randint = mocker.patch("gm_assistant.dice.random.randint", side_effect=[2, 4, 6]) + + assert d666(strict_order=False) == [246, 264, 426, 462, 624, 642] + assert mocked_randint.call_count == 3 + mocked_randint.assert_has_calls([mocker.call(1, 6), mocker.call(1, 6), mocker.call(1, 6)]) + + +@pytest.mark.parametrize("dice_values", [pytest.param((1, 1, 2)), pytest.param((1, 2, 1)), pytest.param((2, 1, 1))]) +def test_d666_equal_dice(dice_values: tuple[int, int, int], mocker: MockerFixture) -> None: + """Test ``d666()`` when some dice are equal""" + + mocked_randint = mocker.patch("gm_assistant.dice.random.randint", side_effect=dice_values) + + assert set(d666()) == set((112, 121, 211)) + assert mocked_randint.call_count == 3 + mocked_randint.assert_has_calls([mocker.call(1, 6), mocker.call(1, 6), mocker.call(1, 6)]) + + +@pytest.mark.parametrize("strict_order", (True, False), ids=("strict", "any")) +def test_d666_all_equal(strict_order: bool, mocker: MockerFixture) -> None: + """Test ``d666()`` when all dice are equal""" + + mocked_randint = mocker.patch("gm_assistant.dice.random.randint", return_value=5) + + assert d666(strict_order=strict_order) == [555] + assert mocked_randint.call_count == 3 + mocked_randint.assert_has_calls([mocker.call(1, 6), mocker.call(1, 6), mocker.call(1, 6)])