Repository: Vlek/roll-cli (language: python)

Issue:
Add dice "keep" (k) notation
Add the ability to denote that a dice should be rolled and only a number of highest or lowest rolls should be kept.

```
(Dice) K/k [count]

2d20K - Keep the highest roll out of the two.

4d6K3 - Keep the highest three rolls out of four. (Like for stat generation for D&D)

2d20k - Keep the lowest roll out of the two.

4d6k2 - Keep the lowest two rolls out of four.
```


Discussion / hints:
Currently working on this and ran into a bit of a thinker of a problem:

The parser that we have is going to throw a fit and be much slower the more I add onto the operators that we have. This is especially the case if I had recursive issues by supplying more dice-specific ones as I have not found a way to use the built-ins well in regards to having order of operations and also optional operands for a given operation.

My current thought is that I should use this as a separate operation outside of the dice notation and have it only accept a dice as the left-side operand. That way it keeps my code cleaner and, more importantly, I don't have to go through all of the headache cases or somehow figure out how to notate that any of these adder things to the dice notation are acceptable in any order. I am going to chew on this some more and try to create a prototype based on the above.

Fix patch:
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..1dc77a6
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,4 @@
+# Good variable names which should always be accepted.
+# We have to go with this because they haven't fixed this yet:
+# https://github.com/PyCQA/pylint/issues/2018
+good-names=x
diff --git a/poetry.lock b/poetry.lock
index 35db60d..7451ff1 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -8,21 +8,21 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "attrs"
-version = "20.3.0"
+version = "21.2.0"
 description = "Classes Without Boilerplate"
 category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [package.extras]
-dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface", "furo", "sphinx", "pre-commit"]
-docs = ["furo", "sphinx", "zope.interface"]
-tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"]
-tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six"]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"]
+docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"]
 
 [[package]]
 name = "certifi"
-version = "2020.12.5"
+version = "2021.5.30"
 description = "Python package for providing Mozilla's CA Bundle."
 category = "dev"
 optional = false
@@ -66,7 +66,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 
 [[package]]
 name = "coverage"
-version = "5.4"
+version = "5.5"
 description = "Code coverage measurement for Python"
 category = "dev"
 optional = false
@@ -85,7 +85,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 
 [[package]]
 name = "importlib-metadata"
-version = "3.4.0"
+version = "4.6.1"
 description = "Read metadata from Python packages"
 category = "dev"
 optional = false
@@ -97,11 +97,12 @@ zipp = ">=0.5"
 
 [package.extras]
 docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
