Package: d20
d20A fast, powerful, and extensible dice engine for D&D, d20 systems, and any other system that needs dice!Key FeaturesQuick to start - just used20.roll()!Optimized for speed and memory efficiencyHighly extensible API for custom behaviour and dice stringificationBuilt-in execution limits against malicious dice expressionsTree-based dice representation for easy traversalInstallingRequires Python 3.6+.python3-mpipinstall-Ud20Quickstart>>>importd20>>>result=d20.roll("1d20+5")>>>str(result)'1d20 (10) + 5 = `15`'>>>result.total15>>>result.crit<CritType.NORMAL:0>>>>str(result.ast)'1d20 + 5'DocumentationCheck out the docs onRead the Docs!Dice SyntaxThis is the grammar supported by the dice parser, roughly ordered in how tightly the grammar binds.NumbersThese are the atoms used at the base of the syntax tree.NameSyntaxDescriptionExamplesliteralINT,DECIMALA literal number.1,0.5,3.14diceINT? "d" (INT | "%")A set of die.d20,3d6set"(" (num ("," num)* ","?)? ")"A set of expressions.(),(2,),(1, 3+3, 1d20)Note that(3d6)is equivalent to3d6, but(3d6,)is the set containing the one element3d6.Set OperationsThese operations can be performed on dice and sets.GrammarNameSyntaxDescriptionExamplesset_opoperation selectorAn operation on a set (see below).kh3,ro<3selectorseltype INTA selection on a set (see below).3,h1,>2OperatorsOperators are always followed by a selector, and operate on the items in the set that match the selector.SyntaxNameDescriptionkkeepKeeps all matched values.pdropDrops all matched values.rrrerollRerolls all matched values until none match. (Dice only)roreroll onceRerolls all matched values once. (Dice only)rareroll and addRerolls up to one matched value once, keeping the original roll. (Dice only)eexplode onRolls another die for each matched value. (Dice only)miminimumSets the minimum value of each die. (Dice only)mamaximumSets the maximum value of each die. (Dice only)SelectorsSelectors select from the remaining kept values in a set.SyntaxNameDescriptionXliteralAll values in this set that are literally this value.hXhighest XThe highest X values in the set.lXlowest XThe lowest X values in the set.>Xgreater than XAll values in this set greater than X.<Xless than XAll values in this set less than X.Unary OperationsSyntaxNameDescription+XpositiveDoes nothing.-XnegativeThe negative value of X.Binary OperationsSyntaxNameX * YmultiplicationX / YdivisionX // Yint divisionX % YmoduloX + YadditionX - YsubtractionX == YequalityX >= Ygreater/equalX <= Yless/equalX > Ygreater thanX < Yless thanX != YinequalityExamples>>>fromd20importroll>>>r=roll("4d6kh3")# highest 3 of 4 6-sided dice>>>r.total14>>>str(r)'4d6kh3 (4, 4, **6**, ~~3~~) = `14`'>>>r=roll("2d6ro<3")# roll 2d6s, then reroll any 1s or 2s once>>>r.total9>>>str(r)'2d6ro<3 (**~~1~~**, 3, **6**) = `9`'>>>r=roll("8d6mi2")# roll 8d6s, with each die having a minimum roll of 2>>>r.total33>>>str(r)'8d6mi2 (1 -> 2, **6**, 4, 2, **6**, 2, 5, **6**) = `33`'>>>r=roll("(1d4 + 1, 3, 2d6kl1)kh1")# the highest of 1d4+1, 3, and the lower of 2 d6s>>>r.total3>>>str(r)'(1d4 (2) + 1, ~~3~~, ~~2d6kl1 (2, 5)~~)kh1 = `3`'Custom StringifierBy default, d20 stringifies the result of each dice roll formatted in Markdown, which may not be useful in your application.
To change this behaviour, you can create a subclass ofd20.Stringifier(ord20.SimpleStringifieras a starting point), and implement the_str_*methods to customize how your dice tree is stringified.Then, simply pass an instance of your stringifier into theroll()function!>>>importd20>>>classMyStringifier(d20.SimpleStringifier):...def_stringify(self,node):...ifnotnode.kept:...return'X'...returnsuper()._stringify(node)......def_str_expression(self,node):...returnf"The result of the roll{self._stringify(node.roll)}was{int(node.total)}">>>result=d20.roll("4d6e6kh3",stringifier=MyStringifier())>>>str(result)'The result of the roll 4d6e6kh3 (X, 5, 6!, 6!, X, X) was 17'Annotations and CommentsEach dice node supports value annotations - i.e., a method to "tag" parts of a roll with some indicator. For example,>>>fromd20importroll>>>str(roll("3d6 [fire] + 1d4 [piercing]"))'3d6 (3, 2, 2) [fire] + 1d4 (3) [piercing] = `10`'>>>str(roll("-(1d8 + 3) [healing]"))'-(1d8 (7) + 3) [healing] = `-10`'>>>str(roll("(1 [one], 2 [two], 3 [three])"))'(1 [one], 2 [two], 3 [three]) = `6`'are all examples of valid annotations. Annotations are purely visual and do not affect the evaluation of the roll by default.Additionally, whenallow_comments=Trueis passed toroll(), the result of the roll may have a comment:>>>fromd20importroll>>>result=roll("1d20 I rolled a d20",allow_comments=True)>>>str(result)'1d20 (13) = `13`'>>>result.comment'I rolled a d20'Note that whileallow_commentsis enabled, AST caching is disabled, which may lead to slightly worse performance.Traversing Dice ResultsThe raw results of dice rolls are returned inExpressionobjects, which can be accessed as such:>>>fromd20importroll>>>result=roll("3d6 + 1d4 + 3")>>>str(result)'3d6 (4, **6**, **6**) + 1d4 (**1**) + 3 = `20`'>>>result.expr<Expressionroll=<BinOpleft=<BinOpleft=<Dicenum=3size=6values=[<Diesize=6values=[<Literal4>]>,<Diesize=6values=[<Literal6>]>,<Diesize=6values=[<Literal6>]>]operations=[]>op=+right=<Dicenum=1size=4values=[<Diesize=4values=[<Literal1>]>]operations=[]>>op=+right=<Literal3>>comment=None>or, in a easier-to-read format,<Expression 
    roll=<BinOp
        left=<BinOp
            left=<Dice
                num=3
                size=6
                values=[
                    <Die size=6 values=[<Literal 4>]>,
                    <Die size=6 values=[<Literal 6>]>,
                    <Die size=6 values=[<Literal 6>]>
                ]
                operations=[]
            >
            op=+
            right=<Dice
                num=1
                size=4
                values=[
                    <Die size=4 values=[<Literal 1>]>
                ]
                operations=[]
            >
        >
        op=+
        right=<Literal 3>
    >
    comment=None
