class DropKeepExpression(IDiceExpression):
    TOKEN_RULE = """expression : DROP_KEEP_DICE"""

    @staticmethod
    def add_production_function(
        parser_generator: rply.ParserGenerator,
        probability_distribution_factory: IProbabilityDistributionFactory
    ) -> Callable:
        @parser_generator.production(DropKeepExpression.TOKEN_RULE)
        def drop_keep(_, tokens) -> IDiceExpression:
            return DropKeepExpression(tokens[0].value,
                                      probability_distribution_factory)

        return drop_keep

    def __init__(
            self, string_form: str,
            probability_distribution_factory: IProbabilityDistributionFactory):
        self._string_form = string_form
        self._number_of_dice = self._get_number_of_dice()
        self._single_dice_outcome_map = get_single_dice_outcome_map(
            re.split(r"[dk]", self._string_form)[1])
        self._number_to_keep_or_drop = self._get_number_to_keep_or_drop()
        self._simplified_form = None
        self._probability_distribution_factory = probability_distribution_factory

        if self._is_keep():
            if self._number_of_dice <= self._number_to_keep_or_drop:
                self._simplified_form = DiceExpression(
                    f"{self._number_of_dice}d{self._get_number_of_sides_string()}",
                    self._probability_distribution_factory,
                )
            elif self._number_to_keep_or_drop == 0:
                self._simplified_form = ConstantIntegerExpression(
                    "0", self._probability_distribution_factory)
        else:
            if self._number_of_dice <= self._number_to_keep_or_drop:
                self._simplified_form = ConstantIntegerExpression(
                    "0", self._probability_distribution_factory)
            elif self._number_to_keep_or_drop == 0:
                self._simplified_form = DiceExpression(
                    f"{self._number_of_dice}d{self._get_number_of_sides_string()}",
                    self._probability_distribution_factory,
                )

    def _get_number_of_dice(self) -> int:
        string_num = re.split(r"[dk]", self._string_form)[0]
        return 1 if string_num == "" else int(string_num)

    def _get_number_to_keep_or_drop(self) -> int:
        return int(re.split(r"[dk]", self._string_form)[2])

    def _get_number_of_sides_string(self) -> str:
        return re.split(r"[dk]", self._string_form)[1]

    def _is_keep(self) -> bool:
        return "k" in self._string_form

    def roll(self) -> int:
        if self._simplified_form is not None:
            return self._simplified_form.roll()

        dice_rolls = random.choices(
            list(self._single_dice_outcome_map.keys()),
            weights=list(self._single_dice_outcome_map.values()),
            k=self._number_of_dice,
        )
        dice_rolls.sort()
        if self._is_keep():
            return sum(dice_rolls[-self._number_to_keep_or_drop:])
        return sum(dice_rolls[:self._number_of_dice -
                              self._number_to_keep_or_drop])

    def max(self) -> int:
        if self._simplified_form is not None:
            return self._simplified_form.max()

        if self._is_keep():
            return self._number_to_keep_or_drop * max(
                self._single_dice_outcome_map.keys())
        return (self._number_of_dice - self._number_to_keep_or_drop) * max(
            self._single_dice_outcome_map.keys())

    def min(self) -> int:
        if self._simplified_form is not None:
            return self._simplified_form.min()

        if self._is_keep():
            return self._number_to_keep_or_drop * min(
                self._single_dice_outcome_map.keys())
        return (self._number_of_dice - self._number_to_keep_or_drop) * min(
            self._single_dice_outcome_map.keys())

    def __str__(self) -> str:
        return f"{self._string_form}"

    def estimated_cost(self) -> int:
        if self._simplified_form is not None:
            return self._simplified_form.estimated_cost()

        if self._is_keep():
            return (self._number_of_dice * self._number_to_keep_or_drop) * len(
                self._single_dice_outcome_map.values())
        return (self._number_of_dice *
                (self._number_of_dice - self._number_to_keep_or_drop)) * len(
                    self._single_dice_outcome_map.values())

    def get_probability_distribution(self) -> IProbabilityDistribution:
        if self._simplified_form is not None:
            return self._simplified_form.get_probability_distribution()

        is_keep = self._is_keep()
        number_of_dice_to_select = self._number_of_dice - self._number_to_keep_or_drop
        number_of_dice_remaining = self._number_to_keep_or_drop
        if is_keep:
            number_of_dice_to_select = self._number_to_keep_or_drop
            number_of_dice_remaining = self._number_of_dice - self._number_to_keep_or_drop

        current = DropKeepExpression._build_dice_dict(
            number_of_dice_to_select, self._single_dice_outcome_map)
        current = DropKeepExpression._compute_iterations(
            current, number_of_dice_remaining, self._single_dice_outcome_map,
            is_keep)

        out_come_map = DropKeepExpression._collapse_outcomes(current)
        return self._probability_distribution_factory.create(out_come_map)

    @staticmethod
    def _build_dice_dict(number_of_dice: int,
                         dice_outcome_map: Dict[int, int]) -> Dict[str, int]:
        def safe_add_to_dict(dictionary: Dict[str, int], key: str,
                             value: int) -> None:
            if key not in dictionary:
                dictionary[key] = 0
            dictionary[key] += value

        current_dict = {"": 1}
        for _ in range(number_of_dice):
            new_dict: Dict[str, int] = {}
            for dice_outcome, count in dice_outcome_map.items():
                for old_key, old_value in current_dict.items():
                    new_key = DropKeepExpression._string_key_to_list(old_key)
                    new_key.append(dice_outcome)
                    new_key.sort()
                    safe_add_to_dict(
                        new_dict,
                        DropKeepExpression._int_list_to_string(new_key),
                        old_value * count,
                    )
            current_dict = new_dict
        return current_dict

    @staticmethod
    def _string_key_to_list(string: str) -> List[int]:
        if string == "":
            return []
        return [int(n) for n in string.split("|")]

    @staticmethod
    def _int_list_to_string(values: List[int]) -> str:
        values.sort()
        return "|".join([str(n) for n in values])

    @staticmethod
    def _update_key_list(old_key_string: str, new_value: int,
                         is_keep: bool) -> str:
        old_value = DropKeepExpression._string_key_to_list(old_key_string)
        old_value.append(new_value)
        old_value.sort()
        if is_keep:
            old_value = old_value[1:]
        else:
            old_value = old_value[:-1]
        return DropKeepExpression._int_list_to_string(old_value)

    @staticmethod
    def _compute_iterations(
        current: Dict[str, int],
        number_of_dice: int,
        dice_outcome_map: Dict[int, int],
        is_keep: bool,
    ) -> Dict[str, int]:
        def safe_add_to_dict(dictionary: Dict[str, int], key: str,
                             value: int) -> None:
            if key not in dictionary:
                dictionary[key] = 0
            dictionary[key] += value

        current_dict = current
        for _ in range(number_of_dice):
            new_dict: Dict[str, int] = {}
            for dice_outcome, count in dice_outcome_map.items():
                for old_key, old_value in current_dict.items():
                    new_key = DropKeepExpression._update_key_list(
                        old_key, dice_outcome, is_keep)
                    safe_add_to_dict(new_dict, new_key, old_value * count)
            current_dict = new_dict
        return current_dict

    @staticmethod
    def _collapse_outcomes(outcomes: Dict[str, int]) -> Dict[int, int]:
        def safe_add_to_dict(dictionary: Dict[int, int], key: int,
                             value: int) -> None:
            if key not in dictionary:
                dictionary[key] = 0
            dictionary[key] += value

        new_dict: Dict[int, int] = {}
        for current_key, current_value in outcomes.items():
            total = sum(DropKeepExpression._string_key_to_list(current_key))
            safe_add_to_dict(new_dict, total, current_value)
        return new_dict

    def get_contained_variables(self, ) -> Set[str]:
        return set()
