mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Refactor dice contrib
This commit is contained in:
parent
093d0ebb07
commit
b351deaadd
6 changed files with 271 additions and 71 deletions
|
|
@ -1,5 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
## Main
|
||||
|
||||
- Contrib: Refactored `dice.roll` contrib function to use `safe_eval`. Can now
|
||||
optionally be used as `dice.roll("2d10 + 4 > 10")`. Old way works too.
|
||||
|
||||
## Evennia 2.0.1
|
||||
|
||||
June 17, 2023
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
# Changelog
|
||||
|
||||
## Main
|
||||
|
||||
- Contrib update: Made `dice.roll` contrib function optionally accept dice
|
||||
definition string e.g. `dice.roll("2d10 + 4 > 10")`. Old way works too.
|
||||
|
||||
## Evennia 2.0.1
|
||||
|
||||
June 17, 2023
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from evennia.contrib.rpg import dice <---
|
|||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
# ...
|
||||
def at_object_creation(self):
|
||||
def at_cmdset_creation(self):
|
||||
# ...
|
||||
self.add(dice.CmdDice()) # <---
|
||||
|
||||
|
|
@ -53,17 +53,73 @@ was.
|
|||
|
||||
Is a hidden roll that does not inform the room it happened.
|
||||
|
||||
### Rolling dice from code
|
||||
## Rolling dice from code
|
||||
|
||||
To roll dice in code, use the `roll` function from this module:
|
||||
To roll dice in code, use the `roll` function from this module. It has two
|
||||
main ways to define the expected roll:
|
||||
|
||||
```python
|
||||
from evennia.contrib.rpg.dice import roll
|
||||
|
||||
from evennia.contrib.rpg import dice
|
||||
dice.roll(3, 10, ("+", 2)) # 3d10 + 2
|
||||
roll(dice, dicetype=6, modifier=None, conditional=None, return_tuple=False,
|
||||
max_dicenum=10, max_dicetype=1000)
|
||||
|
||||
```
|
||||
|
||||
You can only roll one set of dice. If your RPG requires you to roll multiple
|
||||
sets of dice and combine them in more advanced ways, you can do so with multiple
|
||||
`roll()` calls.
|
||||
|
||||
### Roll dice based on a string
|
||||
|
||||
You can specify the first argument as a string on standard RPG d-syntax (NdM,
|
||||
where N is the number of dice to roll, and M is the number sides per dice):
|
||||
|
||||
```python
|
||||
roll("3d10 + 2")
|
||||
```
|
||||
|
||||
You can also give a conditional (you'll then get a `True`/`False` back):
|
||||
|
||||
```python
|
||||
roll("2d6 - 1 >= 10")
|
||||
```
|
||||
|
||||
### Explicit arguments
|
||||
|
||||
If you specify the first argument as an integer, it's interpret as the number of
|
||||
dice to roll and you can then build the roll more explicitly. This can be
|
||||
useful if you are using the roller together with some other system and want to
|
||||
construct the roll from components.
|
||||
|
||||
|
||||
Here's how to roll `3d10 + 2` with explicit syntax:
|
||||
|
||||
```python
|
||||
roll(3, 10, modifier=("+", 2))
|
||||
```
|
||||
|
||||
Here's how to roll `2d6 - 1 >= 10` (you'll get back `True`/`False` back):
|
||||
|
||||
```python
|
||||
roll(2, 6, modifier=("-", 1), conditional=(">=", 10))
|
||||
```
|
||||
|
||||
### Get all roll details
|
||||
|
||||
If you need the individual rolls (e.g. for a dice pool), set the `return_tuple` kwarg:
|
||||
|
||||
```python
|
||||
roll("3d10 > 10", return_tuple=True)
|
||||
(13, True, 3, (3, 4, 6)) # (result, outcome, diff, rolls)
|
||||
```
|
||||
|
||||
The return is a tuple `(result, outcome, diff, rolls)`, where `result` is the
|
||||
result of the roll, `outcome` is `True/False` if a conditional was
|
||||
given (`None` otherwise), `diff` is the absolute difference between the
|
||||
conditional and the result (`None` otherwise) and `rolls` is a tuple containing
|
||||
the individual roll results.
|
||||
|
||||
|
||||
----
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from evennia.contrib.rpg import dice <---
|
|||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
# ...
|
||||
def at_object_creation(self):
|
||||
def at_cmdset_creation(self):
|
||||
# ...
|
||||
self.add(dice.CmdDice()) # <---
|
||||
|
||||
|
|
@ -53,13 +53,69 @@ was.
|
|||
|
||||
Is a hidden roll that does not inform the room it happened.
|
||||
|
||||
### Rolling dice from code
|
||||
## Rolling dice from code
|
||||
|
||||
To roll dice in code, use the `roll` function from this module:
|
||||
To roll dice in code, use the `roll` function from this module. It has two
|
||||
main ways to define the expected roll:
|
||||
|
||||
```python
|
||||
from evennia.contrib.rpg.dice import roll
|
||||
|
||||
from evennia.contrib.rpg import dice
|
||||
dice.roll(3, 10, ("+", 2)) # 3d10 + 2
|
||||
roll(dice, dicetype=6, modifier=None, conditional=None, return_tuple=False,
|
||||
max_dicenum=10, max_dicetype=1000)
|
||||
|
||||
```
|
||||
|
||||
You can only roll one set of dice. If your RPG requires you to roll multiple
|
||||
sets of dice and combine them in more advanced ways, you can do so with multiple
|
||||
`roll()` calls.
|
||||
|
||||
### Roll dice based on a string
|
||||
|
||||
You can specify the first argument as a string on standard RPG d-syntax (NdM,
|
||||
where N is the number of dice to roll, and M is the number sides per dice):
|
||||
|
||||
```python
|
||||
roll("3d10 + 2")
|
||||
```
|
||||
|
||||
You can also give a conditional (you'll then get a `True`/`False` back):
|
||||
|
||||
```python
|
||||
roll("2d6 - 1 >= 10")
|
||||
```
|
||||
|
||||
### Explicit arguments
|
||||
|
||||
If you specify the first argument as an integer, it's interpret as the number of
|
||||
dice to roll and you can then build the roll more explicitly. This can be
|
||||
useful if you are using the roller together with some other system and want to
|
||||
construct the roll from components.
|
||||
|
||||
|
||||
Here's how to roll `3d10 + 2` with explicit syntax:
|
||||
|
||||
```python
|
||||
roll(3, 10, modifier=("+", 2))
|
||||
```
|
||||
|
||||
Here's how to roll `2d6 - 1 >= 10` (you'll get back `True`/`False` back):
|
||||
|
||||
```python
|
||||
roll(2, 6, modifier=("-", 1), conditional=(">=", 10))
|
||||
```
|
||||
|
||||
### Get all roll details
|
||||
|
||||
If you need the individual rolls (e.g. for a dice pool), set the `return_tuple` kwarg:
|
||||
|
||||
```python
|
||||
roll("3d10 > 10", return_tuple=True)
|
||||
(13, True, 3, (3, 4, 6)) # (result, outcome, diff, rolls)
|
||||
```
|
||||
|
||||
The return is a tuple `(result, outcome, diff, rolls)`, where `result` is the
|
||||
result of the roll, `outcome` is `True/False` if a conditional was
|
||||
given (`None` otherwise), `diff` is the absolute difference between the
|
||||
conditional and the result (`None` otherwise) and `rolls` is a tuple containing
|
||||
the individual roll results.
|
||||
|
|
|
|||
|
|
@ -48,48 +48,62 @@ To roll dice in code, use the `roll` function from this module:
|
|||
```python
|
||||
|
||||
from evennia.contrib.rpg import dice
|
||||
dice.roll_dice(3, 10, ("+", 2)) # 3d10 + 2
|
||||
|
||||
dice.roll(3, 10, ("+", 2)) # 3d10 + 2
|
||||
```
|
||||
|
||||
or use the string syntax:
|
||||
|
||||
dice.roll("3d10 + 2")
|
||||
|
||||
"""
|
||||
import re
|
||||
from ast import literal_eval
|
||||
from random import randint
|
||||
|
||||
from evennia import CmdSet, default_cmds
|
||||
from evennia.utils.utils import simple_eval
|
||||
|
||||
|
||||
def roll(dicenum, dicetype, modifier=None, conditional=None, return_tuple=False):
|
||||
def roll(
|
||||
dice,
|
||||
dicetype=6,
|
||||
modifier=None,
|
||||
conditional=None,
|
||||
return_tuple=False,
|
||||
max_dicenum=10,
|
||||
max_dicetype=1000,
|
||||
):
|
||||
"""
|
||||
This is a standard dice roller.
|
||||
|
||||
Args:
|
||||
dicenum (int): Number of dice to roll (the result to be added).
|
||||
dicetype (int): Number of sides of the dice to be rolled.
|
||||
modifier (tuple): A tuple `(operator, value)`, where operator is
|
||||
dice (int or str): If an `int`, this is the number of dice to roll, and `dicetype` is used
|
||||
to determine the type. If a `str`, it should be on the form `NdM` where `N` is the number
|
||||
of dice and `M` is the number of sides on each die. Also
|
||||
`NdM [modifier] [number] [conditional]` is understood, e.g. `1d6 + 3`
|
||||
or `2d10 / 2 > 10`.
|
||||
dicetype (int, optional): Number of sides of the dice to be rolled. Ignored if
|
||||
`dice` is a string.
|
||||
modifier (tuple, optional): A tuple `(operator, value)`, where operator is
|
||||
one of `"+"`, `"-"`, `"/"` or `"*"`. The result of the dice
|
||||
roll(s) will be modified by this value.
|
||||
conditional (tuple): A tuple `(conditional, value)`, where
|
||||
roll(s) will be modified by this value. Ignored if `dice` is a string.
|
||||
conditional (tuple, optional): A tuple `(conditional, value)`, where
|
||||
conditional is one of `"=="`,`"<"`,`">"`,`">="`,`"<=`" or "`!=`".
|
||||
This allows the roller to directly return a result depending
|
||||
on if the conditional was passed or not.
|
||||
Ignored if `dice` is a string.
|
||||
return_tuple (bool): Return a tuple with all individual roll
|
||||
results or not.
|
||||
max_dicenum (int): The max number of dice to allow to be rolled.
|
||||
max_dicetype (int): The max number of sides on the dice to roll.
|
||||
|
||||
Returns:
|
||||
roll_result (int): The result of the roll + modifiers. This is the
|
||||
default return.
|
||||
condition_result (bool): A True/False value returned if `conditional`
|
||||
is set but not `return_tuple`. This effectively hides the result
|
||||
of the roll.
|
||||
full_result (tuple): If, return_tuple` is `True`, instead
|
||||
return a tuple `(result, outcome, diff, rolls)`. Here,
|
||||
`result` is the normal result of the roll + modifiers.
|
||||
`outcome` and `diff` are the boolean result of the roll and
|
||||
absolute difference to the `conditional` input; they will
|
||||
be will be `None` if `conditional` is not set. `rolls` is
|
||||
itself a tuple holding all the individual rolls in the case of
|
||||
multiple die-rolls.
|
||||
int, bool or tuple : By default, this is the result of the roll + modifiers. If
|
||||
`conditional` is given, or `dice` is a string defining a conditional, then a True/False
|
||||
value is returned. Finally, if `return_tuple` is set, this is a tuple
|
||||
`(result, outcome, diff, rolls)`, where, `result` is the the normal result of the
|
||||
roll + modifiers, `outcome` and `diff` are the boolean absolute difference between the roll
|
||||
and the `conditional` input; both will be will be `None` if `conditional` is not set.
|
||||
The `rolls` a tuple holding all the individual rolls (one or more depending on how many
|
||||
dice were rolled).
|
||||
|
||||
Raises:
|
||||
TypeError if non-supported modifiers or conditionals are given.
|
||||
|
|
@ -98,48 +112,100 @@ def roll(dicenum, dicetype, modifier=None, conditional=None, return_tuple=False)
|
|||
All input numbers are converted to integers.
|
||||
|
||||
Examples:
|
||||
print roll_dice(2, 6) # 2d6
|
||||
<<< 7
|
||||
print roll_dice(1, 100, ('+', 5) # 1d100 + 5
|
||||
<<< 34
|
||||
print roll_dice(1, 20, conditional=('<', 10) # let'say we roll 3
|
||||
<<< True
|
||||
print roll_dice(3, 10, return_tuple=True)
|
||||
<<< (11, None, None, (2, 5, 4))
|
||||
print roll_dice(2, 20, ('-', 2), conditional=('>=', 10), return_tuple=True)
|
||||
<<< (8, False, 2, (4, 6)) # roll was 4 + 6 - 2 = 8
|
||||
::
|
||||
# explicit arguments
|
||||
print roll(2, 6) # 2d6
|
||||
7
|
||||
print roll(1, 100, ('+', 5) # 1d100 + 5
|
||||
4
|
||||
print roll(1, 20, conditional=('<', 10) # let'say we roll 3
|
||||
True
|
||||
print roll(3, 10, return_tuple=True)
|
||||
(11, None, None, (2, 5, 4))
|
||||
print roll(2, 20, ('-', 2), conditional=('>=', 10), return_tuple=True)
|
||||
(8, False, 2, (4, 6)) # roll was 4 + 6 - 2 = 8
|
||||
|
||||
# string form
|
||||
print roll("3d6 + 2")
|
||||
10
|
||||
print roll("2d10 + 2 > 10")
|
||||
True
|
||||
print roll("2d20 - 2 >= 10")
|
||||
(8, False, 2, (4, 6)) # roll was 4 + 6 - 2 = 8
|
||||
|
||||
"""
|
||||
dicenum = int(dicenum)
|
||||
dicetype = int(dicetype)
|
||||
|
||||
modifier_string = ""
|
||||
conditional_string = ""
|
||||
conditional_value = None
|
||||
if isinstance(dice, str) and "d" in dice.lower():
|
||||
# A string is given, parse it as NdM dice notation
|
||||
roll_string = dice.lower()
|
||||
|
||||
# split to get the NdM syntax
|
||||
dicenum, rest = roll_string.split("d", 1)
|
||||
|
||||
# parse packwards right-to-left
|
||||
if any(True for cond in ("==", "<", ">", "!=", "<=", ">=") if cond in rest):
|
||||
# split out any conditionals, like '< 12'
|
||||
rest, *conditionals = re.split(r"(==|<=|>=|<|>|!=)", rest, maxsplit=1)
|
||||
try:
|
||||
conditional_value = int(conditionals[1])
|
||||
except ValueError:
|
||||
raise TypeError(
|
||||
f"Conditional '{conditionals[-1]}' was not recognized. Must be a number."
|
||||
)
|
||||
conditional_string = "".join(conditionals)
|
||||
|
||||
if any(True for op in ("+", "-", "*", "/") if op in rest):
|
||||
# split out any modifiers, like '+ 2'
|
||||
rest, *modifiers = re.split(r"(\+|-|/|\*)", rest, maxsplit=1)
|
||||
modifier_string = "".join(modifiers)
|
||||
|
||||
# whatever is left is the dice type
|
||||
dicetype = rest
|
||||
|
||||
else:
|
||||
# an integer is given - explicit modifiers and conditionals as part of kwargs
|
||||
dicenum = int(dice)
|
||||
dicetype = int(dicetype)
|
||||
if modifier:
|
||||
modifier_string = "".join(str(part) for part in modifier)
|
||||
if conditional:
|
||||
conditional_value = int(conditional[1])
|
||||
conditional_string = "".join(str(part) for part in conditional)
|
||||
|
||||
try:
|
||||
dicenum = int(dicenum)
|
||||
dicetype = int(dicetype)
|
||||
except Exception:
|
||||
raise TypeError(
|
||||
f"The number of dice and dice-size must both be numerical. Got '{dicenum}' "
|
||||
f"and '{dicetype}'."
|
||||
)
|
||||
if 0 < dicenum > max_dicenum:
|
||||
raise TypeError(f"Invalid number of dice rolled (must be between 1 and {max_dicenum}).")
|
||||
if 0 < dicetype > max_dicetype:
|
||||
raise TypeError(f"Invalid die-size used (must be between 1 and {max_dicetype} sides).")
|
||||
|
||||
# roll all dice, remembering each roll
|
||||
rolls = tuple([randint(1, dicetype) for roll in range(dicenum)])
|
||||
rolls = tuple([randint(1, dicetype) for _ in range(dicenum)])
|
||||
result = sum(rolls)
|
||||
|
||||
if modifier:
|
||||
# make sure to check types well before eval
|
||||
mod, modvalue = modifier
|
||||
if mod not in ("+", "-", "*", "/"):
|
||||
raise TypeError("Non-supported dice modifier: %s" % mod)
|
||||
modvalue = int(modvalue) # for safety
|
||||
result = eval("%s %s %s" % (result, mod, modvalue))
|
||||
if modifier_string:
|
||||
result = simple_eval(f"{result} {modifier_string}")
|
||||
|
||||
outcome, diff = None, None
|
||||
if conditional:
|
||||
# make sure to check types well before eval
|
||||
cond, condvalue = conditional
|
||||
if cond not in (">", "<", ">=", "<=", "!=", "=="):
|
||||
raise TypeError("Non-supported dice result conditional: %s" % conditional)
|
||||
condvalue = int(condvalue) # for safety
|
||||
outcome = eval("%s %s %s" % (result, cond, condvalue)) # True/False
|
||||
diff = abs(result - condvalue)
|
||||
if conditional_string and conditional_value:
|
||||
outcome = simple_eval(f"{result} {conditional_string}")
|
||||
diff = abs(result - conditional_value)
|
||||
|
||||
if return_tuple:
|
||||
return result, outcome, diff, rolls
|
||||
elif conditional or (conditional_string and conditional_value):
|
||||
return outcome # True|False
|
||||
else:
|
||||
if conditional:
|
||||
return outcome
|
||||
else:
|
||||
return result
|
||||
return result # integer
|
||||
|
||||
|
||||
# legacy alias
|
||||
|
|
@ -235,7 +301,8 @@ class CmdDice(default_cmds.MuxCommand):
|
|||
except ValueError:
|
||||
self.caller.msg(
|
||||
"You need to enter valid integer numbers, modifiers and operators."
|
||||
" |w%s|n was not understood." % self.args
|
||||
" |w%s|n was not understood."
|
||||
% self.args
|
||||
)
|
||||
return
|
||||
# format output
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@ Testing of TestDice.
|
|||
|
||||
"""
|
||||
|
||||
from mock import patch
|
||||
|
||||
from evennia.commands.default.tests import BaseEvenniaCommandTest
|
||||
from mock import patch
|
||||
|
||||
from . import dice
|
||||
|
||||
|
|
@ -13,9 +12,9 @@ from . import dice
|
|||
@patch("evennia.contrib.rpg.dice.dice.randint", return_value=5)
|
||||
class TestDice(BaseEvenniaCommandTest):
|
||||
def test_roll_dice(self, mocked_randint):
|
||||
self.assertEqual(dice.roll_dice(6, 6, modifier=("+", 4)), mocked_randint() * 6 + 4)
|
||||
self.assertEqual(dice.roll_dice(6, 6, conditional=("<", 35)), True)
|
||||
self.assertEqual(dice.roll_dice(6, 6, conditional=(">", 33)), False)
|
||||
self.assertEqual(dice.roll(6, 6, modifier=("+", 4)), mocked_randint() * 6 + 4)
|
||||
self.assertEqual(dice.roll(6, 6, conditional=("<", 35)), True)
|
||||
self.assertEqual(dice.roll(6, 6, conditional=(">", 33)), False)
|
||||
|
||||
def test_cmddice(self, mocked_randint):
|
||||
self.call(
|
||||
|
|
@ -23,3 +22,15 @@ class TestDice(BaseEvenniaCommandTest):
|
|||
)
|
||||
self.call(dice.CmdDice(), "100000d1000", "The maximum roll allowed is 10000d10000.")
|
||||
self.call(dice.CmdDice(), "/secret 3d6 + 4", "You roll 3d6 + 4 (secret, not echoed).")
|
||||
|
||||
def test_string_form(self, mocked_randint):
|
||||
self.assertEqual(dice.roll("6d6 + 4"), mocked_randint() * 6 + 4)
|
||||
self.assertEqual(dice.roll("6d6 < 35"), True)
|
||||
self.assertEqual(dice.roll("6d6 > 35"), False)
|
||||
self.assertEqual(dice.roll("2d10 + 5 >= 14"), True)
|
||||
|
||||
def test_maxvals(self, mocked_randint):
|
||||
with self.assertRaises(TypeError):
|
||||
dice.roll(11, 1001, max_dicenum=10, max_dicetype=1000)
|
||||
with self.assertRaises(TypeError):
|
||||
dice.roll(10, 1001, max_dicenum=10, max_dicetype=1000)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue