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

Issue:
Enhanced verbose printing with full history
Problem:
Currently our verbose output only contains the rolls that were made. However, this does not give a full picture of what is going on under the hood and will make things hard to test as we are going to edit the rolls with things like the keep, re-roll, and drop notations.

Solution:
I believe that the full history should be saved throughout a dice roll. While we are parsing, it can save the step that it is at and what the current state was as well as the operation that was taking place.

Example:
```
roll 4d20K3 -v

# output:
Rolled: 4d20: [ 2, 13, 17, 10 ]
Keep: K3: [ 13, 17, 10 ]
```

Considerations:
- Should we also do this for the mathematical operations? I think it might be difficult or noisy if we do, but, for the full picture, it's likely necessary.
- This will also help with testing because then we can compare the output with what we expect to ensure that we did not just get lucky, it actually performed the order of operations correctly at all points.
- One helpful thing that we could do for this if it is implemented is overloading the __str__ method of EvaluationResults to give the verbose output of the history of the roll as well.
Ensured 100% test coverage.
While working on coverage, some issues were resolved:
1) Because of how the DiceParser object was being instantiated and used
   across multiple tests, the RollOption.Maximize was being used in more
   cases than it was supposed to. Once this was resolved, other tests
   showed that they did not have correct ranges. The output was reviewed
   and either the test was updated or the code was to accomodate
   expected results.
2) Some operations were not being used and it became apparent by the
   coverage report. This was also the case for types that did not need
   to have their logic present in things like __rmod__ where the __mod__
   method would handle the case instead.
3) There are a few methods for objects that we will want to keep because
   they will be needed later on that are not current used. To make sure
   that our coverage is complete, I had to test these internal files
   instead of end-to-end testing. After we add the new features, we
   should re-evaluate these additions to consider whether they're
   actually needed. To do this, we should run pytest specifically
   against test_roll_dice.py or other scripts that do end-to-end
   testing only.


Discussion / hints:

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/.travis.yml b/.travis.yml
index 7f62297..a5f2a52 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,8 @@
 language: python
 python:
-  - 3.6
   - 3.7
   - 3.8
+  - 3.9
 
 before_install:
 - pip install poetry
diff --git a/poetry.lock b/poetry.lock
index 35db60d..96b6f68 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -8,33 +8,36 @@ 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
 python-versions = "*"
 
 [[package]]
-name = "chardet"
-version = "4.0.0"
-description = "Universal encoding detector for Python 2 and 3"
+name = "charset-normalizer"
+version = "2.0.3"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
 category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=3.5.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
 
 [[package]]
 name = "click"
@@ -66,7 +69,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
@@ -77,15 +80,15 @@ toml = ["toml"]
 
 [[package]]
 name = "idna"
-version = "2.10"
+version = "3.2"
 description = "Internationalized Domain Names in Applications (IDNA)"
 category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.5"
 
 [[package]]
 name = "importlib-metadata"
-version = "3.4.0"
+version = "4.6.1"
 description = "Read metadata from Python packages"
 category = "dev"
 optional = false
@@ -97,11 +100,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 +113,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 +177,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,27 +186,28 @@ 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"
-version = "2.25.1"
+version = "2.26.0"
 description = "Python HTTP for Humans."
 category = "dev"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 
 [package.dependencies]
 certifi = ">=2017.4.17"
-chardet = ">=3.0.2,<5"
-idna = ">=2.5,<3"
+charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
+idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
 urllib3 = ">=1.21.1,<1.27"
 
 [package.extras]
-security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
 socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
 
 [[package]]
 name = "toml"
