mirror of
https://github.com/oddluck/limnoria-plugins.git
synced 2025-04-26 04:51:09 -05:00
Remove Unicode dependency, add Dicebot.
This commit is contained in:
parent
82fdf102d2
commit
f4497bf046
69
Dicebot/__init__.py
Normal file
69
Dicebot/__init__.py
Normal 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
49
Dicebot/config.py
Normal 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
73
Dicebot/deck.py
Normal 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
11
Dicebot/docs/7th Sea.txt
Normal 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
9
Dicebot/docs/DH.txt
Normal 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
30
Dicebot/docs/NEWS.txt
Normal 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
62
Dicebot/docs/README.txt
Normal 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.
|
||||
|
23
Dicebot/docs/Shadowrun.txt
Normal file
23
Dicebot/docs/Shadowrun.txt
Normal 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
511
Dicebot/plugin.py
Normal 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
6
Dicebot/readme.md
Normal 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
3
Dicebot/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
limnoria
|
||||
pylint
|
||||
pytest
|
260
Dicebot/sevenSea2EdRaiseRoller.py
Normal file
260
Dicebot/sevenSea2EdRaiseRoller.py
Normal 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
115
Dicebot/test.py
Normal 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
50
Dicebot/test_Raise.py
Normal 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])"
|
82
Dicebot/test_RollResult.py
Normal file
82
Dicebot/test_RollResult.py
Normal 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
145
Dicebot/test_Roller.py
Normal 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
|
@ -1 +0,0 @@
|
||||
simplejson
|
Loading…
x
Reference in New Issue
Block a user