@ -1,36 +1,31 @@
""" Roll dice when asked, intended for RPGs. """
# this breaks yacc, but ply might be happy in py3
#from __future__ import unicode_literals
import logging
import math
import re
import random
import re
from irc. client import NickMask
from django. conf import settings
import ply . lex as lex
import ply . yacc as yacc
from irc . client import NickMask
from dice . roller import DiceRoller
from ircbot . lib import Plugin
logger = logging . getLogger ( __name__ )
class Dice ( Plugin ) :
""" Roll simple or complex dice strings. """
def start ( self ) :
""" Set up the handlers. """
def __init__ ( self , bot , connection , event ) :
""" Set up the plugin. """
self . roller = DiceRoller ( )
super ( Dice , self ) . __init__ ( bot , connection , event )
def start ( self ) :
""" Set up the handlers. """
self . connection . reactor . add_global_regex_handler ( [ ' pubmsg ' , ' privmsg ' ] , r ' ^!roll \ s+(.*)$ ' ,
self . handle_roll , - 20 )
self . connection . reactor . add_global_regex_handler ( [ ' pubmsg ' , ' privmsg ' ] , r ' ^!ctech \ s+(.*)$ ' ,
self . handle_ctech , - 20 )
self . connection . reactor . add_global_regex_handler ( [ ' pubmsg ' , ' privmsg ' ] , r ' ^!random \ s+(.*)$ ' ,
self . handle_random , - 20 )
@ -38,398 +33,48 @@ class Dice(Plugin):
def stop ( self ) :
""" Tear down handlers. """
self . connection . reactor . remove_global_regex_handler ( [ ' pubmsg ' , ' privmsg ' ] , self . handle_roll )
self . connection . reactor . remove_global_regex_handler ( [ ' pubmsg ' , ' privmsg ' ] , self . handle_ctech )
self . connection . reactor . remove_global_regex_handler ( [ ' pubmsg ' , ' privmsg ' ] , self . handle_random )
super ( Dice , self ) . stop ( )
def handle_random ( self , connection , event , match ) :
""" Handle the !random command which picks an item from a list. """
nick = NickMask ( event . source ) . nick
choices = match . group ( 1 )
choices_list = choices . split ( ' ' )
choice = random . choice( choices_list )
choice = random . SystemRandom( ) . choice( choices_list )
logger . debug ( event . recursing )
if event . recursing :
reply = " {0:s} " . format ( choice )
el se:
el if settings. DICE_PREFIX_ROLLER :
reply = " {0:s} : {1:s} " . format ( nick , choice )
else :
reply = " {0:s} " . format ( choice )
return self . bot . reply ( event , reply )
def handle_roll ( self , connection , event , match ) :
""" Handle the !roll command which covers most common dice stuff. """
nick = NickMask ( event . source ) . nick
dicestr = match . group ( 1 )
logger . debug ( event . recursing )
try :
reply_str = self . roller . do_roll ( dicestr )
except AssertionError as aex :
reply_str = f " Could not roll dice: { aex } "
except ValueError :
reply_str = " Unable to parse roll "
if event . recursing :
reply = " {0:s} " . format ( self . roller . do_roll ( dicestr ) )
reply = " {0:s} " . format ( reply_str )
elif settings . DICE_PREFIX_ROLLER :
reply = " {0:s} : {1:s} " . format ( nick , reply_str )
else :
reply = " {0:s} : {1:s} " . format ( nick , self . roller . do_roll ( dicestr ) )
reply = " {0:s} ". format ( reply_str )
return self . bot . reply ( event , re . sub ( r ' ( \ d+)(.*? \ s+)( \ (.*? \ )) ' , r ' \ 1 \ 2 14 \ 3 ' , reply ) )
def handle_ctech ( self , connection , event , match ) :
""" Handle cthulhutech dice rolls. """
nick = NickMask ( event . source ) . nick
rollitrs = re . split ( ' ; \ s* ' , match . group ( 1 ) )
reply = " "
for count , roll in enumerate ( rollitrs ) :
pattern = ' ^( \ d+)d(?:( \ +| \ -)( \ d+))?(?: \ s+(.*))? '
regex = re . compile ( pattern )
matches = regex . search ( roll )
if matches is not None :
dice = int ( matches . group ( 1 ) )
modifier = 0
if matches . group ( 2 ) is not None and matches . group ( 3 ) is not None :
if str ( matches . group ( 2 ) ) == ' - ' :
modifier = - 1 * int ( matches . group ( 3 ) )
else :
modifier = int ( matches . group ( 3 ) )
result = roll + ' : '
rolls = [ ]
for d in range ( dice ) :
rolls . append ( random . randint ( 1 , 10 ) )
rolls . sort ( )
rolls . reverse ( )
# highest single die method
method1 = rolls [ 0 ]
# highest set method
method2 = 0
rolling_sum = 0
for i , r in enumerate ( rolls ) :
# if next roll is same as current, sum and continue, else see if sum is best so far
if i + 1 < len ( rolls ) and rolls [ i + 1 ] == r :
if rolling_sum == 0 :
rolling_sum = r
rolling_sum + = r
else :
if rolling_sum > method2 :
method2 = rolling_sum
rolling_sum = 0
# check for set in progress (e.g. lots of 1s)
if rolling_sum > method2 :
method2 = rolling_sum
# straight method
method3 = 0
rolling_sum = 0
count = 0
for i , r in enumerate ( rolls ) :
# if next roll is one less as current, sum and continue, else check len and see if sum is best so far
if i + 1 < len ( rolls ) and rolls [ i + 1 ] == r - 1 :
if rolling_sum == 0 :
rolling_sum = r
count + = 1
rolling_sum + = r - 1
count + = 1
else :
if count > = 3 and rolling_sum > method3 :
method3 = rolling_sum
rolling_sum = 0
# check for straight in progress (e.g. straight ending in 1)
if count > = 3 and rolling_sum > method3 :
method3 = rolling_sum
# get best roll
best = max ( [ method1 , method2 , method3 ] )
# check for critical failure
botch = False
ones = 0
for r in rolls :
if r == 1 :
ones + = 1
if ones > = math . ceil ( float ( len ( rolls ) ) / 2 ) :
botch = True
if botch :
result + = ' BOTCH '
else :
result + = str ( best + modifier )
rollres = ' '
for i , r in enumerate ( rolls ) :
rollres + = str ( r )
if i is not len ( rolls ) - 1 :
rollres + = ' , '
result + = ' [ ' + rollres
if modifier != 0 :
if modifier > 0 :
result + = ' + ' + str ( modifier )
else :
result + = ' - ' + str ( modifier * - 1 )
result + = ' ] '
reply + = result
if count is not len ( rollitrs ) - 1 :
reply + = " ; "
if reply is not " " :
msg = " {0:s} : {1:s} " . format ( nick , reply )
return self . bot . reply ( event , msg )
class DiceRoller ( object ) :
tokens = [ ' NUMBER ' , ' TEXT ' , ' ROLLSEP ' ]
literals = [ ' # ' , ' / ' , ' + ' , ' - ' , ' d ' ]
t_TEXT = r ' \ s+[^;]+ '
t_ROLLSEP = r ' ; \ s* '
def build ( self ) :
lex . lex ( module = self )
yacc . yacc ( module = self )
def t_NUMBER ( self , t ) :
r ' \ d+ '
t . value = int ( t . value )
return t
def t_error ( self , t ) :
t . lexer . skip ( 1 )
precedence = (
( ' left ' , ' ROLLSEP ' ) ,
( ' left ' , ' + ' , ' - ' ) ,
( ' right ' , ' d ' ) ,
( ' left ' , ' # ' ) ,
( ' left ' , ' / ' )
)
output = " "
def roll_dice ( self , keep , dice , size ) :
""" Takes the parsed dice string for a single roll (eg 3/4d20) and performs
the actual roll . Returns a string representing the result .
"""
a = list ( range ( dice ) )
for i in range ( dice ) :
a [ i ] = random . randint ( 1 , size )
if keep != dice :
b = sorted ( a , reverse = True )
b = b [ 0 : keep ]
else :
b = a
total = sum ( b )
outstr = " [ " + " , " . join ( str ( i ) for i in a ) + " ] "
return ( total , outstr )
def process_roll ( self , trials , mods , comment ) :
""" Processes rolls coming from the parser.
This generates the inputs for the roll_dice ( ) command , and returns
the full string representing the whole current dice string ( the part
up to a semicolon or end of line ) .
"""
output = " "
repeat = 1
if trials != None :
repeat = trials
for i in range ( repeat ) :
mode = 1
total = 0
curr_str = " "
if i > 0 :
output + = " , "
for m in mods :
keep = 0
dice = 1
res = 0
# if m is a tuple, then it is a die roll
# m[0] = (keep, num dice)
# m[1] = num faces on the die
if type ( m ) == tuple :
if m [ 0 ] != None :
if m [ 0 ] [ 0 ] != None :
keep = m [ 0 ] [ 0 ]
dice = m [ 0 ] [ 1 ]
size = m [ 1 ]
if keep > dice or keep == 0 :
keep = dice
if size < 1 :
output = " # of sides for die is incorrect: %d " % size
return output
if dice < 1 :
output = " # of dice is incorrect: %d " % dice
return output
res = self . roll_dice ( keep , dice , size )
curr_str + = " %d %s " % ( res [ 0 ] , res [ 1 ] )
res = res [ 0 ]
elif m == " + " :
mode = 1
curr_str + = " + "
elif m == " - " :
mode = - 1
curr_str + = " - "
else :
res = m
curr_str + = str ( m )
total + = mode * res
if repeat == 1 :
if comment != None :
output = " %d %s ( %s ) " % ( total , comment . strip ( ) , curr_str )
else :
output = " %d ( %s ) " % ( total , curr_str )
else :
output + = " %d ( %s ) " % ( total , curr_str )
if i == repeat - 1 :
if comment != None :
output + = " ( %s ) " % ( comment . strip ( ) )
return output
def p_roll_r ( self , p ) :
# Chain rolls together.
# 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
# have a comment that describes the roll
# Multiple roll strings can be chained with semicolon
' roll : roll ROLLSEP roll '
global output
p [ 0 ] = p [ 1 ] + " ; " + p [ 3 ]
output = p [ 0 ]
def p_roll ( self , p ) :
# Parse a basic roll string.
' roll : trial modifier comment '
global output
mods = [ ]
if type ( p [ 2 ] ) == list :
mods = p [ 2 ]
else :
mods = [ p [ 2 ] ]
p [ 0 ] = self . process_roll ( p [ 1 ] , mods , p [ 3 ] )
output = p [ 0 ]
def p_roll_no_trials ( self , p ) :
# Parse a roll string without trials.
' roll : modifier comment '
global output
mods = [ ]
if type ( p [ 1 ] ) == list :
mods = p [ 1 ]
else :
mods = [ p [ 1 ] ]
p [ 0 ] = self . process_roll ( None , mods , p [ 2 ] )
output = p [ 0 ]
def p_comment ( self , p ) :
# Parse a comment.
''' comment : TEXT
| '''
if len ( p ) == 2 :
p [ 0 ] = p [ 1 ]
else :
p [ 0 ] = None
def p_modifier ( self , p ) :
# Parse a modifier on a roll string.
''' modifier : modifier " + " modifier
| modifier " - " modifier '''
# Use append to prevent nested lists (makes dealing with this easier)
if type ( p [ 1 ] ) == list :
p [ 1 ] . append ( p [ 2 ] )
p [ 1 ] . append ( p [ 3 ] )
p [ 0 ] = p [ 1 ]
elif type ( p [ 3 ] ) == list :
p [ 3 ] . insert ( 0 , p [ 2 ] )
p [ 3 ] . insert ( 0 , p [ 1 ] )
p [ 0 ] = p [ 3 ]
else :
p [ 0 ] = [ p [ 1 ] , p [ 2 ] , p [ 3 ] ]
def p_die ( self , p ) :
# Return the left side before the "d", and the number of faces.
' modifier : left NUMBER '
p [ 0 ] = ( p [ 1 ] , p [ 2 ] )
def p_die_num ( self , p ) :
' modifier : NUMBER '
p [ 0 ] = p [ 1 ]
def p_left ( self , p ) :
# Parse the number of dice we are rolling, and how many we are keeping.
' left : keep dice '
if p [ 1 ] == None :
p [ 0 ] = [ None , p [ 2 ] ]
else :
p [ 0 ] = [ p [ 1 ] , p [ 2 ] ]
def p_left_all ( self , p ) :
' left : dice '
p [ 0 ] = [ None , p [ 1 ] ]
def p_left_e ( self , p ) :
' left : '
p [ 0 ] = None
def p_total ( self , p ) :
' trial : NUMBER " # " '
if len ( p ) > 1 :
p [ 0 ] = p [ 1 ]
else :
p [ 0 ] = None
def p_keep ( self , p ) :
' keep : NUMBER " / " '
if p [ 1 ] != None :
p [ 0 ] = p [ 1 ]
else :
p [ 0 ] = None
def p_dice ( self , p ) :
' dice : NUMBER " d " '
p [ 0 ] = p [ 1 ]
def p_dice_one ( self , p ) :
' dice : " d " '
p [ 0 ] = 1
def p_error ( self , p ) :
# Provide the user with something (albeit not much) when the roll can't be parsed.
global output
output = " Unable to parse roll "
def get_result ( self ) :
global output
return output
def do_roll ( self , dicestr ) :
"""
Roll some dice and get the result ( with broken out rolls ) .
Keyword arguments :
dicestr - format :
N #X/YdS+M label
N #: do the following roll N times (optional)
X / : take the top X rolls of the Y times rolled ( optional )
Y : roll the die specified Y times ( optional , defaults to 1 )
dS : roll a S - sided die
+ M : add M to the result ( - M for subtraction ) ( optional )
"""
self . build ( )
yacc . parse ( dicestr )
return self . get_result ( )
plugin = Dice