Browse Source

Add modifier to dice roll

pull/4/head
Vladislav Glinsky 6 years ago
parent
commit
6b69d07f93
Signed by: cl0ne GPG Key ID: 9D058DD29491782E
  1. 9
      README.md
  2. 17
      bot.py
  3. 13
      dice_parser.py
  4. 115
      test_dice_parser.py

9
README.md

@ -16,7 +16,14 @@ Bot requires authorization token to be set in `BOT_TOKEN` environment variable.
* `/ping`
- Confirms that bot is currently active by responding with 'pong'.
* `/roll`
- 🚧 make a dice roll in simplified [dice notation](https://en.wikipedia.org/wiki/Dice_notation): `AdX`, where `A` stands for number of rolls (can be omitted if 1) and `X` specifies number of sides. Both `A` and `X` are positive integer numbers. Maximum number of rolls is 100, the biggest allowed dice has 120 sides.
- 🚧 make a dice roll in simplified [dice notation](https://en.wikipedia.org/wiki/Dice_notation): `AdB+M`
- `A`: number of rolls (can be omitted if 1)
- `B`: number of sides
- `M`: a modifier that is added to (or subtracted from) roll result ("+" or "-" between `B` and `M` defines modifier's sign)
- `A`, `B` and `M` are integer numbers, `A` and `B` are positive.
- Maximum number of rolls is 100, the biggest allowed dice has 120 sides.
## Planned Features
* `/roll` similar to [RollEm Telegram Bot](https://github.com/treetrnk/rollem-telegram-bot) 🚧
- arbitrary chained rolls (`4+2d6+d10-d7`)
- exploding rolls (`d6!` or `d10x`), where maximum rolled value triggers extra rolls

17
bot.py

@ -19,9 +19,12 @@ logger = logging.getLogger(__name__)
__NOTATION_DESCRIPTION = (
'simplified [dice notation](https://en.wikipedia.org/wiki/Dice_notation): `AdX`\n'
'`A` stands for number of rolls (can be omitted if 1) and `X` for number of sides. '
'Both `A` and `X` are positive integer numbers, '
'simplified [dice notation](https://en.wikipedia.org/wiki/Dice_notation): `AdB+M`\n'
'- `A` is a number of rolls (can be omitted if 1)\n'
'- `B` specifies number of sides\n'
'- `M` is a modifier that is added to the roll result, "+" or "-" between `B` and `M` '
"defines modifier's sign\n"
'Both `A` and `B` are positive integer numbers, `M` is an integer number, '
f'maximum number of rolls is *{Dice.ROLL_LIMIT}*, the biggest dice has '
f'*{Dice.BIGGEST_DICE}* sides'
)
@ -32,13 +35,13 @@ def show_help(update: Update, context: CallbackContext):
update.message.reply_markdown(
'*Available commands:*\n\n'
'`/me` - announce your actions to the chat\n'
'*/me* - announce your actions to the chat\n'
'\n'
'`/ping` - check if bot is currently active\n'
'*/ping* - check if bot is currently active\n'
'\n'
f'`/roll` - make a dice roll in {__NOTATION_DESCRIPTION}\n'
f'*/roll* - make a dice roll in {__NOTATION_DESCRIPTION}\n'
'\n'
'`/fortune` - get a random epigram',
'*/fortune* - get a random epigram',
disable_web_page_preview=True
)

13
dice_parser.py

@ -41,33 +41,38 @@ class Dice:
r'(?P<rolls>\d+)?'
r'd'
r'(?P<sides>\d+)'
r'(?:(?P<modifier_sign>[+-])(?P<modifier>\d+))?'
)
BIGGEST_DICE = 120
ROLL_LIMIT = 100
def __init__(self, rolls, sides):
def __init__(self, rolls, sides, modifier=0):
if not(0 < rolls <= self.ROLL_LIMIT):
raise ValueRangeError('roll count', rolls, (1, self.ROLL_LIMIT))
if not(0 < sides <= self.BIGGEST_DICE):
raise ValueRangeError('sides', sides, (1, self.BIGGEST_DICE))
self.rolls = rolls
self.sides = sides
self.modifier = modifier or 0
@staticmethod
def parse(roll_str):
match = Dice.__regex.fullmatch(roll_str)
if not match:
raise ParseError
rolls, sides = match.groups()
rolls, sides, modifier_sign, modifier = match.groups()
rolls = int(rolls) if rolls else 1
sides = int(sides)
return Dice(rolls, sides)
modifier = int(modifier) if modifier else 0
if modifier and modifier_sign == '-':
modifier = -modifier
return Dice(rolls, sides, modifier)
def _single_roll(self):
return random.randint(1, self.sides)
def get_result(self, item_limit=None) -> Tuple[int, List[int], bool]:
total = 0
total = self.modifier
items = []
if item_limit is None:
item_count = self.rolls

115
test_dice_parser.py

@ -5,39 +5,72 @@ from dice_parser import Dice, ParseError, ValueRangeError
class DiceParserTest(unittest.TestCase):
@staticmethod
def _DiceVars(dice):
return dice.rolls, dice.sides, dice.modifier
def test_basic_notation(self):
for roll_str, expected in (
('1d6', (1, 6)),
('d6', (1, 6)),
('d5', (1, 5)),
('5d1', (5, 1))
('1d6', (1, 6, 0)),
('d6', (1, 6, 0)),
('d5', (1, 5, 0)),
('5d1', (5, 1, 0))
):
with self.subTest(roll_str=roll_str):
self.assertEqual(self._DiceVars(Dice.parse(roll_str)), expected)
for roll_str in (
'd', '1d',
'-1d', '-1d6', 'd+6', '1d-6', '-6d-1'
):
for roll_str in ('d', '1d', '-1d', '-1d6'):
with self.subTest(roll_str=roll_str):
self.assertRaises(ParseError, Dice.parse, roll_str)
for roll_str in ('0d6', '1d0', 'd0', '0d0'):
with self.subTest(roll_str=roll_str):
self.assertRaises(ValueRangeError, Dice.parse, roll_str)
def test_modifier(self):
for roll_str, expected in (
('d6-5', (1, 6, -5)),
('d6+5', (1, 6, 5)),
('d6+0', (1, 6, 0)),
('2d10-1', (2, 10, -1))
):
with self.subTest(roll_str=roll_str):
self.assertEqual(self._DiceVars(Dice.parse(roll_str)), expected)
for roll_str in ('d+6', '1d-6', '-6d-1', 'd6+', 'd6-', 'd6-+6', 'd6+-6', 'd6-+-6', 'd6+d6'):
with self.subTest(roll_str=roll_str):
self.assertRaises(ParseError, Dice.parse, roll_str)
def test_init(self):
for rolls, sides in (
(1, 1),
(1, 6),
(1, 10),
(100, 6),
(100, 1),
(1, 120),
(100, 120)
for rolls, sides, modifier in (
(1, 1, 0),
(1, 6, 0),
(1, 10, 0),
(100, 6, 0),
(100, 1, 0),
(1, 120, 0),
(100, 120, 0)
):
with self.subTest(rolls=rolls, sides=sides):
d = Dice(rolls, sides)
self.assertEqual(self._DiceVars(d), (rolls, sides, modifier))
with self.subTest(rolls=rolls, sides=sides, modifier=None):
d = Dice(rolls, sides, None)
self.assertEqual(self._DiceVars(d), (rolls, sides, modifier))
with self.subTest(rolls=rolls, sides=sides, modifier=modifier):
d = Dice(rolls, sides, modifier)
self.assertEqual(self._DiceVars(d), (rolls, sides, modifier))
for rolls, sides, modifier in (
(1, 1, -1),
(1, 1, 1),
(1, 6, 6),
(1, 6, -6)
):
with self.subTest(rolls=rolls, sides=sides, modifier=modifier):
d = Dice(rolls, sides, modifier)
self.assertEqual(d.rolls, rolls)
self.assertEqual(d.sides, sides)
@ -61,30 +94,34 @@ class DiceParserTest(unittest.TestCase):
@mock.patch('random.randint', return_value=1)
def test_get_result(self, randint):
two_standard = Dice(3, 6)
expected_calls = [mock.call(1, 6)] * 3
for item_limit, expected_total, expected_rolls, expected_was_limited in (
(None, 3, [1, 1, 1], False),
(0, 3, [], True),
(1, 3, [1], True),
(2, 3, [1, 1], True),
(3, 3, [1, 1, 1], False),
(10, 3, [1, 1, 1], False)
for roll_count, sides, modifier in (
(1, 2, 0), (3, 6, 0), (3, 6, -1), (3, 6, +1), (5, 10, -10), (5, 10, +10)
):
with self.subTest(item_limit=item_limit):
roll_total, single_rolls, was_limited = two_standard.get_result(item_limit)
randint.assert_has_calls(expected_calls)
self.assertEqual(roll_total, expected_total)
self.assertEqual(single_rolls, expected_rolls)
self.assertEqual(was_limited, expected_was_limited)
self.assertRaises(ValueError, two_standard.get_result, -1)
self.assertRaises(ValueError, two_standard.get_result, -3)
@staticmethod
def _DiceVars(dice):
return dice.rolls, dice.sides
with self.subTest(rolls=roll_count, sides=sides, modifier=modifier):
d = Dice(roll_count, sides, modifier)
expected_calls = [mock.call(1, sides)] * roll_count
expected_total = roll_count + modifier
expected_rolls = [1] * roll_count
for item_limit in (
None, 0, 1, roll_count - 1, roll_count, roll_count + 1, roll_count + 20
):
with self.subTest(item_limit=item_limit):
roll_total, single_rolls, was_limited = d.get_result(item_limit)
randint.assert_has_calls(expected_calls)
self.assertEqual(randint.call_count, roll_count)
self.assertEqual(roll_total, expected_total)
limit_is_set = item_limit is not None and roll_count > item_limit
self.assertEqual(was_limited, limit_is_set)
roll_subset = expected_rolls[:(item_limit if limit_is_set else roll_count)]
self.assertSequenceEqual(single_rolls, roll_subset)
randint.reset_mock()
for item_limit in (-1, -roll_count, -roll_count-1):
with self.subTest(item_limit=item_limit):
self.assertRaises(ValueError, d.get_result, item_limit)
randint.assert_not_called()
randint.reset_mock()
if __name__ == '__main__':

Loading…
Cancel
Save