示例#2
0
class TestDiceExpression(TestCase):
    def setUp(self):
        self._probability_distribution_factory = ProbabilityDistributionFactory(
        )
        self._test_dice = DiceExpression(
            "4d6", self._probability_distribution_factory)
        self._mock_parser_gen = create_autospec(rply.ParserGenerator)

    def test_dice_add_production_function(self):
        DiceExpression.add_production_function(
            self._mock_parser_gen, self._probability_distribution_factory)
        self._mock_parser_gen.production.assert_called_once_with(
            """expression : DICE""")

    def test_dice_roll(self):
        self._test_dice = DiceExpression(
            "2d10", self._probability_distribution_factory)
        roll_set = set()
        for _ in range(1000):
            roll_set.add(self._test_dice.roll())
        self.assertEqual(19, len(roll_set))
        self.assertEqual(20, max(roll_set))
        self.assertEqual(2, min(roll_set))

    def test_dice_roll_missing_dice_amount(self):
        self._test_dice = DiceExpression(
            "d10", self._probability_distribution_factory)
        roll_set = set()
        for _ in range(1000):
            roll_set.add(self._test_dice.roll())
        self.assertEqual(10, len(roll_set))
        self.assertEqual(10, max(roll_set))
        self.assertEqual(1, min(roll_set))

    def test_dice_roll_percentile_dice(self):
        self._test_dice = DiceExpression(
            "1d%", self._probability_distribution_factory)
        roll_set = set()
        for _ in range(10000):
            roll_set.add(self._test_dice.roll())
        self.assertEqual(100, len(roll_set))
        self.assertEqual(100, max(roll_set))
        self.assertEqual(1, min(roll_set))

    def test_dice_roll_fate_dice(self):
        self._test_dice = DiceExpression(
            "2dF", self._probability_distribution_factory)
        roll_set = set()
        for _ in range(10000):
            roll_set.add(self._test_dice.roll())
        self.assertSetEqual(roll_set, {-2, -1, 0, 1, 2})

    def test_dice_roll_custom_dice_negative(self):
        self._test_dice = DiceExpression(
            "2d[-2,2,100]", self._probability_distribution_factory)
        roll_set = set()
        for _ in range(10000):
            roll_set.add(self._test_dice.roll())
        self.assertSetEqual(roll_set, {-4, 0, 4, 98, 102, 200})

    def test_dice_roll_custom_dice_large_set(self):
        self._test_dice = DiceExpression(
            "2d[-2,0,2,4,6,31]", self._probability_distribution_factory)
        roll_set = set()
        for _ in range(10000):
            roll_set.add(self._test_dice.roll())
        self.assertSetEqual(
            roll_set, {-4, -2, 0, 2, 4, 6, 8, 10, 12, 29, 31, 33, 35, 37, 62})

    def test_dice_max(self):
        self.assertEqual(24, self._test_dice.max())

    def test_dice_max_missing_dice_amount(self):
        self._test_dice = DiceExpression(
            "d10", self._probability_distribution_factory)
        self.assertEqual(10, self._test_dice.max())

    def test_dice_max_percentile_dice(self):
        self._test_dice = DiceExpression(
            "1d%", self._probability_distribution_factory)
        self.assertEqual(100, self._test_dice.max())

    def test_dice_max_fate_dice(self):
        self._test_dice = DiceExpression(
            "2dF", self._probability_distribution_factory)
        self.assertEqual(2, self._test_dice.max())

    def test_dice_max_custom_dice_negative(self):
        self._test_dice = DiceExpression(
            "21d[-2,2,100]", self._probability_distribution_factory)
        self.assertEqual(2100, self._test_dice.max())

    def test_dice_max_custom_dice_large_set(self):
        self._test_dice = DiceExpression(
            "76d[-2,0,2,4,6,31]", self._probability_distribution_factory)
        self.assertEqual(2356, self._test_dice.max())

    def test_dice_min(self):
        self.assertEqual(4, self._test_dice.min())

    def test_dice_min_missing_dice_amount(self):
        self._test_dice = DiceExpression(
            "d10", self._probability_distribution_factory)
        self.assertEqual(1, self._test_dice.min())

    def test_dice_min_percentile_dice(self):
        self._test_dice = DiceExpression(
            "1d%", self._probability_distribution_factory)
        self.assertEqual(1, self._test_dice.min())

    def test_dice_min_fate_dice(self):
        self._test_dice = DiceExpression(
            "4dF", self._probability_distribution_factory)
        self.assertEqual(-4, self._test_dice.min())

    def test_dice_min_custom_dice_negative(self):
        self._test_dice = DiceExpression(
            "21d[-2,2,100]", self._probability_distribution_factory)
        self.assertEqual(-42, self._test_dice.min())

    def test_dice_min_custom_dice_large_set(self):
        self._test_dice = DiceExpression(
            "d[-2,0,2,4,6,31,-2,-24]", self._probability_distribution_factory)
        self.assertEqual(-24, self._test_dice.min())

    def test_dice_str(self):
        self.assertEqual("4d6", str(self._test_dice))

    def test_dice_str_missing_dice_amount(self):
        self._test_dice = DiceExpression(
            "d10", self._probability_distribution_factory)
        self.assertEqual("d10", str(self._test_dice))

    def test_dice_str_percentile_dice(self):
        self._test_dice = DiceExpression(
            "1d%", self._probability_distribution_factory)
        self.assertEqual("1d%", str(self._test_dice))

    def test_dice_str_fate_dice(self):
        self._test_dice = DiceExpression(
            "4dF", self._probability_distribution_factory)
        self.assertEqual("4dF", str(self._test_dice))

    def test_dice_str_custom_dice_negative(self):
        self._test_dice = DiceExpression(
            "21d[-2,2,100]", self._probability_distribution_factory)
        self.assertEqual("21d[-2,2,100]", str(self._test_dice))

    def test_dice_str_custom_dice_large_set(self):
        self._test_dice = DiceExpression(
            "d[-2,0,2,4,6,31,-2,-24]", self._probability_distribution_factory)
        self.assertEqual("d[-2,0,2,4,6,31,-2,-24]", str(self._test_dice))

    def test_dice_estimated_cost(self):
        self.assertEqual(4 * 6, self._test_dice.estimated_cost())

    def test_dice_estimated_cost_missing_dice_amount(self):
        self._test_dice = DiceExpression(
            "d10", self._probability_distribution_factory)
        self.assertEqual(10, self._test_dice.estimated_cost())

    def test_dice_estimated_cost_percentile_dice(self):
        self._test_dice = DiceExpression(
            "1d%", self._probability_distribution_factory)
        self.assertEqual(100, self._test_dice.estimated_cost())

    def test_dice_estimated_cost_fate_dice(self):
        self._test_dice = DiceExpression(
            "10dF", self._probability_distribution_factory)
        self.assertEqual(30, self._test_dice.estimated_cost())

    def test_dice_estimated_custom_dice_negative(self):
        self._test_dice = DiceExpression(
            "21d[-2,2,100]", self._probability_distribution_factory)
        self.assertEqual(63, self._test_dice.estimated_cost())

    def test_dice_estimated_custom_dice_large_set(self):
        self._test_dice = DiceExpression(
            "2d[-2,0,2,4,6*7,31,-2,-24]",
            self._probability_distribution_factory)
        self.assertEqual(14, self._test_dice.estimated_cost())

    def test_dice_get_probability_distribution(self):
        self._test_dice = DiceExpression(
            "4d6", self._probability_distribution_factory)
        possible_rolls = itertools.product(range(1, 7), repeat=4)
        results = [sum(t) for t in possible_rolls]
        self.assertEqual(
            dict(collections.Counter(results)),
            self._test_dice.get_probability_distribution().get_result_map(),
        )

    def test_dice_get_probability_distribution_missing_dice_amount(self):
        self._test_dice = DiceExpression(
            "d10", self._probability_distribution_factory)
        possible_rolls = itertools.product(range(1, 11), repeat=1)
        results = [sum(t) for t in possible_rolls]
        self.assertEqual(
            dict(collections.Counter(results)),
            self._test_dice.get_probability_distribution().get_result_map(),
        )

    def test_dice_get_probability_distribution_percentile_dice(self):
        self._test_dice = DiceExpression(
            "2d%", self._probability_distribution_factory)
        possible_rolls = itertools.product(range(1, 101), repeat=2)
        results = [sum(t) for t in possible_rolls]
        self.assertEqual(
            dict(collections.Counter(results)),
            self._test_dice.get_probability_distribution().get_result_map(),
        )

    def test_dice_get_probability_distribution_fate_dice(self):
        self._test_dice = DiceExpression(
            "4dF", self._probability_distribution_factory)
        self.assertEqual(
            {
                -4: 1,
                -3: 4,
                -2: 10,
                -1: 16,
                0: 19,
                1: 16,
                2: 10,
                3: 4,
                4: 1
            },
            self._test_dice.get_probability_distribution().get_result_map(),
        )

    def test_dice_get_probability_distribution_custom_dice_negative(self):
        self._test_dice = DiceExpression(
            "2d[-2,2,100]", self._probability_distribution_factory)
        self.assertEqual(
            {
                -4: 1,
                0: 2,
                4: 1,
                98: 2,
                102: 2,
                200: 1
            },
            self._test_dice.get_probability_distribution().get_result_map(),
        )

    def test_dice_get_probability_distribution_custom_dice_large_set(self):
        self._test_dice = DiceExpression(
            "d[-2,0,2,4,6,31,-2,-24]", self._probability_distribution_factory)
        self.assertEqual(
            {
                -24: 1,
                -2: 2,
                0: 1,
                2: 1,
                4: 1,
                6: 1,
                31: 1
            },
            self._test_dice.get_probability_distribution().get_result_map(),
        )

    def test_dice_get_probability_distribution_custom_dice_multiplier(self):
        self._test_dice = DiceExpression(
            "d[-2*3,0,2]", self._probability_distribution_factory)
        self.assertEqual(
            {
                -2: 3,
                0: 1,
                2: 1
            },
            self._test_dice.get_probability_distribution().get_result_map(),
        )

    def test_dice_get_probability_distribution_custom_dice_range(self):
        self._test_dice = DiceExpression(
            "d[1-2*3,0,2]", self._probability_distribution_factory)
        self.assertEqual(
            {
                0: 1,
                1: 3,
                2: 4
            },
            self._test_dice.get_probability_distribution().get_result_map(),
        )

    def test_dice_get_probability_distribution_custom_dice_multiplier_range(
            self):
        self._test_dice = DiceExpression(
            "d[1-2*3,-4--9*10]", self._probability_distribution_factory)
        self.assertEqual(
            {
                -9: 10,
                -8: 10,
                -7: 10,
                -6: 10,
                -5: 10,
                -4: 10,
                1: 3,
                2: 3
            },
            self._test_dice.get_probability_distribution().get_result_map(),
        )

    def test_dice_get_contained_variables(self):
        self.assertSetEqual(set(), self._test_dice.get_contained_variables())