|
|
@@ -0,0 +1,144 @@
|
|
|
+'''
|
|
|
+
|
|
|
+'''
|
|
|
+import random
|
|
|
+import re
|
|
|
+
|
|
|
+_ALLOWED = {'abs': abs}
|
|
|
+
|
|
|
+def roll_d(pattern):
|
|
|
+ """ Parse, roll and evaluate the pattern.
|
|
|
+
|
|
|
+ 'pattern' can be any mathematics expression and include dice notations ('xDx').
|
|
|
+ Eg: '1d4+2', '10d6+2-1d20', '3*(1d6+2)'...Etc
|
|
|
+
|
|
|
+ Returns tuple of score (integer) and detailed result (string)
|
|
|
+ """
|
|
|
+ # Parse the pattern
|
|
|
+ parsed = _parse(pattern)
|
|
|
+ raw = _forcejoin(parsed)
|
|
|
+ return _secured_eval(raw), raw
|
|
|
+
|
|
|
+def roll(pattern):
|
|
|
+ """ Similar to roll_d(), but only return the numeric results (integer) """
|
|
|
+ return roll_d(pattern)[0]
|
|
|
+
|
|
|
+def _secured_eval(raw):
|
|
|
+ """ securely evaluate the incoming raw string """
|
|
|
+ return eval(raw, {"__builtins__":None}, _ALLOWED)
|
|
|
+
|
|
|
+def _forcejoin(obj_list):
|
|
|
+ """ force-join the objects of the list by forcing string conversion of its items """
|
|
|
+ return "".join(map(str, obj_list))
|
|
|
+
|
|
|
+def _parse(pattern):
|
|
|
+ """ split the members and symbols of the pattern and roll them when it is possible """
|
|
|
+ return [_roll_if_u_can(m) for m in _split_members(pattern)]
|
|
|
+
|
|
|
+def _normalize(pattern):
|
|
|
+ """ normalize the incoming string to a lower string without spaces"""
|
|
|
+ return str(pattern).replace(" ", "").lower()
|
|
|
+
|
|
|
+def _split_members(pattern):
|
|
|
+ """ split a string by blocks of numeric / symbols / dice notations
|
|
|
+ eg: '1d6+2' becomes ['1d6', '+', '2']
|
|
|
+ """
|
|
|
+ return re.findall(r"[\w']+|[\W']+", _normalize(pattern))
|
|
|
+
|
|
|
+def _roll_if_u_can(member):
|
|
|
+ """ try to interpret member as a dice notation and roll it.
|
|
|
+ If it can not, member is returned as it was."""
|
|
|
+ try:
|
|
|
+ return Dice.parse(member).roll()
|
|
|
+ except ValueError:
|
|
|
+ return member
|
|
|
+
|
|
|
+class Dice():
|
|
|
+ """ Dice(sides, amount=1)
|
|
|
+ Set of dice. Use roll() to get a score.
|
|
|
+ """
|
|
|
+ def __init__(self, sides, amount=1):
|
|
|
+ self._sides = 1
|
|
|
+ self._amount = 0
|
|
|
+
|
|
|
+ self.sides = sides
|
|
|
+ self.amount = amount
|
|
|
+
|
|
|
+ @property
|
|
|
+ def sides(self):
|
|
|
+ return self._sides
|
|
|
+
|
|
|
+ @sides.setter
|
|
|
+ def sides(self, sides):
|
|
|
+ try:
|
|
|
+ if not int(sides) >= 1:
|
|
|
+ raise ValueError()
|
|
|
+ except (TypeError, ValueError):
|
|
|
+ raise TypeError("Invalid value for sides (given: '{}')".format(sides))
|
|
|
+ self._sides = sides
|
|
|
+
|
|
|
+ @property
|
|
|
+ def amount(self):
|
|
|
+ return self._amount
|
|
|
+
|
|
|
+ @amount.setter
|
|
|
+ def amount(self, amount):
|
|
|
+ try:
|
|
|
+ if not int(amount) >= 0:
|
|
|
+ raise ValueError()
|
|
|
+ except (TypeError, ValueError):
|
|
|
+ raise ValueError("Invalid value for amount (given: '{}')".format(amount))
|
|
|
+ self._amount = amount
|
|
|
+
|
|
|
+ def roll(self):
|
|
|
+ """ Role the dice and return a Score object """
|
|
|
+ return Score([random.randint(1, self._sides) for _ in range(self._amount)])
|
|
|
+
|
|
|
+ @classmethod
|
|
|
+ def parse(cls, pattern):
|
|
|
+ """ parse a pattern of the form 'xdx', where x are positive integers """
|
|
|
+ # normalize
|
|
|
+ pattern = str(pattern).replace(" ", "").lower()
|
|
|
+ # parse
|
|
|
+ amount, faces = (int(x) for x in pattern.split("d"))
|
|
|
+ # instanciate
|
|
|
+ return Dice(faces, amount)
|
|
|
+
|
|
|
+class Score(int):
|
|
|
+ """ Score is a subclass of integer.
|
|
|
+ Then you can manipulate it as you would do with an integer.
|
|
|
+
|
|
|
+ It also provides an access to the detail with the property 'results'.
|
|
|
+ 'results' is the list of the scores obtained by each dice.
|
|
|
+
|
|
|
+ Score class can also be used as an iterable, to walk trough the individual scores.
|
|
|
+
|
|
|
+ eg:
|
|
|
+ >>> s = Score([1,2,3])
|
|
|
+ >>> print(s)
|
|
|
+ 6
|
|
|
+ >>> s + 1
|
|
|
+ 7
|
|
|
+ >>> list(s)
|
|
|
+ [1,2,3]
|
|
|
+
|
|
|
+ """
|
|
|
+ def __new__(cls, results):
|
|
|
+ score = super(Score, cls).__new__(cls, sum(results))
|
|
|
+ score._results = results
|
|
|
+ return score
|
|
|
+
|
|
|
+ @property
|
|
|
+ def results(self):
|
|
|
+ return self._results
|
|
|
+
|
|
|
+ def __repr__(self):
|
|
|
+ return "<<{} ({})>>".format(int(self), self.results)
|
|
|
+
|
|
|
+ def __contains__(self, value):
|
|
|
+ return self.results.__contains__(value)
|
|
|
+
|
|
|
+ def __iter__(self):
|
|
|
+ return self.results.__iter__()
|
|
|
+
|
|
|
+# print(roll("2*(2d1+1d1-1d1-3)//2"))
|