+perf = ["ipython"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
 
 [[package]]
 name = "more-itertools"
-version = "8.7.0"
+version = "8.8.0"
 description = "More routines for operating on iterables, beyond itertools"
 category = "dev"
 optional = false
@@ -109,11 +110,11 @@ python-versions = ">=3.5"
 
 [[package]]
 name = "packaging"
-version = "20.9"
+version = "21.0"
 description = "Core utilities for Python packages"
 category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.6"
 
 [package.dependencies]
 pyparsing = ">=2.0.2"
@@ -173,7 +174,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xm
 
 [[package]]
 name = "pytest-cov"
-version = "2.11.1"
+version = "2.12.1"
 description = "Pytest plugin for measuring coverage."
 category = "dev"
 optional = false
@@ -182,9 +183,10 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 [package.dependencies]
 coverage = ">=5.2.1"
 pytest = ">=4.6"
+toml = "*"
 
 [package.extras]
-testing = ["fields", "hunter", "process-tests (==2.0.2)", "six", "pytest-xdist", "virtualenv"]
+testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
 
 [[package]]
 name = "requests"
@@ -214,7 +216,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 
 [[package]]
 name = "typing-extensions"
-version = "3.7.4.3"
+version = "3.10.0.0"
 description = "Backported and Experimental Type Hints for Python 3.5+"
 category = "main"
 optional = false
@@ -222,7 +224,7 @@ python-versions = "*"
 
 [[package]]
 name = "urllib3"
-version = "1.26.3"
+version = "1.26.6"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 category = "dev"
 optional = false
@@ -243,15 +245,15 @@ python-versions = "*"
 
 [[package]]
 name = "zipp"
-version = "3.4.0"
+version = "3.5.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 category = "dev"
 optional = false
 python-versions = ">=3.6"
 
 [package.extras]
-docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
-testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
 
 [metadata]
 lock-version = "1.1"
@@ -264,12 +266,12 @@ atomicwrites = [
     {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
 ]
 attrs = [
-    {file = "attrs-20.3.0-py2.py3-none-any.whl", hash = "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6"},
-    {file = "attrs-20.3.0.tar.gz", hash = "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"},
+    {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"},
+    {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"},
 ]
 certifi = [
-    {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"},
-    {file = "certifi-2020.12.5.tar.gz", hash = "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c"},
+    {file = "certifi-2021.5.30-py2.py3-none-any.whl", hash = "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"},
+    {file = "certifi-2021.5.30.tar.gz", hash = "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee"},
 ]
 chardet = [
     {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"},
@@ -289,71 +291,74 @@ colorama = [
     {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
 ]
 coverage = [
-    {file = "coverage-5.4-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:6d9c88b787638a451f41f97446a1c9fd416e669b4d9717ae4615bd29de1ac135"},
-    {file = "coverage-5.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:66a5aae8233d766a877c5ef293ec5ab9520929c2578fd2069308a98b7374ea8c"},
-    {file = "coverage-5.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9754a5c265f991317de2bac0c70a746efc2b695cf4d49f5d2cddeac36544fb44"},
-    {file = "coverage-5.4-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:fbb17c0d0822684b7d6c09915677a32319f16ff1115df5ec05bdcaaee40b35f3"},
-    {file = "coverage-5.4-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:b7f7421841f8db443855d2854e25914a79a1ff48ae92f70d0a5c2f8907ab98c9"},
-    {file = "coverage-5.4-cp27-cp27m-win32.whl", hash = "sha256:4a780807e80479f281d47ee4af2eb2df3e4ccf4723484f77da0bb49d027e40a1"},
-    {file = "coverage-5.4-cp27-cp27m-win_amd64.whl", hash = "sha256:87c4b38288f71acd2106f5d94f575bc2136ea2887fdb5dfe18003c881fa6b370"},
-    {file = "coverage-5.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:c6809ebcbf6c1049002b9ac09c127ae43929042ec1f1dbd8bb1615f7cd9f70a0"},
-    {file = "coverage-5.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ba7ca81b6d60a9f7a0b4b4e175dcc38e8fef4992673d9d6e6879fd6de00dd9b8"},
-    {file = "coverage-5.4-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:89fc12c6371bf963809abc46cced4a01ca4f99cba17be5e7d416ed7ef1245d19"},
-    {file = "coverage-5.4-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a8eb7785bd23565b542b01fb39115a975fefb4a82f23d407503eee2c0106247"},
-    {file = "coverage-5.4-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:7e40d3f8eb472c1509b12ac2a7e24158ec352fc8567b77ab02c0db053927e339"},
-    {file = "coverage-5.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:1ccae21a076d3d5f471700f6d30eb486da1626c380b23c70ae32ab823e453337"},
-    {file = "coverage-5.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:755c56beeacac6a24c8e1074f89f34f4373abce8b662470d3aa719ae304931f3"},
-    {file = "coverage-5.4-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:322549b880b2d746a7672bf6ff9ed3f895e9c9f108b714e7360292aa5c5d7cf4"},
-    {file = "coverage-5.4-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:60a3307a84ec60578accd35d7f0c71a3a971430ed7eca6567399d2b50ef37b8c"},
-    {file = "coverage-5.4-cp35-cp35m-win32.whl", hash = "sha256:1375bb8b88cb050a2d4e0da901001347a44302aeadb8ceb4b6e5aa373b8ea68f"},
-    {file = "coverage-5.4-cp35-cp35m-win_amd64.whl", hash = "sha256:16baa799ec09cc0dcb43a10680573269d407c159325972dd7114ee7649e56c66"},
-    {file = "coverage-5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:2f2cf7a42d4b7654c9a67b9d091ec24374f7c58794858bff632a2039cb15984d"},
-    {file = "coverage-5.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:b62046592b44263fa7570f1117d372ae3f310222af1fc1407416f037fb3af21b"},
-    {file = "coverage-5.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:812eaf4939ef2284d29653bcfee9665f11f013724f07258928f849a2306ea9f9"},
-    {file = "coverage-5.4-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:859f0add98707b182b4867359e12bde806b82483fb12a9ae868a77880fc3b7af"},
-    {file = "coverage-5.4-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:04b14e45d6a8e159c9767ae57ecb34563ad93440fc1b26516a89ceb5b33c1ad5"},
-    {file = "coverage-5.4-cp36-cp36m-win32.whl", hash = "sha256:ebfa374067af240d079ef97b8064478f3bf71038b78b017eb6ec93ede1b6bcec"},
-    {file = "coverage-5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:84df004223fd0550d0ea7a37882e5c889f3c6d45535c639ce9802293b39cd5c9"},
-    {file = "coverage-5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:1b811662ecf72eb2d08872731636aee6559cae21862c36f74703be727b45df90"},
-    {file = "coverage-5.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6b588b5cf51dc0fd1c9e19f622457cc74b7d26fe295432e434525f1c0fae02bc"},
-    {file = "coverage-5.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:3fe50f1cac369b02d34ad904dfe0771acc483f82a1b54c5e93632916ba847b37"},
-    {file = "coverage-5.4-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:32ab83016c24c5cf3db2943286b85b0a172dae08c58d0f53875235219b676409"},
-    {file = "coverage-5.4-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:68fb816a5dd901c6aff352ce49e2a0ffadacdf9b6fae282a69e7a16a02dad5fb"},
-    {file = "coverage-5.4-cp37-cp37m-win32.whl", hash = "sha256:a636160680c6e526b84f85d304e2f0bb4e94f8284dd765a1911de9a40450b10a"},
-    {file = "coverage-5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:bb32ca14b4d04e172c541c69eec5f385f9a075b38fb22d765d8b0ce3af3a0c22"},
-    {file = "coverage-5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4d7165a4e8f41eca6b990c12ee7f44fef3932fac48ca32cecb3a1b2223c21f"},
-    {file = "coverage-5.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a565f48c4aae72d1d3d3f8e8fb7218f5609c964e9c6f68604608e5958b9c60c3"},
-    {file = "coverage-5.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fff1f3a586246110f34dc762098b5afd2de88de507559e63553d7da643053786"},
-    {file = "coverage-5.4-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:a839e25f07e428a87d17d857d9935dd743130e77ff46524abb992b962eb2076c"},
-    {file = "coverage-5.4-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:6625e52b6f346a283c3d563d1fd8bae8956daafc64bb5bbd2b8f8a07608e3994"},
-    {file = "coverage-5.4-cp38-cp38-win32.whl", hash = "sha256:5bee3970617b3d74759b2d2df2f6a327d372f9732f9ccbf03fa591b5f7581e39"},
-    {file = "coverage-5.4-cp38-cp38-win_amd64.whl", hash = "sha256:03ed2a641e412e42cc35c244508cf186015c217f0e4d496bf6d7078ebe837ae7"},
-    {file = "coverage-5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14a9f1887591684fb59fdba8feef7123a0da2424b0652e1b58dd5b9a7bb1188c"},
-    {file = "coverage-5.4-cp39-cp39-manylinux1_i686.whl", hash = "sha256:9564ac7eb1652c3701ac691ca72934dd3009997c81266807aef924012df2f4b3"},
-    {file = "coverage-5.4-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:0f48fc7dc82ee14aeaedb986e175a429d24129b7eada1b7e94a864e4f0644dde"},
-    {file = "coverage-5.4-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:107d327071061fd4f4a2587d14c389a27e4e5c93c7cba5f1f59987181903902f"},
-    {file = "coverage-5.4-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:0cdde51bfcf6b6bd862ee9be324521ec619b20590787d1655d005c3fb175005f"},
-    {file = "coverage-5.4-cp39-cp39-win32.whl", hash = "sha256:c67734cff78383a1f23ceba3b3239c7deefc62ac2b05fa6a47bcd565771e5880"},
-    {file = "coverage-5.4-cp39-cp39-win_amd64.whl", hash = "sha256:c669b440ce46ae3abe9b2d44a913b5fd86bb19eb14a8701e88e3918902ecd345"},
-    {file = "coverage-5.4-pp36-none-any.whl", hash = "sha256:c0ff1c1b4d13e2240821ef23c1efb1f009207cb3f56e16986f713c2b0e7cd37f"},
-    {file = "coverage-5.4-pp37-none-any.whl", hash = "sha256:cd601187476c6bed26a0398353212684c427e10a903aeafa6da40c63309d438b"},
-    {file = "coverage-5.4.tar.gz", hash = "sha256:6d2e262e5e8da6fa56e774fb8e2643417351427604c2b177f8e8c5f75fc928ca"},
+    {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"},
+    {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"},
+    {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"},
+    {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"},
+    {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"},
+    {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"},
+    {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"},
+    {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"},
+    {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"},
+    {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"},
+    {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"},
+    {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"},
+    {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"},
+    {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"},
+    {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"},
+    {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"},
+    {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"},
+    {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"},
+    {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"},
+    {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"},
+    {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"},
+    {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"},
+    {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"},
+    {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"},
+    {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"},
+    {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"},
+    {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"},
+    {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"},
+    {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"},
+    {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"},
+    {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"},
+    {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"},
+    {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"},
+    {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"},
+    {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"},
+    {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"},
+    {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"},
+    {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"},
+    {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"},
+    {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"},
+    {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"},
+    {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"},
+    {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"},
+    {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"},
+    {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"},
+    {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"},
+    {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"},
+    {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"},
+    {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"},
+    {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"},
+    {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"},
+    {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"},
 ]
 idna = [
     {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"},
     {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"},
 ]
 importlib-metadata = [
-    {file = "importlib_metadata-3.4.0-py3-none-any.whl", hash = "sha256:ace61d5fc652dc280e7b6b4ff732a9c2d40db2c0f92bc6cb74e07b73d53a1771"},
-    {file = "importlib_metadata-3.4.0.tar.gz", hash = "sha256:fa5daa4477a7414ae34e95942e4dd07f62adf589143c875c133c1e53c4eff38d"},
+    {file = "importlib_metadata-4.6.1-py3-none-any.whl", hash = "sha256:9f55f560e116f8643ecf2922d9cd3e1c7e8d52e683178fecd9d08f6aa357e11e"},
+    {file = "importlib_metadata-4.6.1.tar.gz", hash = "sha256:079ada16b7fc30dfbb5d13399a5113110dab1aa7c2bc62f66af75f0b717c8cac"},
 ]
 more-itertools = [
-    {file = "more-itertools-8.7.0.tar.gz", hash = "sha256:c5d6da9ca3ff65220c3bfd2a8db06d698f05d4d2b9be57e1deb2be5a45019713"},
-    {file = "more_itertools-8.7.0-py3-none-any.whl", hash = "sha256:5652a9ac72209ed7df8d9c15daf4e1aa0e3d2ccd3c87f8265a0673cd9cbc9ced"},
+    {file = "more-itertools-8.8.0.tar.gz", hash = "sha256:83f0308e05477c68f56ea3a888172c78ed5d5b3c282addb67508e7ba6c8f813a"},
+    {file = "more_itertools-8.8.0-py3-none-any.whl", hash = "sha256:2cf89ec599962f2ddc4d568a05defc40e0a587fbc10d5989713638864c36be4d"},
 ]
 packaging = [
-    {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"},
-    {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"},
+    {file = "packaging-21.0-py3-none-any.whl", hash = "sha256:c86254f9220d55e31cc94d69bade760f0847da8000def4dfe1c6b872fd14ff14"},
+    {file = "packaging-21.0.tar.gz", hash = "sha256:7dc96269f53a4ccec5c0670940a4281106dd0bb343f47b7471f779df49c2fbe7"},
 ]
 pluggy = [
     {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"},
@@ -372,8 +377,8 @@ pytest = [
     {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"},
 ]
 pytest-cov = [
-    {file = "pytest-cov-2.11.1.tar.gz", hash = "sha256:359952d9d39b9f822d9d29324483e7ba04a3a17dd7d05aa6beb7ea01e359e5f7"},
-    {file = "pytest_cov-2.11.1-py2.py3-none-any.whl", hash = "sha256:bdb9fdb0b85a7cc825269a4c56b48ccaa5c7e365054b6038772c32ddcdc969da"},
+    {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"},
+    {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"},
 ]
 requests = [
     {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"},
@@ -384,19 +389,19 @@ toml = [
     {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
 ]
 typing-extensions = [
-    {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"},
-    {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"},
-    {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"},
+    {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"},
+    {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"},
+    {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"},
 ]
 urllib3 = [
-    {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"},
-    {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"},
+    {file = "urllib3-1.26.6-py2.py3-none-any.whl", hash = "sha256:39fb8672126159acb139a7718dd10806104dec1e2f0f6c88aab05d17df10c8d4"},
+    {file = "urllib3-1.26.6.tar.gz", hash = "sha256:f57b4c16c62fa2760b7e3d97c35b255512fb6b59a259730f36ba32ce9f8e342f"},
 ]
 wcwidth = [
     {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"},
     {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"},
 ]
 zipp = [
-    {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"},
-    {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"},
+    {file = "zipp-3.5.0-py3-none-any.whl", hash = "sha256:957cfda87797e389580cb8b9e3870841ca991e2125350677b2ca83a0e99390a3"},
+    {file = "zipp-3.5.0.tar.gz", hash = "sha256:f5812b1e007e48cff63449a5e9f4e7ebea716b4111f9c4f9a645f91d579bf0c4"},
 ]
diff --git a/pyproject.toml b/pyproject.toml
index 95afeb1..b844c5e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "pyroll"
-version = "2.0.0"
+version = "2.1.0"
 description = "Dice roller with all of the features you could want."
 authors = ["Derek 'Vlek' McCammond <dmccammond25@gmail.com>"]
 license = "gpl-3.0"
@@ -35,4 +35,4 @@ requires = ["poetry>=0.12"]
 build-backend = "poetry.masonry.api"
 
 [tool.poetry.scripts]
-roll = 'roll.click_dice:roll_cli'
+roll = 'roll.click.click_dice:roll_cli'
diff --git a/roll/__init__.py b/roll/__init__.py
index a81d894..ead7473 100644
--- a/roll/__init__.py
+++ b/roll/__init__.py
@@ -1,1 +1,1 @@
-from .roll import roll
+from roll.roll import roll
diff --git a/roll/click/__init__.py b/roll/click/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/roll/click_dice.py b/roll/click/click_dice.py
similarity index 76%
rename from roll/click_dice.py
rename to roll/click/click_dice.py
index ad251b0..2c7f9d7 100644
--- a/roll/click_dice.py
+++ b/roll/click/click_dice.py
@@ -15,10 +15,11 @@
 
 from typing import List, Union
 
-import click
-
+import roll.parser.operations as ops
 from roll import roll
-from roll.diceparser import EvaluationResults, RollOption
+from roll.parser.types import EvaluationResults, RollOption
+
+import click
 
 
 @click.command()
@@ -57,18 +58,24 @@ def roll_cli(expression: List[str] = None,
     """
     command_input = ' '.join(expression) if expression is not None else ''
 
+    # Because of how we are now evaluating during parsing, we have to go back
+    # to the old way of having some globalish variable for handing off the
+    # type of roll that we're attempting to do.
+    ops.ROLL_TYPE = roll_option
+
     result: Union[int, float, EvaluationResults] = roll(
         command_input, verbose, roll_option,
     )
 
-    if verbose and isinstance(result, dict):
-        for r in result['rolls']:
+    if verbose and isinstance(result, EvaluationResults):
+        for r in result.rolls:
             click.echo(
-                f"{r['dice']}: {r['rolls']}"
+                f"{r.dice}: {r.rolls}"
             )
 
-        click.echo(result['total'])
-
+        click.echo(result.total)
+    elif isinstance(result, EvaluationResults):
+        click.echo(result.total)
     else:
         click.echo(result)
 
diff --git a/roll/diceparser.py b/roll/diceparser.py
deleted file mode 100644
index 1069189..0000000
--- a/roll/diceparser.py
+++ /dev/null
@@ -1,342 +0,0 @@
-#!/user/bin/env python3
-
-"""
-Dice rolling PyParser grammar.
-
-Grammar:
-Digit ::= [1234567890]
-Number ::= ( '-' )? Digit Digit* ( '.' Digit Digit*)?
-Add ::= Number '+' Number
-Sub ::= Number '-' Number
-AddOrSub ::= Add | Sub
-Mult ::= Number '*' Number
-Div ::= Number '/' Number
-Mod ::= Number '%' Number
-IntDiv ::= Number '//' Number
-MultOrDiv ::= Mult | Div | Mod | IntDiv
-Exponent ::= Number '**' Number
-PercentDie ::= Number? 'd%'
-Die ::= Number? 'd' Number
-Dice ::= Die | PercentDie
-Parens ::= Number? '(' Expression ')'
-Expression ::= (Parens | Exponent | Dice | MultOrDiv | AddOrSub | Number)+
-Main ::= Expression
-
-Website used to do railroad diagrams: https://www.bottlecaps.de/rr/ui
-"""
-
-from enum import Enum
-from math import ceil, e, factorial, floor, pi, sqrt
-from operator import add, floordiv, mod, mul, sub, truediv
-from random import randint
-from sys import version_info
-from typing import Callable, Dict, List, Optional, Union
-
-# TypedDict was added in 3.8.
-if version_info >= (3, 8):
-    from typing import TypedDict
-else:
-    from typing_extensions import TypedDict
-
-from pyparsing import (CaselessKeyword, CaselessLiteral, Forward, Literal,
-                       ParseException, ParserElement, ParseResults, oneOf,
-                       opAssoc, operatorPrecedence, pyparsing_common)
-
-ParserElement.enablePackrat()
-
-
-class RollResults(TypedDict):
-    total: Union[int, float]
-    dice: str
-    rolls: List[Union[int, float]]
-
-
-class EvaluationResults(TypedDict):
-    total: Union[int, float]
-    rolls: List[RollResults]
-
-
-class RollOption(Enum):
-    Minimum = 0
-    Normal = 1
-    Maximum = 2
-
-
-def _roll_dice(
-        num_dice: Union[int, float],
-        sides: Union[int, float],
-        roll_option: RollOption = RollOption.Normal,
-        ) -> RollResults:
-    """Calculate value of dice roll notation."""
-    starting_num_dice = num_dice
-    starting_sides = sides
-
-    # If it's the case that we were given a dice with negative sides,
-    # then that doesn't mean anything in the real world. I cannot
-    # for the life of me figure out a possible scenario where that
-    # would make sense. We will just error out.
-    if sides < 0:
-        raise ValueError('The sides of a die must be positive or zero.')
-
-    result_is_negative = num_dice < 0
-
-    if result_is_negative:
-        num_dice = abs(num_dice)
-
-    sides = ceil(sides)
-
-    rolls: List[Union[int, float]] = []
-
-    if roll_option == RollOption.Minimum:
-        rolls = [1] * ceil(num_dice)
-    elif roll_option == RollOption.Maximum:
-        rolls = [floor(sides)] * floor(num_dice)
-
-        if isinstance(num_dice, float) and (num_dice % 1) != 0:
-            rolls.append(sides * (num_dice % 1))
-    elif sides != 0:
-        rolls = [randint(1, sides) for _ in range(floor(num_dice))]
-
-        # If it's the case that the number of dice is a float, then
-        # we take that to mean that it is a dice where the sides should
-        # be lowered to reflect the float amount.
-        #
-        # We do not want this to effect all dice rolls however, only the
-        # last one (or the only one if there's only a decimal portion).
-        if isinstance(num_dice, float) and (num_dice % 1) != 0:
-            sides = ceil(sides * (num_dice % 1))
-            rolls.append(randint(1, sides))
-
-    rolls_total = sum(rolls)
-
-    if result_is_negative:
-        rolls_total *= -1
-
-    result: RollResults = {
-        'total': rolls_total,
-        'dice': f'{starting_num_dice}d{starting_sides}',
-        'rolls': rolls
-    }
-
-    return result
-
-
-class DiceParser:
-    """Parser for evaluating dice strings."""
-
-    operations: Dict[
-        str,
-        Union[
-            Callable[
-                [
-                    Union[int, float],
-                    Union[int, float]
-                ], Union[int, float, RollResults]],
-            # This is needed for factorial and sqrt
-            Callable[
-                [Union[int, float]],
-                Union[int, float]
-            ]
-        ]] = {
-        "+": add,
-        "-": sub,
-        "*": mul,
-        "/": truediv,
-        "//": floordiv,
-        "%": mod,
-        "^": pow,
-        "**": pow,
-        "d": _roll_dice,
-        "!": factorial,
-        "sqrt": sqrt
-    }
-
-    constants = {
-        "pi": pi,
-        "e": e
-    }
-
-    def __init__(self: "DiceParser") -> None:
-        """Initialize a parser to handle dice strings."""
-        self._parser = self._create_parser()
-
-    @staticmethod
-    def _create_parser() -> Forward:
-        """Create an instance of a dice roll string parser."""
-        atom = (
-            CaselessLiteral("d%") |
-            pyparsing_common.number |
-            CaselessKeyword("pi") |
-            CaselessKeyword("e")
-        )
-
-        expression = operatorPrecedence(atom, [
-            (Literal('-'), 1, opAssoc.RIGHT),
-            (CaselessLiteral('sqrt'), 1, opAssoc.RIGHT),
-            (oneOf('^ **'), 2, opAssoc.RIGHT),
-
-            (Literal('-'), 1, opAssoc.RIGHT),
-            (Literal('!'), 1, opAssoc.LEFT),
-
-            (CaselessLiteral('d%'), 1, opAssoc.LEFT),
-            (CaselessLiteral('d'), 2, opAssoc.RIGHT),
-
-            # This line causes the recursion debug to go off.
-            # Will have to find a way to have an optional left
-            # operator in this case.
-            (CaselessLiteral('d'), 1, opAssoc.RIGHT),
-
-            (oneOf('* / % //'), 2, opAssoc.LEFT),
-
-            (oneOf('+ -'), 2, opAssoc.LEFT),
-        ])
-
-        return expression
-
-    def parse(self: "DiceParser", dice_string: str) -> List[Union[str, int]]:
-        """Parse well-formed dice roll strings."""
-        try:
-            return self._parser.parseString(dice_string, parseAll=True)
-        except ParseException:
-            raise SyntaxError("Unable to parse input string: " + dice_string)
-
-    def evaluate(
-            self: "DiceParser",
-            parsed_values: Union[List[Union[str, int]], str],
-            roll_option: RollOption = RollOption.Normal,
-    ) -> EvaluationResults:
-        """Evaluate the output parsed values from roll strings."""
-        if isinstance(parsed_values, str):
-            parsed_values = self.parse(parsed_values)
-
-        result: Union[int, float, None] = None
-        operator = None
-        dice_rolls = []
-
-        val: Union[str, int, float, Optional[float]]
-        for val in parsed_values:
-
-            # In addition to dealing with values, we are also going to
-            # handle constants and nested lists here because, after they
-            # are evaluated, they are then used as values in the current
-            # evaluation context.
-            if (
-                    isinstance(val, (int, float, ParseResults)) or
-                    val in self.constants
-            ):
-                if val in self.constants:
-                    val = self.constants[str(val)]
-                elif isinstance(val, ParseResults):
-                    evaluation: EvaluationResults = self.evaluate(val,
-                                                                  roll_option)
-                    val = evaluation['total']
-                    dice_rolls.extend(evaluation['rolls'])
-
-                if operator is not None:
-                    # There are currently only two cases that could
-                    # cause result to be none, either we're dealing
-                    # with a dice roll that doesn't have a left-hand
-                    # number or a unary minus. In either case, we have
-                    # to initialize the result accordingly.
-                    if result is None:
-                        if operator is _roll_dice:
-                            result = 1
-                        else:
-                            result = 0
-
-                    if operator is _roll_dice:
-                        current_rolls = _roll_dice(result, val, roll_option)
-
-                        result = current_rolls['total']
-                        dice_rolls.append(current_rolls)
-                    elif operator is sqrt:
-                        result = operator(val)
-                    else:
-                        result = operator(result, val)
-                else:
-                    result = val
-
-            elif val in self.operations:
-
-                # Since factorials are unary and the value is to the left-
-                # hand side, we will execute the factorial function here
-                # as we do not have to wait for any further input.
-                if val == "!":
-                    result = factorial(result if result is not None else 0)
-                    continue
-
-                operator = self.operations[val]
-
-            elif val in ["D%", "d%"]:
-                current_rolls = _roll_dice(
-                    result if result is not None else 1, 100, roll_option)
-
-                result = current_rolls['total']
-                dice_rolls.append(current_rolls)
-
-        total_results: EvaluationResults = {
-            'total': result if result is not None else 0,
-            'rolls': dice_rolls
-        }
-
-        return total_results
-
-
-if __name__ == "__main__":
-    parser = DiceParser()
-
-    # print("Recursive issues:", parser._parser.validate())
-    roll_strings = [
-        "5-3",
-        "3-5",
-        "3--5",
-        "1d2d3",
-        "5^2d1",
-        "0!d20",
-        "5 + 2!",
-        "5**(2)",
-        "5**2 * 7",
-        "2 + 5 d   6",
-        "(2)d6",
-        "2d(6)",
-        "3",
-        "-3",
-        "--3",
-        "100.",
-        "1 2",
-        "--7",
-        "9.0",
-        "-12.05",
-        "1 + 2",
-        "2 - 1",
-        "100 - 3",
-        "12 // 4",
-        "3+2*4",
-        "2^4",
-        "3 + 2 * 2^1",
-        "9-1+27+(3-5)+9",
-        "1d%",
-        "d%",
-        "D%",
-        "1d20",
-        "d20",
-        "d20 + 5",
-        "2d6 + 1d8 + 4",
-        "5!",
-        "pi",
-        "pi + 2",
-        "pi * e",
-        "(2 + 8 / (9 - 5)) * 3",
-        "100 - 21 / 7",
-        "((((((3))))))",
-    ]
-
-    for rs in roll_strings:
-        try:
-            parsed_string = parser.parse(rs)
-
-            # print(rs)
-            # print(parsed_string)
-            print(parser.evaluate(parsed_string))
-        except Exception as e:
-            print(f"Exception '{e}' occured parsing: " + rs)
diff --git a/roll/parser/__init__.py b/roll/parser/__init__.py
new file mode 100644
index 0000000..4d4e541
--- /dev/null
+++ b/roll/parser/__init__.py
@@ -0,0 +1,1 @@
+from roll.parser.diceparser import DiceParser
diff --git a/roll/parser/diceparser.py b/roll/parser/diceparser.py
new file mode 100644
index 0000000..ce513c7
--- /dev/null
+++ b/roll/parser/diceparser.py
@@ -0,0 +1,280 @@
+#!/user/bin/env python3
+
+"""
+Dice rolling PyParser grammar.
+
+Grammar:
+Digit ::= [1234567890]
+Number ::= ( '-' )? Digit Digit* ( '.' Digit Digit*)?
+Add ::= Number '+' Number
+Sub ::= Number '-' Number
+AddOrSub ::= Add | Sub
+Mult ::= Number '*' Number
+Div ::= Number '/' Number
+Mod ::= Number '%' Number
+IntDiv ::= Number '//' Number
+MultOrDiv ::= Mult | Div | Mod | IntDiv
+Exponent ::= Number '**' Number
+PercentDie ::= Number? 'd%'
+Die ::= Number? 'd' Number
+Dice ::= Die | PercentDie
+Parens ::= Number? '(' Expression ')'
+Expression ::= (Parens | Exponent | Dice | MultOrDiv | AddOrSub | Number)+
+Main ::= Expression
+
+Website used to do railroad diagrams: https://www.bottlecaps.de/rr/ui
+"""
+
+from __future__ import annotations
+
+from math import e, factorial, pi
+from typing import Callable, Dict, List, Union
+
+from pyparsing import (CaselessKeyword, CaselessLiteral, Forward, Literal,
+                       ParseException, ParserElement, infixNotation, oneOf,
+                       opAssoc, pyparsing_common)
+from roll.parser.operations import (ROLL_TYPE, add, expo, floor_div, mod, mult,
+                                    roll_dice, sqrt, sub, true_div)
+from roll.parser.types import EvaluationResults, RollOption, RollResults
+
+ParserElement.enablePackrat()
+
+
+class DiceParser:
+    """Parser for evaluating dice strings."""
+
+    OPERATIONS: Dict[str, Callable] = {
+        "+": add,
+        "-": sub,
+        "*": mult,
+        "/": true_div,
+        "//": floor_div,
+        "%": mod,
+        "^": expo,
+        "d": roll_dice,
+    }
+
+    def __init__(self: "DiceParser") -> None:
+        """Initialize a parser to handle dice strings."""
+        self._parser = self._create_parser()
+
+    @staticmethod
+    def _create_parser() -> Forward:
+        """Create an instance of a dice roll string parser."""
+        atom = (
+            CaselessLiteral("d%").setParseAction(
+                lambda: DiceParser._handle_roll(1, 100)) |
+            pyparsing_common.number |
+            CaselessKeyword("pi").setParseAction(lambda: pi) |
+            CaselessKeyword("e").setParseAction(lambda: e)
+        )
+
+        expression = infixNotation(atom, [
+            # Unary minus
+            (Literal('-'), 1, opAssoc.RIGHT, DiceParser._handle_unary_minus),
+            # Square root
+            (CaselessLiteral('sqrt'), 1, opAssoc.RIGHT,
+             DiceParser._handle_sqrt),
+
+            # Exponents
+            (oneOf('^ **'), 2, opAssoc.RIGHT, DiceParser._handle_expo),
+
+            # Unary minus (#2)
+            (Literal('-'), 1, opAssoc.RIGHT, DiceParser._handle_unary_minus),
+            # Factorial
+            (Literal('!'), 1, opAssoc.LEFT, DiceParser._handle_factorial),
+
+            # Dice notations
+            (CaselessLiteral('d%'), 1, opAssoc.LEFT,
+             lambda toks: DiceParser._handle_roll(toks[0][0], 100)),
+            (CaselessLiteral('d'), 2, opAssoc.RIGHT,
+             lambda toks: DiceParser._handle_roll(toks[0][0], toks[0][2])),
+
+            # This line causes the recursion debug to go off.
+            # Will have to find a way to have an optional left
+            # operand in this case.
+            (CaselessLiteral('d'), 1, opAssoc.RIGHT,
+             lambda toks: DiceParser._handle_roll(1, toks[0][1])),
+
+            # Keep notation
+            (oneOf('k K'), 2, opAssoc.LEFT,
+             DiceParser._handle_keep),
+            (oneOf('k K'), 1, opAssoc.LEFT,
+             DiceParser._handle_keep),
+
+            # Multiplication and division
+            (oneOf('* / % //'), 2, opAssoc.LEFT,
+             DiceParser._handle_standard_operation),
+
+            # Addition and subtraction
+            (oneOf('+ -'), 2, opAssoc.LEFT,
+             DiceParser._handle_standard_operation),
+
+            # TODO: Use this to make a pretty exception message
+            # where we point out and explain the issue.
+        ]).setFailAction(lambda s, loc, expr, err: print(err))
+
+        return expression
+
+    @staticmethod
+    def _handle_unary_minus(
+            toks: list[list[Union[int, float, EvaluationResults
+                                  ]]]) -> Union[int, float, EvaluationResults]:
+        return -toks[0][1]
+
+    @staticmethod
+    def _handle_sqrt(
+            toks: list[list[Union[str, int, float, EvaluationResults
+                                  ]]]) -> EvaluationResults:
+        value: Union[int, float, EvaluationResults] = float(toks[0][1])
+        return sqrt(value)
+
+    @staticmethod
+    def _handle_expo(
+            toks: list[list[Union[int, float, EvaluationResults
+                                  ]]]) -> Union[int, float, EvaluationResults]:
+        return toks[0][0] ** toks[0][2]
+
+    @staticmethod
+    def _handle_factorial(
+            toks: List[List[Union[int, float, EvaluationResults
+                                  ]]]) -> Union[int, float, EvaluationResults]:
+        return factorial(toks[0][0])
+
+    @staticmethod
+    def _handle_roll(sides: Union[int, float, EvaluationResults],
+                     num: Union[int, float, EvaluationResults]
+                     ) -> Union[int, float, EvaluationResults]:
+        roll_option = ROLL_TYPE
+        return roll_dice(sides, num, roll_option)
+
+    @staticmethod
+    def _handle_keep(
+            toks: List[List[Union[int, float, EvaluationResults, str
+                                  ]]]) -> Union[int, float, EvaluationResults]:
+        tokens: List[Union[int, float,
+                           EvaluationResults, str]] = toks[0]
+
+        if not isinstance(tokens[0], EvaluationResults):
+            raise Exception("Left value must contain a dice roll.")
+
+        # We initialize our result with the left-most value.
+        # As we perform operations, this value will be continuously
+        # updated and used as the left-hand side.
+        result: EvaluationResults = tokens[0]
+
+        # If it's the case that we have an implied keep amount, we
+        # need to manually add it to the end here.
+        if len(tokens) % 2 == 0:
+            tokens.append(1)
+
+        # Because we get things like [[1, "+", 2, "+", 3]], we have
+        # to be able to handle additional operations beyond a single
+        # left/right pair.
+        for i in range(1, len(tokens), 2):
+
+            op_index = i
+            right_index = i + 1
+
+            operation_string: str = str(tokens[op_index])
+
+            if operation_string not in ['K', 'k']:
+                raise Exception(
+                    f"Operator at index {op_index} was "
+                    f"not in valid operations: {toks}")
+
+            right: Union[EvaluationResults, float, int,
+                         str] = tokens[right_index]
+
+            if isinstance(right, str):
+                raise Exception(f"right value cannot be a string: {toks}")
+            elif isinstance(right, EvaluationResults):
+                result += right
+                result.total -= right.total
+                right = right.total
+
+            last_roll: RollResults = result.rolls[-1]
+            lower_total_by: Union[int, float] = 0
+
+            if operation_string == 'k':
+                lower_total_by = last_roll.keep_lowest(right)
+            else:
+                lower_total_by = last_roll.keep_highest(right)
+
+            result.total -= lower_total_by
+
+        return result
+
+    @staticmethod
+    def _handle_standard_operation(
+            toks: list[list[Union[str, int, float, EvaluationResults
+                                  ]]]) -> Union[int, float, EvaluationResults]:
+        # We initialize our result with the left-most value.
+        # As we perform operations, this value will be continuously
+        # updated and used as the left-hand side.
+        result: Union[int, float, EvaluationResults, str] = toks[0][0]
+
+        if isinstance(result, str):
+            raise Exception(f"left value cannot be a string: {toks}")
+
+        # Because we get things like [[1, "+", 2, "+", 3]], we have
+        # to be able to handle additional operations beyond a single
+        # left/right pair.
+        for pair in range(1, len(toks[0]), 2):
+
+            right: Union[int, float,
+                         EvaluationResults, str] = toks[0][pair + 1]
+
+            if isinstance(right, str):
+                raise Exception(f"right value cannot be a string: {toks}")
+
+            operation_string: str = str(toks[0][pair])
+
+            if operation_string not in DiceParser.OPERATIONS:
+                raise Exception(
+                    f"Operator was not in valid operations: {toks}")
+
+            op: Callable[
+                [
+                    Union[int, float, EvaluationResults],
+                    Union[int, float, EvaluationResults]
+                ],
+                EvaluationResults
+            ] = DiceParser.OPERATIONS[operation_string]
+
+            result = op(result, right)
+
+        return result
+
+    def parse(self: DiceParser,
+              dice_string: str,
+              roll_option: RollOption = RollOption.Normal
+              ) -> List[Union[int, float, EvaluationResults]]:
+        """Parse well-formed dice roll strings."""
+        try:
+            result: List[
+                Union[
+                    int,
+                    float,
+                    EvaluationResults]
+            ] = self._parser.parseString(dice_string, parseAll=True)
+        except ParseException:
+            raise SyntaxError("Unable to parse input string: " + dice_string)
+
+        if len(result) == 0:
+            raise Exception("Did not receive any value from evaluation")
+        elif len(result) > 1:
+            raise Exception(
+                f"Received more values than expected: {result}")
+        elif not all(isinstance(i, (int, float,
+                                EvaluationResults)) for i in result):
+            raise Exception(f"Unexpected types in output: {result}")
+
+        return result
+
+    def evaluate(self: DiceParser,
+                 dice_string: str,
+                 roll_option: RollOption = RollOption.Normal,
+                 ) -> Union[int, float, EvaluationResults]:
+        """Parse and evaluate the given dice string."""
+        return self.parse(dice_string, roll_option)[0]
diff --git a/roll/parser/operations.py b/roll/parser/operations.py
new file mode 100644
index 0000000..c6cfdb0
--- /dev/null
+++ b/roll/parser/operations.py
@@ -0,0 +1,224 @@
+"""
+Helper class that stores all available operations within the dice roller.
+
+Operations:
+
+    Addition "+"
+    Subtraction "-"
+    Multiplication "*"
+    'True' division "/"
+    'Floor' divison "//"
+    Modulus "%"
+    Exponential "^" OR "**"
+    Factorial "!"
+    Square root "sqrt"
+
+    Dice roll "d"
+    Keep lowest roll "k"
+    Keep highest roll "K"
+"""
+from __future__ import annotations
+
+from math import ceil
+from math import factorial as fact
+from math import floor
+from math import sqrt as squareroot
+from random import randint
+from typing import List, Union
+
+from roll.parser.types import EvaluationResults, RollOption, RollResults
+
+ROLL_TYPE: RollOption = RollOption.Normal
+
+
+def _to_eval_results(
+        x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+    """Change given object to an EvaluationResults object."""
+    if not isinstance(x, EvaluationResults):
+        x = EvaluationResults(x)
+
+    return x
+
+
+def add(x: Union[int, float, EvaluationResults],
+        y: Union[int, float, EvaluationResults]) -> EvaluationResults:
+    """Add x and y together with extended types."""
+    return _to_eval_results(x + y)
+
+
+def sub(x: Union[int, float, EvaluationResults],
+        y: Union[int, float, EvaluationResults]) -> EvaluationResults:
+    """Subtract x and y together with extended types."""
+    return _to_eval_results(x - y)
+
+
+def mult(x: Union[int, float, EvaluationResults],
+         y: Union[int, float, EvaluationResults]) -> EvaluationResults:
+    """Multiply x and y together with extended types."""
+    return _to_eval_results(x * y)
+
+
+def true_div(x: Union[int, float, EvaluationResults],
+             y: Union[int, float, EvaluationResults]) -> EvaluationResults:
+    """Divide (true) x and y with extended types."""
+    return _to_eval_results(x / y)
+
+
+def floor_div(x: Union[int, float, EvaluationResults],
+              y: Union[int, float, EvaluationResults]) -> EvaluationResults:
+    """Divide (floor) x and y with extended types."""
+    return _to_eval_results(x // y)
+
+
+def mod(x: Union[int, float, EvaluationResults],
+        y: Union[int, float, EvaluationResults]) -> EvaluationResults:
+    """Divide (modulus) x and y with extended types."""
+    # We get a false positive here for modulus string op
+    return _to_eval_results(x % y)  # noqa: S001
+
+
+def expo(x: Union[int, float, EvaluationResults],
+         y: Union[int, float, EvaluationResults]) -> EvaluationResults:
+    """Exponentiate x by y with extended types."""
+    return _to_eval_results(x ** y)
+
+
+def factorial(
+        x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+    """
+    Handle the factorial operation for int, float, and EvaluationResults.
+
+    Depending on the passed datatype, we do different things:
+        int: No change required, it is able to use the built-in function
+            and then change the resulting value into an EvaluationResults.
+        float: The value is ceil'd and then passed as an int.
+        EvaluationResults: The total is ceil'd and passed.
+    """
+    result: Union[int, EvaluationResults]
+
+    if isinstance(x, EvaluationResults):
+        x.total = fact(ceil(x.total))
+        result = x
+    else:
+        result = fact(ceil(x))
+
+    return _to_eval_results(result)
+
+
+def sqrt(x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+    """Perform sqrt on x with extended types."""
+    result: Union[int, float, EvaluationResults]
+
+    if isinstance(x, EvaluationResults):
+        x.total = squareroot(x.total)
+        result = x
+    else:
+        result = squareroot(x)
+
+    return _to_eval_results(result)
+
+
+def roll_dice(
+        num_dice: Union[int, float, EvaluationResults],
+        sides: Union[int, float, EvaluationResults],
+        roll_option: RollOption = RollOption.Normal,
+        ) -> EvaluationResults:
+    """Calculate value of dice roll notation."""
+    result: EvaluationResults = EvaluationResults()
+
+    global ROLL_TYPE
+    roll_option = ROLL_TYPE
+
+    # In order to ensure our types later on, we are going to first
+    # take out all of the EvaluationResults objects and take their
+    # totals to be used down the line. Their history will be used
+    # as the basis for our returned value going forward.
+    #
+    # Since the totals for either are being used for our count
+    # and sides values, we do not include the totals in the final
+    # value.
+    if isinstance(num_dice, EvaluationResults):
+        result += num_dice
+        num_dice = num_dice.total
+
+    if isinstance(sides, EvaluationResults):
+        result += sides
+        sides = sides.total
+
+    result.total = 0
+
+    starting_num_dice: Union[int, float] = num_dice
+    starting_sides: Union[int, float] = sides
+
+    # If it's the case that we were given a dice with negative sides,
+    # then that doesn't mean anything in the real world. I cannot
+    # for the life of me figure out a possible scenario where that
+    # would make sense. We will just error out.
+    if sides < 0:
+        raise ValueError('The sides of a die must be positive or zero.')
+
+    result_is_negative: bool = num_dice < 0
+
+    if result_is_negative:
+        num_dice = abs(num_dice)
+
+    sides = ceil(sides)
+
+    rolls: List[Union[int, float]] = []
+
+    if roll_option == RollOption.Minimum:
+        rolls = [1] * ceil(num_dice)
+    elif roll_option == RollOption.Maximum:
+        # TODO: Ensure that this logic is correct. I am not sure that
+        # we want to floor either of these numbers.
+        # Example: 1.5d5.5 maxed should be the max of 1.5d6, or 9.
+        rolls = [floor(sides)] * floor(num_dice)
+
+        if isinstance(num_dice, float) and (num_dice % 1) != 0:
+            rolls.append(sides * (num_dice % 1))
+    elif sides != 0:
+        # Because this is not cryptographically secure, we have to noqa it
+        # otherwise we get an S311 warning.
+        rolls = [randint(1, sides) for _ in range(floor(num_dice))]  # noqa
+
+        # If it's the case that the number of dice is a float, then
+        # we take that to mean that it is a dice where the sides should
+        # be lowered to reflect the float amount.
+        #
+        # We do not want this to effect all dice rolls however, only the
+        # last one (or the only one if there's only a decimal portion).
+        if isinstance(num_dice, float) and (num_dice % 1) != 0:
+            sides = ceil(sides * (num_dice % 1))
+            # Not cryptographically secure
+            rolls.append(randint(1, sides))  # noqa
+
+    if result_is_negative:
+        for roll_num in range(len(rolls)):
+            rolls[roll_num] = -rolls[roll_num]
+
+    result.add_roll(
+        RollResults(f'{starting_num_dice}d{starting_sides}', rolls))
+
+    return result
+
+
+def keep_lowest_dice(results: EvaluationResults,
+                     k: Union[int, float] = 1) -> EvaluationResults:
+    """Remove k number of lowest rolls from last roll."""
+    if len(results.rolls) == 0:
+        return results
+
+    results.rolls[-1].keep_lowest(k)
+
+    return results
+
+
+def keep_highest_dice(results: EvaluationResults,
+                      k: Union[int, float] = 1) -> EvaluationResults:
+    """Trim the results of a roll based on the provided amount to keep."""
+    if len(EvaluationResults.rolls) == 0:
+        return results
+
+    results.rolls[-1].keep_highest(k)
+
+    return results
diff --git a/roll/parser/types/__init__.py b/roll/parser/types/__init__.py
new file mode 100644
index 0000000..c8b145d
--- /dev/null
+++ b/roll/parser/types/__init__.py
@@ -0,0 +1,3 @@
+from roll.parser.types.evaluationresults import EvaluationResults
+from roll.parser.types.rolloption import RollOption
+from roll.parser.types.rollresults import RollResults
diff --git a/roll/parser/types/evaluationresults.py b/roll/parser/types/evaluationresults.py
new file mode 100644
index 0000000..7cd3052
--- /dev/null
+++ b/roll/parser/types/evaluationresults.py
@@ -0,0 +1,353 @@
+"""
+Defines EvaluationResults object to hold parsing state throughout evaluation.
+
+The EvaluationResults object is meant to solve an issue that was encountered
+when attempting to add in dice roll modifiers. Instead of having the
+heavy-lifting happening within our expression parser (which also caused
+issues with recursive expressions and verbose expressions), we are going to
+have an object that acts like an onion and coalesce all of the history
+together into a comprehensible bundle.
+
+What this will allow us to do is have things like `4d6K3` be two separate
+expressions to be parsed by our parser without having to state that there
+is a dice expression and another for the keep (K) notation. The real
+beauty of this comes from our ability to them chain these statements
+together, allowing for things like 10d4K5k2K1 without having to write
+our expression parser to handle this case.
+
+This method should also help with dealing with testing. By having the
+ability to capture our history for math and dice rolls, we can see
+whether it is the case that it is acting normally throughout the
+process and not just relying on the end result being within a certain
+acceptable range. Better, more informative tests can be made this way.
+"""
+from __future__ import annotations
+
+from enum import Enum
+from typing import List, Optional, Union
+
+import roll.parser.types
+
+
+class Operators(Enum):
+    """
+    Helper operator names for handling operations.
+
+    This makes it so that we don't have magic strings
+    everywhere. Instead, we can do things like:
+        Operators.add
+    """
+
+    add = "+"
+    sub = "-"
+    mul = "*"
+    truediv = "/"
+    floordiv = "//"
+    mod = "%"
+    expo = "^"
+    expo2 = "**"
+    die = "d"
+
+
+class EvaluationResults():
+    """Hold the current state of all rolls and the total from other ops."""
+
+    def __init__(self: EvaluationResults,
+                 value: Union[int, float, EvaluationResults] = None,
+                 rolls: List[roll.parser.types.RollResults] = None,
+                 last_operation: Operators = None) -> None:
+        """Initialize an EvaluationResults object."""
+        if rolls is None:
+            rolls = []
+
+        total: Optional[Union[int, float]] = None
+
+        if isinstance(value, EvaluationResults):
+            rolls.extend(value.rolls)
+            total = value.total
+        else:
+            total = value
+
+        self.total: Union[int, float] = total or 0
+        self.rolls: List[roll.parser.types.RollResults] = rolls
+        self.last_operation: Union[Operators, None] = last_operation
+
+    def add_roll(self: EvaluationResults,
+                 roll: roll.parser.types.RollResults) -> None:
+        """Add the results of a roll to the total evaluation results."""
+        self.total += roll.total()
+        self.rolls.append(roll)
+
+    def _collect_rolls(self: EvaluationResults, er: EvaluationResults) -> None:
+        """
+        Add all rolls together if both objects are EvaluationResults.
+
+        The way that we do this is by extending the other object's rolls
+        in place for efficiency's sake and then we set our object to that
+        one instead of the other way around so that we hopefully are going
+        to have the most recently roll as our last.
+        """
+        er.rolls.extend(self.rolls)
+        self.rolls = er.rolls
+
+    def __str__(self: EvaluationResults) -> str:
+        """Return a string representation of the eval results."""
+        return f"{self.total}"
+
+    def __int__(self: EvaluationResults) -> int:
+        """Change the evaluation result total to an integer."""
+        return int(self.total)
+
+    def __float__(self: EvaluationResults) -> float:
+        """Change the evaluation result total to a float."""
+        return float(self.total)
+
+    def __len__(self: EvaluationResults) -> int:
+        """Return the number of rolls that have been rolled."""
+        return len(self.rolls)
+
+    def __eq__(self: EvaluationResults, other: object) -> bool:
+        """Return whether or not a given value is numerically equal."""
+        if not isinstance(other, (int, float, EvaluationResults)):
+            return NotImplemented
+
+        if isinstance(other, (int, float)):
+            return other == self.total
+
+        return self.total == other.total
+
+    def __neg__(self: EvaluationResults) -> EvaluationResults:
+        """Negate the total value."""
+        self.total = -self.total
+        return self
+
+    def __add__(self: EvaluationResults,
+                x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Add a given value to the evaluation result total."""
+        self.last_operation = Operators.add
+
+        if isinstance(x, (int, float)):
+            self.total += x
+        elif isinstance(x, EvaluationResults):
+            self._collect_rolls(x)
+            self.total += x.total
+        else:
+            raise TypeError("The supplied type is not valid: " + type(x))
+
+        return self
+
+    def __iadd__(self: EvaluationResults,
+                 x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Add a given value to the evaluation result total."""
+        return self.__add__(x)
+
+    def __radd__(self: EvaluationResults,
+                 x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Add a given value to the evaluation result total."""
+        return self.__add__(x)
+
+    def __sub__(self: EvaluationResults,
+                x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Subtract a given value from the evaluation result total."""
+        self.last_operation = Operators.sub
+
+        if isinstance(x, (int, float)):
+            self.total -= x
+        elif isinstance(x, EvaluationResults):
+            self._collect_rolls(x)
+            self.total -= x.total
+        else:
+            raise TypeError("The supplied type is not valid: " + type(x))
+
+        return self
+
+    def __isub__(self: EvaluationResults,
+                 x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Subtract a given value from the evaluation result total."""
+        return self.__sub__(x)
+
+    def __rsub__(self: EvaluationResults,
+                 x: Union[int, float]) -> EvaluationResults:
+        """Subtract a given value from the evaluation result total."""
+        self.last_operation = Operators.sub
+
+        if isinstance(x, (int, float)):
+            self.total = x - self.total
+        elif isinstance(x, EvaluationResults):
+            self._collect_rolls(x)
+            self.total = x.total - self.total
+        else:
+            raise TypeError("The supplied type is not valid: " + type(x))
+
+        return self
+
+    def __mul__(self: EvaluationResults,
+                x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Multiply the evaluation result total by a given value."""
+        self.last_operation = Operators.mul
+
+        if isinstance(x, (int, float)):
+            self.total *= x
+        elif isinstance(x, EvaluationResults):
+            self._collect_rolls(x)
+            self.total *= x.total
+        else:
+            raise TypeError("The supplied type is not valid: " + type(x))
+
+        return self
+
+    def __imul__(self: EvaluationResults,
+                 x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Multiply the evaluation result total by a given value."""
+        return self.__mul__(x)
+
+    def __rmul__(self: EvaluationResults,
+                 x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Multiply the evaluation result total by a given value."""
+        return self.__mul__(x)
+
+    def __truediv__(self: EvaluationResults,
+                    x: Union[int, float, EvaluationResults]
+                    ) -> EvaluationResults:
+        """Divide the evaluation result total by a given number."""
+        self.last_operation = Operators.truediv
+
+        if isinstance(x, (int, float)):
+            self.total /= x
+        elif isinstance(x, EvaluationResults):
+            self._collect_rolls(x)
+            self.total /= x.total
+        else:
+            raise TypeError("The supplied type is not valid: " + type(x))
+
+        return self
+
+    def __itruediv__(self: EvaluationResults,
+                     x: Union[int, float, EvaluationResults]
+                     ) -> EvaluationResults:
+        """Divide the evaluation result total by a given number."""
+        return self.__truediv__(x)
+
+    def __rtruediv__(self: EvaluationResults,
+                     x: Union[int, float, EvaluationResults]
+                     ) -> EvaluationResults:
+        """Divide the evaluation result total by a given number."""
+        self.last_operation = Operators.truediv
+
+        if isinstance(x, (int, float)):
+            self.total = x / self.total
+        elif isinstance(x, EvaluationResults):
+            self._collect_rolls(x)
+            self.total = x.total / self.total
+        else:
+            raise TypeError("The supplied type is not valid: " + type(x))
+
+        return self
+
+    def __floordiv__(self: EvaluationResults,
+                     x: Union[int, float, EvaluationResults]
+                     ) -> EvaluationResults:
+        """Divide the evaluation result total by a given number and floor."""
+        self.last_operation = Operators.floordiv
+
+        if isinstance(x, (int, float)):
+            self.total //= x
+        elif isinstance(x, EvaluationResults):
+            self._collect_rolls(x)
+            self.total //= x.total
+        else:
+            raise TypeError("The supplied type is not valid: " + type(x))
+
+        return self
+
+    def __ifloordiv__(self: EvaluationResults,
+                      x: Union[int, float, EvaluationResults]
+                      ) -> EvaluationResults:
+        """Divide the evaluation result total by a given number and floor."""
+        return self.__floordiv__(x)
+
+    def __rfloordiv__(self: EvaluationResults,
+                      x: Union[int, float, EvaluationResults]
+                      ) -> EvaluationResults:
+        """Divide the evaluation result total by a given number and floor."""
+        self.last_operation = Operators.floordiv
+
+        if isinstance(x, (int, float)):
+            self.total = x // self.total
+        elif isinstance(x, EvaluationResults):
+            self._collect_rolls(x)
+            self.total = x.total // self.total
+        else:
+            raise TypeError("The supplied type is not valid: " + type(x))
+
+        return self
+
+    def __mod__(self: EvaluationResults,
+                x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Perform modulus divison on the evaluation total with given value."""
+        self.last_operation = Operators.mod
+
+        if isinstance(x, (int, float)):
+            self.total %= x
+        elif isinstance(x, EvaluationResults):
+            self._collect_rolls(x)
+            self.total %= x.total
+        else:
+            raise TypeError("The supplied type is not valid: " + type(x))
+
+        return self
+
+    def __imod__(self: EvaluationResults,
+                 x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Perform modulus divison on the evaluation total with given value."""
+        return self.__mod__(x)
+
+    def __rmod__(self: EvaluationResults,
+                 x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Perform modulus divison on the evaluation total with given value."""
+        self.last_operation = Operators.mod
+
+        if isinstance(x, (int, float)):
+            self.total = float(x) % self.total
+        elif isinstance(x, EvaluationResults):
+            self._collect_rolls(x)
+            self.total = x.total % self.total
+        else:
+            raise TypeError("The supplied type is not valid: " + type(x))
+
+        return self
+
+    def __pow__(self: EvaluationResults,
+                x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Exponentiate the evaluation results by the given value."""
+        self.last_operation = Operators.expo
+
+        if isinstance(x, (int, float)):
+            self.total **= x
+        elif isinstance(x, EvaluationResults):
+            self._collect_rolls(x)
+            self.total **= x.total
+        else:
+            raise TypeError("The supplied type is not valid: " + type(x))
+
+        return self
+
+    def __ipow__(self: EvaluationResults,
+                 x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Exponentiate the evaluation results by the given value."""
+        return self.__pow__(x)
+
+    def __rpow__(self: EvaluationResults,
+                 x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Exponentiate the evaluation results by the given value."""
+        self.last_operation = Operators.expo
+
+        if isinstance(x, (int, float)):
+            self.total = x ** self.total
+        elif isinstance(x, EvaluationResults):
+            self._collect_rolls(x)
+            self.total = x.total ** self.total
+        else:
+            raise TypeError("The supplied type is not valid: " + type(x))
+
+        return self
diff --git a/roll/parser/types/rolloption.py b/roll/parser/types/rolloption.py
new file mode 100644
index 0000000..d0d8eda
--- /dev/null
+++ b/roll/parser/types/rolloption.py
@@ -0,0 +1,31 @@
+"""
+Create options that change the outcomes of dice rolls.
+
+In order to have the feature that we allow for minimum and maximum rolls,
+we need to be able to flag that that is an option within our dice roller.
+This feature is required to play some games, including D&D where critical
+hits and misses are sometimes used with these options.
+"""
+
+from enum import Enum
+
+
+class RollOption(Enum):
+    """
+    Enum for expressing dice roll behavior.
+
+    Available roll options:
+        Minimum: The dice will return the lowest possible roll.
+            e.g. 1d6 -> 1
+
+        Normal: The dice are rolled normally, a random number
+                is given in range.
+            e.g. 1d6 -> (Number between 1 - 6 inclusive)
+
+        Maximum: The dice will return the highest possible roll.
+            e.g. 1d6 -> 6
+    """
+
+    Minimum = 0
+    Normal = 1
+    Maximum = 2
diff --git a/roll/parser/types/rollresults.py b/roll/parser/types/rollresults.py
new file mode 100644
index 0000000..81c765a
--- /dev/null
+++ b/roll/parser/types/rollresults.py
@@ -0,0 +1,49 @@
+"""
+Roll result object for saving information for a group of rolls.
+
+The RollResult is an object that keeps information regarding a
+roll (either a singular die roll or a group of like dice rolled).
+"""
+
+from __future__ import annotations
+
+from math import ceil
+from typing import List, Union
+
+
+class RollResults:
+    """
+    Saves results for a single or group of dice.
+
+    dice: The dice string that was rolled
+    rolls: A collection of the values of all rolls
+
+    Contains helper methods for handling changes to a given
+    roll in order to contain the logic for roll collections.
+    """
+
+    def __init__(self: RollResults, dice: str,
+                 rolls: List[Union[int, float]]) -> None:
+        """Initialize a RollResults object."""
+        self.dice: str = dice
+        self.rolls: List[Union[int, float]] = rolls
+
+    def total(self: RollResults) -> Union[int, float]:
+        """Return the sum of the values of the rolls."""
+        return sum(self.rolls)
+
+    def keep_lowest(self: RollResults,
+                    num: Union[int, float] = 1) -> Union[int, float]:
+        """Keep the lowest num rolls."""
+        previous_sum: Union[int, float] = sum(self.rolls)
+        self.rolls = sorted(self.rolls)[:ceil(num)]
+
+        return previous_sum - sum(self.rolls)
+
+    def keep_highest(self: RollResults,
+                     num: Union[int, float] = 1) -> Union[int, float]:
+        """Keep the highest num rolls."""
+        previous_sum: Union[int, float] = sum(self.rolls)
+        self.rolls = sorted(self.rolls)[-ceil(num):]
+
+        return previous_sum - sum(self.rolls)
diff --git a/roll/roll.py b/roll/roll.py
index 8043f0b..e7d939d 100644
--- a/roll/roll.py
+++ b/roll/roll.py
@@ -15,11 +15,12 @@
 """
 from typing import Union
 
-from roll.diceparser import DiceParser, EvaluationResults, RollOption
+from roll.parser import DiceParser
+from roll.parser.types import EvaluationResults, RollOption
 
 _DICE_PARSER = DiceParser()
 
-GOOD_CHARS: str = "0123456789d-/*() %+.!^piesqrt"
+GOOD_CHARS: str = "0123456789d-/*() %+.!^pPiIeEsSqQrRtTkK"
 
 
 def roll(expression: str = '',
@@ -35,12 +36,15 @@ def roll(expression: str = '',
     if expression.strip() == '':
         expression = "1d20"
 
-    result: EvaluationResults = _DICE_PARSER.evaluate(expression, roll_option)
+    result: Union[
+        int,
+        float,
+        EvaluationResults] = _DICE_PARSER.evaluate(expression, roll_option)
 
     if verbose:
         return result
 
-    return result['total']
+    return result.total if isinstance(result, EvaluationResults) else result
 
 
 if __name__ == "__main__":


Test patch:
diff --git a/tests/test_basic_math.py b/tests/test_basic_math.py
index fda325d..e70c98f 100644
--- a/tests/test_basic_math.py
+++ b/tests/test_basic_math.py
@@ -151,7 +151,11 @@ def test_add_explonential():
 
 @pytest.mark.parametrize("equation,result", [
     ('pi', math.pi),
+    ('Pi', math.pi),
+    ('PI', math.pi),
+    ('pI', math.pi),
     ('e', math.e),
+    ('E', math.e),
 ])
 def test_constants(equation: str, result: Union[int, float]):
     assert roll(equation) == result
@@ -159,6 +163,11 @@ def test_constants(equation: str, result: Union[int, float]):
 
 @pytest.mark.parametrize("equation,result", [
     ('sqrt 25', 5),
+    ('Sqrt 25', 5),
+    ('sqrT 25', 5),
+    ('sQRt 25', 5),
+    ('sQRT 25', 5),
+    ('SQRT 25', 5),
     # Addition
     ('2 + sqrt 9', 5),
     ('sqrt 36 + 7', 13),
diff --git a/tests/test_click.py b/tests/test_click.py
index d02bd73..0b7beff 100644
--- a/tests/test_click.py
+++ b/tests/test_click.py
@@ -1,6 +1,6 @@
 
 from click.testing import CliRunner
-from roll.click_dice import roll_cli
+from roll.click.click_dice import roll_cli
 
 runner = CliRunner()
 
diff --git a/tests/test_dice_rolling.py b/tests/test_dice_rolling.py
index ff8b508..b0891f5 100644
--- a/tests/test_dice_rolling.py
+++ b/tests/test_dice_rolling.py
@@ -19,6 +19,9 @@ def test_basic_roll():
     ('2.0d100.0', 2, 200),
     ('100.5d6', 101, 603),
     ('1.0d6.5', 1, 7),
+    ('1d0', 0, 0),
+    ('1d1', 1, 1),
+    ('1d2', 1, 2),
 ])
 def test_roll(equation: str, range_low: int, range_high: int):
     assert roll(equation) in range(range_low, range_high + 1)
@@ -86,3 +89,29 @@ def test_float_sides1():
 
 def test_float_sides2():
     assert roll('2d19.99') in range(2, 41)
+
+
+@pytest.mark.parametrize('equation,range_low,range_high', [
+    # Implied number of dice, implied number to keep
+    ('d6K', 1, 6),
+    ('d10k', 1, 10),
+    ('d%k', 1, 100),
+
+    # Verbose dice, implied number to keep
+    ('10d6K', 1, 6),
+    ('10d9k', 1, 9),
+
+    # Implied number of dice, verbose number to keep
+    ('d100K1', 1, 100),
+    ('d1k5', 1, 1),
+
+    # Verbose dice, verbose keep
+    ('10d6k1', 1, 6),
+    ('4d6K3', 3, 18),
+
+    # Multiple keeps
+    ('10d5k4k3k2k1', 1, 5),
+    ('10d6K5k4', 4, 24),
+])
+def test_keep(equation: str, range_low: int, range_high: int):
+    assert roll(equation) in range(range_low, range_high + 1)
diff --git a/tests/test_dice_rolling_with_math.py b/tests/test_dice_rolling_with_math.py
index f88950d..cedbb38 100644
--- a/tests/test_dice_rolling_with_math.py
+++ b/tests/test_dice_rolling_with_math.py
@@ -1,6 +1,9 @@
 
+from typing import Union
+
 import pytest
 from roll import roll
+from roll.parser.types import EvaluationResults
 
 
 def test_d6_plus_d8():
@@ -41,6 +44,10 @@ def test_dice_expo1():
 ])
 def test_dice_sqrt(equation: str, range_low: int, range_high: int):
     result = roll(equation, verbose=True)
-    print(result)
-    total = result['total']
+
+    if isinstance(result, EvaluationResults):
+        total: Union[int, float] = result.total
+    else:
+        total = result
+
     assert total in range(range_low, range_high + 1)
diff --git a/tests/test_evaluationresults.py b/tests/test_evaluationresults.py
new file mode 100644
index 0000000..a2d0819
--- /dev/null
+++ b/tests/test_evaluationresults.py
@@ -0,0 +1,103 @@
+from roll.parser.types import EvaluationResults
+
+
+def test_add() -> None:
+    er = EvaluationResults()
+    er = er + 2
+    assert er == 2
+
+
+def test_unary_minus() -> None:
+    er = EvaluationResults()
+    er += 5
+    er = -er
+    assert er == -5
+
+
+def test_add_reversed() -> None:
+    er = EvaluationResults()
+    er = 2 + er
+    assert er == 2
+
+
+def test_sub() -> None:
+    er = EvaluationResults()
+    er = er - 2
+    assert er == -2
+
+
+def test_sub_reversed() -> None:
+    er = EvaluationResults()
+    er = 2 - er
+    assert er == 2
+
+
+def test_mul() -> None:
+    er = EvaluationResults()
+    er += 5
+    er *= 2
+    er = 7 * er
+    assert er == 70
+
+
+def test_mul_reversed() -> None:
+    er = EvaluationResults()
+    er += 5
+    er = 7 * er
+    assert er == 35
+
+
+def test_truediv() -> None:
+    er = EvaluationResults()
+    er += 60
+    er /= 10
+    assert er == 6
+
+
+def test_truediv_reversed() -> None:
+    er = EvaluationResults()
+    er += 8
+    er = 24 / er
+    assert er == 3
+
+
+def test_floordiv() -> None:
+    er = EvaluationResults()
+    er += 120
+    er //= 10
+    assert er == 12
+
+
+def test_floordiv_reversed() -> None:
+    er = EvaluationResults()
+    er += 14
+    er = 112 // er
+    assert er == 8
+
+
+def test_modulus() -> None:
+    er = EvaluationResults()
+    er += 240
+    er %= 9
+    assert er == 6
+
+
+def test_modulus_reversed() -> None:
+    er = EvaluationResults()
+    er += 7
+    er = 6 % er
+    assert er == 6
+
+
+def test_exponential() -> None:
+    er = EvaluationResults()
+    er += 2
+    er **= 5
+    assert er == 32
+
+
+def test_exponential_reversed() -> None:
+    er = EvaluationResults()
+    er += 8
+    er = 7 ** er
+    assert er == 5764801
diff --git a/tests/test_expression_parsing.py b/tests/test_expression_parsing.py
index 4e28c14..a198da6 100644
--- a/tests/test_expression_parsing.py
+++ b/tests/test_expression_parsing.py
@@ -1,7 +1,7 @@
 
 # from roll import roll
 import pytest
-import roll.diceparser as dp
+import roll.parser.diceparser as dp
 from pyparsing import ParseException
 
 parser = dp.DiceParser()
@@ -21,62 +21,62 @@
 
 
 def test_interpret_number():
-    assert parser.evaluate('42')['total'] == 42
+    assert parser.evaluate('42') == 42
 
 
 def test_interpret_neg_number():
-    assert parser.evaluate('-64')['total'] == -64
+    assert parser.evaluate('-64') == -64
 
 
 def test_interpret_number_with_spaces():
-    assert parser.evaluate('       239      ')['total'] == 239
+    assert parser.evaluate('       239      ') == 239
 
 
 def test_float1():
-    assert parser.evaluate('1.0')['total'] == 1.0
+    assert parser.evaluate('1.0') == 1.0
 
 
 def test_float2():
-    assert parser.evaluate('3.1415')['total'] == 3.1415
+    assert parser.evaluate('3.1415') == 3.1415
 
 
 def test_float3():
     # I don't like this, but that's what pyparsing does.
-    assert parser.evaluate('9.')['total'] == 9.0
+    assert parser.evaluate('9.') == 9.0
 
 
 def test_float4():
     # I don't like this either, but it handles this.
-    assert parser.evaluate('.098')['total'] == 0.098
+    assert parser.evaluate('.098') == 0.098
 
 
 def test_neg_float1():
-    assert parser.evaluate('-2.0')['total'] == -2.0
+    assert parser.evaluate('-2.0') == -2.0
 
 
 def test_neg_float2():
-    assert parser.evaluate('-700.')['total'] == -700.0
+    assert parser.evaluate('-700.') == -700.0
 
 
 def test_interpret_dice():
-    assert parser.evaluate('d20')['total'] in range(1, 21)
+    assert parser.evaluate('d20') in range(1, 21)
 
 
 def test_interpret_dice2():
-    assert parser.evaluate('1d20')['total'] in range(1, 21)
+    assert parser.evaluate('1d20') in range(1, 21)
 
 
 def test_interpret_dice_with_spaces():
-    assert parser.evaluate('       2    d  8           ')['total'] in range(2, 17)
+    assert parser.evaluate('       2    d  8           ') in range(2, 17)
 
 
 def test_interpret_subtract_negative():
-    assert parser.evaluate('1 - -5')['total'] == 6
+    assert parser.evaluate('1 - -5') == 6
 
 
 # This test currently fails on the master branch as well.
 # def test_unary_negative():
-#     assert parser.evaluate('--10')['total'] == 10
+#     assert parser.evaluate('--10') == 10
 
 
 def test_bad_input1():