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:
parent
8aaf150f14
commit
6a7ec8eef7
3 changed files with 192 additions and 20 deletions
|
@ -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"]
|
202
cogs/random.py
202
cogs/random.py
|
@ -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}')
|
||||
)
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
|
|
7
pdm.lock
7
pdm.lock
|
@ -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" = [
|
||||
|
|
Loading…
Reference in a new issue