Skip to content

Commit

Permalink
Better histogram calculations, minor improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
dokzlo13 committed Mar 12, 2024
1 parent b5b9c1a commit 68a495c
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 102 deletions.
90 changes: 52 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,57 +62,71 @@ Rolling (d20 + 4) = 7
## Highlights

```python
from dice_roller import s, d, rng, x, r, kh, kl, dl, dh
from dice_roller import s, d, rng

def roll_info(roll: BaseDice):
print(f"For dice '{roll}' min is {roll.min()} and max is {roll.max()}")

d20 = d(20) # creating dice with possible outcomes 1-20
roll_info(d20) # For dice 'd20' min is 1 and max is 20
d20.roll() # Get one roll result
d20.generate(10) # Generates 10 rolls as numpy array
d20 = d(20) # creating dice with possible outcomes 1-20
roll_info(d20) # For dice 'd20' min is 1 and max is 20
d20.roll() # Get one roll result
d20.generate(10) # Generates 10 rolls as numpy array
roll_info(d20 + 5) # For dice '(d20 + 5)' min is 6 and max is 25

modifier = s(5) # Scalar, `dice_roller` converts integers to Scalar automatically, if you use suggested mathematical api.
roll_info(modifier) # For dice '5' min is 5 and max is 5
roll_info(d20 + modifier) # For dice '(d20 + 5)' min is 6 and max is 25

fudge_dice = rng(-1, 2) # Creating custom range dice
roll_info(fudge_dice) # For dice 'rng(-1,2)' min is -1 and max is 1
fudge_dice.generate(5*3).reshape((5, 3)) # rolling a pool of dices and reshape outcome with numpy tools
fudge_dice = rng(-1, 2) # Creating custom range dice
roll_info(fudge_dice) # For dice 'rng(-1,2)' min is -1 and max is 1
fudge_dice.generate(5*3).reshape((5, 3)) # rolling a pool of dices and reshape outcome with numpy tools
# array([[-1, -1, 0],
# [-1, 1, -1],
# [-1, 1, 0],
# [ 0, 0, 0],
# [-1, -1, 0]])

attack_roll = kh(2@d20) + d(4) + 3 # Roll to hit with advantage, use bless (+d4) and add +3 modifier
roll_info(attack_roll) # For dice '(2d20kh + d4 + 3)' min is 5 and max is 27
advantage_roll = (2@d20).kh() # Keep Highest of 2 dices
disadvantage_roll = (2@d20).kl() # Keep Lowest of 2 dices

attack_roll = (2@d20).kh() + d(4) + 3 # Roll to hit with advantage, use bless (+d4) and add +3 modifier
roll_info(attack_roll) # For dice '(2d20kh + d4 + 3)' min is 5 and max is 27
attack_results = attack_roll.generate(10_000) # Generates 10000 rolls
hit_ac = attack_results[attack_results >= 16] # Checking hits with numpy masks

damage_roll = (d(6).x == 6) # Roll d6 (explode on 6). Explode max 100 times (default)
roll_info(damage_roll) # For dice 'd6x6' min is 1 and max is 600
explode_on_six = (x() == 6) # Functional API for explode
roll_info(explode_on_six(d(6))) # For dice 'd6x6' min is 1 and max is 600

d20_with_luck = (d20.r == 1) # Roll d20 and reroll ones. Reroll once (default)
roll_info(d20_with_luck) # For dice 'd20r1' min is 1 and max is 20
reroll_ones = (r() == 1) # Functional API for explode
d20_with_luck_plus_4 = reroll_ones(d20) + 4 # Roll d20 and reroll ones (max 1 reroll), add 4
kl(2@d20_with_luck_plus_4).roll() # roll 2 rolls, keeping one lowest result

# More examples
(4@fudge_dice) # roll 4 fudge dices, add results together
(d20 - 4) >= 1 # roll d20-4, ensure result greater or equal to 1
d20 * 2 + d(2) - 1 # roll d20, multiply by 2, add d2, sub 1
dh(10@d20, drop=5) # roll 10 d20, drop 5 highest and return sum of rest
kh(5@d20, keep=d(2)) # roll 5 d20, keep d2 (new reroll value each time) highest and return their sum
d(6).r == rng(1, 3) # roll d6, rerolls on 1 or 2 (new reroll value each time), 1 reroll max (default)
d(20).reroll(reroll_limit=10) == 1 # roll d20, rerolls on 1, max 10 rerolls
d(6).x >= 5 # roll d6, explodes on 5 and 6. Maximum 100 explodes (default)
d(6).explode(explode_depth=2) > 4 # roll d6, explodes on 5 and 6. Maximum 2 explodes
(10@d(10)) * (10@d(10)) # roll 2 sets of 10d10 and multiply results
(4 @ d(4)) @ d(10) # roll 4d4 of d10 dices
damage_roll = (d(6).x == 6) # Roll d6 (explode on 6). Explode max 100 times (default)
roll_info(damage_roll) # For dice 'd6x6' min is 1 and max is 600

d20_luck = (d20.r == 1) # Roll d20 and reroll ones. Reroll once (default)
roll_info(d20_luck) # For dice 'd20r1' min is 1 and max is 20

debuff_roll = (d20 - 4) >= 1 # Limiting your roll outcomes
roll_info(debuff_roll) # For dice '(d20 - 4)>=1' min is 1 and max is 16

# Let's use some functional api
from dice_roller import x, r, kh, kl, dl, dh, lim

kh(2@d(20)) # Keep Highest of 2 dices
kl(2@d(20)) # Keep Lowest of 2 dices
dh(10@d20, drop=5) # roll 10 d20, drop 5 highest and return sum of rest
kh(5@d20, keep=d(2)) # roll 5 d20, keep d2 (new reroll value each time) highest and return their sum

explode_on_six = (x() == 6) # Explode on 6, max 100 times (default)
explode_on_six = (x(explode_depth=10) == 6) # Explode on 6, max 10 times
roll_info(explode_on_six(d(6))) # For dice 'd6x6' min is 1 and max is 600

reroll_ones = (r() == 1) # reroll ones, 1 reroll max (default)
reroll_ones = (r(reroll_limit=10) == 1) # reroll ones, 100 reroll max
d20_luck = reroll_ones(d20) # roll d20 and
kl((2@d20_luck)).roll() # roll 2 d20 rolls, keeping one lowest

# Even more examples
(4@fudge_dice) # roll 4 fudge dices, add results together
(d20 - d(4)) >= 1 # roll d20-d4, ensure result greater or equal to 1
d20 * 2 + d(2) - 1 # roll d20, multiply by 2, add d2, sub 1

d(6).r == d(2) # roll d6, rerolls on 1 or 2 (new reroll value each time), 1 reroll max (default)
d(20).reroll(reroll_limit=10) == 1 # roll d20, rerolls on 1, max 10 rerolls
d(6).x >= rng(5, 7) # roll d6, explodes on 5 and 6. Maximum 100 explodes (default)
d(6).explode(explode_depth=2) > 4 # roll d6, explodes on 5 and 6. Maximum 2 explodes
(10@d(10)) * (10@d(10)) # roll 2 sets of 10d10 and multiply results
(4@d(4)) @ d(10) # roll 4d4 of d10 dices

# roll d6 of (roll d4 of d20 dice, keep 1 highest) and drop d4 lowest. Ensure (d4 explodes on 4) <= result <= (d100 reroll <= 50).
(dl(d(6) @ kl(d(4) @ d(20)), drop=d(4)) >= (d(4).x == 4)) <= (d(100).r <= 50)
Expand Down Expand Up @@ -684,7 +698,7 @@ from dice_roller import Reroll
(Reroll() == 6)(some_dice).roll() # is okay
```

