Skip to content
21 changes: 13 additions & 8 deletions src/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
__version__ = '0.3.1'
__version__ = '0.3.1.dev1'

# core classes
from .randvar import RV, Seq

# core decorators
from .randvar import anydice_casting, max_func_depth
from .randvar import RV
from .seq import Seq

# core functions
from .randvar import output, roll, settings_set, myrange
from .roller import myrange
from .settings import settings_set
from .output import output
from .blackrv import BlankRV

# core decorators
from .decorators import anydice_casting, max_func_depth

# helpful functions
from .randvar import roller, settings_reset
from .settings import settings_reset
from .roller import roll, roller

# function library
from .funclib import absolute as absolute_X, contains as X_contains_X, count_in as count_X_in_X, explode as explode_X, highest_N_of_D as highest_X_of_X
Expand All @@ -22,7 +27,7 @@


__all__ = [
'RV', 'Seq', 'anydice_casting', 'max_func_depth', 'output', 'roll', 'settings_set', 'myrange',
'RV', 'Seq', 'anydice_casting', 'BlankRV', 'max_func_depth', 'output', 'roll', 'settings_set', 'myrange',
'roller', 'settings_reset',
'absolute_X', 'X_contains_X', 'count_X_in_X', 'explode_X', 'highest_X_of_X', 'lowest_X_of_X', 'middle_X_of_X', 'highest_of_X_and_X', 'lowest_of_X_and_X', 'maximum_of_X', 'reverse_X', 'sort_X',
'myMatmul', 'myLen', 'myInvert', 'myAnd', 'myOr'
Expand Down
2 changes: 1 addition & 1 deletion src/altcast.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .randvar import roll
from .roller import roll


class __D_BASE:
Expand Down
175 changes: 175 additions & 0 deletions src/blackrv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
from typing import Iterable

from . import randvar as rv
from . import seq
from .typings import T_ifs, T_is, T_ifsr
from . import output


class BlankRV:
def __init__(self, _special_null=False):
self._special_null = _special_null # makes it such that it's _special_null, in operations like (X**2 + 1) still is blank (X). see https://anydice.com/program/395da

def mean(self):
return 0

def std(self):
return 0

def output(self, *args, **kwargs):
return output.output(self, *args, **kwargs)

def __matmul__(self, other: T_ifs):
# ( self:RV @ other ) thus not allowed,
raise TypeError(f'A position selector must be either a number or a sequence, but you provided "{other}"')

def __rmatmul__(self, other: T_is):
if self._special_null:
return 0 if other != 1 else self
return self

def __add__(self, other: T_ifsr):
if self._special_null:
return self
return other

def __radd__(self, other: T_ifsr):
if self._special_null:
return self
return other

def __sub__(self, other: T_ifsr):
if self._special_null:
return self
if isinstance(other, Iterable):
other = seq.Seq(*other).sum()
return (-other)

def __rsub__(self, other: T_ifsr):
if self._special_null:
return self
return other

def __mul__(self, other: T_ifsr):
return self

def __rmul__(self, other: T_ifsr):
return self

def __floordiv__(self, other: T_ifsr):
return self

def __rfloordiv__(self, other: T_ifsr):
return self

def __truediv__(self, other: T_ifsr):
return self

def __rtruediv__(self, other: T_ifsr):
return self

def __pow__(self, other: T_ifsr):
return self

def __rpow__(self, other: T_ifsr):
return self

def __mod__(self, other: T_ifsr):
return self

def __rmod__(self, other: T_ifsr):
return self

# comparison operators
def __eq__(self, other: T_ifsr):
if self._special_null:
return 1
return self

def __ne__(self, other: T_ifsr):
if self._special_null:
return 1
return self

def __lt__(self, other: T_ifsr):
if self._special_null:
return 1
return self

def __le__(self, other: T_ifsr):
if self._special_null:
return 1
return self

def __gt__(self, other: T_ifsr):
if self._special_null:
return 1
return self

def __ge__(self, other: T_ifsr):
if self._special_null:
return 1
return self

# boolean operators
def __or__(self, other: T_ifsr):
if self._special_null:
return 1
return self if isinstance(other, BlankRV) else other

def __ror__(self, other: T_ifsr):
if self._special_null:
return 1
return self if isinstance(other, BlankRV) else other

def __and__(self, other: T_ifsr):
if self._special_null:
return 1
return self

def __rand__(self, other: T_ifsr):
if self._special_null:
return 1
return self

def __bool__(self):
raise TypeError('Boolean values can only be numbers, but you provided RV')

def __len__(self):
if self._special_null:
return 1
return 0

def __pos__(self):
return self

def __neg__(self):
return self

def __invert__(self):
if self._special_null:
return 1
return self

def __abs__(self):
return self

def __round__(self, n=0):
return self

def __floor__(self):
return self

def __ceil__(self):
return self

def __trunc__(self):
return self

def __str__(self):
if self._special_null:
return 'd{?}'
return 'd{}'

def __repr__(self):
return output.output(self, print_=False)
154 changes: 154 additions & 0 deletions src/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import inspect
import logging
import math
from itertools import product
from typing import Iterable, Union

from .typings import T_ifsr
from .settings import SETTINGS
from . import randvar as rv
from . import seq
from . import blackrv


logger = logging.getLogger(__name__)


def anydice_casting(verbose=False): # noqa: C901
# verbose = True
# in the documenation of the anydice language https://anydice.com/docs/functions
# it states that "The behavior of a function depends on what type of value it expects and what type of value it actually receives."
# Thus there are 9 scenarios for each parameters
# expect: int, actual: int = no change
# expect: int, actual: seq = seq.sum()
# expect: int, actual: rv = MUST CALL FUNCTION WITH EACH VALUE OF RV ("If a die is provided, then the function will be invoked for all numbers on the die – or the sums of a collection of dice – and the result will be a new die.")
# expect: seq, actual: int = [int]
# expect: seq, actual: seq = no change
# expect: seq, actual: rv = MUST CALL FUNCTION WITH SEQUENCE OF EVERY ROLL OF THE RV ("If Expecting a sequence and dice are provided, then the function will be invoked for all possible sequences that can be made by rolling those dice. In that case the result will be a new die.") # noqa: E501
# expect: rv, actual: int = dice([int])
# expect: rv, actual: seq = dice(seq)
# expect: rv, actual: rv = no change
def decorator(func):
def wrapper(*args, **kwargs):
args, kwargs = list(args), dict(kwargs)
fullspec = inspect.getfullargspec(func)
arg_names = fullspec.args # list of arg names for args (not kwargs)
param_annotations = fullspec.annotations # (arg_names): (arg_type) that have been annotated

hard_params = {} # update parameters that are easy to update, keep the hard ones for later
combined_args = list(enumerate(args)) + list(kwargs.items())
if verbose:
logger.debug(f'#args {len(combined_args)}')
for k, arg_val in combined_args:
arg_name = k if isinstance(k, str) else (arg_names[k] if k < len(arg_names) else None) # get the name of the parameter (args or kwargs)
if arg_name not in param_annotations: # only look for annotated parameters
if verbose:
logger.debug(f'no anot {k}')
continue
expected_type = param_annotations[arg_name]
actual_type = type(arg_val)
new_val = None
if expected_type not in (int, seq.Seq, rv.RV):
if verbose:
logger.debug(f'not int seq rv {k}')
continue
if isinstance(arg_val, blackrv.BlankRV): # EDGE CASE abort calling if casting int/Seq to BlankRV (https://github.com/Ar-Kareem/PythonDice/issues/11)
if expected_type in (int, seq.Seq):
if verbose:
logger.debug(f'abort calling func due to BlankRV! {k}')
return blackrv.BlankRV(_special_null=True)
continue # casting BlankRV to RV means the function IS called and nothing changes
casted_iter_to_seq = False
if isinstance(arg_val, Iterable) and not isinstance(arg_val, seq.Seq): # if val is iter then need to convert to Seq
arg_val = seq.Seq(*arg_val)
new_val = arg_val
actual_type = seq.Seq
casted_iter_to_seq = True
if (expected_type, actual_type) == (int, seq.Seq):
new_val = arg_val.sum()
elif (expected_type, actual_type) == (int, rv.RV):
hard_params[k] = (arg_val, expected_type)
continue
elif (expected_type, actual_type) == (seq.Seq, int):
new_val = seq.Seq([arg_val])
elif (expected_type, actual_type) == (seq.Seq, rv.RV):
hard_params[k] = (arg_val, expected_type)
if verbose:
logger.debug(f'EXPL {k}')
continue
elif (expected_type, actual_type) == (rv.RV, int):
new_val = rv.RV.from_const(arg_val) # type: ignore
elif (expected_type, actual_type) == (rv.RV, seq.Seq):
new_val = rv.RV.from_seq(arg_val)
elif not casted_iter_to_seq: # no cast made and one of the two types is not known, no casting needed
if verbose:
logger.debug(f'no cast, {k}, {expected_type}, {actual_type}')
continue
if isinstance(k, str):
kwargs[k] = new_val
else:
args[k] = new_val
if verbose:
logger.debug('cast {k}')
if verbose:
logger.debug(f'hard {[(k, v[1]) for k, v in hard_params.items()]}')
if not hard_params:
return func(*args, **kwargs)

var_name = tuple(hard_params.keys())
all_rolls_and_probs = []
for k in var_name:
v, expected_type = hard_params[k]
assert isinstance(v, rv.RV), 'expected type RV'
if expected_type == seq.Seq:
r, p = v._get_expanded_possible_rolls()
elif expected_type == int:
r, p = v.vals, v.probs
else:
raise ValueError(f'casting RV to {expected_type} not supported')
all_rolls_and_probs.append(zip(r, p))
# FINALLY take product of all possible rolls
all_rolls_and_probs = product(*all_rolls_and_probs)

res_vals: list[Union[rv.RV, blackrv.BlankRV, seq.Seq, int, float, None]] = []
res_probs: list[int] = []
for rolls_and_prob in all_rolls_and_probs:
rolls = tuple(r for r, _ in rolls_and_prob)
prob = math.prod(p for _, p in rolls_and_prob)
# will update args and kwargs with each possible roll using var_name
for k, v in zip(var_name, rolls):
if isinstance(k, str):
kwargs[k] = v
else:
args[k] = v
val: T_ifsr = func(*args, **kwargs) # single result of the function call
if isinstance(val, Iterable):
if not isinstance(val, seq.Seq):
val = seq.Seq(*val)
val = val.sum()
if verbose:
logger.debug(f'val {val} prob {prob}')
res_vals.append(val)
res_probs.append(prob)
return rv.RV.from_rvs(rvs=res_vals, weights=res_probs)
return wrapper
return decorator


def max_func_depth():
# decorator to limit the depth of the function calls
def decorator(func):
def wrapper(*args, **kwargs):
if SETTINGS['INTERNAL_CURR_DEPTH'] >= SETTINGS['maximum function depth']:
msg = 'The maximum function depth was exceeded, results are truncated.'
if not SETTINGS['INTERNAL_CURR_DEPTH_WARNING_PRINTED']:
logger.warning(msg)
print(msg)
SETTINGS['INTERNAL_CURR_DEPTH_WARNING_PRINTED'] = True
return blackrv.BlankRV()
SETTINGS['INTERNAL_CURR_DEPTH'] += 1
res = func(*args, **kwargs)
SETTINGS['INTERNAL_CURR_DEPTH'] -= 1
return res if res is not None else blackrv.BlankRV()
return wrapper
return decorator
8 changes: 6 additions & 2 deletions src/funclib.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# recreations of functions in https://anydice.com/docs/function-library/
from .randvar import Seq, RV, roll, anydice_casting, SETTINGS

from .randvar import RV
from .seq import Seq
from .settings import SETTINGS
from .roller import roll
from .decorators import anydice_casting

# BASE FUNCTIONS


@anydice_casting()
def absolute(NUMBER: int, *args, **kwargs):
if NUMBER < 0:
Expand Down
Loading
Loading