@@ -214,7 +219,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 +227,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,20 +248,20 @@ 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"
-python-versions = "^3.6"
-content-hash = "16fc2c2fff26c92c3dd1b14fad2d9a7fc6cae06d20a2c52e5619d90bb8d34cec"
+python-versions = "^3.7"
+content-hash = "134498755251648f1f289c8fc2f772611f31fb68d03d5f8499b480b9b2df85dd"
 
 [metadata.files]
 atomicwrites = [
@@ -264,16 +269,16 @@ 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"},
-    {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"},
+charset-normalizer = [
+    {file = "charset-normalizer-2.0.3.tar.gz", hash = "sha256:c46c3ace2d744cfbdebceaa3c19ae691f53ae621b39fd7570f59d14fb7f2fd12"},
+    {file = "charset_normalizer-2.0.3-py3-none-any.whl", hash = "sha256:88fce3fa5b1a84fdcb3f603d889f723d1dd89b26059d0123ca435570e848d5e1"},
 ]
 click = [
     {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"},
@@ -289,71 +294,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"},
+    {file = "idna-3.2-py3-none-any.whl", hash = "sha256:14475042e284991034cb48e06f6851428fb14c4dc953acd9be9a5e95c7b6dd7a"},
+    {file = "idna-3.2.tar.gz", hash = "sha256:467fbad99067910785144ce333826c71fb0e63a425657295239737f7ecd125f3"},
 ]
 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,31 +380,31 @@ 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"},
-    {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"},
+    {file = "requests-2.26.0-py2.py3-none-any.whl", hash = "sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24"},
+    {file = "requests-2.26.0.tar.gz", hash = "sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7"},
 ]
 toml = [
     {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
     {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..da29c0e 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"
@@ -18,7 +18,7 @@ packages = [
 ]
 
 [tool.poetry.dependencies]
-python = "^3.6"
+python = "^3.7"
 typing_extensions = "^3.7.4"
 toml = "^0.10.1"
 pyparsing = "^2.4.7"
@@ -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 85%
rename from roll/click_dice.py
rename to roll/click/click_dice.py
index ad251b0..1bbcc43 100644
--- a/roll/click_dice.py
+++ b/roll/click/click_dice.py
@@ -15,10 +15,10 @@
 
 from typing import List, Union
 
-import click
-
 from roll import roll
-from roll.diceparser import EvaluationResults, RollOption
+from roll.parser.types import EvaluationResults, RollOption
+
+import click
 
 
 @click.command()
@@ -61,16 +61,7 @@ def roll_cli(expression: List[str] = None,
         command_input, verbose, roll_option,
     )
 
-    if verbose and isinstance(result, dict):
-        for r in result['rolls']:
-            click.echo(
-                f"{r['dice']}: {r['rolls']}"
-            )
-
-        click.echo(result['total'])
-
-    else:
-        click.echo(result)
+    click.echo(result)
 
 
 if __name__ == '__main__':
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..2133afb
--- /dev/null
+++ b/roll/parser/diceparser.py
@@ -0,0 +1,263 @@
+#!/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, 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, factorial, floor_div, mod,
+                                    mult, roll_dice, sqrt, sub, true_div, expo)
+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,
+        "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, str] = toks[0][1]
+
+        if not isinstance(value, (int, float, EvaluationResults)):
+            raise TypeError(
+                "The given value must be int, float, or EvaluationResults")
+        return sqrt(value)
+
+    @staticmethod
+    def _handle_expo(
+            toks: list[list[Union[int, float, EvaluationResults
+                                  ]]]) -> Union[int, float, EvaluationResults]:
+        return expo(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]:
+        global ROLL_TYPE
+        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 TypeError("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])
+
+            right: Union[EvaluationResults, float, int,
+                         str] = tokens[right_index]
+
+            if 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(float(right))
+                result.history.append(f"Keeping lowest: {right}: {last_roll.rolls}")
+            else:
+                lower_total_by = last_roll.keep_highest(float(right))
+                result.history.append(f"Keeping highest: {right}: {last_roll.rolls}")
+
+            result.total -= lower_total_by
+
+        return result
+
+    @staticmethod
+    def _handle_standard_operation(
+            toks: list[list[Union[str, int, float, EvaluationResults
+                                  ]]]) -> Union[str, 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]
+
+        # 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]
+
+            operation_string: str = str(toks[0][pair])
+
+            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."""
+        global ROLL_TYPE
+        ROLL_TYPE = roll_option
+        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)
+
+        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..5e618de
--- /dev/null
+++ b/roll/parser/operations.py
@@ -0,0 +1,187 @@
+"""
+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 floor
+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."""
+    return _to_eval_results(x) % y
+
+
+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.
+    """
+    if not isinstance(x, EvaluationResults):
+        x = _to_eval_results(x)
+
+    x.factorial()
+
+    return x
+
+
+def sqrt(x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+    """Perform sqrt on x with extended types."""
+    if not isinstance(x, EvaluationResults):
+        x = _to_eval_results(x)
+
+    x.sqrt()
+
+    return x
+
+
+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()
+
+    # 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:
+        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, floor(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
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..34f3380
--- /dev/null
+++ b/roll/parser/types/evaluationresults.py
@@ -0,0 +1,368 @@
+"""
+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
+from math import sqrt, ceil, factorial
+
+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] = 0,
+                 rolls: Union[List[roll.parser.types.RollResults], None] = None
+                 ) -> None:
+        """Initialize an EvaluationResults object."""
+        if rolls is None:
+            rolls = []
+
+        total: Optional[Union[int, float]] = None
+        history: list[str] = []
+
+        if isinstance(value, EvaluationResults):
+            rolls.extend(value.rolls)
+            history = value.history
+            total = value.total
+        else:
+            total = value
+
+        self.total: Union[int, float] = total
+        self.rolls: List[roll.parser.types.RollResults] = rolls
+        self.history: List[str] = history
+
+    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)
+        self.history.append(f"Rolled: {roll.dice}: {roll.rolls}")
+
+    def sqrt(self: EvaluationResults) -> None:
+        """Take the square root of the total value."""
+        new_total = sqrt(self.total)
+        self.history.append(f"Square Root: {self.total}: {new_total}")
+
+        self.total = new_total
+
+    def factorial(self: EvaluationResults) -> None:
+        """Factorial the total value."""
+        new_total = factorial(ceil(self.total))
+        self.history.append(f"Factorial: {self.total}! = {new_total}")
+
+        self.total = new_total
+
+    def _process_right_hand_value(self: EvaluationResults,
+            x: Union[EvaluationResults, int, float]) -> Union[int, float]:
+        """
+        Allows two ER objects to have their dice and history combined.
+
+        This is important so that the history and rolls are preserved
+        otherwise we lose key pieces of information that would disallow us
+        from being able to make modifications to rolls, i.e., keep notation,
+        as well as being able to give a complete verbose output when finished.
+        """
+        right_hand_value: Union[int, float, EvaluationResults]
+
+        if isinstance(x, (int, float)):
+            right_hand_value = x
+        elif isinstance(x, EvaluationResults):
+            self._collect_rolls(x)
+            self.history.extend(x.history)
+            right_hand_value = x.total
+        else:
+            raise TypeError("The supplied type is not valid: " + type(x).__name__)
+
+        return right_hand_value
+
+    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.
+
+        This will output all of the history of the dice roll
+        along with the total sum.
+
+        e.g.
+        Roll: 4d6 + 2
+
+        4d6: [3, 1, 1, 2]
+        7 + 2: 9
+        9
+
+        Roll: 10d10K5 + 4
+
+        10d10: [10, 6, 2, 1, 3, 3, 4, 8, 6, 4]
+        K5: [4, 6, 6, 8, 10]
+        34 + 4: 38
+        38
+        """
+        history_string: str = '\n'.join([h for h in self.history]) + "\n"
+
+        return f"{history_string}{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 False
+
+        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."""
+        right_hand_value: Union[int, float] = self._process_right_hand_value(x)
+        previous_total = self.total
+
+        self.total += right_hand_value
+        self.history.append(f"Adding: {previous_total} + {right_hand_value} = {self.total}")
+
+        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."""
+        right_hand_value: Union[int, float] = self._process_right_hand_value(x)
+        previous_total = self.total
+
+        self.total -= right_hand_value
+        self.history.append(f"Subtracting: {previous_total} - {right_hand_value} = {self.total}")
+
+        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."""
+        right_hand_value: Union[int, float] = self._process_right_hand_value(x)
+        previous_total = self.total
+
+        self.total = right_hand_value - self.total
+        self.history.append(f"Adding: {right_hand_value} - {previous_total} = {self.total}")
+
+        return self
+
+
+    def __mul__(self: EvaluationResults,
+                x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Multiply the evaluation result total by a given value."""
+        right_hand_value: Union[int, float] = self._process_right_hand_value(x)
+        previous_total = self.total
+
+        self.total *= right_hand_value
+        self.history.append(f"Multiplying: {previous_total} * {right_hand_value} = {self.total}")
+
+        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."""
+        right_hand_value: Union[int, float] = self._process_right_hand_value(x)
+        previous_total = self.total
+
+        self.total /= right_hand_value
+        self.history.append(f"Dividing: {previous_total} / {right_hand_value} = {self.total}")
+
+        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."""
+        right_hand_value: Union[int, float] = self._process_right_hand_value(x)
+        previous_total = self.total
+
+        self.total = right_hand_value / self.total
+        self.history.append(f"Dividing: {right_hand_value} / {previous_total} = {self.total}")
+
+        return self
+
+    def __floordiv__(self: EvaluationResults,
+                     x: Union[int, float, EvaluationResults]
+                     ) -> EvaluationResults:
+        """Divide the evaluation result total by a given number and floor."""
+        right_hand_value: Union[int, float] = self._process_right_hand_value(x)
+        previous_total = self.total
+
+        self.total //= right_hand_value
+        self.history.append(f"Floor dividing: {previous_total} // {right_hand_value} = {self.total}")
+        
+        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."""
+        right_hand_value: Union[int, float] = self._process_right_hand_value(x)
+        previous_total = self.total
+
+        self.total = right_hand_value // self.total
+        self.history.append(f"Floor dividing: {right_hand_value} // {previous_total} = {self.total}")
+
+        return self
+
+    def __mod__(self: EvaluationResults,
+                x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Perform modulus divison on the evaluation total with given value."""
+        right_hand_value: Union[int, float] = self._process_right_hand_value(x)
+        previous_total = self.total
+
+        self.total %= right_hand_value
+        self.history.append(f"Modulus dividing: {previous_total} % {right_hand_value} = {self.total}")
+
+        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."""
+        right_hand_value: Union[int, float] = self._process_right_hand_value(x)
+        previous_total = self.total
+
+        self.total = right_hand_value % self.total
+        self.history.append(f"Modulus dividing: {right_hand_value} % {previous_total} = {self.total}")
+
+        return self
+
+    def __pow__(self: EvaluationResults,
+                x: Union[int, float, EvaluationResults]) -> EvaluationResults:
+        """Exponentiate the evaluation results by the given value."""
+        right_hand_value: Union[int, float] = self._process_right_hand_value(x)
+        previous_total = self.total
+
+        self.total **= right_hand_value
+        self.history.append(f"Exponentiating: {previous_total} ** {right_hand_value} = {self.total}")
+
+        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."""
+        right_hand_value: Union[int, float] = self._process_right_hand_value(x)
+        previous_total = self.total
+
+        self.total = right_hand_value ** self.total
+        self.history.append(f"Exponentiating: {right_hand_value} ** {previous_total} = {self.total}")
+
+        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..7f7c4e8 100644
--- a/tests/test_basic_math.py
+++ b/tests/test_basic_math.py
@@ -58,6 +58,25 @@ def test_division(equation: str, result: Union[int, float]):
     assert roll(equation) == result
 
 
+@pytest.mark.parametrize(("equation", "result"), [
+    ('5 // 5', 1),
+    ('5.5 // 5', 1),
+])
+def test_floor_division(equation: str, result: Union[int, float]):
+    """Ensure that the floor division operator works correctly."""
+    assert roll(equation) == result
+
+
+@pytest.mark.parametrize(("equation", "result"), [
+    ('5**2', 25),
+    ('2 ** 8', 256),
+    ('1000 ** 0', 1),
+])
+def test_exponential(equation: str, result: Union[int, float]):
+    """Ensure that the floor division operator works correctly."""
+    assert roll(equation) == result
+
+
 def test_add_mul():
     assert roll('1 + 2 * 3') == 7
 
@@ -151,7 +170,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 +182,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..c28508f 100644
--- a/tests/test_click.py
+++ b/tests/test_click.py
@@ -1,6 +1,7 @@
 
+import pytest
 from click.testing import CliRunner
-from roll.click_dice import roll_cli
+from roll.click.click_dice import roll_cli
 
 runner = CliRunner()
 
@@ -33,15 +34,47 @@ def test_verbose_ouput_mult_dice():
     assert int(result.output.split()[-1]) in range(9, 75)
 
 
-def test_min_roll():
-    result = runner.invoke(roll_cli, ["10d6", "-m", "-v"])
-    print(result.output)
-    assert result.exit_code == 0
-    assert int(result.output.split()[-1]) == 10
+@pytest.mark.parametrize(('equation', 'result'), [
+    # Int num dice and sides
+    ("1d6", 1),
+    ("10d6", 10),
+    ("1000d10000", 1000),
 
+    # Float number of dice
+    ("0.5d1000", 1),
+    ("0.99d9", 1),
 
-def test_max_roll():
-    result = runner.invoke(roll_cli, ["10d6", "-M", "-v"])
-    print(result.output)
-    assert result.exit_code == 0
-    assert int(result.output.split()[-1]) == 60
+    # Float sides
+    ("15d5.5", 15),
+
+    # Float num dice and sides
+    ("100.5d2.75", 101),
+])
+def test_min_roll(equation: str, result: int):
+    roll_result = runner.invoke(roll_cli, [equation, "-m", "-v"])
+    print(roll_result.output)
+    assert roll_result.exit_code == 0
+    assert int(roll_result.output.split()[-1]) == result
+
+
+@pytest.mark.parametrize(('equation', 'result'), [
+    # Int num dice and sides
+    ("1d6", 6),
+
+    # Float number of dice
+    ("0.01d100", 1),
+
+    # Float number of sides
+    ("1d0.5", 1),
+    ("10d99.5", 1000),
+
+    # float num dice and sides
+    ("0.5d0.5", 0.5),
+    ("0.5d19.5", 10),
+    ("1.5d5.5", 9),
+])
+def test_max_roll(equation: str, result: int):
+    roll_result = runner.invoke(roll_cli, [equation, "-M", "-v"])
+    print(roll_result.output)
+    assert roll_result.exit_code == 0
+    assert float(roll_result.output.split()[-1]) == result
diff --git a/tests/test_dice_rolling.py b/tests/test_dice_rolling.py
index ff8b508..847db9f 100644
--- a/tests/test_dice_rolling.py
+++ b/tests/test_dice_rolling.py
@@ -1,4 +1,3 @@
-
 import pytest
 from roll import roll
 
@@ -17,13 +16,38 @@ def test_basic_roll():
     ('100.0d6', 100, 600),
     ('100d6.0', 100, 600),
     ('2.0d100.0', 2, 200),
+    ('2.5d300', 3, 750),
+    ('0.25d100', 1, 25),
+    ('0.01d1000', 1, 10),
     ('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)
 
 
+@pytest.mark.parametrize(('equation', 'range_low', 'range_high'), [
+    ('0.5d100', 1, 50),
+    ('0.1d10', 1, 1),
+    ('0.9d100', 1, 90),
+])
+def test_float_roll(equation: str, range_low: int, range_high: int) -> None:
+    """
+    Test the case where we give a non-int value for num dice.
+
+    The logic that we have chosen for this is that any floating point
+    value that contains a decimal value means that another dice should
+    be rolled but we should only allow for a portion of the amount for
+    the sides to be returned.
+
+    e.g. 0.5d100 == 1d50
+    """
+    assert roll(equation) in range(range_low, range_high + 1)
+
+
 def test_zero_dice_roll():
     assert roll('0d20') == 0
 
@@ -86,3 +110,66 @@ def test_float_sides1():
 
 def test_float_sides2():
     assert roll('2d19.99') in range(2, 41)
+
+
+@pytest.mark.parametrize(('equation', 'range_low', 'range_high'), [
+    ('2d6d100', 2, 1200),
+    ('1d2d3d4', 1, 24),
+])
+def test_inception_dice(equation: str, range_low: int,
+                        range_high: int) -> None:
+    """
+    Test cases like '1d2d3d4' to ensure proper evaluation.
+
+    The worry here is that the parser may only interpret the
+    left-most dice roll without continuing to parse the right
+    ones. It may also not parse them in order from
+    left-to-right
+    """
+    assert roll(equation) in range(range_low, range_high + 1)
+
+
+@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),
+
+    # Multiple dice rolls
+    ('4d6K1d2', 2, 12),
+
+    # With parens
+    ('(4+1)d100k5', 5, 500),
+    ('(4d100k2)K1', 1, 100),
+    ('(4)d(3)k((2))', 2, 6),
+])
+def test_keep(equation: str, range_low: int, range_high: int) -> None:
+    assert roll(equation) in range(range_low, range_high + 1)
+
+
+@pytest.mark.parametrize(('equation', 'exception_msg'), [
+    # No dice given
+    # 'Left value must contain a dice roll.'
+    ('((1+2))k', 'Unable to parse input string'),
+    ('sqrt10k', 'Unable to parse input string'),
+])
+def test_keep_exceptions(equation: str, exception_msg: str) -> None:
+    """Test the exception paths for the keep notation."""
+    with pytest.raises(Exception, match=exception_msg):
+        roll(equation)
diff --git a/tests/test_dice_rolling_with_math.py b/tests/test_dice_rolling_with_math.py
index f88950d..ef44251 100644
--- a/tests/test_dice_rolling_with_math.py
+++ b/tests/test_dice_rolling_with_math.py
@@ -1,6 +1,10 @@
 
+from math import ceil
+from typing import Union
+
 import pytest
 from roll import roll
+from roll.parser.types import EvaluationResults
 
 
 def test_d6_plus_d8():
@@ -27,20 +31,60 @@ def test_d6_mul_4():
     assert roll('d6 * 4') in range(4, 25)
 
 
-def test_dice_expo():
-    assert roll('5**2d1') == 25
+@pytest.mark.parametrize(("equation", "result"), [
+    ("5**2d1", 25),
+    ("5d1**5", 5),
+])
+def test_dice_expo(equation: str, result: Union[int, float]) -> None:
+    """
+    Ensure that the exponential op works with dice rolls.
 
+    One of the major things that could be wrong with this is
+    the desired order of operations may not be followed.
 
-def test_dice_expo1():
-    assert roll('5d1**5') == 5
+    Based on what feels right, I believe exponentiation should
+    take place before dice rolling. So '5**2d1' should be
+    evaluated as '(5**2)d1'.
+    """
+    assert roll(equation) == result
 
 
 @pytest.mark.parametrize("equation,range_low,range_high", [
+    # Number of dice is being square rooted
     ('sqrt 25 d 6', 5, 30),
+
+    # Sides are being square rooted
     ('1d sqrt 36', 1, 6),
+
+    # Square root an EvaluationResults object after a dice roll
+    ('sqrt(1d16)', 1, 4)
 ])
 def test_dice_sqrt(equation: str, range_low: int, range_high: int):
     result = roll(equation, verbose=True)
-    print(result)
-    total = result['total']
-    assert total in range(range_low, range_high + 1)
+
+    if isinstance(result, EvaluationResults):
+        total: Union[int, float] = result.total
+    else:
+        total = result
+
+    # It's important to ceil the value because some of the long
+    # decimal places will throw this off.
+    # Python states 1.390823490823 not in range(1, 5) otherwise
+    assert ceil(total) in range(range_low, range_high + 1)
+
+
+@pytest.mark.parametrize(('equation', 'range_low', 'range_high'), [
+    # Sides is being factorial'd
+    ('1d6!', 1, 720),
+
+    # Number is factorial
+    ('6!d1', 720, 720),
+
+    # Factorial an EvaluationResults object with a dice roll
+    # TODO: Ensure that we have the rolls still present.
+    ('(1d6)!', 1, 720),
+])
+def test_dice_factorial(equation: str, range_low: int,
+                        range_high: int) -> None:
+    """Ensure that dice rolling works with the factorial operation."""
+    assert roll(equation) in range(range_low, range_high + 1)
diff --git a/tests/test_diceparser.py b/tests/test_diceparser.py
new file mode 100644
index 0000000..e08f120
--- /dev/null
+++ b/tests/test_diceparser.py
@@ -0,0 +1,50 @@
+"""
+Test the inner workings of the DiceParser object.
+
+In order to perform comprehensive testing, we are testing
+both the underlying class methods as well as the CLIs
+functionality.
+
+The reason specifically why I am doing this now is because
+of the code coverage not being up to admissible levels.
+This is due to the fact that our code is overly defensive,
+likely taking care of edgecases that may not be possible
+due to how the parser works. Still, the type checking is
+flagging these as issues when there are not exception
+cases being raised if the types do not match what we are
+expecting.
+"""
+
+import pytest
+from roll.parser import DiceParser
+
+
+def test_handle_keep() -> None:
+    """Test whether we properly throw erros for no rolls."""
+    dp = DiceParser()
+
+    with pytest.raises(
+        TypeError,
+        match="Left value must contain a dice roll."
+    ):
+        dp._handle_keep([["hello", "k", 1]])
+
+
+def test_handle_sqrt() -> None:
+    """Test whether we properly throw errors for string values."""
+    dp = DiceParser()
+
+    with pytest.raises(
+        TypeError,
+        match="The given value must be int, float, or EvaluationResults"
+    ):
+        dp._handle_sqrt([["sqrt", "banana"]])
+
+
+@pytest.mark.skip(
+    reason="""This currently fails and the fix is part of another
+    github issue (#45)""")
+def test_pyparsing_recursion_issues() -> None:
+    """Ensure no recursion issues present."""
+    dp = DiceParser()
+    dp._parser.validate()
diff --git a/tests/test_evaluationresults.py b/tests/test_evaluationresults.py
new file mode 100644
index 0000000..25404f2
--- /dev/null
+++ b/tests/test_evaluationresults.py
@@ -0,0 +1,273 @@
+import pytest
+from roll.parser.types import EvaluationResults, RollResults
+
+
+def test_eq() -> None:
+    er = EvaluationResults(200)
+    assert er == 200
+
+
+def test_to_float() -> None:
+    er = EvaluationResults(1000)
+    assert isinstance(float(er), float)
+
+
+def test_rtruediv() -> None:
+    er = EvaluationResults(2)
+    assert 10 / er == 5
+
+
+def test_rtruediv_bad_type() -> None:
+    er = EvaluationResults()
+    with pytest.raises(
+        TypeError,
+        match="The supplied type is not valid: str"
+    ):
+        'banana' / er
+
+
+def test_floordiv_er() -> None:
+    er = EvaluationResults(6)
+    second_er = EvaluationResults(13)
+
+    assert second_er // er == 2
+
+
+def test_floordiv_bad_type() -> None:
+    er = EvaluationResults(100)
+    with pytest.raises(
+        TypeError,
+        match="The supplied type is not valid: str"
+    ):
+        er // 'banana'
+
+
+def test_rfloordiv_bad_type() -> None:
+    er = EvaluationResults(50)
+    with pytest.raises(
+        TypeError,
+        match="The supplied type is not valid: str"
+    ):
+        'banana' // er
+
+
+def test_mod_er() -> None:
+    er = EvaluationResults(1)
+    second_er = EvaluationResults(4)
+    assert er % second_er == 1
+
+
+def test_mod_bad_type() -> None:
+    er = EvaluationResults(250)
+    with pytest.raises(
+        TypeError,
+        match="The supplied type is not valid: str"
+    ):
+        er % 'banana'
+
+
+def test_rmod_bad_type() -> None:
+    er = EvaluationResults(5556)
+    with pytest.raises(
+        TypeError,
+        match="The supplied type is not valid"
+    ):
+        ['Skippy'] % er
+
+
+def test_pow_er() -> None:
+    er = EvaluationResults(2)
+    second_er = EvaluationResults(8)
+    assert er ** second_er == 256
+
+
+def test_pow_bad_type() -> None:
+    er = EvaluationResults(1298084.12)
+    with pytest.raises(
+        TypeError,
+        match="The supplied type is not valid"
+    ):
+        er ** 'apple'
+
+
+def test_rpow_bad_type() -> None:
+    er = EvaluationResults(109)
+    with pytest.raises(
+        TypeError,
+        match="The supplied type is not valid: str"
+    ):
+        'apple' ** er
+
+
+def test_er_eq_er() -> None:
+    er = EvaluationResults(200)
+    second_er = EvaluationResults()
+    second_er += 200
+    assert er == second_er
+
+
+def test_eq_non_num() -> None:
+    er = EvaluationResults(9001)
+    assert er != "Banana"
+
+
+def test_add() -> None:
+    er = EvaluationResults()
+    er = er + 2
+    assert er == 2
+
+
+def test_add_non_num() -> None:
+    er = EvaluationResults()
+    with pytest.raises(TypeError, match="The supplied type is not valid: str"):
+        er + 'Banana'  # noqa
+
+
+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_non_num() -> None:
+    er = EvaluationResults()
+    with pytest.raises(TypeError, match="The supplied type is not valid: str"):
+        er - 'Banana'  # noqa
+
+
+def test_rsub_non_num() -> None:
+    er = EvaluationResults()
+    with pytest.raises(TypeError, match="The supplied type is not valid: str"):
+        'Banana' - er  # noqa
+
+
+def test_isub() -> None:
+    er = EvaluationResults(0)
+    er -= -10
+    assert er == 10
+
+
+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_non_num() -> None:
+    with pytest.raises(TypeError, match="The supplied type is not valid: str"):
+        EvaluationResults() * "banana"
+
+
+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_truediv_er() -> None:
+    assert EvaluationResults(10) / EvaluationResults(2) == 5
+
+
+def test_truediv_non_num() -> None:
+    with pytest.raises(TypeError, match="The supplied type is not valid: str"):
+        EvaluationResults(0) / "Pants"  # noqa
+
+
+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
+
+
+def test_make_er_from_er() -> None:
+    er = EvaluationResults()
+    er += 234
+    second_er = EvaluationResults(er)
+    assert second_er == 234
+
+
+def test_to_int() -> None:
+    er = EvaluationResults(5.5)
+    assert isinstance(int(er), int)
+    assert int(er) == 5
+
+
+def test_len() -> None:
+    er = EvaluationResults()
+    rr = RollResults("1d6", [6])
+
+    assert len(er) == 0
+    er.add_roll(rr)
+    assert len(er) == 1
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():
diff --git a/tests/test_operations.py b/tests/test_operations.py
new file mode 100644
index 0000000..c36fa7e
--- /dev/null
+++ b/tests/test_operations.py
@@ -0,0 +1,29 @@
+"""
+Test the project's operations to ensure proper functioning.
+
+These tests should include targeted parameters to ensure that
+the individual functions are working correctly by themselves
+without being used in the greater project.
+"""
+
+
+from typing import Union
+
+import pytest
+from roll.parser.operations import roll_dice
+from roll.parser.types import EvaluationResults, RollOption
+
+
+@pytest.mark.parametrize(
+    ('num_dice', 'sides', 'range_low', 'range_high'), [
+        (0.5, 10, 1, 5),
+        (0.01, 1000, 1, 10),
+    ])
+def test_dice_roll(
+    num_dice: Union[int, float, EvaluationResults],
+    sides: Union[int, float, EvaluationResults],
+    range_low: int, range_high: int
+) -> None:
+    """Test the dice_roll function."""
+    assert roll_dice(
+        num_dice, sides, RollOption.Normal) in range(range_low, range_high + 1)
diff --git a/tests/test_speed.py b/tests/test_speed.py
index e69de29..688267d 100644
--- a/tests/test_speed.py
+++ b/tests/test_speed.py
@@ -0,0 +1,66 @@
+"""
+Perform speed tests on the project to ensure performant parsing.
+
+One of the main things that I set out to do with this project is
+create a performant, feature-rich dice roller. To ensure that I
+have reached that goal, we will include several tests to measure
+this.
+
+One issue though is that measuring this is not as straight
+forward as simply measuring how quickly something is done timewise.
+This is due to differing computer speeds. While this may run quickly
+on my desktop, it may not run as well on Termux on an older phone.
+
+Further reading:
+- https://therenegadecoder.com/code/how-to-performance-test-python-code/
+"""
+
+import timeit
+
+import pytest
+
+# The acceptable speed is set to 1/10th of a second.
+ACCEPTABLE_SPEED_AVERAGE = 0.1
+
+
+def test_basic_roll_time() -> None:
+    """Test how long it takes on average to do a simple roll."""
+    iterations: int = 1000
+
+    result = timeit.timeit(
+        "roll()",
+        "gc.enable();from roll import roll",
+        number=iterations
+    )
+    current_speed: float = result / iterations
+
+    print(f"The current speed is: {current_speed}")
+    assert current_speed < ACCEPTABLE_SPEED_AVERAGE
+
+
+@pytest.mark.skip(
+    reason="""This is failing because of recursion depth, not speed.
+    This issue likely will be addressed once the recursion check
+    issues are addressed.""")
+def test_inception_parens() -> None:
+    """
+    Test where we are performant even with multiple parens.
+
+    This is something that has been a real performance killer
+    for us in the past. What happens is it causes repeated,
+    recursive parsing of the inner parens which pyparsing does
+    not seem to do well?
+
+    e.g. ((((((((((((((((1 + 1))))))))))))))))
+    """
+    iterations: int = 1000
+
+    result = timeit.timeit(
+        "roll('((((((((((((((((1 + 1))))))))))))))))')",
+        "gc.enable();from roll import roll",
+        number=iterations
+    )
+    current_speed: float = result / iterations
+
+    print(f"The current speed is: {current_speed}")
+    assert current_speed < ACCEPTABLE_SPEED_AVERAGE
diff --git a/tests/test_verbose_output.py b/tests/test_verbose_output.py
new file mode 100644
index 0000000..7a4821f
--- /dev/null
+++ b/tests/test_verbose_output.py
@@ -0,0 +1,60 @@
+from roll import roll
+from roll.parser.types import RollOption
+import pytest
+
+
+@pytest.mark.parametrize("equation,expected_output", [
+    ("1+2", "Adding: 1 + 2 = 3\n3"),
+    ("2-1", "Subtracting: 2 - 1 = 1\n1"),
+    ("3*7", "Multiplying: 3 * 7 = 21\n21"),
+    ("100/5", "Dividing: 100 / 5 = 20.0\n20.0"),
+    ("50//7", "Floor dividing: 50 // 7 = 7\n7"),
+    ("37%100", "Modulus dividing: 37 % 100 = 37\n37"),
+    ("2**8", "Exponentiating: 2 ** 8 = 256\n256"),
+    ("2^8", "Exponentiating: 2 ** 8 = 256\n256"),
+    ("5!", "Factorial: 5! = 120\n120"),
+])
+def test_verbose_math(equation: str, expected_output: str) -> None:
+    output = roll(equation, verbose=True, roll_option=RollOption.Minimum)
+
+    if str(output) != expected_output:
+        raise Exception(f"The string output of the roll was not what we expected.\nRecieved: {output}\n\nExpected: {expected_output}")
+
+
+@pytest.mark.parametrize("equation,expected_output", [
+    ("1d6", "Rolled: 1d6: [1]\n1"),
+    ("1d4+1d5+1d6", "\n".join([
+        "Rolled: 1d4: [1]",
+        "Rolled: 1d5: [1]",
+        "Adding: 1 + 1 = 2",
+        "Rolled: 1d6: [1]",
+        "Adding: 2 + 1 = 3",
+        "3",
+    ])),
+    ("4d6K3", "\n".join([
+        "Rolled: 4d6: [1, 1, 1, 1]",
+        "Keeping highest: 3: [1, 1, 1]",
+        "3"
+    ])),
+    ("10d50k7", "\n".join([
+        "Rolled: 10d50: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]",
+        "Keeping lowest: 7: [1, 1, 1, 1, 1, 1, 1]",
+        "7"
+    ])),
+    ("10d50k7k6K5k4K3k2K1", "\n".join([
+        "Rolled: 10d50: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]",
+        "Keeping lowest: 7: [1, 1, 1, 1, 1, 1, 1]",
+        "Keeping lowest: 6: [1, 1, 1, 1, 1, 1]",
+        "Keeping highest: 5: [1, 1, 1, 1, 1]",
+        "Keeping lowest: 4: [1, 1, 1, 1]",
+        "Keeping highest: 3: [1, 1, 1]",
+        "Keeping lowest: 2: [1, 1]",
+        "Keeping highest: 1: [1]",
+        "1"
+    ])),
+])
+def test_verbose_dice(equation: str, expected_output: str) -> None:
+    output = roll(equation, verbose=True, roll_option=RollOption.Minimum)
+
+    if str(output) != expected_output:
+        raise Exception(f"The string output of the roll was not what we expected.\nRecieved: {output}\n\nExpected: {expected_output}")