By default, roll can be rerolled once, but you can override this behavior:
**By default, roll can be rerolled once**, but you can override this behavior:

```python
from dice_roller import r, d # r is alias for Reroll
Expand Down
3 changes: 2 additions & 1 deletion dice_roller/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from . import random
from .callback import WithGenerateCallback, WithRollCallback
from .compare import Ge, Gt, Le, Lt
from .compare import Ge, Gt, Le, Lt, Limit
from .core import (
BaseDice,
Dice,
Expand All @@ -27,3 +27,4 @@
x = Explode
r = Reroll
rng = RangeDice
lim = Limit
42 changes: 35 additions & 7 deletions dice_roller/compare.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from dataclasses import dataclass
from typing import Callable, Protocol
from functools import partial
from typing import Protocol

import numpy as np
from dyce import H
from dyce.evaluation import HResult, expandable
from numpy.typing import ArrayLike

from .core import BaseDice
from .misc import DiceModifier, _wrap_scalar


@dataclass(slots=True)
Expand All @@ -20,14 +22,18 @@ def _with_cap(roll_values: ArrayLike, cmp_values: ArrayLike) -> tuple[ArrayLike,
@staticmethod
def _compare_histogram_outcome(dice: int, compare: int) -> bool: ...

@staticmethod
def _modify_input_histogram(dice: H, compare: H) -> tuple[H, H]:
return dice, compare

def histogram(self) -> H:
@expandable
def cmp(dice: HResult, compare: HResult):
if self._compare_histogram_outcome(dice.outcome, compare.outcome): # type: ignore
if not self._compare_histogram_outcome(dice.outcome, compare.outcome): # type: ignore
return compare.outcome
return dice.outcome

return cmp(self.dice.histogram(), self.compare.histogram())
return cmp(*self._modify_input_histogram(self.dice.histogram(), self.compare.histogram()))

def generate(self, items: int) -> ArrayLike:
result_rolls = self.dice.generate(items)
Expand All @@ -48,7 +54,11 @@ def min(self) -> int:

@staticmethod
def _compare_histogram_outcome(dice: int, compare: int) -> bool:
return dice > compare
return dice < compare

@staticmethod
def _modify_input_histogram(dice: H, compare: H) -> tuple[H, H]:
return dice, compare - 1 # type: ignore

@staticmethod
def _with_cap(roll_values: ArrayLike, cmp_values: ArrayLike) -> tuple[ArrayLike, ArrayLike]:
Expand All @@ -68,7 +78,7 @@ def min(self) -> int:

@staticmethod
def _compare_histogram_outcome(dice: int, compare: int) -> bool:
return dice >= compare
return dice <= compare

@staticmethod
def _with_cap(roll_values: ArrayLike, cmp_values: ArrayLike) -> tuple[ArrayLike, ArrayLike]:
Expand All @@ -88,7 +98,11 @@ def min(self) -> int:

@staticmethod
def _compare_histogram_outcome(dice: int, compare: int) -> bool:
return dice < compare
return dice > compare

@staticmethod
def _modify_input_histogram(dice: H, compare: H) -> tuple[H, H]:
return dice, compare + 1 # type: ignore

@staticmethod
def _with_cap(roll_values: ArrayLike, cmp_values: ArrayLike) -> tuple[ArrayLike, ArrayLike]:
Expand All @@ -108,8 +122,22 @@ def min(self) -> int:

@staticmethod
def _compare_histogram_outcome(dice: int, compare: int) -> bool:
return dice <= compare
return dice >= compare

@staticmethod
def _with_cap(roll_values: ArrayLike, cmp_values: ArrayLike) -> tuple[ArrayLike, ArrayLike]:
return np.maximum(roll_values, cmp_values) # type: ignore


class Limit:
def __gt__(self, value: BaseDice | int) -> DiceModifier:
return partial(Gt, compare=_wrap_scalar(value))

def __ge__(self, value: BaseDice | int) -> DiceModifier:
return partial(Ge, compare=_wrap_scalar(value))

def __lt__(self, value: BaseDice | int) -> DiceModifier:
return partial(Lt, compare=_wrap_scalar(value))

def __le__(self, value: BaseDice | int) -> DiceModifier:
return partial(Le, compare=_wrap_scalar(value))
30 changes: 27 additions & 3 deletions dice_roller/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,32 @@ def __str__(self) -> str:

def histogram(self) -> H: ...

# Interface end
# Base roll

def roll(self) -> int:
return np.sum(self.generate(1))

# Modifiers

def kh(self, keep: BaseDice | int = 1) -> BaseDice:
from .transformations import KeepHighest

return KeepHighest(self, keep=keep) # type: ignore

def kl(self, keep: BaseDice | int = 1) -> BaseDice:
from .transformations import KeepLowest

return KeepLowest(self, keep=keep) # type: ignore

def dh(self, drop: BaseDice | int = 1) -> BaseDice:
from .transformations import DropHighest

return DropHighest(self, drop=drop) # type: ignore

def dl(self, drop: BaseDice | int = 1) -> BaseDice:
from .transformations import DropLowest

return DropLowest(self, drop=drop) # type: ignore

@property
def r(self):
Expand All @@ -48,8 +73,7 @@ def explode(self, explode_depth: int = 100):

return _ExplodeFactory(dice=self, explode_depth=explode_depth)

def roll(self) -> int:
return np.sum(self.generate(1))
# Magic

def __matmul__(self, other):
if isinstance(other, int):
Expand Down
27 changes: 9 additions & 18 deletions dice_roller/explode.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
from dataclasses import dataclass, field
from functools import partial
from typing import Callable, Protocol
from typing import Protocol

import numpy as np
from dyce import H
from dyce.evaluation import expandable, HResult
from dyce.evaluation import HResult, expandable
from numpy.typing import ArrayLike

from .core import BaseDice, Scalar

ExplodeDiceModifier = Callable[[BaseDice], BaseDice]
from .core import BaseDice
from .misc import DiceModifier, _wrap_scalar


@dataclass(slots=True)
Expand Down Expand Up @@ -133,31 +132,23 @@ def _calculate_explode_mask(roll_values: ArrayLike, cmp_values: ArrayLike) -> Ar
return roll_values <= cmp_values # type: ignore


def _wrap_scalar(value: BaseDice | int) -> BaseDice:
if isinstance(value, int):
value = Scalar(value)
if not isinstance(value, BaseDice):
raise TypeError("Explode only support other dices or integers")
return value


class Explode:
def __init__(self, explode_depth: int = 100) -> None:
self.explode_depth = explode_depth

def __eq__(self, value: BaseDice | int) -> ExplodeDiceModifier: # type: ignore
def __eq__(self, value: BaseDice | int) -> DiceModifier: # type: ignore
return partial(ExplodeEq, compare=_wrap_scalar(value), explode_depth=self.explode_depth)

def __gt__(self, value: BaseDice | int) -> ExplodeDiceModifier:
def __gt__(self, value: BaseDice | int) -> DiceModifier:
return partial(ExplodeIfGreater, compare=_wrap_scalar(value), explode_depth=self.explode_depth)

def __ge__(self, value: BaseDice | int) -> ExplodeDiceModifier:
def __ge__(self, value: BaseDice | int) -> DiceModifier:
return partial(ExplodeIfGreaterOrEq, compare=_wrap_scalar(value), explode_depth=self.explode_depth)

def __lt__(self, value: BaseDice | int) -> ExplodeDiceModifier:
def __lt__(self, value: BaseDice | int) -> DiceModifier:
return partial(ExplodeIfLess, compare=_wrap_scalar(value), explode_depth=self.explode_depth)

def __le__(self, value: BaseDice | int) -> ExplodeDiceModifier:
def __le__(self, value: BaseDice | int) -> DiceModifier:
return partial(ExplodeIfLessOrEq, compare=_wrap_scalar(value), explode_depth=self.explode_depth)


Expand Down
27 changes: 11 additions & 16 deletions dice_roller/misc.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
class SingletonMeta(type):
"""
The Singleton class can be implemented in different ways in Python. Some
possible methods include: base class, decorator, metaclass. We will use the
metaclass because it is best suited for this purpose.
"""
from .core import BaseDice, Scalar
from typing import Callable

_instances = {} # type: ignore

def __call__(cls, *args, **kwargs):
"""
Possible changes to the value of the `__init__` argument do not affect
the returned instance.
"""
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]
def _wrap_scalar(value: BaseDice | int) -> BaseDice:
if isinstance(value, int):
value = Scalar(value)
if not isinstance(value, BaseDice):
raise TypeError("Reroll only support other dices or integers")
return value


DiceModifier = Callable[[BaseDice], BaseDice]
20 changes: 19 additions & 1 deletion dice_roller/random.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
import numpy as np

from .misc import SingletonMeta

class SingletonMeta(type):
"""
The Singleton class can be implemented in different ways in Python. Some
possible methods include: base class, decorator, metaclass. We will use the
metaclass because it is best suited for this purpose.
"""

_instances = {} # type: ignore

def __call__(cls, *args, **kwargs):
"""
Possible changes to the value of the `__init__` argument do not affect
the returned instance.
"""
if cls not in cls._instances:
instance = super().__call__(*args, **kwargs)
cls._instances[cls] = instance
return cls._instances[cls]


class Rng(metaclass=SingletonMeta):
Expand Down
Loading

0 comments on commit 68a495c

Please sign in to comment.