diff --git a/src/tomservobot/chance.py b/src/tomservobot/chance.py index 7059f67..4322659 100644 --- a/src/tomservobot/chance.py +++ b/src/tomservobot/chance.py @@ -1,16 +1,20 @@ +""" +Chance - Commands for simulating 'random' chance, such as dice rolls, picking +cards from a deck, etc. +""" + import asyncio from random import choice, randint import re -import niobot +import niobot # type: ignore from niobot import ( CommandParserError, Context, Module, - NioBot, ) -from .stubs import ERROR_STARTS, get_random_stub, format_mention +from .stubs import ERROR_STARTS, get_random_stub _DICE_LOCATIONS = [ @@ -19,20 +23,44 @@ _DICE_LOCATIONS = [ "spins around and a number of dice appear in his hands.", "opens the closest drawer and takes out a container of random dice.", "powers on a tablet and executes a dice-rolling application.", + "steals several dice from various board game boxes on the shelf.", +] + +_DICE_EXCLAMATIONS = [ + "Daddy needs a new pair of shoes!", + "Blessed Mother of Acceleration, don't fail me now!", + "Come on, Box Car Willy!", + "I cast Magic Missle into the darkness!", + "Lightning Bolt! Lightning Bolt! SLEEEP!", + "RNGesus--please bless this roll we are about to receive.", + "I'm going to backstab with a ballista!", + "Who wants to blow on my dice!? anyone!? ...guys?", ] class ChanceModule(Module): """Commands that simulate chance (Dice rolls, card selection, etc).""" - def __init__(self, bot: NioBot): - self.bot = bot - @niobot.command() async def roll(self, ctx: Context, dice: str): - """Roll some dice with an optional modifier.""" + """ + Roll some dice with an optional modifier. + + Syntax: + !roll d[<+,-,*,/>] + + n = number of dice (1-100) + s = number of sides on dice (2-100) + m = optional modifier (1-1000000) + + Examples: + !roll 1d20 + !roll 6d6+3 + """ err_prelude = get_random_stub(ERROR_STARTS, ctx.message.sender) - valid = re.match(r"(\d+)d(\d+)([\+-/\*]\d+)?", dice) + + # Validate the entire dice roll syntax + valid = re.match(r"^(\d+)d(\d+)([\+-/\*]\d+)?", dice) if valid is None: await ctx.client.send_message( ctx.room, @@ -42,8 +70,9 @@ class ChanceModule(Module): content_type="html", ) raise CommandParserError("Invalid dice roll notation.") - dice_parts = (int(valid.group(1)), int(valid.group(2)), valid.group(3)) + # Validate number of dice and dice surface count. + dice_parts = (int(valid.group(1)), int(valid.group(2)), valid.group(3)) if not 1 <= dice_parts[0] <= 100: await ctx.client.send_message( ctx.room, @@ -64,26 +93,100 @@ class ChanceModule(Module): ) raise CommandParserError("Dice must have between 2 and 100 faces.") + # Validate the modifier, if one was detected. + extra = dice.replace(f"{dice_parts[0]}d{dice_parts[1]}", "") + if dice_parts[2] is None and extra != "": + await ctx.client.send_message( + ctx.room, + ( + f"{err_prelude}that's an invalid modifier. I only take " + f"addition (+), subtraction (-), multiplication (*), and " + f"division (/). The number following must be between 1 and " + f"1000000." + ), + reply_to=ctx.message, + message_type="m.text", + content_type="html", + ) + raise CommandParserError("Invalid dice roll modifier.") + if dice_parts[2] is not None: + modifier = int(dice_parts[2][1:]) + if not 1 <= modifier <= 1000000: + await ctx.client.send_message( + ctx.room, + ( + f"{err_prelude}the modifier amount can only be between " + f"1 and 1000000." + ), + reply_to=ctx.message, + message_type="m.text", + content_type="html", + ) + raise CommandParserError("Modifier must be between 1 and 1000000.") + + # Tom gets the dice. + await ctx.client.send_message( + ctx.room, + f"{choice(_DICE_LOCATIONS)}", + message_type="m.emote", + content_type="html", + ) + + await asyncio.sleep(1) + + # Makes a silly comment before rolling. + await ctx.client.send_message( + ctx.room, + f"{choice(_DICE_EXCLAMATIONS)}", + message_type="m.text", + content_type="html", + ) + + await asyncio.sleep(1) + rolls: list[int] = [] for _ in range(dice_parts[0]): rolls.append(randint(1, dice_parts[1])) total = sum(rolls) - await ctx.client.send_message( - ctx.room, - choice(_DICE_LOCATIONS), - message_type="m.emote", - content_type="html", - ) - - # Give some delay to allow for reading the emote. - await asyncio.sleep(1) - - html_sender = format_mention(ctx.message.sender) - await ctx.client.send_message( - ctx.room, - f"{html_sender}: {dice} = {total} ({rolls})", - reply_to=ctx.message, - message_type="m.text", - content_type="html", - ) + # Then reveals the results. + if dice_parts[2] is not None: + match dice_parts[2][0]: + case "+": + action = "adds" + new_total = total + modifier + case "-": + action = "subtracts" + new_total = total - modifier + case "*": + action = "multiplies by" + new_total = total * modifier + case "/": + action = "divides by" + new_total = total // modifier + case _: + raise CommandParserError("Strange dice modifier parse error.") + await ctx.client.send_message( + ctx.room, + ( + f"rolls {dice_parts[0]}d{dice_parts[1]} " + f"with a count of {total}. He then " + f"{action} {modifier} for a final count " + f"of {new_total}." + ), + reply_to=ctx.message, + message_type="m.emote", + content_type="html", + ) + else: + # html_sender = format_mention(ctx.message.sender) + await ctx.client.send_message( + ctx.room, + ( + f"rolls {dice_parts[0]}d{dice_parts[1]} " + f"and the final count is {total}." + ), + reply_to=ctx.message, + message_type="m.emote", + content_type="html", + ) diff --git a/src/tomservobot/stubs.py b/src/tomservobot/stubs.py index 6f18fc4..8b27d00 100644 --- a/src/tomservobot/stubs.py +++ b/src/tomservobot/stubs.py @@ -1,3 +1,8 @@ +""" +Stubs - A collection of personalized message starters/finishers and functions to +give the bot some personality. +""" + from random import choice