{"id":13993729,"url":"https://github.com/avrae/d20","last_synced_at":"2026-04-28T21:09:16.695Z","repository":{"id":46734521,"uuid":"232649082","full_name":"avrae/d20","owner":"avrae","description":"A fast, powerful, and extensible dice engine for D\u0026D, d20 systems, and any other system that needs dice!","archived":false,"fork":false,"pushed_at":"2024-10-04T02:51:46.000Z","size":107,"stargazers_count":129,"open_issues_count":6,"forks_count":29,"subscribers_count":7,"default_branch":"master","last_synced_at":"2025-06-18T19:20:15.982Z","etag":null,"topics":["dice","dice-roller","dungeons-and-dragons","parser","tabletop"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/avrae.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-01-08T20:08:31.000Z","updated_at":"2025-06-17T15:09:00.000Z","dependencies_parsed_at":"2024-11-29T15:32:38.755Z","dependency_job_id":null,"html_url":"https://github.com/avrae/d20","commit_stats":{"total_commits":95,"total_committers":3,"mean_commits":"31.666666666666668","dds":0.03157894736842104,"last_synced_commit":"ad2ce2533af82645a29bbb79df5d814c93a4a590"},"previous_names":["avrae/formaldice"],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/avrae/d20","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avrae%2Fd20","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avrae%2Fd20/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avrae%2Fd20/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avrae%2Fd20/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/avrae","download_url":"https://codeload.github.com/avrae/d20/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/avrae%2Fd20/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265934240,"owners_count":23852089,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dice","dice-roller","dungeons-and-dragons","parser","tabletop"],"created_at":"2024-08-09T14:02:31.856Z","updated_at":"2026-04-28T21:09:11.664Z","avatar_url":"https://github.com/avrae.png","language":"Python","funding_links":[],"categories":["Python"],"sub_categories":[],"readme":"# d20\n\n[![PyPI version shields.io](https://img.shields.io/pypi/v/d20.svg)](https://pypi.python.org/pypi/d20/)\n[![PyPI license](https://img.shields.io/pypi/l/d20.svg)](https://pypi.python.org/pypi/d20/)\n[![PyPI pyversions](https://img.shields.io/pypi/pyversions/d20.svg)](https://pypi.python.org/pypi/d20/)\n![](https://github.com/avrae/d20/workflows/Test%20Package/badge.svg)\n[![codecov](https://codecov.io/gh/avrae/d20/branch/master/graph/badge.svg)](https://codecov.io/gh/avrae/d20)\n[![Documentation Status](https://readthedocs.org/projects/d20/badge/?version=latest)](https://d20.readthedocs.io/en/latest/start.html?badge=latest)\n\n\n\n\nA fast, powerful, and extensible dice engine for D\u0026D, d20 systems, and any other system that needs dice!\n\n## Key Features\n- Quick to start - just use `d20.roll()`!\n- Optimized for speed and memory efficiency\n- Highly extensible API for custom behaviour and dice stringification\n- Built-in execution limits against malicious dice expressions\n- Tree-based dice representation for easy traversal \n\n## Installing\n**Requires Python 3.6+**.\n\n```bash\npython3 -m pip install -U d20\n```\n\n## Quickstart\n\n```python\n\u003e\u003e\u003e import d20\n\u003e\u003e\u003e result = d20.roll(\"1d20+5\")\n\u003e\u003e\u003e str(result)\n'1d20 (10) + 5 = `15`'\n\u003e\u003e\u003e result.total\n15\n\u003e\u003e\u003e result.crit\n\u003cCritType.NORMAL: 0\u003e\n\u003e\u003e\u003e str(result.ast)\n'1d20 + 5'\n```\n\n## Documentation\n\nCheck out the docs on [Read the Docs](https://d20.readthedocs.io/en/latest/start.html)!\n\n## Dice Syntax\nThis is the grammar supported by the dice parser, roughly ordered in how tightly the grammar binds.\n\n### Numbers\nThese are the atoms used at the base of the syntax tree.\n\n| Name    | Syntax                            | Description           | Examples                       |\n|---------|-----------------------------------|-----------------------|--------------------------------|\n| literal | `INT`, `DECIMAL`                  | A literal number.     | `1`, `0.5`, `3.14`             |\n| dice    | `INT? \"d\" (INT \\| \"%\")`           | A set of die.         | `d20`, `3d6`                   |\n| set     | `\"(\" (num (\",\" num)* \",\"?)? \")\"`  | A set of expressions. | `()`, `(2,)`, `(1, 3+3, 1d20)` |\n\nNote that `(3d6)` is equivalent to `3d6`, but `(3d6,)` is the set containing the one element `3d6`.\n\n### Set Operations\nThese operations can be performed on dice and sets.\n\n#### Grammar\n| Name    | Syntax            | Description       | Examples           |\n|---------|-------------------|-------------------|--------------------|\n| set_op  | `operation selector` | An operation on a set (see below). | `kh3`, `ro\u003c3` |\n| selector | `seltype INT` | A selection on a set (see below). | `3`, `h1`, `\u003e2` |\n\n#### Operators\nOperators are always followed by a selector, and operate on the items in the set that match the selector.\n\n| Syntax | Name | Description |\n|---|---|---|\n| k | keep | Keeps all matched values. |\n| p | drop | Drops all matched values. |\n| rr | reroll | Rerolls all matched values until none match. (Dice only) |\n| ro | reroll once | Rerolls all matched values once. (Dice only) |\n| ra | reroll and add | Rerolls up to one matched value once, keeping the original roll. (Dice only) |\n| e | explode on | Rolls another die for each matched value. (Dice only) |\n| mi | minimum | Sets the minimum value of each die. (Dice only) |\n| ma | maximum | Sets the maximum value of each die. (Dice only) |\n\n#### Selectors\nSelectors select from the remaining kept values in a set.\n\n| Syntax | Name | Description |\n|---|---|---|\n| X | literal | All values in this set that are literally this value. |\n| hX | highest X | The highest X values in the set. |\n| lX | lowest X | The lowest X values in the set. |\n| \\\u003eX | greater than X | All values in this set greater than X. |\n| \u003cX | less than X | All values in this set less than X. |\n\n### Unary Operations\n| Syntax | Name | Description |\n|---|---|---|\n| +X | positive | Does nothing. |\n| -X | negative | The negative value of X. |\n\n### Binary Operations\n| Syntax | Name |\n|---|---|\n| X * Y | multiplication |\n| X / Y | division |\n| X // Y | int division |\n| X % Y | modulo |\n| X + Y | addition |\n| X - Y | subtraction |\n| X == Y | equality |\n| X \u003e= Y | greater/equal |\n| X \u003c= Y | less/equal |\n| X \u003e Y | greater than |\n| X \u003c Y | less than |\n| X != Y | inequality |\n\n### Examples\n```python\n\u003e\u003e\u003e from d20 import roll\n\u003e\u003e\u003e r = roll(\"4d6kh3\")  # highest 3 of 4 6-sided dice\n\u003e\u003e\u003e r.total\n14\n\u003e\u003e\u003e str(r)\n'4d6kh3 (4, 4, **6**, ~~3~~) = `14`'\n\n\u003e\u003e\u003e r = roll(\"2d6ro\u003c3\")  # roll 2d6s, then reroll any 1s or 2s once\n\u003e\u003e\u003e r.total\n9\n\u003e\u003e\u003e str(r)\n'2d6ro\u003c3 (**~~1~~**, 3, **6**) = `9`'\n\n\u003e\u003e\u003e r = roll(\"8d6mi2\")  # roll 8d6s, with each die having a minimum roll of 2\n\u003e\u003e\u003e r.total\n33\n\u003e\u003e\u003e str(r)\n'8d6mi2 (1 -\u003e 2, **6**, 4, 2, **6**, 2, 5, **6**) = `33`'\n\n\u003e\u003e\u003e r = roll(\"(1d4 + 1, 3, 2d6kl1)kh1\")  # the highest of 1d4+1, 3, and the lower of 2 d6s\n\u003e\u003e\u003e r.total\n3\n\u003e\u003e\u003e str(r)\n'(1d4 (2) + 1, ~~3~~, ~~2d6kl1 (2, 5)~~)kh1 = `3`'\n```\n\n## Custom Stringifier\nBy default, d20 stringifies the result of each dice roll formatted in Markdown, which may not be useful in your application. \nTo change this behaviour, you can create a subclass of [`d20.Stringifier`](https://github.com/avrae/d20/blob/master/d20/stringifiers.py) \n(or `d20.SimpleStringifier` as a starting point), and implement the `_str_*` methods to customize how your dice tree is stringified. \n\nThen, simply pass an instance of your stringifier into the `roll()` function!\n```python\n\u003e\u003e\u003e import d20\n\u003e\u003e\u003e class MyStringifier(d20.SimpleStringifier):\n...     def _stringify(self, node):\n...         if not node.kept:\n...             return 'X'\n...         return super()._stringify(node)\n...\n...     def _str_expression(self, node):\n...         return f\"The result of the roll {self._stringify(node.roll)} was {int(node.total)}\"\n\n\u003e\u003e\u003e result = d20.roll(\"4d6e6kh3\", stringifier=MyStringifier())\n\u003e\u003e\u003e str(result)\n'The result of the roll 4d6e6kh3 (X, 5, 6!, 6!, X, X) was 17'\n```\n\n## Annotations and Comments\nEach dice node supports value annotations - i.e., a method to \"tag\" parts of a roll with some indicator. For example,\n```python\n\u003e\u003e\u003e from d20 import roll\n\u003e\u003e\u003e str(roll(\"3d6 [fire] + 1d4 [piercing]\"))\n'3d6 (3, 2, 2) [fire] + 1d4 (3) [piercing] = `10`'\n\n\u003e\u003e\u003e str(roll(\"-(1d8 + 3) [healing]\"))\n'-(1d8 (7) + 3) [healing] = `-10`'\n\n\u003e\u003e\u003e str(roll(\"(1 [one], 2 [two], 3 [three])\"))\n'(1 [one], 2 [two], 3 [three]) = `6`'\n```\nare all examples of valid annotations. Annotations are purely visual and do not affect the evaluation of the roll by default.\n\nAdditionally, when `allow_comments=True` is passed to `roll()`, the result of the roll may have a comment:\n```python\n\u003e\u003e\u003e from d20 import roll\n\u003e\u003e\u003e result = roll(\"1d20 I rolled a d20\", allow_comments=True)\n\u003e\u003e\u003e str(result)\n'1d20 (13) = `13`'\n\u003e\u003e\u003e result.comment\n'I rolled a d20'\n```\nNote that while `allow_comments` is enabled, AST caching is disabled, which may lead to slightly worse performance.\n\n## Traversing Dice Results\nThe raw results of dice rolls are returned in [`Expression`](https://github.com/avrae/d20/blob/master/d20/models.py#L76) \nobjects, which can be accessed as such: \n```python\n\u003e\u003e\u003e from d20 import roll\n\u003e\u003e\u003e result = roll(\"3d6 + 1d4 + 3\")\n\u003e\u003e\u003e str(result)\n'3d6 (4, **6**, **6**) + 1d4 (**1**) + 3 = `20`'\n\u003e\u003e\u003e result.expr\n\u003cExpression roll=\u003cBinOp left=\u003cBinOp left=\u003cDice num=3 size=6 values=[\u003cDie size=6 values=[\u003cLiteral 4\u003e]\u003e, \u003cDie size=6 values=[\u003cLiteral 6\u003e]\u003e, \u003cDie size=6 values=[\u003cLiteral 6\u003e]\u003e] operations=[]\u003e op=+ right=\u003cDice num=1 size=4 values=[\u003cDie size=4 values=[\u003cLiteral 1\u003e]\u003e] operations=[]\u003e\u003e op=+ right=\u003cLiteral 3\u003e\u003e comment=None\u003e\n```\nor, in a easier-to-read format,\n```text\n\u003cExpression \n    roll=\u003cBinOp\n        left=\u003cBinOp\n            left=\u003cDice\n                num=3\n                size=6\n                values=[\n                    \u003cDie size=6 values=[\u003cLiteral 4\u003e]\u003e,\n                    \u003cDie size=6 values=[\u003cLiteral 6\u003e]\u003e,\n                    \u003cDie size=6 values=[\u003cLiteral 6\u003e]\u003e\n                ]\n                operations=[]\n            \u003e\n            op=+\n            right=\u003cDice\n                num=1\n                size=4\n                values=[\n                    \u003cDie size=4 values=[\u003cLiteral 1\u003e]\u003e\n                ]\n                operations=[]\n            \u003e\n        \u003e\n        op=+\n        right=\u003cLiteral 3\u003e\n    \u003e\n    comment=None\n\u003e\n```\nFrom here, `Expression.children` returns a tree of nodes representing the expression from left to right, each of which\nmay have children of their own. This can be used to easily search for specific dice, look for the left-most operand,\nor modify the result by adding in resistances or other modifications.\n\n### Examples\nFinding the left and right-most operands:\n```python\n\u003e\u003e\u003e from d20 import roll\n\n\u003e\u003e\u003e binop = roll(\"1 + 2 + 3 + 4\")\n\u003e\u003e\u003e left = binop.expr\n\u003e\u003e\u003e while left.children:\n...     left = left.children[0]\n\u003e\u003e\u003e left\n\u003cLiteral 1\u003e\n\n\u003e\u003e\u003e right = binop.expr\n\u003e\u003e\u003e while right.children:\n...     right = right.children[-1]\n\u003e\u003e\u003e right\n\u003cLiteral 4\u003e\n\n\u003e\u003e\u003e from d20 import utils  # these patterns are available in the utils submodule:\n\u003e\u003e\u003e utils.leftmost(binop.expr)\n\u003cLiteral 1\u003e\n\u003e\u003e\u003e utils.rightmost(binop.expr)\n\u003cLiteral 4\u003e\n```\n\n\nSearching for the d4:\n```python\n\u003e\u003e\u003e from d20 import roll, Dice, SimpleStringifier, utils\n\n\u003e\u003e\u003e mixed = roll(\"-1d8 + 4 - (3, 1d4)kh1\")\n\u003e\u003e\u003e str(mixed)\n'-1d8 (**8**) + 4 - (3, ~~1d4 (3)~~)kh1 = `-7`'\n\u003e\u003e\u003e root = mixed.expr\n\u003e\u003e\u003e result = utils.dfs(root, lambda node: isinstance(node, Dice) and node.num == 1 and node.size == 4)\n\u003e\u003e\u003e result\n\u003cDice num=1 size=4 values=[\u003cDie size=4 values=[\u003cLiteral 3\u003e]\u003e] operations=[]\u003e\n\u003e\u003e\u003e SimpleStringifier().stringify(result)\n'1d4 (3)'\n```\nAs a note, even though a `Dice` object is the parent of `Die` objects, `Dice.children` returns an empty list, since it's \nmore common to look for the dice, and not each individual component of that dice.\n\n## Performance\nBy default, the parser caches the 256 most frequently used dice expressions in an LFU cache, allowing for a significant \nspeedup when rolling many of the same kinds of rolls. This caching is disabled when `allow_comments` is True.\n\nWith caching:\n```bash\n$ python3 -m timeit -s \"from d20 import roll\" \"roll('1d20')\"\n10000 loops, best of 5: 21.6 usec per loop\n$ python3 -m timeit -s \"from d20 import roll\" \"roll('100d20')\"\n500 loops, best of 5: 572 usec per loop\n$ python3 -m timeit -s \"from d20 import roll; expr='1d20+'*50+'1d20'\" \"roll(expr)\"\n500 loops, best of 5: 732 usec per loop\n$ python3 -m timeit -s \"from d20 import roll\" \"roll('10d20rr\u003c20')\"\n1000 loops, best of 5: 1.13 msec per loop\n```\n\nWithout caching:\n```bash\n$ python3 -m timeit -s \"from d20 import roll\" \"roll('1d20')\"\n5000 loops, best of 5: 61.6 usec per loop\n$ python3 -m timeit -s \"from d20 import roll\" \"roll('100d20')\"\n500 loops, best of 5: 620 usec per loop\n$ python3 -m timeit -s \"from d20 import roll; expr='1d20+'*50+'1d20'\" \"roll(expr)\"\n500 loops, best of 5: 2.1 msec per loop\n$ python3 -m timeit -s \"from d20 import roll\" \"roll('10d20rr\u003c20')\"\n1000 loops, best of 5: 1.26 msec per loop\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Favrae%2Fd20","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Favrae%2Fd20","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Favrae%2Fd20/lists"}