"""
# Dice
Rolls dice for roleplaying, in-game gambling or GM:ing
Evennia contribution - Griatch 2012
This module implements a a dice-roller and a `dice`/`roll` command
to go with it. It uses standard RPG 'd'-syntax (e.g. 2d6 to roll two
six-sided die) and also supports modifiers such as 3d6 + 5.
> roll 1d20 + 2
One can also specify a standard Python operator in order to specify
eventual target numbers and get results in a fair and guaranteed
unbiased way. For example a GM could (using the dice command) from
the start define the roll as 2d6 < 8 to show that a roll below 8 is
required to succeed. The command will normally echo this result to all
parties (although it also has options for hidden and secret rolls).
# Installation:
Add the `CmdDice` command from this module to your character's cmdset
(and then restart the server):
```python
# in mygame/commands/default_cmdsets.py
# ...
from evennia.contrib.rpg import dice <---
class CharacterCmdSet(default_cmds.CharacterCmdSet):
# ...
def at_object_creation(self):
# ...
self.add(dice.CmdDice()) # <---
```
# Usage:
roll 1d100 + 10
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
```
"""
import re
from random import randint
from evennia import CmdSet, default_cmds
[docs]def roll(dicenum, dicetype, modifier=None, conditional=None, return_tuple=False):
"""
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
one of `"+"`, `"-"`, `"/"` or `"*"`. The result of the dice
roll(s) will be modified by this value.
conditional (tuple): 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.
return_tuple (bool): Return a tuple with all individual roll
results or not.
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.
Raises:
TypeError if non-supported modifiers or conditionals are given.
Notes:
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
"""
dicenum = int(dicenum)
dicetype = int(dicetype)
# roll all dice, remembering each roll
rolls = tuple([randint(1, dicetype) for roll 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))
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 return_tuple:
return result, outcome, diff, rolls
else:
if conditional:
return outcome
else:
return result
# legacy alias
roll_dice = roll
RE_PARTS = re.compile(r"(d|\+|-|/|\*|<|>|<=|>=|!=|==)")
RE_MOD = re.compile(r"(\+|-|/|\*)")
RE_COND = re.compile(r"(<|>|<=|>=|!=|==)")
[docs]class CmdDice(default_cmds.MuxCommand):
"""
roll dice
Usage:
dice[/switch] <nr>d<sides> [modifier] [success condition]
Switch:
hidden - tell the room the roll is being done, but don't show the result
secret - don't inform the room about neither roll nor result
Examples:
dice 3d6 + 4
dice 1d100 - 2 < 50
This will roll the given number of dice with given sides and modifiers.
So e.g. 2d6 + 3 means to 'roll a 6-sided die 2 times and add the result,
then add 3 to the total'.
Accepted modifiers are +, -, * and /.
A success condition is given as normal Python conditionals
(<,>,<=,>=,==,!=). So e.g. 2d6 + 3 > 10 means that the roll will succeed
only if the final result is above 8. If a success condition is given, the
outcome (pass/fail) will be echoed along with how much it succeeded/failed
with. The hidden/secret switches will hide all or parts of the roll from
everyone but the person rolling.
"""
key = "dice"
aliases = ["roll", "@dice"]
locks = "cmd:all()"
[docs] def func(self):
"""Mostly parsing for calling the dice roller function"""
if not self.args:
self.caller.msg("Usage: @dice <nr>d<sides> [modifier] [conditional]")
return
argstring = "".join(str(arg) for arg in self.args)
parts = [part for part in RE_PARTS.split(self.args) if part]
len_parts = len(parts)
modifier = None
conditional = None
if len_parts < 3 or parts[1] != "d":
self.caller.msg(
"You must specify the die roll(s) as <nr>d<sides>."
" For example, 2d6 means rolling a 6-sided die 2 times."
)
return
# Limit the number of dice and sides a character can roll to prevent server slow down and crashes
ndicelimit = 10000 # Maximum number of dice
nsidelimit = 10000 # Maximum number of sides
if int(parts[0]) > ndicelimit or int(parts[2]) > nsidelimit:
self.caller.msg("The maximum roll allowed is %sd%s." % (ndicelimit, nsidelimit))
return
ndice, nsides = parts[0], parts[2]
if len_parts == 3:
# just something like 1d6
pass
elif len_parts == 5:
# either e.g. 1d6 + 3 or something like 1d6 > 3
if parts[3] in ("+", "-", "*", "/"):
modifier = (parts[3], parts[4])
else: # assume it is a conditional
conditional = (parts[3], parts[4])
elif len_parts == 7:
# the whole sequence, e.g. 1d6 + 3 > 5
modifier = (parts[3], parts[4])
conditional = (parts[5], parts[6])
else:
# error
self.caller.msg("You must specify a valid die roll")
return
# do the roll
try:
result, outcome, diff, rolls = roll_dice(
ndice, nsides, modifier=modifier, conditional=conditional, return_tuple=True
)
except ValueError:
self.caller.msg(
"You need to enter valid integer numbers, modifiers and operators."
" |w%s|n was not understood." % self.args
)
return
# format output
if len(rolls) > 1:
rolls = ", ".join(str(roll) for roll in rolls[:-1]) + " and " + str(rolls[-1])
else:
rolls = rolls[0]
if outcome is None:
outcomestring = ""
elif outcome:
outcomestring = " This is a |gsuccess|n (by %s)." % diff
else:
outcomestring = " This is a |rfailure|n (by %s)." % diff
yourollstring = "You roll %s%s."
roomrollstring = "%s rolls %s%s."
resultstring = " Roll(s): %s. Total result is |w%s|n."
if "secret" in self.switches:
# don't echo to the room at all
string = yourollstring % (argstring, " (secret, not echoed)")
string += "\n" + resultstring % (rolls, result)
string += outcomestring + " (not echoed)"
self.caller.msg(string)
elif "hidden" in self.switches:
# announce the roll to the room, result only to caller
string = yourollstring % (argstring, " (hidden)")
self.caller.msg(string)
string = roomrollstring % (self.caller.key, argstring, " (hidden)")
self.caller.location.msg_contents(string, exclude=self.caller)
# handle result
string = resultstring % (rolls, result)
string += outcomestring + " (not echoed)"
self.caller.msg(string)
else:
# normal roll
string = yourollstring % (argstring, "")
self.caller.msg(string)
string = roomrollstring % (self.caller.key, argstring, "")
self.caller.location.msg_contents(string, exclude=self.caller)
string = resultstring % (rolls, result)
string += outcomestring
self.caller.location.msg_contents(string)
[docs]class DiceCmdSet(CmdSet):
"""
a small cmdset for testing purposes.
Add with @py self.cmdset.add("contrib.dice.DiceCmdSet")
"""
[docs] def at_cmdset_creation(self):
"""Called when set is created"""
self.add(CmdDice())