First set of updates from the major "backendification" rewrite #1
528
dice/roller.py
528
dice/roller.py
@ -1,264 +1,264 @@
|
|||||||
"""Dice rollers used by the views, bots, etc."""
|
"""Dice rollers used by the views, bots, etc."""
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
|
|
||||||
import ply.lex as lex
|
import ply.lex as lex
|
||||||
import ply.yacc as yacc
|
import ply.yacc as yacc
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DiceRoller(object):
|
class DiceRoller(object):
|
||||||
|
|
||||||
tokens = ['NUMBER', 'TEXT', 'ROLLSEP']
|
tokens = ['NUMBER', 'TEXT', 'ROLLSEP']
|
||||||
literals = ['#', '/', '+', '-', 'd']
|
literals = ['#', '/', '+', '-', 'd']
|
||||||
|
|
||||||
t_TEXT = r'\s+[^;]+'
|
t_TEXT = r'\s+[^;]+'
|
||||||
t_ROLLSEP = r';\s*'
|
t_ROLLSEP = r';\s*'
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
lex.lex(module=self)
|
lex.lex(module=self)
|
||||||
yacc.yacc(module=self)
|
yacc.yacc(module=self)
|
||||||
|
|
||||||
def t_NUMBER(self, t):
|
def t_NUMBER(self, t):
|
||||||
r'\d+'
|
r'\d+'
|
||||||
t.value = int(t.value)
|
t.value = int(t.value)
|
||||||
return t
|
return t
|
||||||
|
|
||||||
def t_error(self, t):
|
def t_error(self, t):
|
||||||
t.lexer.skip(1)
|
t.lexer.skip(1)
|
||||||
|
|
||||||
precedence = (
|
precedence = (
|
||||||
('left', 'ROLLSEP'),
|
('left', 'ROLLSEP'),
|
||||||
('left', '+', '-'),
|
('left', '+', '-'),
|
||||||
('right', 'd'),
|
('right', 'd'),
|
||||||
('left', '#'),
|
('left', '#'),
|
||||||
('left', '/')
|
('left', '/')
|
||||||
)
|
)
|
||||||
|
|
||||||
output = ""
|
output = ""
|
||||||
|
|
||||||
def roll_dice(self, keep, dice, size):
|
def roll_dice(self, keep, dice, size):
|
||||||
"""Takes the parsed dice string for a single roll (eg 3/4d20) and performs
|
"""Takes the parsed dice string for a single roll (eg 3/4d20) and performs
|
||||||
the actual roll. Returns a string representing the result.
|
the actual roll. Returns a string representing the result.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
a = list(range(dice))
|
a = list(range(dice))
|
||||||
for i in range(dice):
|
for i in range(dice):
|
||||||
a[i] = random.randint(1, size)
|
a[i] = random.randint(1, size)
|
||||||
if keep != dice:
|
if keep != dice:
|
||||||
b = sorted(a, reverse=True)
|
b = sorted(a, reverse=True)
|
||||||
b = b[0:keep]
|
b = b[0:keep]
|
||||||
else:
|
else:
|
||||||
b = a
|
b = a
|
||||||
total = sum(b)
|
total = sum(b)
|
||||||
outstr = "[" + ",".join(str(i) for i in a) + "]"
|
outstr = "[" + ",".join(str(i) for i in a) + "]"
|
||||||
|
|
||||||
return (total, outstr)
|
return (total, outstr)
|
||||||
|
|
||||||
def process_roll(self, trials, mods, comment):
|
def process_roll(self, trials, mods, comment):
|
||||||
"""Processes rolls coming from the parser.
|
"""Processes rolls coming from the parser.
|
||||||
|
|
||||||
This generates the inputs for the roll_dice() command, and returns
|
This generates the inputs for the roll_dice() command, and returns
|
||||||
the full string representing the whole current dice string (the part
|
the full string representing the whole current dice string (the part
|
||||||
up to a semicolon or end of line).
|
up to a semicolon or end of line).
|
||||||
"""
|
"""
|
||||||
rolls = mods[0][0][1] if mods[0][0][1] else 1
|
rolls = mods[0][0][1] if mods[0][0][1] else 1
|
||||||
if trials:
|
if trials:
|
||||||
assert trials <= 10, "Too many rolls (max: 10)."
|
assert trials <= 10, "Too many rolls (max: 10)."
|
||||||
assert rolls <= 50, "Too many dice (max: 50) in roll."
|
assert rolls <= 50, "Too many dice (max: 50) in roll."
|
||||||
|
|
||||||
output = ""
|
output = ""
|
||||||
repeat = 1
|
repeat = 1
|
||||||
if trials != None:
|
if trials != None:
|
||||||
repeat = trials
|
repeat = trials
|
||||||
for i in range(repeat):
|
for i in range(repeat):
|
||||||
mode = 1
|
mode = 1
|
||||||
total = 0
|
total = 0
|
||||||
curr_str = ""
|
curr_str = ""
|
||||||
if i > 0:
|
if i > 0:
|
||||||
output += ", "
|
output += ", "
|
||||||
for m in mods:
|
for m in mods:
|
||||||
keep = 0
|
keep = 0
|
||||||
dice = 1
|
dice = 1
|
||||||
res = 0
|
res = 0
|
||||||
# if m is a tuple, then it is a die roll
|
# if m is a tuple, then it is a die roll
|
||||||
# m[0] = (keep, num dice)
|
# m[0] = (keep, num dice)
|
||||||
# m[1] = num faces on the die
|
# m[1] = num faces on the die
|
||||||
if type(m) == tuple:
|
if type(m) == tuple:
|
||||||
if m[0] != None:
|
if m[0] != None:
|
||||||
if m[0][0] != None:
|
if m[0][0] != None:
|
||||||
keep = m[0][0]
|
keep = m[0][0]
|
||||||
dice = m[0][1]
|
dice = m[0][1]
|
||||||
size = m[1]
|
size = m[1]
|
||||||
if keep > dice or keep == 0:
|
if keep > dice or keep == 0:
|
||||||
keep = dice
|
keep = dice
|
||||||
if size < 1:
|
if size < 1:
|
||||||
output = "# of sides for die is incorrect: %d" % size
|
output = "# of sides for die is incorrect: %d" % size
|
||||||
return output
|
return output
|
||||||
if dice < 1:
|
if dice < 1:
|
||||||
output = "# of dice is incorrect: %d" % dice
|
output = "# of dice is incorrect: %d" % dice
|
||||||
return output
|
return output
|
||||||
res = self.roll_dice(keep, dice, size)
|
res = self.roll_dice(keep, dice, size)
|
||||||
curr_str += "%d%s" % (res[0], res[1])
|
curr_str += "%d%s" % (res[0], res[1])
|
||||||
res = res[0]
|
res = res[0]
|
||||||
elif m == "+":
|
elif m == "+":
|
||||||
mode = 1
|
mode = 1
|
||||||
curr_str += "+"
|
curr_str += "+"
|
||||||
elif m == "-":
|
elif m == "-":
|
||||||
mode = -1
|
mode = -1
|
||||||
curr_str += "-"
|
curr_str += "-"
|
||||||
else:
|
else:
|
||||||
res = m
|
res = m
|
||||||
curr_str += str(m)
|
curr_str += str(m)
|
||||||
total += mode * res
|
total += mode * res
|
||||||
if repeat == 1:
|
if repeat == 1:
|
||||||
if comment != None:
|
if comment != None:
|
||||||
output = "%d %s (%s)" % (total, comment.strip(), curr_str)
|
output = "%d %s (%s)" % (total, comment.strip(), curr_str)
|
||||||
else:
|
else:
|
||||||
output = "%d (%s)" % (total, curr_str)
|
output = "%d (%s)" % (total, curr_str)
|
||||||
else:
|
else:
|
||||||
output += "%d (%s)" % (total, curr_str)
|
output += "%d (%s)" % (total, curr_str)
|
||||||
if i == repeat - 1:
|
if i == repeat - 1:
|
||||||
if comment != None:
|
if comment != None:
|
||||||
output += " (%s)" % (comment.strip())
|
output += " (%s)" % (comment.strip())
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def p_roll_r(self, p):
|
def p_roll_r(self, p):
|
||||||
# Chain rolls together.
|
# Chain rolls together.
|
||||||
|
|
||||||
# General idea I had when creating this grammar: A roll string is a chain
|
# General idea I had when creating this grammar: A roll string is a chain
|
||||||
# of modifiers, which may be repeated for a certain number of trials. It can
|
# of modifiers, which may be repeated for a certain number of trials. It can
|
||||||
# have a comment that describes the roll
|
# have a comment that describes the roll
|
||||||
# Multiple roll strings can be chained with semicolon
|
# Multiple roll strings can be chained with semicolon
|
||||||
|
|
||||||
'roll : roll ROLLSEP roll'
|
'roll : roll ROLLSEP roll'
|
||||||
global output
|
global output
|
||||||
p[0] = p[1] + "; " + p[3]
|
p[0] = p[1] + "; " + p[3]
|
||||||
output = p[0]
|
output = p[0]
|
||||||
|
|
||||||
def p_roll(self, p):
|
def p_roll(self, p):
|
||||||
# Parse a basic roll string.
|
# Parse a basic roll string.
|
||||||
|
|
||||||
'roll : trial modifier comment'
|
'roll : trial modifier comment'
|
||||||
global output
|
global output
|
||||||
mods = []
|
mods = []
|
||||||
if type(p[2]) == list:
|
if type(p[2]) == list:
|
||||||
mods = p[2]
|
mods = p[2]
|
||||||
else:
|
else:
|
||||||
mods = [p[2]]
|
mods = [p[2]]
|
||||||
p[0] = self.process_roll(p[1], mods, p[3])
|
p[0] = self.process_roll(p[1], mods, p[3])
|
||||||
output = p[0]
|
output = p[0]
|
||||||
|
|
||||||
def p_roll_no_trials(self, p):
|
def p_roll_no_trials(self, p):
|
||||||
# Parse a roll string without trials.
|
# Parse a roll string without trials.
|
||||||
|
|
||||||
'roll : modifier comment'
|
'roll : modifier comment'
|
||||||
global output
|
global output
|
||||||
mods = []
|
mods = []
|
||||||
if type(p[1]) == list:
|
if type(p[1]) == list:
|
||||||
mods = p[1]
|
mods = p[1]
|
||||||
else:
|
else:
|
||||||
mods = [p[1]]
|
mods = [p[1]]
|
||||||
p[0] = self.process_roll(None, mods, p[2])
|
p[0] = self.process_roll(None, mods, p[2])
|
||||||
output = p[0]
|
output = p[0]
|
||||||
|
|
||||||
def p_comment(self, p):
|
def p_comment(self, p):
|
||||||
# Parse a comment.
|
# Parse a comment.
|
||||||
|
|
||||||
'''comment : TEXT
|
'''comment : TEXT
|
||||||
|'''
|
|'''
|
||||||
if len(p) == 2:
|
if len(p) == 2:
|
||||||
p[0] = p[1]
|
p[0] = p[1]
|
||||||
else:
|
else:
|
||||||
p[0] = None
|
p[0] = None
|
||||||
|
|
||||||
def p_modifier(self, p):
|
def p_modifier(self, p):
|
||||||
# Parse a modifier on a roll string.
|
# Parse a modifier on a roll string.
|
||||||
|
|
||||||
'''modifier : modifier "+" modifier
|
'''modifier : modifier "+" modifier
|
||||||
| modifier "-" modifier'''
|
| modifier "-" modifier'''
|
||||||
# Use append to prevent nested lists (makes dealing with this easier)
|
# Use append to prevent nested lists (makes dealing with this easier)
|
||||||
if type(p[1]) == list:
|
if type(p[1]) == list:
|
||||||
p[1].append(p[2])
|
p[1].append(p[2])
|
||||||
p[1].append(p[3])
|
p[1].append(p[3])
|
||||||
p[0] = p[1]
|
p[0] = p[1]
|
||||||
elif type(p[3]) == list:
|
elif type(p[3]) == list:
|
||||||
p[3].insert(0, p[2])
|
p[3].insert(0, p[2])
|
||||||
p[3].insert(0, p[1])
|
p[3].insert(0, p[1])
|
||||||
p[0] = p[3]
|
p[0] = p[3]
|
||||||
else:
|
else:
|
||||||
p[0] = [p[1], p[2], p[3]]
|
p[0] = [p[1], p[2], p[3]]
|
||||||
|
|
||||||
def p_die(self, p):
|
def p_die(self, p):
|
||||||
# Return the left side before the "d", and the number of faces.
|
# Return the left side before the "d", and the number of faces.
|
||||||
|
|
||||||
'modifier : left NUMBER'
|
'modifier : left NUMBER'
|
||||||
p[0] = (p[1], p[2])
|
p[0] = (p[1], p[2])
|
||||||
|
|
||||||
def p_die_num(self, p):
|
def p_die_num(self, p):
|
||||||
'modifier : NUMBER'
|
'modifier : NUMBER'
|
||||||
p[0] = p[1]
|
p[0] = p[1]
|
||||||
|
|
||||||
def p_left(self, p):
|
def p_left(self, p):
|
||||||
# Parse the number of dice we are rolling, and how many we are keeping.
|
# Parse the number of dice we are rolling, and how many we are keeping.
|
||||||
'left : keep dice'
|
'left : keep dice'
|
||||||
if p[1] == None:
|
if p[1] == None:
|
||||||
p[0] = [None, p[2]]
|
p[0] = [None, p[2]]
|
||||||
else:
|
else:
|
||||||
p[0] = [p[1], p[2]]
|
p[0] = [p[1], p[2]]
|
||||||
|
|
||||||
def p_left_all(self, p):
|
def p_left_all(self, p):
|
||||||
'left : dice'
|
'left : dice'
|
||||||
p[0] = [None, p[1]]
|
p[0] = [None, p[1]]
|
||||||
|
|
||||||
def p_left_e(self, p):
|
def p_left_e(self, p):
|
||||||
'left :'
|
'left :'
|
||||||
p[0] = None
|
p[0] = None
|
||||||
|
|
||||||
def p_total(self, p):
|
def p_total(self, p):
|
||||||
'trial : NUMBER "#"'
|
'trial : NUMBER "#"'
|
||||||
if len(p) > 1:
|
if len(p) > 1:
|
||||||
p[0] = p[1]
|
p[0] = p[1]
|
||||||
else:
|
else:
|
||||||
p[0] = None
|
p[0] = None
|
||||||
|
|
||||||
def p_keep(self, p):
|
def p_keep(self, p):
|
||||||
'keep : NUMBER "/"'
|
'keep : NUMBER "/"'
|
||||||
if p[1] != None:
|
if p[1] != None:
|
||||||
p[0] = p[1]
|
p[0] = p[1]
|
||||||
else:
|
else:
|
||||||
p[0] = None
|
p[0] = None
|
||||||
|
|
||||||
def p_dice(self, p):
|
def p_dice(self, p):
|
||||||
'dice : NUMBER "d"'
|
'dice : NUMBER "d"'
|
||||||
p[0] = p[1]
|
p[0] = p[1]
|
||||||
|
|
||||||
def p_dice_one(self, p):
|
def p_dice_one(self, p):
|
||||||
'dice : "d"'
|
'dice : "d"'
|
||||||
p[0] = 1
|
p[0] = 1
|
||||||
|
|
||||||
def p_error(self, p):
|
def p_error(self, p):
|
||||||
"""Raise ValueError on unparseable strings."""
|
"""Raise ValueError on unparseable strings."""
|
||||||
raise ValueError("Error occurred in parser")
|
raise ValueError("Error occurred in parser")
|
||||||
|
|
||||||
def get_result(self):
|
def get_result(self):
|
||||||
global output
|
global output
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def do_roll(self, dicestr):
|
def do_roll(self, dicestr):
|
||||||
"""
|
"""
|
||||||
Roll some dice and get the result (with broken out rolls).
|
Roll some dice and get the result (with broken out rolls).
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
dicestr - format:
|
dicestr - format:
|
||||||
N#X/YdS+M label
|
N#X/YdS+M label
|
||||||
N#: do the following roll N times (optional)
|
N#: do the following roll N times (optional)
|
||||||
X/: take the top X rolls of the Y times rolled (optional)
|
X/: take the top X rolls of the Y times rolled (optional)
|
||||||
Y : roll the die specified Y times (optional, defaults to 1)
|
Y : roll the die specified Y times (optional, defaults to 1)
|
||||||
dS: roll a S-sided die
|
dS: roll a S-sided die
|
||||||
+M: add M to the result (-M for subtraction) (optional)
|
+M: add M to the result (-M for subtraction) (optional)
|
||||||
"""
|
"""
|
||||||
self.build()
|
self.build()
|
||||||
yacc.parse(dicestr)
|
yacc.parse(dicestr)
|
||||||
return self.get_result()
|
return self.get_result()
|
||||||
|
16
dice/urls.py
16
dice/urls.py
@ -1,8 +1,8 @@
|
|||||||
"""URL patterns for the dice views."""
|
"""URL patterns for the dice views."""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from dice.views import rpc_roll_dice
|
from dice.views import rpc_roll_dice
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('rpc/roll/', rpc_roll_dice, name='dice_rpc_roll_dice'),
|
path('rpc/roll/', rpc_roll_dice, name='dice_rpc_roll_dice'),
|
||||||
]
|
]
|
||||||
|
@ -1,36 +1,36 @@
|
|||||||
"""Views for rolling dice."""
|
"""Views for rolling dice."""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from rest_framework.authentication import BasicAuthentication
|
from rest_framework.authentication import BasicAuthentication
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import api_view, authentication_classes, permission_classes
|
from rest_framework.decorators import api_view, authentication_classes, permission_classes
|
||||||
|
|
||||||
from dice.roller import DiceRoller
|
from dice.roller import DiceRoller
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
roller = DiceRoller()
|
roller = DiceRoller()
|
||||||
|
|
||||||
|
|
||||||
@api_view(['POST'])
|
@api_view(['POST'])
|
||||||
@authentication_classes((BasicAuthentication, ))
|
@authentication_classes((BasicAuthentication, ))
|
||||||
@permission_classes((IsAuthenticated, ))
|
@permission_classes((IsAuthenticated, ))
|
||||||
def rpc_roll_dice(request):
|
def rpc_roll_dice(request):
|
||||||
"""Get a dice string from the client, roll the dice, and give the result."""
|
"""Get a dice string from the client, roll the dice, and give the result."""
|
||||||
if request.method != 'POST':
|
if request.method != 'POST':
|
||||||
return Response({'detail': "Supported method: POST."}, status=405)
|
return Response({'detail': "Supported method: POST."}, status=405)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
roll_data = json.loads(request.body)
|
roll_data = json.loads(request.body)
|
||||||
dice_str = roll_data['dice']
|
dice_str = roll_data['dice']
|
||||||
except (json.decoder.JSONDecodeError, KeyError):
|
except (json.decoder.JSONDecodeError, KeyError):
|
||||||
return Response({'detail': "Request must be JSON with a 'dice' parameter."}, status=400)
|
return Response({'detail': "Request must be JSON with a 'dice' parameter."}, status=400)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result_str = roller.do_roll(dice_str)
|
result_str = roller.do_roll(dice_str)
|
||||||
return Response({'dice': dice_str, 'result': result_str})
|
return Response({'dice': dice_str, 'result': result_str})
|
||||||
except AssertionError as aex:
|
except AssertionError as aex:
|
||||||
return Response({'detail': f"Could not roll dice: {aex}"}, status=400)
|
return Response({'detail': f"Could not roll dice: {aex}"}, status=400)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return Response({'detail': f"Could not parse requested dice '{dice_str}'."}, status=400)
|
return Response({'detail': f"Could not parse requested dice '{dice_str}'."}, status=400)
|
||||||
|
Loading…
Reference in New Issue
Block a user