xdice.py 4.8 KB

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