Dicey
[!TIP] You may be viewing documentation for an older (or newer) version of the gem than intended. Look at Changelog to see all versions, including unreleased changes.
The premier solution in total paradigm shift for resolving dicey problems of tomorrow, today, used by industry-leading professionals around the world!
In seriousness, this program is mainly useful for calculating total frequency (probability) distributions of all possible dice rolls for a given set of dice. Dice in such a set can be different or even have arbitrary numbers on the sides. It can also be used to roll any dice that it supports.
Table of contents
- No installation
- Installation
- Usage / CLI (command line interface)
- Usage / API
- Diving deeper
- Development
- Contributing
- License
No installation
Thanks to the efforts of Ruby developers, you can try Dicey online!
- Head over to https://runruby.dev/gist/476679a55c24520782613d9ceb89d9a3
- Make sure that "-main.rb" is open
- Input arguments between "ARGUMENTS" lines, separated by spaces.
- Click "Run code" button below the editor.
Installation
Install manually via gem:
gem install dicey
Or, if using Bundler, add it to your Gemfile:
gem "dicey", "~> 0.13"
[!TIP] Versions upto 0.12.1 were packaged as a single executable file. You can still download it from the release.
[!NOTE]
dicey0.0.1 was a completely separate project by Adam Rogers. Big thanks for transfering the name!
Requirements
Dicey is tested to work on CRuby 3.0+, latest JRuby and TruffleRuby. Compatible implementations should work too.
- JSON and YAML formatting require
jsonandyaml. - Decimal dice require
bigdecimal.
Otherwise, there are no direct dependencies.
Usage / CLI (command line interface)
Following examples assume that dicey (or dicey-to-gnuplot) is executable and is in $PATH. You can also run it with ruby dicey instead.
[!NOTE] 💡 Run
dicey --helpto get a list of all possible options.
Example 1 — Basic distribution
Let's start with something simple. Imagine that your Bard character has Vicious Mockery cantrip with 2d4 damage, and you would like to know the distribution of possible damage rolls. Run Dicey with two 4s as arguments:
$ dicey 4 4
It should output the following:
# ⚃;⚃
2 => 1
3 => 2
4 => 3
5 => 4
6 => 3
7 => 2
8 => 1
First line is a comment telling you that calculation ran for two D4s. Every line after that has the form roll sum => frequency, where frequency is the number of different rolls which result in this sum. As can be seen, 5 is the most common result with 4 possible different rolls.
If probability is preferred, there is an option for that:
$ dicey 4 4 --result probabilities # or -r p for short
# ⚃;⚃
2 => 0.0625
3 => 0.125
4 => 0.1875
5 => 0.25
6 => 0.1875
7 => 0.125
8 => 0.0625
This shows that 5 will probably be rolled a quarter of the time.
Example 2 — Complex distribution with different dice
During your quest to end all ends you find a cool Burning Sword which deals 1d8 slashing damage and 2d4 fire damage on attack. You run Dicey with these dice:
# Note the shorthand notation for two dice!
$ dicey 8 2d4
# [8];⚃;⚃
3 => 1
4 => 3
5 => 6
6 => 10
7 => 13
8 => 15
9 => 16
10 => 16
11 => 15
12 => 13
13 => 10
14 => 6
15 => 3
16 => 1
Results show that while the total range is 3–16, it is much more likely to roll numbers in the 6–13 range. That's pretty fire, huh?
Example 2.1 — Graph
If you downloaded dicey-to-gnuplot and have gnuplot installed, it is possible to turn these results into a graph with a somewhat clunky command:
$ dicey 8 2d4 --format gnuplot | dicey-to-gnuplot
# `--format gnuplot` can be abbreviated as `-f g`
This will create a PNG image named [8];⚃;⚃.png:

