support more dice notation

squashed commits:
- fix unpacking regex matches for dice notation
- fix missing cast to integer
- fix raw result output
- fix formatting of chat message for raw result
- faster dockerfile builds?
- fancier output format
- fix variable name collision
- remove unnecessary newline
- more robust handling of dF
- fix typo
- fix formatting
- use blank character for dF
This commit is contained in:
a 2023-01-11 05:44:04 -06:00
parent 8aaf150f14
commit 6a7ec8eef7
3 changed files with 192 additions and 20 deletions

View File

@ -1,7 +1,8 @@
FROM archlinux:latest
WORKDIR /umi
COPY . .
COPY "requirements.txt" .
RUN pacman -Syu --noconfirm
RUN pacman -S --noconfirm python-pip libffi libsodium ffmpeg
RUN pip install -r requirements.txt
COPY . .
CMD ["python", "-u", "app.py"]

View File

@ -5,6 +5,7 @@ import httpx
from decouple import config
from collections import Counter
import re
from typing import Iterator
GUILD = config("DISCORD_GUILD_ID", cast=int)
@ -20,31 +21,193 @@ class Random(Cog):
# ==============================================================================
@staticmethod
def roll(faces: int = 6, number: int = 1) -> str:
"""Roll die(s) using random.org integer generation"""
def roll(
number: int = 1,
faces: int = 6,
) -> str:
"""
Roll standard dice using random.org integer generation
Params:
int number How many dice to roll
int faces How many faces on a standard die
"""
columns = number if number < 11 else 10
return httpx.get(f'https://www.random.org/integers/?num={number}&min=1&max={faces}&col={columns}&base=10&format=plain&rnd=new').text
@staticmethod
def roll_results(faces: int = 6, number: int = 1) -> str:
def parse_dice_notation(input: str) -> Iterator[re.Match[str]]:
"""
Parse dice notation to get numbers, faces, modifiers, and tokens.
https://en.wikipedia.org/wiki/Dice_notation
Dice notation can take the form of AdX+B (chainable):
A how many dice to roll (if not provided, roll once)
dX die with X faces (1-X)
+ add the result to the previous result
- subtract the result from the previous result
B constant modifier
Extensions implemented:
d(a,b,...) custom die with faces defined by a sequence
d% special die with 00-99
dF fudge dice (two +, two -, two blanks)
dF.1 fudge dice variant (one +, one -, four blanks)
dF.2 alias for dF
Unimplemented:
-L subtract the lowest
-H subtract the highest
klL keep lowest L values
khH keep highest H values
xC multiply the result by C
Cx(AdX+B) multiply the dice group by C (including modifier)
/C divide by C?
Params:
str input some dice notation to parse
Returns:
tuple(plus_minus_dice, number, faces, plus_minus_mod, modifier)
str plus_minus_dice a token for adding or removing from total
str number the number of dice to roll
str faces the faces on the dice (sequence or number)
---
str plus_minus_mod a token for adding or subtracting a modifier
str modifier some number to add/subtract from total
"""
pattern = re.compile(
r'''
# dice group
(\A|\+|\-) # start-of-string or plus or minus dice result (group 1)
(\d+)? # number of dice (group 2)
d # the literal 'd'
( # faces (group 3)
\d+ # either plain number
|\(.+?\) # or sequence of strings
|\[\d+:\d+(?:\d+)?\] # or range of numbers as start:end:step
|% # or literal '%' (for 00-99)
|F(?:\.\d+?)? # or literal 'F' (for fudge dice ++--__)
)
# OR
|
# number modifier
(\+|\-) # plus or minus (group 4)
(\d+)? # some number value (group 5)
''',
re.IGNORECASE | re.VERBOSE
)
# returns (plus_minus_dice, number, faces, plus_minus_mod, modifier)
return pattern.finditer(input)
@staticmethod
def roll_result(roll: str) -> str:
"""Calculates the result of a roll and output a summary"""
matches = Random.parse_dice_notation(roll)
total = 0
roll_results = []
for match in matches:
plus_minus_dice, number, faces, plus_minus_mod, modifier = match.groups()
if faces and faces[0] == '(': # non-standard dice sequence (a,b,c,...).
number = int(number) if number else 1 # number may be omitted for single die.
outcomes = faces[1:-1].split(',') # remove parens and get list of faces,
result = Random.roll(number, len(outcomes)) # then roll for an index in that list
result = [outcomes[int(r)-1] for r in result.split()] # that we can then lookup.
try: # we don't actually know if the faces are numeric...
subtotal = sum(map(int, result))
if plus_minus_dice == '-':
total -= subtotal
else:
total += subtotal
except (ValueError, TypeError): # in case faces were not all numeric (for this group or a previous group),
total = None # there is no total.
result = ' '.join(result) # finally, repack the result
roll_results.append( (f'{number}d{faces}', result) ) # and append it to output
elif faces and faces == '%': # special 00-99
number = int(number) if number else 1 # number may be omitted for single die.
result = Random.roll(number, 100) # we roll a d100,
result = re.sub('100', '00', result) # but we sub in 00
result = re.sub(r'\b(\d){1}\b', r'0\1', result) # and zero pad single digits.
try: # this may be part of a larger group of rolls...
subtotal = sum(map(int, result.split()))
if plus_minus_dice == '-':
total -= int(subtotal) # this may fail because total is None
else:
total += int(subtotal) # this may fail because total is None
except (ValueError, TypeError):
total = None
roll_results.append( (f'{number}d{faces}', result) ) # and append it to output
elif faces and faces[0] == 'F': # fudge dice
number = int(number) if number else 1 # number may be omitted for single die.
if faces == 'F' or faces == 'F.2':
outcomes = '(+,+,-,-,␢,␢)'
elif faces == 'F.1':
outcomes = '(+,+,-,-,␢,␢)'
outcomes = outcomes[1:-1].split(',') # remove parens and get list of faces,
result = Random.roll(number, len(outcomes)) # then roll for an index in that list
result = [outcomes[int(r)-1] for r in result.split()] # that we can then lookup.
try: # we don't actually know if the faces are numeric...
subtotal = sum(map(int, result))
if plus_minus_dice == '-':
total -= subtotal
else:
total += subtotal
except (ValueError, TypeError): # in case faces were not all numeric (for this group or a previous group),
total = None # there is no total.
result = ' '.join(result) # finally, repack the result
roll_results.append( (f'{number}d{faces}', f'{result}\n') ) # and append it to output
elif faces: # standard dice
number = int(number) if number else 1 # number may be omitted for single die.
result = Random.roll(number, faces)
try: # this may be part of a larger group of rolls...
subtotal = sum(map(int, result.split()))
if plus_minus_dice == '-':
total -= subtotal # this may fail because total is None
else:
total += subtotal # this may fail because total is None
except (ValueError, TypeError):
total = None
roll_results.append( (f'{number}d{faces}', result) ) # and append it to output
elif modifier: # no dice
try: # this may be part of a larger group of rolls...
if plus_minus_mod == '-':
total -= int(modifier) # this may fail because total is None
elif plus_minus_mod == '+':
total += int(modifier) # this may fail because total is None
except (ValueError, TypeError):
total = None
raw_result_list = []
for raw_roll in roll_results:
raw_result_list.append(f'=== {raw_roll[0]} ===\n**{raw_roll[1]}**')
raw = ''.join(raw_result_list)
output = f"*You rolled {roll} and got **{total}**.*\n\nRaw result:\n{raw}"
if total is None:
output = f"*You rolled {roll} and got:*\n\n{raw}"
return output
@staticmethod
def rolls_results(rolls: str) -> str:
"""Returns a formatted result of dice outcomes"""
result = Random.roll(faces=int(faces), number=int(number))
if faces == 100: # 00 - 99
result = re.sub('100', '00', result)
result = re.sub(r'\b(\d){1}\b', r'0\1', result) # zero pad single digit
return f"*You rolled {number}d{faces} and got:*\n**{result}**"
results = [Random.roll_result(roll.strip()) for roll in rolls.split(';')]
return '\n'.join(results)
@command(
name='roll',
aliases=['dice']
)
async def roll_prefix(self, ctx: Context, query: str = '1d6'):
async def roll_prefix(self, ctx: Context, rolls: str = '1d6'):
"""Roll some dice (default 1d6)"""
number, faces = query.split('d')
if not number: # handle raw dN rolls, e.g. 'd6' or 'd20'
number = 1
return await ctx.send(Random.roll_results(faces=int(faces), number=int(number)))
return await ctx.send(Random.rolls_results(rolls))
@slash_command(
name='roll',
guild_ids=[GUILD]
@ -52,10 +215,15 @@ class Random(Cog):
async def roll_slash(self,
ctx: discord.ApplicationContext,
faces: Option(int, "How many faces are on each die?", min_value=1, max_value=1_000_000_000, default=6),
number: Option(int, "How many dies should be rolled?", min_value=1, max_value=10_000, default=1)
number: Option(int, "How many dies should be rolled?", min_value=1, max_value=10_000, default=1),
modifier: Option(int, "Add this number to the final result", default=0),
):
"""Roll some dice (default 1d6)"""
return await ctx.respond(Random.roll_results(faces=faces, number=number))
"""Roll some standard dice with an optional modifier (default 1d6 with +0)"""
if modifier and modifier > 0:
modifier = f'+{modifier}'
return await ctx.respond(
Random.rolls_results(f'{number}d{faces}{modifier}')
)
# ==============================================================================

View File

@ -284,8 +284,8 @@ dependencies = [
]
[metadata]
lock_version = "4.0"
content_hash = "sha256:889439b64bada10d9afdf954f0b552fb3a224b91dfac0b20c3531ea0fdf2c69d"
lock_version = "4.1"
content_hash = "sha256:3a6be9c0370a67e849f5f92299e3bc8d4bb2f81a009c80ba91f75a2978b75677"
[metadata.files]
"aiodns 3.0.0" = [
@ -958,7 +958,9 @@ content_hash = "sha256:889439b64bada10d9afdf954f0b552fb3a224b91dfac0b20c3531ea0f
{url = "https://files.pythonhosted.org/packages/85/c9/d465977afb008f0721e63fdf9a0e8e37bf6f188dc3c787b6ec9568621a48/pycryptodomex-3.15.0-cp35-abi3-win32.whl", hash = "sha256:46b3f05f2f7ac7841053da4e0f69616929ca3c42f238c405f6c3df7759ad2780"},
{url = "https://files.pythonhosted.org/packages/89/71/bd68f1c0654ee9c82a841feb41b8e05328ff4c154eeae98508e51d163256/pycryptodomex-3.15.0-cp35-abi3-manylinux2010_i686.whl", hash = "sha256:65204412d0c6a8e3c41e21e93a5e6054a74fea501afa03046a388cf042e3377a"},
{url = "https://files.pythonhosted.org/packages/89/d2/1666f32e1ab5c2716447821800ecb39764d00453fddb23d45fee4d422bde/pycryptodomex-3.15.0-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:996e1ba717077ce1e6d4849af7a1426f38b07b3d173b879e27d5e26d2e958beb"},
{url = "https://files.pythonhosted.org/packages/8a/c8/2fa65115759682e9505752e188a038d5cb4ea2f88ff4dba85ca0bfcc21f3/pycryptodomex-3.15.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:04a5d6a17560e987272fc1763e9772a87689a08427b8cbdebe3ca7cba95d6156"},
{url = "https://files.pythonhosted.org/packages/92/97/fafba850c72cffb4e794b827d196a01b1b7ba332686354cc5e01527e1bf1/pycryptodomex-3.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:3709f13ca3852b0b07fc04a2c03b379189232b24007c466be0f605dd4723e9d4"},
{url = "https://files.pythonhosted.org/packages/95/6f/48b103bea4ccc8351cf4a94af86d9c5799aac8de8882d00f8cbb59fcf703/pycryptodomex-3.15.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:7db44039cc8b449bd08ab338a074e87093bd170f1a1b76d2fcef8a3e2ee11199"},
{url = "https://files.pythonhosted.org/packages/99/72/1f22fc479774170abc6e10690e412155af071dea9bc9ce37f0a55ff241e1/pycryptodomex-3.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:f8be976cec59b11f011f790b88aca67b4ea2bd286578d0bd3e31bcd19afcd3e4"},
{url = "https://files.pythonhosted.org/packages/99/b0/e44654a967809bb8c3225e40f4d58686784b6a3ea3aecae7710296a965ef/pycryptodomex-3.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:fc9bc7a9b79fe5c750fc81a307052f8daabb709bdaabb0fb18fb136b66b653b5"},
{url = "https://files.pythonhosted.org/packages/a7/b0/e60dd0b215420dff6423614de65852bcba5e17539ae50c45acb4a29eac97/pycryptodomex-3.15.0-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:78d9621cf0ea35abf2d38fa2ca6d0634eab6c991a78373498ab149953787e5e5"},
@ -969,6 +971,7 @@ content_hash = "sha256:889439b64bada10d9afdf954f0b552fb3a224b91dfac0b20c3531ea0f
{url = "https://files.pythonhosted.org/packages/ce/1c/0f93364b744f75770ac4d928413f89a9978ab96cfe6416d84c808e754c40/pycryptodomex-3.15.0-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:b9279adc16e4b0f590ceff581f53a80179b02cba9056010d733eb4196134a870"},
{url = "https://files.pythonhosted.org/packages/e5/3f/4e92e56ee0eefaedbb1bc4e85848b3fbbfed34f929a88e39088b2052ce1f/pycryptodomex-3.15.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e2b12968522a0358b8917fc7b28865acac002f02f4c4c6020fcb264d76bfd06d"},
{url = "https://files.pythonhosted.org/packages/e7/c2/24b88c306d9ee909061e1535b355be43693be2e5557321417ca2448a1125/pycryptodomex-3.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:191e73bc84a8064ad1874dba0ebadedd7cce4dedee998549518f2c74a003b2e1"},
{url = "https://files.pythonhosted.org/packages/f7/4f/84cbd54a5a28cdc59df165c6f4cde9ea9847ae0b9055ebcc5a6b35913ba5/pycryptodomex-3.15.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:781efd04ea6762bb2ef7d4fa632c9c89895433744b6c345bd0c239d5ab058dfc"},
{url = "https://files.pythonhosted.org/packages/f8/e5/7e21896a03affd83cb9bc5cbf8576f63c5f6f26cccfb4196c874d466b5c7/pycryptodomex-3.15.0-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:5676a132169a1c1a3712edf25250722ebc8c9102aa9abd814df063ca8362454f"},
]
"pynacl 1.5.0" = [