Remove Unicode dependency, add Dicebot.

This commit is contained in:
oddluck 2020-02-08 09:27:08 +00:00
parent 82fdf102d2
commit f4497bf046
17 changed files with 1498 additions and 1 deletions

69
Dicebot/__init__.py Normal file
View File

@ -0,0 +1,69 @@
###
# Copyright (c) 2007-2010, Andrey Rahmatullin
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
Dice bot
"""
import supybot
import supybot.world as world
# Use this for the version of this plugin. You may wish to put a CVS keyword
# in here if you're keeping the plugin in CVS or some similar system.
__version__ = ""
__author__ = supybot.Author('Andrey Rahmatullin', 'wRAR', 'wrar@wrar.name')
__maintainer__ = getattr(supybot.authors, 'oddluck',
supybot.Author('oddluck', 'oddluck', 'oddluck@riseup.net'))
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {}
# This is a url where the most recent plugin package can be downloaded.
__url__ = 'https://github.com/oddluck/limnoria-plugins/'
from . import config
from . import plugin
from . import deck
from imp import reload
# In case we're being reloaded.
reload(deck)
reload(plugin)
# Add more reloads here if you add third-party modules and want them to be
# reloaded when this plugin is reloaded. Don't forget to import them as well!
if world.testing:
from . import test
Class = plugin.Class
configure = config.configure
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

49
Dicebot/config.py Normal file
View File

@ -0,0 +1,49 @@
###
# Copyright (c) 2007-2008, Andrey Rahmatullin
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import supybot.conf as conf
import supybot.registry as registry
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified himself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
conf.registerPlugin('Dicebot', True)
Dicebot = conf.registerPlugin('Dicebot')
conf.registerChannelValue(Dicebot, 'autoRoll',
registry.Boolean(False, """Determines whether the bot will automatically
roll the dice it sees in the channel."""))
conf.registerGlobalValue(Dicebot, 'autoRollInPrivate',
registry.Boolean(False, """Determines whether the bot will automatically
roll the dice it sees in private messages."""))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78

73
Dicebot/deck.py Normal file
View File

@ -0,0 +1,73 @@
###
# Copyright (c) 2008, Anatoly Popov
# Copyright (c) 2008, Andrey Rahmatullin
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import random
class Deck:
"""
54-card deck simulator.
This class represents a standard 54-card deck (with 2 different Jokers)
and supports shuffling and drawing.
"""
titles = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
suits = ['', '', '', '']
def __init__(self):
"""
Initialize a new deck and shuffle it.
"""
self.deck = []
self.base_deck = ['Black Joker', 'Red Joker'] + [t + s
for t in self.titles
for s in self.suits]
self.shuffle()
def shuffle(self):
"""
Restore and shuffle the deck.
All cards are returned to the deck and then shuffled randomly.
"""
new_deck = self.base_deck[:]
random.shuffle(new_deck)
self.deck = new_deck
def __next__(self):
"""
Draw the top card from the deck and return it.
Drawn card is removed from the deck. If it was the last card, deck is
shuffled.
"""
card = self.deck.pop()
if not self.deck:
self.shuffle()
return card

11
Dicebot/docs/7th Sea.txt Normal file
View File

@ -0,0 +1,11 @@
7th Sea RnK support
~~~~~~~~~~~~~~~~~~~
There is a special support for RnK mechanics. The base form is '5k2', it shows
all kept dice and their sum. Exploded dice are shown as result numbers (23 for
10+10+3, for example). A static modifier may be applied to the total sum using
'5k2+2' or '5k2-2'. '-' prefix disables explosion of 10's, '+' prefix enables
printing of unkept dice. If you want to disable explosion and to print unkept
dice, you can use '-5kk2' ('5kk2' is identical to '+5k2'). Attempts to roll
and/or keep more than 10 dice are resolved according to Player's Guide,
wrapping excess dice from unkept to kept and from kept to the static modifier.
You can roll the same combination several times using, for example, '3#5k2'.

9
Dicebot/docs/DH.txt Normal file
View File

@ -0,0 +1,9 @@
Dark Heresy/Rogue Trader/Deathwatch support
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
DH/RT/DW systems use a d100 die, the result is compared to a target number
with some modifiers. You can roll a die using 'vs(40)' or 'vs(40+20-10)'
syntax. Unlimited number of modifiers is supported. The result is displayed as
the threshold value minus the roll value, so a positive number means success
and tens digit equals the number of degrees of success/failure.
You can roll several identical tests using syntax '3vs(30)'.

30
Dicebot/docs/NEWS.txt Normal file
View File

@ -0,0 +1,30 @@
1.0
~~~
- Port to Python 3.
0.6
~~~
- Extended tests for Shadowrun.
- Support any number of dice and modifiers in the standard roll.
0.5
~~~
- Dark Heresy support added.
- New WoD support added (undocumented and untested, though).
- Multiroll support for 7th Sea.
0.4
~~~
- 7th Sea support added.
- Basic card deck simulation added.
- Tests added.
0.3
~~~
- Shadowrun 4 support added (see Shadowrun.txt).
- Multiple expressions per message are now supported.
0.2
~~~
- First public release.

62
Dicebot/docs/README.txt Normal file
View File

@ -0,0 +1,62 @@
Description
~~~~~~~~~~~
Dicebot plugin contains the commands which simulate rolling of dice.
Though core supybot plugin Games contain 'dice' command, it is very simple and
is not sufficient for advanced uses, such as online playing of tabletop
role-playing games using IRC.
The most basic feature of any dicebot is, of course, rolling of one or several
identical dice and showing the results. That is what core 'dice' command can
do. It takes an expression such as 3d6 and returns a series of numbers -
results of rolling each (of 3) die (6-sided): '2, 4, and 4'. This may be
sufficient for some games, but usually you need more. Here is what this plugin
can do for you.
Features
~~~~~~~~
1. Sum of dice rolled. Expression form is just '2d6', plugin returns the sum
of dice values as one number.
2. Sum of several different dice and some fixed numbers. Expression:
'2d6+3d8-2+10'. After summing up dice results the specified number is added (or
subtracted) to the sum.
3. Separate results of several identical rolls which use previously described
form. This is written as '3#1d20+7' and yields 3 numbers, each meaning the
result of rolling 1d20+7 as described above.
4. Possibility to omit leading 1 as dice count and use just 'd6' or '3#d20'.
5. Two (three?) distinct modes of operation: roll command and autorolling (can
be enabled per-channel and for private messages, see configuration below).
roll command accepts just one expression and shows its result (if the
expression is valid). Autorolling means that bot automatically rolls and
displays all recognized expressions it sees (be it on the channel or in the
query). Autorolling is much more convenient during online play, but may be
confusing during normal talk, so it can be enabled only when it is needed.
6. To distinguish between different rolls, all results are prefixed with
requesting user's nick and requested expression.
7. If you use several expressions in one message, bot will roll all of them and
return all the results in one reply, separated with semicolon.
8. Shadowrun 4ed support, see included Shadowrun.txt; 7th Sea RnK support, see
7th Sea.txt; Dark Heresy/Rogue Trader/Deathwatch support, see DH.txt.
9. Concerning extensibility, you just need to add a regex for your expression
and a function which parses that expression and returns a string which will be
displayed.
10. Also includes basic card deck simulator, see below.
Configuration
~~~~~~~~~~~~~
autoRoll (per-channel): whether to roll all expressions seen on the channel
autoRollInPrivate (global): whether to roll expressions in the queries
Both settings are off by default, so that bot replies only to explicit !roll.
Deck
~~~~
Bot has a 54-card deck which it can shuffle (!shuffle command) and from which
you can draw (!draw or !deal command, with optional number argument if you want
to draw several cards). Drawn card is removed from the deck, but shuffle
restores full deck. If the last card is drawn, the deck is automatically
shuffled before drawing next card.
Thanks
~~~~~~
Ur-DnD roleplaying community (#dnd @ RusNet) for games, talking and fun, and
personally Zuzuzu for describing basic dicebot requirements, which led to
writing the first version of this plugin in August 2007.

View File

@ -0,0 +1,23 @@
Shadowrun 4th ed. support
~~~~~~~~~~~~~~~~~~~~~~~~~
In SR4 you roll a number of d6 (this number is called a dice pool) and count
how many dice show 5 or 6, the more the better (SR4 p.54-55). You also need to
count 1's, because if half or more of the dice show 1, you have a *glitch*
(some bad game effect). Because you don't need to sum roll results, roll dice
other than d6 or add some fixed modifiers, SR4 dicer can (and should) be
simpler and easier to use. So you just say 10#sd (where 10 is your dice pool)
and bot will show the total hits number and/or a glitch message.
Also you can declare the use of Edge before a roll (SR4 p.67) to use the Rule
of Six (SR4, p.56), which means rerolling all 6's, potentially increasing the
total hits number. This mode is used by saying 10#sdx (x stands for 'eXploding
dice').
You can make Extended tests by saying i.e. 10,8#sde. Here 10 is the pool size
and 8 is the threshold. The output will include the number of passes, resulting
hit number and, in case of glitches, the pass number of the first glitch.
The current version of Shadowrun code will log all the rolled dice values
(with the DEBUG level), to check the algorithm and for curious players. This
may be removed in future versions.

511
Dicebot/plugin.py Normal file
View File

@ -0,0 +1,511 @@
###
# Copyright (c) 2007-2010, Andrey Rahmatullin
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
from .deck import Deck
from .sevenSea2EdRaiseRoller import SevenSea2EdRaiseRoller
from operator import itemgetter
import re
import random
from supybot.commands import additional, wrap
from supybot.utils.str import format, ordinal
import supybot.ircmsgs as ircmsgs
import supybot.callbacks as callbacks
class Dicebot(callbacks.Plugin):
"""This plugin supports rolling the dice using !roll 4d20+3 as well as
automatically rolling such combinations it sees in the channel (if
autoRoll option is enabled for that channel) or query (if
autoRollInPrivate option is enabled).
"""
rollReStandard = re.compile(r'((?P<rolls>\d+)#)?(?P<spec>[+-]?(\d*d\d+|\d+)([+-](\d*d\d+|\d+))*)$')
rollReSR = re.compile(r'(?P<rolls>\d+)#sd$')
rollReSRX = re.compile(r'(?P<rolls>\d+)#sdx$')
rollReSRE = re.compile(r'(?P<pool>\d+),(?P<thr>\d+)#sde$')
rollRe7Sea = re.compile(r'((?P<count>\d+)#)?(?P<prefix>[-+])?(?P<rolls>\d+)(?P<k>k{1,2})(?P<keep>\d+)(?P<mod>[+-]\d+)?$')
rollRe7Sea2ed = re.compile(r'(?P<rolls>([-+]|\d)+)s(?P<skill>\d)(?P<vivre>-)?(l(?P<lashes>\d+))?(?P<explode>ex)?(?P<cursed>r15)?$')
rollReWoD = re.compile(r'(?P<rolls>\d+)w(?P<explode>\d|-)?$')
rollReDH = re.compile(r'(?P<rolls>\d*)vs\((?P<thr>([-+]|\d)+)\)$')
rollReWG = re.compile(r'(?P<rolls>\d+)#wg$')
validationDH = re.compile(r'^[+\-]?\d{1,4}([+\-]\d{1,4})*$')
validation7sea2ed = re.compile(r'^[+\-]?\d{1,2}([+\-]\d{1,2})*$')
MAX_DICE = 1000
MIN_SIDES = 2
MAX_SIDES = 100
MAX_ROLLS = 30
def __init__(self, irc):
super(Dicebot, self).__init__(irc)
self.deck = Deck()
def _roll(self, dice, sides, mod=0):
"""
Roll a die several times, return sum of the results plus the static modifier.
Arguments:
dice -- number of dice rolled;
sides -- number of sides each die has;
mod -- number added to the total result (optional);
"""
res = int(mod)
for _ in range(dice):
res += random.randrange(1, sides+1)
return res
def _rollMultiple(self, dice, sides, rolls=1, mod=0):
"""
Roll several dice several times, return a list of results.
Specified number of dice with specified sides is rolled several times.
Each time the sum of results is calculated, with optional modifier
added. The list of these sums is returned.
Arguments:
dice -- number of dice rolled each time;
sides -- number of sides each die has;
rolls -- number of times dice are rolled;
mod -- number added to the each total result (optional);
"""
return [self._roll(dice, sides, mod) for i in range(rolls)]
@staticmethod
def _formatMod(mod):
"""
Format a numeric modifier for printing expressions such as 1d20+3.
Nonzero numbers are formatted with a sign, zero is formatted as an
empty string.
"""
return ('%+d' % mod) if mod != 0 else ''
def _process(self, irc, text):
"""
Process a message and reply with roll results, if any.
The message is split to the words and each word is checked against all
known expression forms (first applicable form is used). All results
are printed together in the IRC reply.
"""
checklist = [
(self.rollReStandard, self._parseStandardRoll),
(self.rollReSR, self._parseShadowrunRoll),
(self.rollReSRX, self._parseShadowrunXRoll),
(self.rollReSRE, self._parseShadowrunExtRoll),
(self.rollRe7Sea, self._parse7SeaRoll),
(self.rollRe7Sea2ed, self._parse7Sea2edRoll),
(self.rollReWoD, self._parseWoDRoll),
(self.rollReDH, self._parseDHRoll),
(self.rollReWG, self._parseWGRoll),
]
results = []
for word in text.split():
for expr, parser in checklist:
m = expr.match(word)
if m:
r = parser(m)
if r:
results.append(r)
break
if results:
irc.reply('; '.join(results))
def _parseStandardRoll(self, m):
"""
Parse rolls such as 3#2d6+1d4+2.
This is a roll (or several rolls) of several dice with optional
static modifiers. It yields one number (the sum of results and
modifiers) for each roll series.
"""
rolls = int(m.group('rolls') or 1)
spec = m.group('spec')
if not spec[0] in '+-':
spec = '+' + spec
r = re.compile(r'(?P<sign>[+-])((?P<dice>\d*)d(?P<sides>\d+)|(?P<mod>\d+))')
totalMod = 0
totalDice = {}
for m in r.finditer(spec):
if not m.group('mod') is None:
totalMod += int(m.group('sign') + m.group('mod'))
continue
dice = int(m.group('dice') or 1)
sides = int(m.group('sides'))
if dice > self.MAX_DICE or sides > self.MAX_SIDES or sides < self.MIN_SIDES:
return
if m.group('sign') == '-':
sides *= -1
totalDice[sides] = totalDice.get(sides, 0) + dice
if len(totalDice) == 0:
return
results = []
for _ in range(rolls):
result = totalMod
for sides, dice in totalDice.items():
if sides > 0:
result += self._roll(dice, sides)
else:
result -= self._roll(dice, -sides)
results.append(result)
specFormatted = ''
self.log.debug(repr(totalDice))
for sides, dice in sorted(list(totalDice.items()), key=itemgetter(0), reverse=True):
if sides > 0:
if len(specFormatted) > 0:
specFormatted += '+'
specFormatted += '%dd%d' % (dice, sides)
else:
specFormatted += '-%dd%d' % (dice, -sides)
specFormatted += self._formatMod(totalMod)
return '[%s] %s' % (specFormatted, ', '.join([str(i) for i in results]))
def _parseShadowrunRoll(self, m):
"""
Parse Shadowrun-specific roll such as 3#sd.
"""
rolls = int(m.group('rolls'))
if rolls < 1 or rolls > self.MAX_DICE:
return
L = self._rollMultiple(1, 6, rolls)
self.log.debug(format("%L", [str(i) for i in L]))
return self._processSRResults(L, rolls)
def _parseShadowrunXRoll(self, m):
"""
Parse Shadowrun-specific 'exploding' roll such as 3#sdx.
"""
rolls = int(m.group('rolls'))
if rolls < 1 or rolls > self.MAX_DICE:
return
L = self._rollMultiple(1, 6, rolls)
self.log.debug(format("%L", [str(i) for i in L]))
reroll = L.count(6)
while reroll:
rerolled = self._rollMultiple(1, 6, reroll)
self.log.debug(format("%L", [str(i) for i in rerolled]))
L.extend([r for r in rerolled if r >= 5])
reroll = rerolled.count(6)
return self._processSRResults(L, rolls, True)
@staticmethod
def _processSRResults(results, pool, isExploding=False):
hits = results.count(6) + results.count(5)
ones = results.count(1)
isHit = hits > 0
isGlitch = ones >= (pool + 1) / 2
explStr = ', exploding' if isExploding else ''
if isHit:
hitsStr = format('%n', (hits, 'hit'))
glitchStr = ', glitch' if isGlitch else ''
return '(pool %d%s) %s%s' % (pool, explStr, hitsStr, glitchStr)
if isGlitch:
return '(pool %d%s) critical glitch!' % (pool, explStr)
return '(pool %d%s) 0 hits' % (pool, explStr)
def _parseShadowrunExtRoll(self, m):
"""
Parse Shadowrun-specific Extended test roll such as 14,3#sde.
"""
pool = int(m.group('pool'))
if pool < 1 or pool > self.MAX_DICE:
return
threshold = int(m.group('thr'))
if threshold < 1 or threshold > self.MAX_DICE:
return
result = 0
passes = 0
glitches = []
critGlitch = None
while result < threshold:
L = self._rollMultiple(1, 6, pool)
self.log.debug(format('%L', [str(i) for i in L]))
hits = L.count(6) + L.count(5)
result += hits
passes += 1
isHit = hits > 0
isGlitch = L.count(1) >= (pool + 1) / 2
if isGlitch:
if not isHit:
critGlitch = passes
break
glitches.append(ordinal(passes))
glitchStr = format(', glitch at %L', glitches) if len(glitches) > 0 else ''
if critGlitch is None:
return format('(pool %i, threshold %i) %n, %n%s',
pool, threshold, (passes, 'pass'), (result, 'hit'), glitchStr)
else:
return format('(pool %i, threshold %i) critical glitch at %s pass%s, %n so far',
pool, threshold, ordinal(critGlitch), glitchStr, (result, 'hit'))
def _parse7Sea2edRoll(self, m):
"""
Parse 7th Sea 2ed roll (4s2 is its simplest form). Full spec: https://redd.it/80l7jm
"""
rolls = m.group('rolls')
if rolls is None:
return
# additional validation
if not re.match(self.validation7sea2ed, rolls):
return
roll_count = eval(rolls)
if roll_count < 1 or roll_count > self.MAX_ROLLS:
return
skill = int(m.group('skill'))
vivre = m.group('vivre') == '-'
explode = m.group('explode') == 'ex'
lashes = 0 if m.group('lashes') is None else int(m.group('lashes'))
cursed = m.group('cursed') is not None
self.log.debug(format('7sea2ed: %i (%s) dices at %i skill. lashes = %i. explode is %s. vivre is %s',
roll_count,
str(rolls),
skill,
lashes,
"enabled" if explode else "disabled",
"enabled" if vivre else "disabled"
))
roller = SevenSea2EdRaiseRoller(
lambda x: self._rollMultiple(1, 10, x),
skill_rank=skill,
explode=explode,
lash_count=lashes,
joie_de_vivre=vivre,
raise_target=15 if cursed else 10)
return '[%s]: %s' % (m.group(0), str(roller.roll_and_count(roll_count)))
def _parse7SeaRoll(self, m):
"""
Parse 7th Sea-specific roll (4k2 is its simplest form).
"""
rolls = int(m.group('rolls'))
if rolls < 1 or rolls > self.MAX_ROLLS:
return
count = int(m.group('count') or 1)
keep = int(m.group('keep'))
mod = int(m.group('mod') or 0)
prefix = m.group('prefix')
k = m.group('k')
explode = prefix != '-'
if keep < 1 or keep > self.MAX_ROLLS:
return
if keep > rolls:
keep = rolls
if rolls > 10:
keep += rolls - 10
rolls = 10
if keep > 10:
mod += (keep - 10) * 10
keep = 10
unkept = (prefix == '+' or k == 'kk') and keep < rolls
explodeStr = ', not exploding' if not explode else ''
results = []
for _ in range(count):
L = self._rollMultiple(1, 10, rolls)
if explode:
for i in range(len(L)):
if L[i] == 10:
while True:
rerolled = self._roll(1, 10)
L[i] += rerolled
if rerolled < 10:
break
self.log.debug(format("%L", [str(i) for i in L]))
L.sort(reverse=True)
keptDice, unkeptDice = L[:keep], L[keep:]
unkeptStr = ' | %s' % ', '.join([str(i) for i in unkeptDice]) if unkept else ''
keptStr = ', '.join([str(i) for i in keptDice])
results.append('(%d) %s%s' % (sum(keptDice) + mod, keptStr, unkeptStr))
return '[%dk%d%s%s] %s' % (rolls, keep, self._formatMod(mod), explodeStr,
'; '.join(results))
def _parseWoDRoll(self, m):
"""
Parse New World of Darkness roll (5w)
"""
rolls = int(m.group('rolls'))
if rolls < 1 or rolls > self.MAX_ROLLS:
return
if m.group('explode') == '-':
explode = 0
elif m.group('explode') is not None and m.group('explode').isdigit():
explode = int(m.group('explode'))
if explode < 8 or explode > 10:
explode = 10
else:
explode = 10
L = self._rollMultiple(1, 10, rolls)
self.log.debug(format("%L", [str(i) for i in L]))
successes = len([x for x in L if x >= 8])
if explode:
for i in range(len(L)):
if L[i] >= explode:
while True:
rerolled = self._roll(1, 10)
self.log.debug(str(rerolled))
if rerolled >= 8:
successes += 1
if rerolled < explode:
break
if explode == 0:
explStr = ', not exploding'
elif explode != 10:
explStr = ', %d-again' % explode
else:
explStr = ''
result = format('%n', (successes, 'success')) if successes > 0 else 'FAIL'
return '(%d%s) %s' % (rolls, explStr, result)
def _parseDHRoll(self, m):
"""
Parse Dark Heresy roll (3vs(20+30-10))
"""
rolls = int(m.group('rolls') or 1)
if rolls < 1 or rolls > self.MAX_ROLLS:
return
thresholdExpr = m.group('thr')
# additional validation
if not re.match(self.validationDH, thresholdExpr):
return
threshold = eval(thresholdExpr)
rollResults = self._rollMultiple(1, 100, rolls)
results = [threshold - roll for roll in rollResults]
return '%s (%s vs %d)' % (', '.join([str(i) for i in results]),
', '.join([str(i) for i in rollResults]),
threshold)
def _parseWGRoll(self, m):
"""
Parse WH40K: Wrath & Glory roll (10#wg)
"""
rolls = int(m.group('rolls') or 1)
if rolls < 1 or rolls > self.MAX_ROLLS:
return
L = self._rollMultiple(1, 6, rolls)
self.log.debug(format("%L", [str(i) for i in L]))
return self._processWGResults(L, rolls)
@staticmethod
def _processWGResults(results, pool):
wrathstrings=["","","","","",""]
strTag=""
wrathDie=results.pop(0)
n6 = results.count(6)
n5 = results.count(5)
n4 = results.count(4)
icons = 2 * n6 + n5 + n4
Glory = wrathDie == 6
Complication = wrathDie == 1
iconssymb = wrathstrings[wrathDie-1] + " "
if Glory:
strTag += "| Glory"
icons += 2
elif wrathDie > 3:
icons += 1
elif Complication:
strTag += "| Complication"
iconssymb += n6 * "" + n5 * "5 " + n4 * "4 "
isNonZero = icons > 0
if isNonZero:
iconsStr = str(icons) + " icon(s): " + iconssymb + strTag
return '[pool %d] %s' % (pool, iconsStr)
def _autoRollEnabled(self, irc, channel):
"""
Check if automatic rolling is enabled for this context.
"""
return ((irc.isChannel(channel) and
self.registryValue('autoRoll', channel)) or
(not irc.isChannel(channel) and
self.registryValue('autoRollInPrivate')))
def roll(self, irc, msg, args, text):
"""<dice>d<sides>[<modifier>]
Rolls a die with <sides> number of sides <dice> times, summarizes the
results and adds optional modifier <modifier>
For example, 2d6 will roll 2 six-sided dice; 10d10-3 will roll 10
ten-sided dice and subtract 3 from the total result.
"""
if self._autoRollEnabled(irc, msg.args[0]):
return
self._process(irc, text)
roll = wrap(roll, ['somethingWithoutSpaces'])
def shuffle(self, irc, msg, args):
"""takes no arguments
Restores and shuffles the deck.
"""
self.deck.shuffle()
irc.reply('shuffled')
shuffle = wrap(shuffle)
def draw(self, irc, msg, args, count):
"""[<count>]
Draws <count> cards (1 if omitted) from the deck and shows them.
"""
cards = [next(self.deck) for i in range(count)]
irc.reply(', '.join(cards))
draw = wrap(draw, [additional('positiveInt', 1)])
deal = draw
def doPrivmsg(self, irc, msg):
if not self._autoRollEnabled(irc, msg.args[0]):
return
if ircmsgs.isAction(msg):
text = ircmsgs.unAction(msg)
else:
text = msg.args[1]
self._process(irc, text)
Class = Dicebot
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

6
Dicebot/readme.md Normal file
View File

@ -0,0 +1,6 @@
# Testing
1. Use python3+, python2 is not supported
2. I recommend to use virtualenv: `virtualenv -p python3 .venv && source .venv/bin/activate`
3. Install dependencies: `pip3 install -r requirements.txt`
4. Run tests: `supybot-test Dicebot`

3
Dicebot/requirements.txt Normal file
View File

@ -0,0 +1,3 @@
limnoria
pylint
pytest

View File

@ -0,0 +1,260 @@
###
# Copyright (c) 2018, Anatoly Popov
# Copyright (c) 2018, Andrey Rahmatullin
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import random
import pytest
from collections import defaultdict
class RollResult:
def __init__(self, result, lash_count=0, joie_de_vivre_target=0, suffix=''):
self.result = result
self.suffix = suffix
if result < lash_count:
self.value = 0
elif result <= joie_de_vivre_target:
self.value = 10
else:
self.value = result
def __str__(self):
if self.result == self.value:
return "%d%s" % (self.result, self.suffix)
else:
return "%d%s [%d]" % (self.value, self.suffix, self.result)
class Raise:
def __init__(self, raise_count=0, rolls=[]):
self.rolls = list(map(lambda x: x if isinstance(x, RollResult) else RollResult(x), rolls))
self.raise_count = raise_count
@property
def Sum(self):
return sum(x.value for x in self.rolls)
def __str__(self):
if self.raise_count == 0:
return "(%s)" % (" + ".join(map(str, self.rolls)))
else:
return "%s(%s)" % ("*" * self.raise_count, " + ".join(map(str, self.rolls)))
class RaiseRollResult:
def __init__(self, raises=[], unused=[], discarded=None):
self.raises = raises
self.unused = unused
self.discarded = discarded
def __str__(self):
total_raises = sum(x.raise_count for x in self.raises)
result = "0 raises" if total_raises == 0 else "%d %s: %s" % (
total_raises,
"raises" if total_raises != 1 else "raise",
", ".join(map(str, self.raises))
)
if self.unused:
result = "%s, unused: %s" % (
result,
", ".join(map(str, self.unused))
)
if self.discarded:
result = "%s, discarded: %s" % (
result,
", ".join(map(str, self.discarded))
)
return result
class RaiseAggregator:
def __init__(self, raise_target, raises_per_target, rolls):
self.raise_target = raise_target
self.raises_per_target = raises_per_target
self.ten_is_still_raise = self.raise_target == 10 or self.raises_per_target != 1
self.exhausted = False
self.rolled_dices = defaultdict(list)
self.rolled_dice_count = 0
self.dices = defaultdict(list)
self.dice_count = 0
self.max_roll = 0
for x in rolls:
self.rolled_dices[x.value].append(x)
self.rolled_dice_count += 1
if x.value > self.max_roll:
self.max_roll = x.value
def get_dice(self, numbers_to_check):
for x in numbers_to_check:
if len(self.dices[x]) > 0:
self.dice_count -= 1
return self.dices[x].pop()
return None
def tostr(self):
r = '{'
for x in self.dices:
r += '%d: [' % x
for y in self.dices[x]:
r += '%s, ' % str(y)
r += '], '
return r + '}'
def get_lower_dice(self, target):
return self.get_dice(range(target, 0, -1))
def get_higher_dice(self, target):
return self.get_dice(range(target + 1, self.max_roll + 1))
def get_raise_candidate(self, first_dice, down):
raise_candidate = [first_dice]
while True:
raise_sum = sum(x.value for x in raise_candidate)
if raise_sum >= self.raise_target:
return Raise(self.raises_per_target, raise_candidate)
target = self.raise_target - raise_sum
next_dice = self.get_lower_dice(target) if down else self.get_higher_dice(target)
if next_dice is not None:
raise_candidate.append(next_dice)
elif self.dice_count > 0 and down:
# we are going down. Let's grab one dice above and continue
raise_candidate.append(self.get_higher_dice(0))
continue
elif self.ten_is_still_raise and raise_sum >= 10:
return Raise(1, raise_candidate)
else:
return Raise(0, raise_candidate)
def return_dice_to_pool(self, dice):
self.dice_count += 1
self.dices[dice.value].append(dice)
def return_raise_to_pool(self, first_dice, raise_candidate):
for x in raise_candidate.rolls:
if x != first_dice:
self.return_dice_to_pool(x)
def __iter__(self):
self.dices = defaultdict(list)
self.dice_count = self.rolled_dice_count
for value in self.rolled_dices:
for roll in self.rolled_dices[value]:
self.dices[value].append(roll)
self.exhausted = False
return self
def __next__(self):
if self.exhausted:
raise StopIteration
first_dice = self.get_lower_dice(self.max_roll)
if first_dice is None:
self.exhausted = True
raise StopIteration
lower = self.get_raise_candidate(first_dice, True)
if lower.Sum == self.raise_target:
return lower
higher = self.get_raise_candidate(first_dice, False)
if higher.raise_count == 0 and lower.raise_count == 0:
self.exhausted = True
self.return_raise_to_pool(first_dice, higher)
self.return_raise_to_pool(first_dice, lower)
self.return_dice_to_pool(first_dice)
raise StopIteration
if higher.raise_count == lower.raise_count:
if higher.Sum >= lower.Sum:
self.return_raise_to_pool(first_dice, higher)
return lower
else:
self.return_raise_to_pool(first_dice, lower)
return higher
elif higher.raise_count > lower.raise_count:
self.return_raise_to_pool(first_dice, lower)
return higher
else:
self.return_raise_to_pool(first_dice, higher)
return lower
class SevenSea2EdRaiseRoller:
"""
Raise roller for 7sea, 2ed. Spec: https://redd.it/80l7jm
"""
def __init__(self, roller, raise_target=10, raises_per_target=1, explode=False, lash_count=0, skill_rank=0, joie_de_vivre=False):
self.roller = roller
self.explode = skill_rank >= 5 or explode
self.lash_count = lash_count
self.joie_de_vivre_target = skill_rank if joie_de_vivre else 0
self.reroll_one_dice = skill_rank >= 3
default_roll = raise_target == 10 and raises_per_target == 1
self.aggregator_template = lambda x: RaiseAggregator(
15 if skill_rank >= 4 and default_roll else raise_target,
2 if skill_rank >= 4 and default_roll else raises_per_target,
x
)
def roll_and_count(self, dice_count):
"""
Assemble raises, according to spec
"""
rolls = self.roll(dice_count)
if not self.reroll_one_dice:
discarded_dice = None
else:
reroll = self.roll(1, 'r')
min_value_dice = min(rolls, key=lambda x: x.value)
if min_value_dice.value < sum(x.value for x in reroll):
rolls.remove(min_value_dice)
rolls += reroll
discarded_dice = [min_value_dice]
else:
discarded_dice = reroll
aggregator = self.aggregator_template(rolls)
raises = list(aggregator)
unused = []
for value in aggregator.dices:
for dice in aggregator.dices[value]:
unused.append(dice)
return RaiseRollResult(raises, sorted(unused, key=lambda x: x.value, reverse=True), discarded_dice)
def roll(self, dice_count, suffix=''):
if dice_count == 0:
return []
rolls = [RollResult(x, self.lash_count, self.joie_de_vivre_target, suffix) for x in self.roller(dice_count)]
return rolls + self.roll(len([x for x in rolls if x.result == 10]), suffix + 'x') if self.explode else rolls

115
Dicebot/test.py Normal file
View File

@ -0,0 +1,115 @@
###
# Copyright (c) 2007-2010, Andrey Rahmatullin
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
from supybot.test import PluginTestCase
class DicebotTestCase(PluginTestCase):
plugins = ('Dicebot',)
def testPlugin(self):
self.assertHelp('dicebot roll')
self.assertNotError('dicebot roll 1d2')
self.assertNoResponse('dicebot roll dummy')
def testRollStd(self):
self.assertRegexp('dicebot roll 1d20', r'\[1d20\] \d+')
self.assertRegexp('dicebot roll d20', r'\[1d20\] \d+')
self.assertRegexp('dicebot roll 1d20+5', r'\[1d20\+5\] \d+')
self.assertRegexp('dicebot roll d20+5', r'\[1d20\+5\] \d+')
self.assertRegexp('dicebot roll 1d20-30', r'\[1d20-30\] -\d+')
self.assertRegexp('dicebot roll d20-30', r'\[1d20-30\] -\d+')
self.assertRegexp('dicebot roll 2d20-1', r'\[2d20-1\] \d+')
self.assertRegexp('dicebot roll d20-1d6+3', r'\[1d20-1d6\+3\] -?\d+')
self.assertRegexp('dicebot roll 1d20+d20+3', r'\[2d20\+3\] \d+')
self.assertRegexp('dicebot roll 1d20+4+d6-3', r'\[1d20\+1d6\+1\] \d+')
self.assertNoResponse('dicebot roll 1d1')
def testRollMult(self):
self.assertRegexp('dicebot roll 2#1d20', r'\[1d20\] \d+, \d+')
self.assertRegexp('dicebot roll 2#d20', r'\[1d20\] \d+, \d+')
self.assertRegexp('dicebot roll 2#1d20+5', r'\[1d20\+5\] \d+, \d+')
self.assertRegexp('dicebot roll 2#d20+5', r'\[1d20\+5\] \d+, \d+')
self.assertRegexp('dicebot roll 2#1d20-30', r'\[1d20-30\] -\d+, -\d+')
self.assertRegexp('dicebot roll 2#d20-30', r'\[1d20-30\] -\d+, -\d+')
self.assertRegexp('dicebot roll 2#2d20-1', r'\[2d20-1\] \d+, \d+')
self.assertNoResponse('dicebot roll 2#1d1')
def testRollSR(self):
self.assertRegexp('dicebot roll 2#sd', r'\(pool 2\) (\d hits?|critical glitch!)')
self.assertRegexp('dicebot roll 4#sd', r'\(pool 4\) (\d hits?(, glitch)?|critical glitch!)')
self.assertNoResponse('dicebot roll 0#sd')
def testRollSRX(self):
self.assertRegexp('dicebot roll 2#sdx', r'\(pool 2, exploding\) (\d hits?|critical glitch!)')
self.assertRegexp('dicebot roll 4#sdx', r'\(pool 4, exploding\) (\d hits?(, glitch)?|critical glitch!)')
self.assertNoResponse('dicebot roll 0#sdx')
def testRoll7S(self):
self.assertRegexp('dicebot roll 3k2', r'\[3k2\] \(\d+\) \d+, \d+')
self.assertRegexp('dicebot roll 2k3', r'\[2k2\] \(\d+\) \d+, \d+')
self.assertRegexp('dicebot roll 3kk2', r'\[3k2\] \(\d+\) \d+, \d+ \| \d+')
self.assertRegexp('dicebot roll +3k2', r'\[3k2\] \(\d+\) \d+, \d+ \| \d+')
self.assertRegexp('dicebot roll -3k2', r'\[3k2, not exploding\] \(\d+\) \d+, \d+')
self.assertRegexp('dicebot roll +3kk2', r'\[3k2\] \(\d+\) \d+, \d+ \| \d+')
self.assertRegexp('dicebot roll -3kk2', r'\[3k2, not exploding\] \(\d+\) \d+, \d+ \| \d+')
self.assertRegexp('dicebot roll 3k2+1', r'\[3k2\+1\] \(\d+\) \d+, \d+')
self.assertRegexp('dicebot roll 3k2-1', r'\[3k2-1\] \(\d+\) \d+, \d+')
self.assertRegexp('dicebot roll -3k2-1', r'\[3k2-1, not exploding\] \(\d+\) \d+, \d+')
self.assertRegexp('dicebot roll 10k10', r'\[10k10\] \(\d+\) (\d+, ){9}\d+')
self.assertRegexp('dicebot roll 12k10', r'\[10k10\+20\] \(\d+\) (\d+, ){9}\d+')
self.assertRegexp('dicebot roll 12k9', r'\[10k10\+10\] \(\d+\) (\d+, ){9}\d+')
self.assertRegexp('dicebot roll 12k8', r'\[10k10\] \(\d+\) (\d+, ){9}\d+')
self.assertRegexp('dicebot roll 12k9+5', r'\[10k10\+15\] \(\d+\) (\d+, ){9}\d+')
self.assertRegexp('dicebot roll 12kk9', r'\[10k10\+10\] \(\d+\) (\d+, ){9}\d+')
self.assertRegexp('dicebot roll 12kk7', r'\[10k9\] \(\d+\) (\d+, ){8}\d+ \| \d+')
self.assertRegexp('dicebot roll 3#3k2', r'\[3k2\] \(\d+\) \d+, \d+(; \(\d+\) \d+, \d+){2}')
def testDeck(self):
validator = r'(2|3|4|5|6|7|8|9|10|J|Q|K|A)(♣|♦|♥|♠)|(Black|Red) Joker'
self.assertRegexp('dicebot draw', validator)
self.assertResponse('dicebot shuffle', 'shuffled')
for i in range(0, 54):
self.assertRegexp('dicebot draw', validator)
def testWoD(self):
self.assertRegexp('dicebot roll 3w', r'\(3\) (\d success(es)?|FAIL)')
self.assertRegexp('dicebot roll 3w-', r'\(3, not exploding\) (\d success(es)?|FAIL)')
self.assertRegexp('dicebot roll 3w8', r'\(3, 8-again\) (\d success(es)?|FAIL)')
self.assertNoResponse('dicebot roll 0w')
def testDH(self):
self.assertRegexp('dicebot roll vs(10)', r'-?\d+ \(\d+ vs 10\)')
self.assertRegexp('dicebot roll vs(10+20)', r'-?\d+ \(\d+ vs 30\)')
self.assertRegexp('dicebot roll vs(10+20-5)', r'-?\d+ \(\d+ vs 25\)')
self.assertRegexp('dicebot roll 3vs(10+20)', r'-?\d+, -?\d+, -?\d+ \(\d+, \d+, \d+ vs 30\)')
def testWG(self):
self.assertRegexp('dicebot roll 10#wg', r'\[pool 10\] \d+ icon\(s\): [❶❷❸❹❺❻] ([1-5➅] )*(\| Glory|\| Complication)?')
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

50
Dicebot/test_Raise.py Normal file
View File

@ -0,0 +1,50 @@
###
# Copyright (c) 2018, Anatoly Popov
# Copyright (c) 2018, Andrey Rahmatullin
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import random
import pytest
from .sevenSea2EdRaiseRoller import Raise, RollResult
class TestRaise:
def test_str_no_raises(self):
x = Raise(rolls=[10])
assert str(x) == "(10)"
def test_str_some_raises(self):
x = Raise(2, rolls=[10, 5])
assert str(x) == "**(10 + 5)"
def test_str_no_raises_complex_roll(self):
x = Raise(rolls=[RollResult(1, lash_count=5)])
assert str(x) == "(0 [1])"
def test_str_some_raises_complex_roll(self):
x = Raise(2, rolls=[10, RollResult(1, joie_de_vivre_target=5)])
assert str(x) == "**(10 + 10 [1])"

View File

@ -0,0 +1,82 @@
###
# Copyright (c) 2018, Anatoly Popov
# Copyright (c) 2018, Andrey Rahmatullin
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import random
import pytest
from .sevenSea2EdRaiseRoller import RollResult
class TestRollResult:
def test_default(self):
x = RollResult(1, 0, 0)
assert x.value == 1
assert x.result == 1
def test_joie_de_vivre(self):
x = RollResult(1, joie_de_vivre_target=1)
assert x.value == 10
assert x.result == 1
x = RollResult(2, joie_de_vivre_target=1)
assert x.value == 2
assert x.result == 2
def test_lashes(self):
x = RollResult(1, lash_count=2)
assert x.value == 0
assert x.result == 1
x = RollResult(2, lash_count=2)
assert x.value == 2
assert x.result == 2
def test_lashes_precede_joie_de_vivre(self):
x = RollResult(1, lash_count=2, joie_de_vivre_target=1)
assert x.value == 0
assert x.result == 1
def test_joie_de_vivre_works_greater_lashes(self):
x = RollResult(3, lash_count=2, joie_de_vivre_target=5)
assert x.value == 10
assert x.result == 3
x = RollResult(1, lash_count=2, joie_de_vivre_target=5)
assert x.value == 0
assert x.result == 1
def test_output_no_changes(self):
x = RollResult(3)
assert str(x) == '3'
def test_output_any_change(self):
x = RollResult(3, lash_count=5)
assert str(x) == '0 [3]'
x = RollResult(3, joie_de_vivre_target=5)
assert str(x) == '10 [3]'

145
Dicebot/test_Roller.py Normal file
View File

@ -0,0 +1,145 @@
###
# Copyright (c) 2018, Anatoly Popov
# Copyright (c) 2018, Andrey Rahmatullin
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import random
import pytest
from .sevenSea2EdRaiseRoller import Raise, RollResult, SevenSea2EdRaiseRoller
class TestRoller:
def test_zero_dice(self):
x = SevenSea2EdRaiseRoller(lambda x: range(1, x+1)).roll_and_count(0)
assert len(x.raises) == 0
assert len(x.unused) == 0
assert str(x) == "0 raises"
def test_zero_raises_one_dice(self):
x = SevenSea2EdRaiseRoller(lambda x: range(1, x+1)).roll_and_count(1)
assert len(x.raises) == 0
assert len(x.unused) == 1
assert str(x) == "0 raises, unused: 1"
def test_green(self):
x = SevenSea2EdRaiseRoller(lambda x: range(1, x+1)).roll_and_count(4)
assert len(x.raises) == 1
assert len(x.unused) == 0
assert str(x) == "1 raise: *(4 + 3 + 2 + 1)"
def test_explode(self):
rolls = SevenSea2EdRaiseRoller(ExplodingRoller().roll).roll(1)
assert ', '.join(map(str, rolls)) == "10"
rolls = SevenSea2EdRaiseRoller(ExplodingRoller().roll, explode=True).roll(1)
assert ', '.join(map(str, rolls)) == "10, 5x"
rolls = SevenSea2EdRaiseRoller(ExplodingRoller(3).roll, explode=True).roll(1)
assert ', '.join(map(str, rolls)) == "10, 10x, 10xx, 5xxx"
rolls = SevenSea2EdRaiseRoller(ExplodingRoller(3).roll, explode=True).roll(3)
assert ', '.join(map(str, rolls)) == "10, 10, 10, 5x, 10x, 10x, 10xx, 5xx, 10xxx, 10xxxx, 10xxxxx, 5xxxxxx"
def test_big_skill(self):
rolls = SevenSea2EdRaiseRoller(
RerollRoller([8, 6, 1, 8, 5, 2, 4]).roll,
skill_rank=7
).roll_and_count(7)
assert str(rolls) == "4 raises: **(8 + 6 + 1), **(8 + 5 + 2), unused: 4, discarded: 1r"
def test_nines_without_ones(self):
rolls = SevenSea2EdRaiseRoller(
RerollRoller([10, 9, 9, 9, 8, 7, 6, 2]).roll,
skill_rank=3
).roll_and_count(8)
assert str(rolls) == "4 raises: *(10), *(9 + 2), *(9 + 6), *(9 + 7), unused: 8, discarded: 1r"
def test_discard_one_of_the_initial(self):
rolls = SevenSea2EdRaiseRoller(
RerollRoller([10, 9, 9, 9, 8, 7, 6, 2], [10, 5]).roll,
skill_rank=3
).roll_and_count(8)
assert str(rolls) == "5 raises: *(10r), *(10), *(9 + 6), *(9 + 7), *(9 + 8), discarded: 2"
def test_discard_one_of_the_initial_explode(self):
rolls = SevenSea2EdRaiseRoller(
RerollRoller([10, 9, 9, 9, 8, 7, 6, 2], [10, 5]).roll,
skill_rank=3,
explode=True
).roll_and_count(7)
assert str(rolls) == "5 raises: *(10r), *(10), *(9 + 5rx), *(9 + 6), *(9 + 7), unused: 8, discarded: 2x"
def test_optimal_solution_is_one_step_up(self):
rolls = SevenSea2EdRaiseRoller(
RerollRoller([10, 10, 10, 10, 5, 5, 5, 4, 4, 7, 6]).roll,
skill_rank=5
).roll_and_count(7)
assert str(rolls) == "10 raises: **(10 + 5), **(10 + 5), **(10 + 5), **(10 + 6x), **(7x + 4x + 4x), discarded: 1r"
def test_optimal_solution_is_one_step_up2(self):
rolls = SevenSea2EdRaiseRoller(
RerollRoller([10, 5, 10, 5, 6, 4, 3, 4, 3], [2]).roll,
skill_rank=5
).roll_and_count(7)
assert str(rolls) == "6 raises: **(10 + 5), **(10 + 5), **(6 + 4x + 4 + 3x), unused: 3, discarded: 2r"
# will wait boosting trees
# def test_optimal_solution_is_one_step_up3(self):
# rolls = SevenSea2EdRaiseRoller(
# RerollRoller([7, 6, 3, 1, 6, 4, 4]).roll,
# skill_rank=5
# ).roll_and_count(7)
# assert str(rolls) == "4 raises: **(7 + 6 + 3), **(6 + 4 + 4 + 1), discarded: 1"
class Roller:
def roll(self, count):
return [next(self) for _ in range(count)]
class RerollRoller(Roller):
def __init__(self, result, reroll_result=[1]):
self.result = result + reroll_result
self.index = 0
def __next__(self):
self.index %= len(self.result)
value = self.result[self.index]
self.index += 1
return value
class ExplodingRoller(Roller):
def __init__(self, ten_count=1, default_value=5):
self.ten_count = ten_count
self.current_ten_count = ten_count
self.default_value = default_value
def __next__(self):
if self.current_ten_count == 0:
self.current_ten_count = self.ten_count
return self.default_value
else:
self.current_ten_count -= 1
return 10

View File

@ -1 +0,0 @@
simplejson