>From here,Expression.childrenreturns a tree of nodes representing the expression from left to right, each of which
may have children of their own. This can be used to easily search for specific dice, look for the left-most operand,
or modify the result by adding in resistances or other modifications.ExamplesFinding the left and right-most operands:>>>fromd20importroll>>>binop=roll("1 + 2 + 3 + 4")>>>left=binop.expr>>>whileleft.children:...left=left.children[0]>>>left<Literal1>>>>right=binop.expr>>>whileright.children:...right=right.children[-1]>>>right<Literal4>>>>fromd20importutils# these patterns are available in the utils submodule:>>>utils.leftmost(binop.expr)<Literal1>>>>utils.rightmost(binop.expr)<Literal4>Searching for the d4:>>>fromd20importroll,Dice,SimpleStringifier,utils>>>mixed=roll("-1d8 + 4 - (3, 1d4)kh1")>>>str(mixed)'-1d8 (**8**) + 4 - (3, ~~1d4 (3)~~)kh1 = `-7`'>>>root=mixed.expr>>>result=utils.dfs(root,lambdanode:isinstance(node,Dice)andnode.num==1andnode.size==4)>>>result<Dicenum=1size=4values=[<Diesize=4values=[<Literal3>]>]operations=[]>>>>SimpleStringifier().stringify(result)'1d4 (3)'As a note, even though aDiceobject is the parent ofDieobjects,Dice.childrenreturns an empty list, since it's
more common to look for the dice, and not each individual component of that dice.PerformanceBy default, the parser caches the 256 most frequently used dice expressions in an LFU cache, allowing for a significant
speedup when rolling many of the same kinds of rolls. This caching is disabled whenallow_commentsis True.With caching:$python3-mtimeit-s"from d20 import roll""roll('1d20')"10000loops,bestof5:21.6usecperloop
$python3-mtimeit-s"from d20 import roll""roll('100d20')"500loops,bestof5:572usecperloop
$python3-mtimeit-s"from d20 import roll; expr='1d20+'*50+'1d20'""roll(expr)"500loops,bestof5:732usecperloop
$python3-mtimeit-s"from d20 import roll""roll('10d20rr<20')"1000loops,bestof5:1.13msecperloopWithout caching:$python3-mtimeit-s"from d20 import roll""roll('1d20')"5000loops,bestof5:61.6usecperloop
$python3-mtimeit-s"from d20 import roll""roll('100d20')"500loops,bestof5:620usecperloop
$python3-mtimeit-s"from d20 import roll; expr='1d20+'*50+'1d20'""roll(expr)"500loops,bestof5:2.1msecperloop
$python3-mtimeit-s"from d20 import roll""roll('10d20rr<20')"1000loops,bestof5:1.26msecperloop