xdice.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. '''
  2. xdice is a lightweight python 3.3+ library for managing rolls of dice.
  3. License: GNU
  4. @author: Olivier Massot <croki.contact@gmail.com>, 2017
  5. '''
  6. import random
  7. import re
  8. __VERSION__ = 1.0
  9. # TODO: 'L', 'LX', 'H' and 'HX' notations: drop the x lowest or highest results => eg: 'AdXl3'
  10. # TODO: (?) 'Rx(...)' notation: roll x times the pattern in the parenthesis => eg: R3(1d4+3)
  11. # TODO: 'd%' notation: d% <=> d100
  12. # TODO: (?) Dice pools, 6-sided variations, 10-sided variations, Open-ended variations (https://en.wikipedia.org/wiki/Dice_notation)
  13. def compile(pattern_string): # @ReservedAssignment
  14. p = Pattern(pattern_string)
  15. p.compile()
  16. return p
  17. def roll(pattern_string):
  18. return Pattern(pattern_string).roll()
  19. def rolldice(faces, amount=1):
  20. return Dice(faces, amount).roll()
  21. _ALLOWED = {'abs': abs, 'max': max, 'min': min}
  22. def _secured_eval(raw):
  23. """ securely evaluate the incoming raw string by avoiding the use of any non-allowed function """
  24. return eval(raw, {"__builtins__":None}, _ALLOWED)
  25. class Dice():
  26. """
  27. Dice(sides, amount=1):
  28. Set of dice.
  29. Use roll() to get a Score() object.
  30. """
  31. DEFAULT_SIDES = 20
  32. def __init__(self, sides, amount=1):
  33. self._sides = 1
  34. self._amount = 0
  35. self.sides = sides
  36. self.amount = amount
  37. @property
  38. def sides(self):
  39. return self._sides
  40. @sides.setter
  41. def sides(self, sides):
  42. try:
  43. if not int(sides) >= 1:
  44. raise ValueError()
  45. except (TypeError, ValueError):
  46. raise ValueError("Invalid value for sides (given: '{}')".format(sides))
  47. self._sides = sides
  48. @property
  49. def amount(self):
  50. return self._amount
  51. @amount.setter
  52. def amount(self, amount):
  53. try:
  54. if not int(amount) >= 0:
  55. raise ValueError()
  56. except (TypeError, ValueError):
  57. raise ValueError("Invalid value for amount (given: '{}')".format(amount))
  58. self._amount = amount
  59. def __repr__(self):
  60. return "<Dice; sides={}; amount={}>".format(self.sides, self.amount)
  61. def __eq__(self, d):
  62. return self.sides == d.sides and self.amount == d.amount
  63. def roll(self):
  64. """ Role the dice and return a Score object """
  65. return Score([random.randint(1, self._sides) for _ in range(self._amount)])
  66. @classmethod
  67. def parse(cls, pattern):
  68. """ parse a pattern of the form 'xdx', where x are positive integers """
  69. pattern = str(pattern).replace(" ", "").lower()
  70. a, x = pattern.split("d")
  71. if not a:
  72. a = 1
  73. if not x:
  74. x = cls.DEFAULT_SIDES
  75. return Dice(*map(int, [x, a]))
  76. class Score(int):
  77. """ Score is a subclass of integer.
  78. Then you can manipulate it as you would do with an integer.
  79. It also provides an access to the detailed score with the property 'detail'.
  80. 'detail' is the list of the scores obtained by each dice.
  81. Score class can also be used as an iterable, to walk trough the individual scores.
  82. eg:
  83. >>> s = Score([1,2,3])
  84. >>> print(s)
  85. 6
  86. >>> s + 1
  87. 7
  88. >>> list(s)
  89. [1,2,3]
  90. """
  91. def __new__(cls, detail):
  92. score = super(Score, cls).__new__(cls, sum(detail))
  93. score._detail = detail
  94. return score
  95. @property
  96. def detail(self):
  97. return self._detail
  98. def __repr__(self):
  99. return "<Score; score={}; detail={}>".format(int(self), self.detail)
  100. def __contains__(self, value):
  101. return self.detail.__contains__(value)
  102. def __iter__(self):
  103. return self.detail.__iter__()
  104. class Pattern():
  105. def __init__(self, instr):
  106. if not instr:
  107. raise ValueError("Invalid value for 'instr' ('{}')".format(instr))
  108. self.instr = Pattern._normalize(instr)
  109. self.dices = []
  110. self.format_string = ""
  111. @staticmethod
  112. def _normalize(instr):
  113. """ normalize the incoming string to a lower string without spaces"""
  114. return str(instr).replace(" ", "").lower()
  115. def compile(self):
  116. def _submatch(match):
  117. dice = Dice.parse(match.group(0))
  118. index = len(self.dices)
  119. self.dices.append(dice)
  120. return "{{{}}}".format(index)
  121. self.format_string = re.sub('\d*d\d*', _submatch, self.instr)
  122. def roll(self):
  123. if not self.format_string:
  124. self.compile()
  125. scores = [dice.roll() for dice in self.dices]
  126. return PatternScore(self.format_string, scores)
  127. class PatternScore(int):
  128. def __new__(cls, eval_string, scores):
  129. ps = super(PatternScore, cls).__new__(cls, _secured_eval(eval_string.format(*scores)))
  130. ps._eval_string = eval_string
  131. ps._scores = scores
  132. return ps
  133. def format(self):
  134. return self._eval_string.format(*[str(list(score)) for score in self._scores])
  135. def score(self, i):
  136. return self._scores[i]
  137. def scores(self):
  138. return self._scores