diff --git a/Gwendolyn.py b/Gwendolyn.py index ce549b1..03e7b37 100644 --- a/Gwendolyn.py +++ b/Gwendolyn.py @@ -88,7 +88,7 @@ async def parseCommands(message,content): await message.channel.send(roll_dice(message.author.display_name, content.replace("roll",""))) except: logThis("Something fucked up (error code 400)",str(message.channel)) - await message.channel.send("Something fucked up (error code 400)") + await message.channel.send("Not a valid command (error code 400)") # Looks up a spell with the spellFunc function from funcs/lookup/lookuppy elif content.startswith("spell "): diff --git a/funcs/games/__init__.py b/funcs/games/__init__.py index aac0119..49deac1 100644 --- a/funcs/games/__init__.py +++ b/funcs/games/__init__.py @@ -7,4 +7,4 @@ from .trivia import triviaCountPoints, triviaStart, triviaAnswer from .blackjack import blackjackShuffle, blackjackStart, blackjackPlayerDrawHand, blackjackContinue, blackjackFinish, blackjackHit, blackjackStand, blackjackDouble, blackjackSplit from .fourInARow import parseFourInARow, fourInARowAI from .hex import parseHex, hexAI -from .monopoly import parseMonopoly, monopo;lyContinue \ No newline at end of file +from .monopoly import parseMonopoly, monopolyContinue \ No newline at end of file diff --git a/funcs/roll/__init__.py b/funcs/roll/__init__.py index 243ed83..e356957 100644 --- a/funcs/roll/__init__.py +++ b/funcs/roll/__init__.py @@ -1,4 +1,4 @@ -"""I stole this.""" +"""Rolling dice.""" __all__ = ["roll_dice"] diff --git a/funcs/roll/dice.py b/funcs/roll/dice.py index 4b331eb..9fe7761 100644 --- a/funcs/roll/dice.py +++ b/funcs/roll/dice.py @@ -1,560 +1,20 @@ -import random -import re -import traceback -from heapq import nlargest, nsmallest -from math import floor -from re import IGNORECASE +import d20 -import numexpr +class MyStringifier(d20.MarkdownStringifier): -from . import errors -#from funcs import logThis + def _str_expression(self, node): -VALID_OPERATORS = 'k|rr|ro|mi|ma|ra|e|p' -VALID_OPERATORS_ARRAY = VALID_OPERATORS.split('|') -VALID_OPERATORS_2 = re.compile('|'.join(["({})".format(i) for i in VALID_OPERATORS_ARRAY])) -DICE_PATTERN = re.compile( - r'^\s*(?:(?:(\d*d\d+)(?:(?:' + VALID_OPERATORS + r')(?:[lh<>]?\d+))*|(\d+)|([-+*/().=])?)\s*(\[.*\])?)(.*?)\s*$', - IGNORECASE) - - -def roll_dice(author : str, rollStr : str = "1d20"): - if rollStr == '0/0': # easter eggs - return("What do you expect me to do, destroy the universe?") - - adv = 0 - if re.search('(^|\s+)(adv|dis)(\s+|$)', rollStr) is not None: - adv = 1 if re.search('(^|\s+)adv(\s+|$)', rollStr) is not None else -1 - rollStr = re.sub('(adv|dis)(\s+|$)', '', rollStr) - res = roll(rollStr, adv=adv) - out = res.result - outStr = author + ' :game_die:\n' + out - if len(outStr) > 1999: - outputs = author + ' :game_die:\n[Output truncated due to length]\n**Result:** ' + str(res.plain) - else: - outputs = outStr - return(outputs) - -def list_get(index, default, l): - try: - a = l[index] - except IndexError: - a = default - return a - - -def roll(rollStr, adv: int = 0, rollFor='', inline=False, double=False, show_blurbs=True, **kwargs): - roller = Roll() - result = roller.roll(rollStr, adv, rollFor, inline, double, show_blurbs, **kwargs) - return result - - -def get_roll_comment(rollStr): - """Returns: A two-tuple (dice without comment, comment)""" - try: - comment = '' - no_comment = '' - dice_set = re.split('([-+*/().=])', rollStr) - dice_set = [d for d in dice_set if not d in (None, '')] - #logThis("Found dice set: " + str(dice_set)) - for index, dice in enumerate(dice_set): - match = DICE_PATTERN.match(dice) - #logThis("Found dice group: " + str(match.groups())) - no_comment += dice.replace(match.group(5), '') - if match.group(5): - comment = match.group(5) + ''.join(dice_set[index + 1:]) - break - - return no_comment, comment - except: - pass - return rollStr, '' - - -class Roll(object): - def __init__(self, parts=None): - if parts ==None: - parts = [] - self.parts = parts - - def get_crit(self): - """Returns: 0 for no crit, 1 for 20, 2 for 1.""" - try: - crit = next(p.get_crit() for p in self.parts if isinstance(p, SingleDiceGroup)) - except StopIteration: - crit = 0 - return crit - - def get_total(self): - """Returns: int""" - return numexpr.evaluate(''.join(p.get_eval() for p in self.parts if not isinstance(p, Comment))) - - # # Dice Roller - def roll(self, rollStr, adv: int = 0, rollFor='', inline=False, double=False, show_blurbs=True, **kwargs): - try: - if '**' in rollStr: - raise errors.InvalidArgument("Exponents are currently disabled.") - self.parts = [] - # split roll string into XdYoptsSel [comment] or Op - # set remainder to comment - # parse each, returning a SingleDiceResult - dice_set = re.split('([-+*/().=])', rollStr) - dice_set = [d for d in dice_set if not d in (None, '')] - #logThis("Found dice set: " + str(dice_set)) - for index, dice in enumerate(dice_set): - match = DICE_PATTERN.match(dice) - #logThis("Found dice group: " + str(match.groups())) - # check if it's dice - if match.group(1): - roll = self.roll_one(dice.replace(match.group(5), ''), adv) - self.parts.append(roll) - # or a constant - elif match.group(2): - self.parts.append(Constant(value=int(match.group(2)), annotation=match.group(4))) - # or an operator - elif not match.group(5): - self.parts.append(Operator(op=match.group(3), annotation=match.group(4))) - - if match.group(5): - self.parts.append(Comment(match.group(5) + ''.join(dice_set[index + 1:]))) - break - - # calculate total - crit = self.get_crit() - try: - total = self.get_total() - except SyntaxError: - raise errors.InvalidArgument("No dice found to roll.") - rolled = ' '.join(str(res) for res in self.parts if not isinstance(res, Comment)) - if rollFor =='': - rollFor = ''.join(str(c) for c in self.parts if isinstance(c, Comment)) - # return final solution - if not inline: - # Builds end result while showing rolls - reply = ' '.join( - str(res) for res in self.parts if not isinstance(res, Comment)) + '\n**Total:** ' + str( - floor(total)) - skeletonReply = reply - rollFor = rollFor if rollFor != '' else 'Result' - reply = '**{}:** '.format(rollFor) + reply - if show_blurbs: - if adv == 1: - reply += '\n**Rolled with Advantage**' - elif adv == -1: - reply += '\n**Rolled with Disadvantage**' - if crit == 1: - critStr = "\n_**Critical Hit!**_ " - reply += critStr - elif crit == 2: - critStr = "\n_**Critical Fail!**_ " - reply += critStr - else: - # Builds end result while showing rolls - reply = ' '.join(str(res) for res in self.parts if not isinstance(res, Comment)) + ' = `' + str( - floor(total)) + '`' - skeletonReply = reply - rollFor = rollFor if rollFor != '' else 'Result' - reply = '**{}:** '.format(rollFor) + reply - if show_blurbs: - if adv == 1: - reply += '\n**Rolled with Advantage**' - elif adv == -1: - reply += '\n**Rolled with Disadvantage**' - if crit == 1: - critStr = "\n_**Critical Hit!**_ " - reply += critStr - elif crit == 2: - critStr = "\n_**Critical Fail!**_ " - reply += critStr - reply = re.sub(' +', ' ', reply) - skeletonReply = re.sub(' +', ' ', str(skeletonReply)) - return DiceResult(result=int(floor(total)), verbose_result=reply, crit=crit, rolled=rolled, - skeleton=skeletonReply, raw_dice=self) - except Exception as ex: - if not isinstance(ex, (SyntaxError, KeyError, errors.AvraeException)): - #logThis('Error in roll() caused by roll {}:'.format(rollStr)) - traceback.print_exc() - return DiceResult(verbose_result="Invalid input: {}".format(ex)) - - def roll_one(self, dice, adv: int = 0): - result = SingleDiceGroup() - result.rolled = [] - # splits dice and comments - split = re.match(r'^([^\[\]]*?)\s*(\[.*\])?\s*$', dice) - dice = split.group(1).strip() - annotation = split.group(2) - result.annotation = annotation if annotation != None else '' - # Recognizes dice - obj = re.findall('\d+', dice) - obj = [int(x) for x in obj] - numArgs = len(obj) - - ops = [] - if numArgs == 1: - if not dice.startswith('d'): - raise errors.InvalidArgument('Please pass in the value of the dice.') - numDice = 1 - diceVal = obj[0] - if adv != 0 and diceVal == 20: - numDice = 2 - ops = ['k', 'h1'] if adv ==1 else ['k', 'l1'] - elif numArgs == 2: - numDice = obj[0] - diceVal = obj[-1] - if adv != 0 and diceVal == 20: - ops = ['k', 'h' + str(numDice)] if adv ==1 else ['k', 'l' + str(numDice)] - numDice = numDice * 2 - else: # split into xdy and operators - numDice = obj[0] - diceVal = obj[1] - dice = re.split('(\d+d\d+)', dice)[-1] - ops = VALID_OPERATORS_2.split(dice) - ops = [a for a in ops if a != None] - - # dice repair/modification - if numDice > 300 or diceVal < 1: - raise errors.InvalidArgument('Too many dice rolled.') - - result.max_value = diceVal - result.num_dice = numDice - result.operators = ops - - for _ in range(numDice): - try: - tempdice = SingleDice() - tempdice.value = random.randint(1, diceVal) - tempdice.rolls = [tempdice.value] - tempdice.max_value = diceVal - tempdice.kept = True - result.rolled.append(tempdice) - except: - result.rolled.append(SingleDice()) - - if ops != None: - - rerollList = [] - reroll_once = [] - keep = None - to_explode = [] - to_reroll_add = [] - - valid_operators = VALID_OPERATORS_ARRAY - last_operator = None - for index, op in enumerate(ops): - if last_operator != None and op in valid_operators and not op == last_operator: - result.reroll(reroll_once, 1) - reroll_once = [] - result.reroll(rerollList, greedy=True) - rerollList = [] - result.keep(keep) - keep = None - result.reroll(to_reroll_add, 1, keep_rerolled=True, unique=True) - to_reroll_add = [] - result.reroll(to_explode, greedy=True, keep_rerolled=True) - to_explode = [] - if op == 'rr': - rerollList += parse_selectors([list_get(index + 1, 0, ops)], result, greedy=True) - if op == 'k': - keep = [] if keep ==None else keep - keep += parse_selectors([list_get(index + 1, 0, ops)], result) - if op == 'p': - keep = [] if keep ==None else keep - keep += parse_selectors([list_get(index + 1, 0, ops)], result, inverse=True) - if op == 'ro': - reroll_once += parse_selectors([list_get(index + 1, 0, ops)], result) - if op == 'mi': - _min = list_get(index + 1, 0, ops) - for r in result.rolled: - if r.value < int(_min): - r.update(int(_min)) - if op == 'ma': - _max = list_get(index + 1, 0, ops) - for r in result.rolled: - if r.value > int(_max): - r.update(int(_max)) - if op == 'ra': - to_reroll_add += parse_selectors([list_get(index + 1, 0, ops)], result) - if op == 'e': - to_explode += parse_selectors([list_get(index + 1, 0, ops)], result, greedy=True) - if op in valid_operators: - last_operator = op - result.reroll(reroll_once, 1) - result.reroll(rerollList, greedy=True) - result.keep(keep) - result.reroll(to_reroll_add, 1, keep_rerolled=True, unique=True) - result.reroll(to_explode, greedy=True, keep_rerolled=True) - - return result - - -class Part: - """Class to hold one part of the roll string.""" - pass - - -class SingleDiceGroup(Part): - def __init__(self, num_dice: int = 0, max_value: int = 0, rolled=None, annotation: str = "", result: str = "", - operators=None): - if operators ==None: - operators = [] - if rolled ==None: - rolled = [] - self.num_dice = num_dice - self.max_value = max_value - self.rolled = rolled # list of SingleDice - self.annotation = annotation - self.result = result - self.operators = operators - - def keep(self, rolls_to_keep): - if rolls_to_keep ==None: return - for _roll in self.rolled: - if not _roll.value in rolls_to_keep: - _roll.kept = False - elif _roll.kept: - rolls_to_keep.remove(_roll.value) - - def reroll(self, rerollList, max_iterations=1000, greedy=False, keep_rerolled=False, unique=False): - if not rerollList: return # don't reroll nothing - minor optimization - if unique: - rerollList = list(set(rerollList)) # remove duplicates - if len(rerollList) > 100: - raise OverflowError("Too many dice to reroll (max 100)") - last_index = 0 - count = 0 - should_continue = True - while should_continue: # let's only iterate 250 times for sanity - should_continue = False - if any(d.value in set(rerollList) for d in self.rolled[last_index:] if d.kept and not d.exploded): - should_continue = True - to_extend = [] - for r in self.rolled[last_index:]: # no need to recheck everything - count += 1 - if count > max_iterations: - should_continue = False - if r.value in rerollList and r.kept and not r.exploded: - try: - tempdice = SingleDice() - tempdice.value = random.randint(1, self.max_value) - tempdice.rolls = [tempdice.value] - tempdice.max_value = self.max_value - tempdice.kept = True - to_extend.append(tempdice) - if not keep_rerolled: - r.drop() - else: - r.explode() - except: - to_extend.append(SingleDice()) - if not keep_rerolled: - r.drop() - else: - r.explode() - if not greedy: - rerollList.remove(r.value) - last_index = len(self.rolled) - self.rolled.extend(to_extend) - - def get_total(self): - """Returns: - int - The total value of the dice.""" - return sum(r.value for r in self.rolled if r.kept) - - def get_eval(self): - return str(self.get_total()) - - def get_num_kept(self): - return sum(1 for r in self.rolled if r.kept) - - def get_crit(self): - """Returns: - int - 0 for no crit, 1 for crit, 2 for crit fail.""" - if self.get_num_kept() == 1 and self.max_value == 20: - if self.get_total() == 20: - return 1 - elif self.get_total() == 1: - return 2 - return 0 - - def __str__(self): - return "{0.num_dice}d{0.max_value}{1} ({2}) {0.annotation}".format( - self, ''.join(self.operators), ', '.join(str(r) for r in self.rolled)) - - def to_dict(self): - return {'type': 'dice', 'dice': [d.to_dict() for d in self.rolled], 'annotation': self.annotation, - 'value': self.get_total(), 'is_crit': self.get_crit(), 'num_kept': self.get_num_kept(), - 'text': str(self), 'num_dice': self.num_dice, 'dice_size': self.max_value, 'operators': self.operators} - - -class SingleDice: - def __init__(self, value: int = 0, max_value: int = 0, kept: bool = True, exploded: bool = False): - self.value = value - self.max_value = max_value - self.kept = kept - self.rolls = [value] # list of ints (for X -> Y -> Z) - self.exploded = exploded - - def drop(self): - self.kept = False - - def explode(self): - self.exploded = True - - def update(self, new_value): - self.value = new_value - self.rolls.append(new_value) - - def __str__(self): - formatted_rolls = [str(r) for r in self.rolls] - if int(formatted_rolls[-1]) == self.max_value or int(formatted_rolls[-1]) == 1: - formatted_rolls[-1] = '**' + formatted_rolls[-1] + '**' - if self.exploded: - formatted_rolls[-1] = '__' + formatted_rolls[-1] + '__' - if self.kept: - return ' -> '.join(formatted_rolls) + if node.comment == None: + resultText = "Result" else: - return '~~' + ' -> '.join(formatted_rolls) + '~~' + resultText = node.comment.capitalize() - def __repr__(self): - return "".format( - self) + return f"**{resultText}**: {self._stringify(node.roll)}\n**Total**: {int(node.total)}" - def to_dict(self): - return {'type': 'single_dice', 'value': self.value, 'size': self.max_value, 'is_kept': self.kept, - 'rolls': self.rolls, 'exploded': self.exploded} +def roll_dice(user,rollString = "1d20"): + while len(rollString) > 1 and rollString[0] == " ": + rollString = rollString[1:] + return user+" :game_die:\n"+str(d20.roll(rollString, allow_comments=True,stringifier=MyStringifier())) -class Constant(Part): - def __init__(self, value: int = 0, annotation: str = ""): - self.value = value - self.annotation = annotation if annotation != None else '' - def __str__(self): - return "{0.value} {0.annotation}".format(self) - - def get_eval(self): - return str(self.value) - - def to_dict(self): - return {'type': 'constant', 'value': self.value, 'annotation': self.annotation} - - -class Operator(Part): - def __init__(self, op: str = "+", annotation: str = ""): - self.op = op if op != None else '' - self.annotation = annotation if annotation != None else '' - - def __str__(self): - return "{0.op} {0.annotation}".format(self) - - def get_eval(self): - return self.op - - def to_dict(self): - return {'type': 'operator', 'value': self.op, 'annotation': self.annotation} - - -class Comment(Part): - def __init__(self, comment: str = ""): - self.comment = comment - - def __str__(self): - return self.comment.strip() - - def to_dict(self): - return {'type': 'comment', 'value': self.comment} - - -def parse_selectors(opts, res, greedy=False, inverse=False): - """Returns a list of ints.""" - for o in range(len(opts)): - if opts[o][0] =='h': - opts[o] = nlargest(int(opts[o].split('h')[1]), (d.value for d in res.rolled if d.kept)) - elif opts[o][0] =='l': - opts[o] = nsmallest(int(opts[o].split('l')[1]), (d.value for d in res.rolled if d.kept)) - elif opts[o][0] =='>': - if greedy: - opts[o] = list(range(int(opts[o].split('>')[1]) + 1, res.max_value + 1)) - else: - opts[o] = [d.value for d in res.rolled if d.value > int(opts[o].split('>')[1])] - elif opts[o][0] =='<': - if greedy: - opts[o] = list(range(1, int(opts[o].split('<')[1]))) - else: - opts[o] = [d.value for d in res.rolled if d.value < int(opts[o].split('<')[1])] - out = [] - for o in opts: - if isinstance(o, list): - out.extend(int(l) for l in o) - elif not greedy: - out.extend(int(o) for a in res.rolled if a.value ==int(o) and a.kept) - else: - out.append(int(o)) - - if not inverse: - return out - - inverse_out = [] - for rolled in res.rolled: - if rolled.kept and rolled.value in out: - out.remove(rolled.value) - elif rolled.kept: - inverse_out.append(rolled.value) - return inverse_out - - -class DiceResult: - """Class to hold the output of a dice roll.""" - - def __init__(self, result: int = 0, verbose_result: str = '', crit: int = 0, rolled: str = '', skeleton: str = '', - raw_dice: Roll = None): - self.plain = result - self.total = result - self.result = verbose_result - self.crit = crit - self.rolled = rolled - self.skeleton = skeleton if skeleton != '' else verbose_result - self.raw_dice = raw_dice # Roll - - def __str__(self): - return self.result - - def __repr__(self): - return ''.format(self.total) - - def consolidated(self): - """Gets the most simplified version of the roll string.""" - if self.raw_dice ==None: - return "0" - parts = [] # list of (part, annotation) - last_part = "" - for p in self.raw_dice.parts: - if isinstance(p, SingleDiceGroup): - last_part += str(p.get_total()) - else: - last_part += str(p) - if not isinstance(p, Comment) and p.annotation: - parts.append((last_part, p.annotation)) - last_part = "" - if last_part: - parts.append((last_part, "")) - - to_roll = "" - last_annotation = "" - out = "" - for numbers, annotation in parts: - if annotation and annotation != last_annotation and to_roll: - out += f"{roll(to_roll).total:+} {last_annotation}" - to_roll = "" - if annotation: - last_annotation = annotation - to_roll += numbers - if to_roll: - out += f"{roll(to_roll).total:+} {last_annotation}" - out = out.strip('+ ') - return out - - -if __name__ == '__main__': - while True: - print(roll(input().strip())) diff --git a/funcs/roll/errors.py b/funcs/roll/errors.py deleted file mode 100644 index 0f78629..0000000 --- a/funcs/roll/errors.py +++ /dev/null @@ -1,186 +0,0 @@ -class AvraeException(Exception): - - """A base exception class.""" - - def __init__(self, msg): - - """A.""" - super().__init__(msg) - - -class NoCharacter(AvraeException): - - """Raised when a user has no active character.""" - - def __init__(self): - - """A.""" - super().__init__("You have no character active.") - - -class NoActiveBrew(AvraeException): - - """Raised when a user has no active homebrew of a certain type.""" - - def __init__(self): - super().__init__("You have no homebrew of this type active.") - - -class ExternalImportError(AvraeException): - - """Raised when something fails to import.""" - - def __init__(self, msg): - super().__init__(msg) - - -class InvalidArgument(AvraeException): - - """Raised when an argument is invalid.""" - pass - - -class EvaluationError(AvraeException): - - """Raised when a cvar evaluation causes an error.""" - - def __init__(self, original): - super().__init__(f"Error evaluating expression: {original}") - self.original = original - - -class FunctionRequiresCharacter(AvraeException): - - """Raised when a function that requires a character is called without one.""" - - def __init__(self, msg=None): - super().__init__(msg or "This alias requires an active character.") - - -class OutdatedSheet(AvraeException): - - """Raised when a feature is used that requires an updated sheet.""" - - def __init__(self, msg=None): - super().__init__(msg or "This command requires an updated character sheet. Try running `!update`.") - - -class NoSpellDC(AvraeException): - def __init__(self): - super().__init__("No spell save DC found.") - - -class NoSpellAB(AvraeException): - def __init__(self): - super().__init__("No spell attack bonus found.") - - -class InvalidSaveType(AvraeException): - def __init__(self): - super().__init__("Invalid save type.") - - -class ConsumableException(AvraeException): - - """A base exception for consumable exceptions to stem from.""" - pass - - -class ConsumableNotFound(ConsumableException): - - """Raised when a consumable is not found.""" - - def __init__(self): - super().__init__("The requested counter does not exist.") - - -class CounterOutOfBounds(ConsumableException): - - """Raised when a counter is set to a value out of bounds.""" - - def __init__(self): - super().__init__("The new value is out of bounds.") - - -class NoReset(ConsumableException): - - """Raised when a consumable without a reset is reset.""" - - def __init__(self): - super().__init__("The counter does not have a reset value.") - - -class InvalidSpellLevel(ConsumableException): - - """Raised when a spell level is invalid.""" - - def __init__(self): - super().__init__("The spell level is invalid.") - - -class SelectionException(AvraeException): - - """A base exception for message awaiting exceptions to stem from.""" - pass - - -class NoSelectionElements(SelectionException): - - """Raised when get_selection() is called with no choices.""" - - def __init__(self, msg=None): - super().__init__(msg or "There are no choices to select from.") - - -class SelectionCancelled(SelectionException): - - """Raised when get_selection() is cancelled or times out.""" - - def __init__(self): - super().__init__("Selection timed out or was cancelled.") - - -class CombatException(AvraeException): - - """A base exception for combat-related exceptions to stem from.""" - pass - - -class CombatNotFound(CombatException): - - """Raised when a channel is not in combat.""" - - def __init__(self): - super().__init__("This channel is not in combat.") - - -class RequiresContext(CombatException): - - """Raised when a combat is committed without context.""" - - def __init__(self): - super().__init__("Combat not contextualized.") - - -class ChannelInCombat(CombatException): - - """Raised when a combat is started with an already active combat.""" - - def __init__(self): - super().__init__("Channel already in combat.") - - -class CombatChannelNotFound(CombatException): - - """Raised when a combat's channel is not in the channel list.""" - - def __init__(self): - super().__init__("Combat channel does not exist.") - - -class NoCombatants(CombatException): - - """Raised when a combat tries to advance turn with no combatants.""" - - def __init__(self): - super().__init__("There are no combatants.")