dice.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. '''
  2. pydice is a lightweight python library for managing rolls of dice.
  3. License: GNU
  4. @author: olinox14, 2017
  5. '''
  6. import random
  7. import re
  8. __VERSION__ = 0.1
  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. def __init__(self, sides, amount=1):
  28. self._sides = 1
  29. self._amount = 0
  30. self.sides = sides
  31. self.amount = amount
  32. @property
  33. def sides(self):
  34. return self._sides
  35. @sides.setter
  36. def sides(self, sides):
  37. try:
  38. if not int(sides) >= 1:
  39. raise ValueError()
  40. except (TypeError, ValueError):
  41. raise ValueError("Invalid value for sides (given: '{}')".format(sides))
  42. self._sides = sides
  43. @property
  44. def amount(self):
  45. return self._amount
  46. @amount.setter
  47. def amount(self, amount):
  48. try:
  49. if not int(amount) >= 0:
  50. raise ValueError()
  51. except (TypeError, ValueError):
  52. raise ValueError("Invalid value for amount (given: '{}')".format(amount))
  53. self._amount = amount
  54. def __repr__(self):
  55. return "<Dice; sides={}; amount={}>".format(self.sides, self.amount)
  56. def __eq__(self, d):
  57. return self.sides == d.sides and self.amount == d.amount
  58. def roll(self):
  59. """ Role the dice and return a Score object """
  60. return Score([random.randint(1, self._sides) for _ in range(self._amount)])
  61. @classmethod
  62. def parse(cls, pattern):
  63. """ parse a pattern of the form 'xdx', where x are positive integers """
  64. # normalize
  65. pattern = str(pattern).replace(" ", "").lower()
  66. # parse
  67. amount, faces = (int(x) for x in pattern.split("d"))
  68. # instanciate
  69. return Dice(faces, amount)
  70. class Score(int):
  71. """ Score is a subclass of integer.
  72. Then you can manipulate it as you would do with an integer.
  73. It also provides an access to the detail with the property 'results'.
  74. 'results' is the list of the scores obtained by each dice.
  75. Score class can also be used as an iterable, to walk trough the individual scores.
  76. eg:
  77. >>> s = Score([1,2,3])
  78. >>> print(s)
  79. 6
  80. >>> s + 1
  81. 7
  82. >>> list(s)
  83. [1,2,3]
  84. """
  85. def __new__(cls, detail):
  86. score = super(Score, cls).__new__(cls, sum(detail))
  87. score._detail = detail
  88. return score
  89. @property
  90. def detail(self):
  91. return self._detail
  92. def __repr__(self):
  93. return "<Score; score={}; detail={}>".format(int(self), self.detail)
  94. def __contains__(self, value):
  95. return self.detail.__contains__(value)
  96. def __iter__(self):
  97. return self.detail.__iter__()
  98. class Pattern():
  99. def __init__(self, instr):
  100. if not instr:
  101. raise ValueError("Invalid value for 'instr' ('{}')".format(instr))
  102. self.instr = Pattern._normalize(instr)
  103. self.dices = []
  104. self.format_string = ""
  105. @staticmethod
  106. def _normalize(instr):
  107. """ normalize the incoming string to a lower string without spaces"""
  108. return str(instr).replace(" ", "").lower()
  109. def compile(self):
  110. def _submatch(match):
  111. dice = Dice.parse(match.group(0))
  112. index = len(self.dices)
  113. self.dices.append(dice)
  114. return "{{{}}}".format(index)
  115. self.format_string = re.sub('\d+d\d+', _submatch, self.instr)
  116. def roll(self):
  117. if not self.format_string:
  118. self.compile()
  119. scores = [dice.roll() for dice in self.dices]
  120. return PatternScore(self.format_string, scores)
  121. class PatternScore(int):
  122. def __new__(cls, eval_string, scores):
  123. ps = super(PatternScore, cls).__new__(cls, _secured_eval(eval_string.format(*scores)))
  124. ps._eval_string = eval_string
  125. ps._scores = scores
  126. return ps
  127. def format(self):
  128. return self._eval_string.format(*[str(list(score)) for score in self._scores])
  129. def score(self, i):
  130. return self._scores[i]
  131. def scores(self):
  132. return self._scores