roll.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. #!/usr/bin/env python
  2. import logging
  3. import re
  4. import sys
  5. import readline
  6. from random import randint
  7. logFormatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s')
  8. logger = logging.getLogger(__name__)
  9. logger.setLevel(logging.INFO)
  10. logger.handlers = []
  11. logger.addHandler(logging.StreamHandler())
  12. for handler in logger.handlers:
  13. handler.setFormatter(logFormatter)
  14. def format_roll(s, n=1, mod=0, drop_low=0, drop_high=0):
  15. fmt = ''
  16. if n > 1:
  17. fmt += str(n)
  18. fmt += 'd' + str(s)
  19. if drop_low > 0:
  20. fmt += "-L"
  21. if drop_low > 1:
  22. fmt += str(drop_low)
  23. if drop_high > 0:
  24. fmt += "-H"
  25. if drop_high > 1:
  26. fmt += str(drop_high)
  27. if mod != 0:
  28. fmt += '%+i' % (mod,)
  29. return fmt
  30. def roll_die(s, mod=0, adv=0):
  31. '''Roll a single S-sided die. '''
  32. roll = randint(1, s) + mod
  33. logger.debug('%s rolled: %s', format_roll(s, 1, mod), roll)
  34. return roll
  35. def roll_dice(s, n=1, mod=0, drop_low=0, drop_high=0):
  36. '''Roll n s-sided dice, then add modifier.
  37. (The modifier feature of this function currently isn't used by the
  38. script.)
  39. '''
  40. s = int(s)
  41. n = int(n)
  42. mod = int(mod)
  43. drop_low = int(drop_low)
  44. drop_high = int(drop_high)
  45. if n < 1:
  46. raise ValueError('Must roll at least one die.')
  47. if s < 2:
  48. raise ValueError('Dice must have at least 2 sides')
  49. if drop_low < 0 or drop_high < 0:
  50. raise ValueError('Cannot drop negative number of dice.')
  51. if drop_low + drop_high >= n:
  52. raise ValueError('Dropping too many dice; must keep at least one die.')
  53. n = int(n)
  54. rolls = [ roll_die(s) for i in xrange(n) ]
  55. dropped_low_rolls = sorted(rolls)[:drop_low]
  56. dropped_high_rolls = sorted(rolls, reverse=True)[:drop_high]
  57. kept_rolls = list(rolls)
  58. # Cannot use setdiff because of possible repeated values
  59. for drop in dropped_low_rolls + dropped_high_rolls:
  60. kept_rolls.remove(drop)
  61. total = mod + sum(kept_rolls)
  62. # TODO: Special reporting for natural 1 and natural 20 (only when a single d20 roll is returned)
  63. natural = ''
  64. if len(kept_rolls) == 1 and s == 20:
  65. if kept_rolls[0] == 1:
  66. natural = " (NATURAL 1)"
  67. elif kept_rolls[0] == 20:
  68. natural = " (NATURAL 20)"
  69. if n > 1:
  70. paren_stmts = [ ('Individual rolls', kept_rolls), ]
  71. if dropped_low_rolls:
  72. paren_stmts.append( ('Dropped low', dropped_low_rolls) )
  73. if dropped_high_rolls:
  74. paren_stmts.append( ('Dropped high', dropped_high_rolls) )
  75. if dropped_low_rolls or dropped_high_rolls:
  76. paren_stmts.append( ('Original rolls', rolls) )
  77. paren_stmt = "; ".join("%s: %s" % (k, repr(v)) for k,v in paren_stmts)
  78. logger.info('%s rolled: %s%s\n(%s)', format_roll(s, n, mod, drop_low, drop_high), total, natural, paren_stmt)
  79. else:
  80. logger.info('%s rolled: %s%s', format_roll(s, n, mod, drop_low, drop_high), total, natural)
  81. return total
  82. def _roll_matchgroup(m):
  83. sides = int(m.group(2))
  84. n = int(m.group(1) or 1)
  85. dropspec = m.group(3) or ''
  86. mod = int(m.group(4) or 0)
  87. drop_low, drop_high = 0,0
  88. drops = re.findall('-([HL])(\\d*)', dropspec)
  89. for (dtype, dnum) in drops:
  90. if dnum == '':
  91. dnum = 1
  92. else:
  93. dnum = int(dnum)
  94. if dtype == 'L':
  95. drop_low += dnum
  96. else:
  97. drop_high += dnum
  98. return str(roll_dice(sides, n, mod, drop_low, drop_high))
  99. def roll(expr):
  100. try:
  101. logger.info("Rolling %s", expr)
  102. # Multi dice rolls, e.g. "4d6"
  103. subbed = re.sub('(\\d+)?\\s*d\\s*(\\d+)((?:-[HL]\\d*)+)?([+-]\\d+)?',
  104. lambda m: _roll_matchgroup(m),
  105. expr)
  106. logger.debug("Expression with dice rolls substituted: %s", repr(subbed))
  107. return int(eval(subbed))
  108. except Exception as exc:
  109. raise Exception("Cannot parse expression %s: %s" % (repr(expr), exc))
  110. def read_roll(handle=sys.stdin):
  111. return raw_input("Enter roll> ")
  112. if __name__ == '__main__':
  113. expr = " ".join(sys.argv[1:])
  114. if re.search("\\S", expr):
  115. try:
  116. result = roll(expr)
  117. logger.info("Total roll: %s", result)
  118. except Exception as exc:
  119. logger.error("Error while rolling: %s", repr(exc))
  120. sys.exit(1)
  121. else:
  122. try:
  123. while True:
  124. try:
  125. expr = read_roll()
  126. if expr in ('exit', 'quit', 'q'):
  127. break
  128. if re.search("\\S", expr):
  129. try:
  130. result = roll(expr)
  131. logger.info("Total roll: %s", result)
  132. except Exception as exc:
  133. logger.error("Error while rolling: %s", repr(exc))
  134. except KeyboardInterrupt:
  135. print "\n",
  136. except EOFError:
  137. # Print a newline before exiting
  138. print "\n",