Example 2.2 — JSON and YAML
If you find that you need to export results for further processing, it would be great if a common data interchange format was used. Dicey supports output as JSON and YAML with --format json (or -f j) and --format yaml (or -f y) respectively.
JSON via dicey 8 2d4 --format json:
{"description":"[8];⚃;⚃","results":{"3":1,"4":3,"5":6,"6":10,"7":13,"8":15,"9":16,"10":16,"11":15,"12":13,"13":10,"14":6,"15":3,"16":1}}
YAML via dicey 8 2d4 --format yaml:
---
description: "[8];⚃;⚃"
results:
3: 1
4: 3
5: 6
6: 10
7: 13
8: 15
9: 16
10: 16
11: 15
12: 13
13: 10
14: 6
15: 3
16: 1
Example 3 — Custom dice
While walking home from work you decide to take a shortcut through a dark alleyway. Suddenly, you notice a die lying on the ground. Looking closer, it turns out to be a D4, but its 3 side was erased from reality. You just have to learn what impact this has on a roll together with a normal D4. Thankfully, you know just the program for the job.
Having ran to a computer as fast as you can, you sic Dicey on the problem:
$ dicey 1,2,4 4
# (1,2,4);⚃
2 => 1
3 => 2
4 => 2
5 => 3
6 => 2
7 => 1
8 => 1
Hmm, this looks normal, doesn't it? But wait, why are there two 2s in a row? Turns out that not having one of the sides just causes the roll frequencies to slightly dip in the middle. Good to know.
[!TIP] 💡 A single positive integer argument N practically is a shorthand for listing every side from 1 to N.
But what if you had TWO weird D4s?
$ dicey 2d1,2,4
# (1,2,4);(1,2,4)
2 => 1
3 => 2
4 => 1
5 => 2
6 => 2
8 => 1
Hah, now this is a properly cursed distribution!
Example 4 — Rolling even more custom dice
You have a sudden urge to roll dice while only having boring integer dice at home. Where to find the cool dice though?
Look no further than roll mode introduced in Dicey 0.12:
$ dicey 0.5,1.5,2.5 4 --mode roll # As always, can be abbreviated to -m r
# (0.5e0,0.15e1,0.25e1);⚃
roll => 0.35e1 # You probably will get a different value here.
[!NOTE] 💡 Roll mode is compatible with
--format, but not--result.
Usage / API
[!Note]
- Latest documentation from
mainbranch is automatically deployed to GitHub Pages.- Documentation for published versions is available on RubyDoc.
Dice
There are 3 classes of dice currently:
Dicey::AbstractDieis the base class for other dice, but can be used on its own. It has no restrictions on values of sides. For now, it is only useful for rolling and can't be used for distribution calculations.Dicey::NumericDiebehaves much the same asDicey::AbstractDie, except for checking that all values are instances ofNumeric. It can be initialized with an Array or Range.Dicey::RegularDieis a subclass ofDicey::NumericDie. It is defined by a single integer which is expanded to range (1..N).
All dice classes have constructor methods aside from .new:
.from_listtakes a list of definitions and calls.newwith each one;.from_counttakes a count and a definition and calls.newwith it specified number of times.
See Diving deeper for more information.
[!NOTE] 💡 Using
Floatvalues is liable to cause precision issues. Due to in-built result verification, this will raise errors. UseRationalorBigDecimalinstead.
DieFoundry
Dicey::DieFoundry#call provides the string interface for creating dice as available in CLI:
Dicey::DieFoundry.new.call("100")
# same as Dicey::RegularDie.new(100)
Dicey::DieFoundry.new.call("2d6")
# same as Dicey::RegularDie.from_count(2, 6)
Dicey::DieFoundry.new.call("1d1,2,4")
# same as Dicey::NumericDie.from_list([1,2,4])
It only takes a single argument and may return both an array of dice and a single die. You will probably want to use Enumerable#flat_map:
foundry = Dicey::DieFoundry.new
%w[8 2d4].flat_map { foundry.call(_1) }
# same as [Dicey::RegularDie.new(8), Dicey::RegularDie.new(4), Dicey::RegularDie.new(4)]
Rolling
Dicey::AbstractDie#roll implements the rolling:
Dicey::AbstractDie.new([0, 1, 5, "10"]).roll
# almost same as [0, 1, 5, "10"].sample
Dicey::RegularDie.new(6).roll
# almost same as rand(1..6)
Dice retain their roll state, with #current returning the last roll (or initial side if never rolled):
die = Dicey::RegularDie.new(6)
die.current
# => 1
die.roll
# => 3
die.current
# => 3
Rolls can be reproducible if a specific seed is set:
Dicey::AbstractDie.srand(493_525)
die = Dicey::RegularDie.new(6)
die.roll
# => 4
die.roll
# => 1
# Repeat:
Dicey::AbstractDie.srand(493_525)
die = Dicey::RegularDie.new(6)
die.roll
# => 4
die.roll
# => 1
Randomness source is global, shared between all dice and probably not thread-safe.
Calculators
Frequency calculators live in Dicey::SumFrequencyCalculators module. There are three implemented calculators:
Dicey::SumFrequencyCalculators::KroneckerSubstitutionis the recommended calculator, able to handle allDicey::RegularDie. It is very fast, calculating distribution for 100d6 in about 0.1 seconds on my laptop.Dicey::SumFrequencyCalculators::MultinomialCoefficientsis specialized for repeated numeric dice, with performance only slightly worse. However, it is currently limited to dice with arithmetic sequences.Dicey::SumFrequencyCalculators::BruteForceis the most generic and slowest one, but can handle any dice. Currently, it is also limited toDicey::NumericDie, as it's unclear how to handle other values.
Calculators inherit from Dicey::SumFrequencyCalculators::BaseCalculator and provide the following public interface:
#call(dice, result_type: {:frequencies | :probabilities}) : Hash#valid_for?(dice) : Boolean
See next section for more details on limitations and complexity considerations.
Diving deeper
For a further discussion of calculations, it is important to understand which classes of dice exist.
- Regular die — a die with N sides with sequential integers from 1 to N, like a classic cubic D6, D20, or even a coin if you assume that it rolls 1 and 2. These are dice used for many tabletop games, including role-playing games. Most probably, you will only ever need these and not anything beyond.
[!TIP] 💡 If you only need to roll regular dice, this section will not contain anything important.
- Natural die has sides with only positive integers or 0. For example, (1,2,3,4,5,6), (5,1,6,5), (1,10000), (1,1,1,1,1,1,1,0).
- Arithmetic die's sides form an arithmetic sequence. For example, (1,2,3,4,5,6), (1,0,-1), (2.6,2.1,1.6,1.1).
- Numeric die is limited by having sides confined to ℝ (or ℂ if you are feeling particularly adventurous).
- Abstract die is not limited by anything other than not having partial sides (and how would that work anyway?).
[!NOTE] 💡 If your die starts with a negative number or only has a single natural side, brackets can be employed to force treating it as a sides list, e.g.
dicey '(-1)'(quotation is required due to shell processing).
Dicey is in principle able to handle any numeric dice and some abstract dice with well-defined summation (tested on complex numbers), though not every possibility is exposed through command-line interface: that is limited to floating-point values.
Currently, three algorithms are implemented, with different possibilities and trade-offs.
[!NOTE] 💡 Complexity is listed for
ndice with at mostmsides and has not been rigorously proven.
Kronecker substitution
An algorithm based on fast polynomial multiplication. This is the default algorithm, used for most reasonable dice.
- Limitations: only natural dice are allowed, including regular dice.
- Example:
dicey 5 3,4,1 '(0)' - Complexity:
O(m⋅n)wheremis the highest value
Multinomial coefficients
This algorithm is based on raising a univariate polynomial to a power and using the coefficients of the result, though certain restrictions are lifted as they don't actually matter for the calculation.
- Limitations: only equal arithmetic dice are allowed.
- Example:
dicey 1.5,3,4.5,6 1.5,3,4.5,6 1.5,3,4.5,6 - Complexity:
O(m⋅n²)
Brute force
As a last resort, there is a brute force algorithm which goes through every possible dice roll and adds results together. While quickly growing terrible in performace, it has the largest input space, allowing to work with completely nonsensical dice, including aforementioned dice with complex numbers.
- Limitations: objects on dice sides must be numbers.
- Example:
dicey 5 1,0.1,2 1,-1,1,-1,0 - Complexity:
O(mⁿ)
Development
After checking out the repo, run bundle install to install dependencies. Then, run rake spec to run the tests, rake rubocop to lint code and check style compliance, rake rbs to validate signatures or just rake to do everything above. There is also rake steep to check typing, and rake docs to generate YARD documentation.
You can also run bin/console for an interactive prompt that will allow you to experiment, or bin/benchmark to run a benchmark script and generate a StackProf flamegraph.
To install this gem onto your local machine, run rake install.
To release a new version, run rake version:{major|minor|patch}, and then run rake release, which will build the package and push the .gem file to rubygems.org. After that, push the release commit and tags to the repository with git push --follow-tags.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/trinistr/dicey.
Checklist for a new or updated feature
- Running
rake specreports 100% coverage (unless it's impossible to achieve in one run). - Running
rake rubocopreports no offenses. - Running
rake steepreports no new warnings or errors. - Tests cover the behavior and its interactions. 100% coverage is not enough, as it does not guarantee that all code paths are tested.
- Documentation is up-to-date: generate it with
rake docsand read it. - "CHANGELOG.md" lists the change if it has impact on users.
- "README.md" is updated if the feature should be visible there, including the Kanban board.
License
This gem is available as open source under the terms of the MIT License, see LICENSE.txt.