dicey 0.0.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +397 -0
- data/exe/dicey +6 -0
- data/exe/dicey-to-gnuplot +27 -0
- data/lib/dicey/abstract_die.rb +136 -0
- data/lib/dicey/cli/blender.rb +81 -0
- data/lib/dicey/cli/options.rb +102 -0
- data/lib/dicey/die_foundry.rb +76 -0
- data/lib/dicey/numeric_die.rb +24 -0
- data/lib/dicey/output_formatters/gnuplot_formatter.rb +12 -0
- data/lib/dicey/output_formatters/hash_formatter.rb +32 -0
- data/lib/dicey/output_formatters/json_formatter.rb +12 -0
- data/lib/dicey/output_formatters/key_value_formatter.rb +20 -0
- data/lib/dicey/output_formatters/list_formatter.rb +12 -0
- data/lib/dicey/output_formatters/yaml_formatter.rb +12 -0
- data/lib/dicey/regular_die.rb +28 -0
- data/lib/dicey/roller.rb +27 -0
- data/lib/dicey/sum_frequency_calculators/base_calculator.rb +90 -0
- data/lib/dicey/sum_frequency_calculators/brute_force.rb +56 -0
- data/lib/dicey/sum_frequency_calculators/kronecker_substitution.rb +81 -0
- data/lib/dicey/sum_frequency_calculators/multinomial_coefficients.rb +101 -0
- data/lib/dicey/sum_frequency_calculators/runner.rb +38 -0
- data/lib/dicey/sum_frequency_calculators/test_runner.rb +111 -0
- data/lib/dicey/version.rb +5 -0
- data/lib/dicey.rb +10 -2
- metadata +74 -59
- data/Rakefile +0 -7
- data/Readme.md +0 -21
- data/lib/dicey/dice.rb +0 -14
- data/test/dice_test.rb +0 -17
- data/test/die_test.rb +0 -13
- data/test/test_helper.rb +0 -2
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b23d3fe347080f349190417819aba5f2778a6196569ab39e38d79049f7557f02
|
4
|
+
data.tar.gz: 5bd9295a6bb1a15fe33fe3b967fd1d2c0784a19e713977f57da7910589687ebe
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 778d6f36d896b5db19411bedf0bf272082310eb2c9a1403b57962932979ec0da863b6d408c9245b49bbfb914c99cc2bfa9e8443e0a02d732d2723f93f8b1f6bb
|
7
|
+
data.tar.gz: 694960b274505249fc705e0a61bb1452b6efed062d8b183c154734a9677cc9c28309cf859917443fd997a60de70e00c7f45abc9f9e36e96007949c1bdbee7e0f
|
data/README.md
ADDED
@@ -0,0 +1,397 @@
|
|
1
|
+
# Dicey
|
2
|
+
|
3
|
+
[](https://rubygems.org/gems/dicey)
|
4
|
+
[](https://github.com/trinistr/dicey/actions/workflows/CI.yaml)
|
5
|
+
|
6
|
+
> [!TIP]
|
7
|
+
> You may be viewing documentation for an older (or newer) version of the gem than intended. Look at [Changelog](https://github.com/trinistr/dicey/blob/main/CHANGELOG.md) to see all versions, including unreleased changes.
|
8
|
+
|
9
|
+
***
|
10
|
+
|
11
|
+
The premier solution in total paradigm shift for resolving dicey problems of tomorrow, today, used by industry-leading professionals around the world!
|
12
|
+
|
13
|
+
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.
|
14
|
+
|
15
|
+
## Table of contents
|
16
|
+
|
17
|
+
- [No installation](#no-installation)
|
18
|
+
- [Installation](#installation)
|
19
|
+
- [Requirements](#requirements)
|
20
|
+
- [Usage / CLI (command line interface)](#usage--cli-command-line-interface)
|
21
|
+
- [Example 1 — Basic distribution](#example-1--basic-distribution)
|
22
|
+
- [Example 2 — Complex distribution with different dice](#example-2--complex-distribution-with-different-dice)
|
23
|
+
- [Example 3 — Custom dice](#example-3--custom-dice)
|
24
|
+
- [Example 4 — Rolling even more custom dice](#example-4--rolling-even-more-custom-dice)
|
25
|
+
- [Usage / API](#usage--api)
|
26
|
+
- [Dice](#dice)
|
27
|
+
- [Rolling](#rolling)
|
28
|
+
- [Calculators](#calculators)
|
29
|
+
- [Diving deeper](#diving-deeper)
|
30
|
+
- [Development](#development)
|
31
|
+
- [Contributing](#contributing)
|
32
|
+
- [License](#license)
|
33
|
+
|
34
|
+
## No installation
|
35
|
+
|
36
|
+
Thanks to the efforts of Ruby developers, you can try **Dicey** online!
|
37
|
+
1. Head over to https://runruby.dev/gist/476679a55c24520782613d9ceb89d9a3
|
38
|
+
2. Make sure that "*-main.rb*" is open
|
39
|
+
3. Input arguments between "ARGUMENTS" lines, separated by spaces.
|
40
|
+
4. Click "**Run code**" button below the editor.
|
41
|
+
|
42
|
+
## Installation
|
43
|
+
|
44
|
+
Install manually via `gem`:
|
45
|
+
```sh
|
46
|
+
gem install dicey
|
47
|
+
```
|
48
|
+
|
49
|
+
Or, if using Bundler, add it to your `Gemfile`:
|
50
|
+
```rb
|
51
|
+
gem "dicey", "~> 0.13"
|
52
|
+
```
|
53
|
+
|
54
|
+
> [!TIP]
|
55
|
+
> Versions upto 0.12.1 were packaged as a single executable file. You can still download it from the [release](https://github.com/trinistr/dicey/releases/tag/v0.12.1).
|
56
|
+
|
57
|
+
> [!NOTE]
|
58
|
+
> `dicey` 0.0.1 was a completely separate [project](https://github.com/rodreegez/dicey) by [Adam Rogers](https://github.com/rodreegez). Big thanks for transfering the name!
|
59
|
+
|
60
|
+
### Requirements
|
61
|
+
|
62
|
+
**Dicey** is tested to work on CRuby 3.0+, latest JRuby and TruffleRuby. Compatible implementations should work too.
|
63
|
+
- JSON and YAML formatting require `json` and `yaml`.
|
64
|
+
- Decimal dice require `bigdecimal`.
|
65
|
+
|
66
|
+
Otherwise, there are no direct dependencies.
|
67
|
+
|
68
|
+
## Usage / CLI (command line interface)
|
69
|
+
|
70
|
+
Following examples assume that `dicey` (or `dicey-to-gnuplot`) is executable and is in `$PATH`. You can also run it with `ruby dicey` instead.
|
71
|
+
|
72
|
+
> [!NOTE]
|
73
|
+
> 💡 Run `dicey --help` to get a list of all possible options.
|
74
|
+
|
75
|
+
### Example 1 — Basic distribution
|
76
|
+
|
77
|
+
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:
|
78
|
+
```sh
|
79
|
+
$ dicey 4 4
|
80
|
+
```
|
81
|
+
|
82
|
+
It should output the following:
|
83
|
+
```sh
|
84
|
+
# ⚃;⚃
|
85
|
+
2 => 1
|
86
|
+
3 => 2
|
87
|
+
4 => 3
|
88
|
+
5 => 4
|
89
|
+
6 => 3
|
90
|
+
7 => 2
|
91
|
+
8 => 1
|
92
|
+
```
|
93
|
+
|
94
|
+
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.
|
95
|
+
|
96
|
+
If probability is preferred, there is an option for that:
|
97
|
+
```sh
|
98
|
+
$ dicey 4 4 --result probabilities # or -r p for short
|
99
|
+
# ⚃;⚃
|
100
|
+
2 => 0.0625
|
101
|
+
3 => 0.125
|
102
|
+
4 => 0.1875
|
103
|
+
5 => 0.25
|
104
|
+
6 => 0.1875
|
105
|
+
7 => 0.125
|
106
|
+
8 => 0.0625
|
107
|
+
```
|
108
|
+
|
109
|
+
This shows that 5 will probably be rolled a quarter of the time.
|
110
|
+
|
111
|
+
### Example 2 — Complex distribution with different dice
|
112
|
+
|
113
|
+
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:
|
114
|
+
```sh
|
115
|
+
# Note the shorthand notation for two dice!
|
116
|
+
$ dicey 8 2d4
|
117
|
+
# [8];⚃;⚃
|
118
|
+
3 => 1
|
119
|
+
4 => 3
|
120
|
+
5 => 6
|
121
|
+
6 => 10
|
122
|
+
7 => 13
|
123
|
+
8 => 15
|
124
|
+
9 => 16
|
125
|
+
10 => 16
|
126
|
+
11 => 15
|
127
|
+
12 => 13
|
128
|
+
13 => 10
|
129
|
+
14 => 6
|
130
|
+
15 => 3
|
131
|
+
16 => 1
|
132
|
+
```
|
133
|
+
|
134
|
+
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?
|
135
|
+
|
136
|
+
#### Example 2.1 — Graph
|
137
|
+
|
138
|
+
If you downloaded `dicey-to-gnuplot` and have [gnuplot](http://gnuplot.info) installed, it is possible to turn these results into a graph with a somewhat clunky command:
|
139
|
+
```sh
|
140
|
+
$ dicey 8 2d4 --format gnuplot | dicey-to-gnuplot
|
141
|
+
# `--format gnuplot` can be abbreviated as `-f g`
|
142
|
+
```
|
143
|
+
|
144
|
+
This will create a PNG image named `[8];⚃;⚃.png`:
|
145
|
+

|
146
|
+
|
147
|
+
#### Example 2.2 — JSON and YAML
|
148
|
+
|
149
|
+
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.
|
150
|
+
|
151
|
+
JSON via `dicey 8 2d4 --format json`:
|
152
|
+
```json
|
153
|
+
{"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}}
|
154
|
+
```
|
155
|
+
|
156
|
+
YAML via `dicey 8 2d4 --format yaml`:
|
157
|
+
```yaml
|
158
|
+
---
|
159
|
+
description: "[8];⚃;⚃"
|
160
|
+
results:
|
161
|
+
3: 1
|
162
|
+
4: 3
|
163
|
+
5: 6
|
164
|
+
6: 10
|
165
|
+
7: 13
|
166
|
+
8: 15
|
167
|
+
9: 16
|
168
|
+
10: 16
|
169
|
+
11: 15
|
170
|
+
12: 13
|
171
|
+
13: 10
|
172
|
+
14: 6
|
173
|
+
15: 3
|
174
|
+
16: 1
|
175
|
+
```
|
176
|
+
|
177
|
+
### Example 3 — Custom dice
|
178
|
+
|
179
|
+
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.
|
180
|
+
|
181
|
+
Having ran to a computer as fast as you can, you sic **Dicey** on the problem:
|
182
|
+
```sh
|
183
|
+
$ dicey 1,2,4 4
|
184
|
+
# (1,2,4);⚃
|
185
|
+
2 => 1
|
186
|
+
3 => 2
|
187
|
+
4 => 2
|
188
|
+
5 => 3
|
189
|
+
6 => 2
|
190
|
+
7 => 1
|
191
|
+
8 => 1
|
192
|
+
```
|
193
|
+
|
194
|
+
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.
|
195
|
+
|
196
|
+
> [!TIP]
|
197
|
+
> 💡 A single positive integer argument N practically is a shorthand for listing every side from 1 to N.
|
198
|
+
|
199
|
+
But what if you had TWO weird D4s?
|
200
|
+
```sh
|
201
|
+
$ dicey 2d1,2,4
|
202
|
+
# (1,2,4);(1,2,4)
|
203
|
+
2 => 1
|
204
|
+
3 => 2
|
205
|
+
4 => 1
|
206
|
+
5 => 2
|
207
|
+
6 => 2
|
208
|
+
8 => 1
|
209
|
+
```
|
210
|
+
|
211
|
+
Hah, now this is a properly cursed distribution!
|
212
|
+
|
213
|
+
### Example 4 — Rolling even more custom dice
|
214
|
+
|
215
|
+
You have a sudden urge to roll dice while only having boring integer dice at home. Where to find *the cool* dice though?
|
216
|
+
|
217
|
+
Look no further than **roll** mode introduced in **Dicey** 0.12:
|
218
|
+
```sh
|
219
|
+
$ dicey 0.5,1.5,2.5 4 --mode roll # As always, can be abbreviated to -m r
|
220
|
+
# (0.5e0,0.15e1,0.25e1);⚃
|
221
|
+
roll => 0.35e1 # You probably will get a different value here.
|
222
|
+
```
|
223
|
+
|
224
|
+
> [!NOTE]
|
225
|
+
> 💡 Roll mode is compatible with `--format`, but not `--result`.
|
226
|
+
|
227
|
+
## Usage / API
|
228
|
+
|
229
|
+
> [!Note]
|
230
|
+
> - Latest documentation from `main` branch is automatically deployed to [GitHub Pages](https://trinistr.github.io/dicey).
|
231
|
+
> - Documentation for published versions is available on [RubyDoc](https://rubydoc.info/gems/dicey).
|
232
|
+
|
233
|
+
### Dice
|
234
|
+
|
235
|
+
There are 3 classes of dice currently:
|
236
|
+
- `Dicey::AbstractDie` is 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.
|
237
|
+
- `Dicey::NumericDie` behaves much the same as `Dicey::AbstractDie`, except for checking that all values are instances of `Numeric`. It can be initialized with an Array or Range.
|
238
|
+
- `Dicey::RegularDie` is a subclass of `Dicey::NumericDie`. It is defined by a single integer which is expanded to range (1..N).
|
239
|
+
|
240
|
+
All dice classes have constructor methods aside from `.new`:
|
241
|
+
- `.from_list` takes a list of definitions and calls `.new` with each one;
|
242
|
+
- `.from_count` takes a count and a definition and calls `.new` with it specified number of times.
|
243
|
+
|
244
|
+
See [Diving deeper](#diving-deeper) for more information.
|
245
|
+
|
246
|
+
> [!NOTE]
|
247
|
+
> 💡 Using `Float` values is liable to cause precision issues. Due to in-built result verification, this **will** raise errors. Use `Rational` or `BigDecimal` instead.
|
248
|
+
|
249
|
+
#### DieFoundry
|
250
|
+
|
251
|
+
`Dicey::DieFoundry#call` provides the string interface for creating dice as available in CLI:
|
252
|
+
```rb
|
253
|
+
Dicey::DieFoundry.new.call("100")
|
254
|
+
# same as Dicey::RegularDie.new(100)
|
255
|
+
Dicey::DieFoundry.new.call("2d6")
|
256
|
+
# same as Dicey::RegularDie.from_count(2, 6)
|
257
|
+
Dicey::DieFoundry.new.call("1d1,2,4")
|
258
|
+
# same as Dicey::NumericDie.from_list([1,2,4])
|
259
|
+
```
|
260
|
+
|
261
|
+
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`:
|
262
|
+
```rb
|
263
|
+
foundry = Dicey::DieFoundry.new
|
264
|
+
%w[8 2d4].flat_map { foundry.call(_1) }
|
265
|
+
# same as [Dicey::RegularDie.new(8), Dicey::RegularDie.new(4), Dicey::RegularDie.new(4)]
|
266
|
+
```
|
267
|
+
|
268
|
+
### Rolling
|
269
|
+
|
270
|
+
`Dicey::AbstractDie#roll` implements the rolling:
|
271
|
+
```rb
|
272
|
+
Dicey::AbstractDie.new([0, 1, 5, "10"]).roll
|
273
|
+
# almost same as [0, 1, 5, "10"].sample
|
274
|
+
Dicey::RegularDie.new(6).roll
|
275
|
+
# almost same as rand(1..6)
|
276
|
+
```
|
277
|
+
|
278
|
+
Dice retain their roll state, with `#current` returning the last roll (or initial side if never rolled):
|
279
|
+
```rb
|
280
|
+
die = Dicey::RegularDie.new(6)
|
281
|
+
die.current
|
282
|
+
# => 1
|
283
|
+
die.roll
|
284
|
+
# => 3
|
285
|
+
die.current
|
286
|
+
# => 3
|
287
|
+
```
|
288
|
+
|
289
|
+
Rolls can be reproducible if a specific seed is set:
|
290
|
+
```rb
|
291
|
+
Dicey::AbstractDie.srand(493_525)
|
292
|
+
die = Dicey::RegularDie.new(6)
|
293
|
+
die.roll
|
294
|
+
# => 4
|
295
|
+
die.roll
|
296
|
+
# => 1
|
297
|
+
# Repeat:
|
298
|
+
Dicey::AbstractDie.srand(493_525)
|
299
|
+
die = Dicey::RegularDie.new(6)
|
300
|
+
die.roll
|
301
|
+
# => 4
|
302
|
+
die.roll
|
303
|
+
# => 1
|
304
|
+
```
|
305
|
+
|
306
|
+
Randomness source is *global*, shared between all dice and probably not thread-safe.
|
307
|
+
|
308
|
+
### Calculators
|
309
|
+
|
310
|
+
Frequency calculators live in `Dicey::SumFrequencyCalculators` module. There are three implemented calculators:
|
311
|
+
- `Dicey::SumFrequencyCalculators::KroneckerSubstitution` is the recommended calculator, able to handle all `Dicey::RegularDie`. It is very fast, calculating distribution for *100d6* in about 0.1 seconds on my laptop.
|
312
|
+
- `Dicey::SumFrequencyCalculators::MultinomialCoefficients` is specialized for repeated numeric dice, with performance only slightly worse. However, it is currently limited to dice with arithmetic sequences.
|
313
|
+
- `Dicey::SumFrequencyCalculators::BruteForce` is the most generic and slowest one, but can handle any dice. Currently, it is also limited to `Dicey::NumericDie`, as it's unclear how to handle other values.
|
314
|
+
|
315
|
+
Calculators inherit from `Dicey::SumFrequencyCalculators::BaseCalculator` and provide the following public interface:
|
316
|
+
- `#call(dice, result_type: {:frequencies | :probabilities}) : Hash`
|
317
|
+
- `#valid_for?(dice) : Boolean`
|
318
|
+
|
319
|
+
See [next section](#diving-deeper) for more details on limitations and complexity considerations.
|
320
|
+
|
321
|
+
## Diving deeper
|
322
|
+
|
323
|
+
For a further discussion of calculations, it is important to understand which classes of dice exist.
|
324
|
+
- **Regular** die — a die with N sides with sequential integers from 1 to N,
|
325
|
+
like a classic cubic D6, D20, or even a coin if you assume that it rolls 1 and 2.
|
326
|
+
These are dice used for many tabletop games, including role-playing games.
|
327
|
+
Most probably, you will only ever need these and not anything beyond.
|
328
|
+
|
329
|
+
> [!TIP]
|
330
|
+
> 💡 If you only need to roll **regular** dice, this section will not contain anything important.
|
331
|
+
|
332
|
+
- **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).
|
333
|
+
- **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).
|
334
|
+
- **Numeric** die is limited by having sides confined to ℝ (or ℂ if you are feeling particularly adventurous).
|
335
|
+
- **Abstract** die is not limited by anything other than not having partial sides (and how would that work anyway?).
|
336
|
+
|
337
|
+
> [!NOTE]
|
338
|
+
> 💡 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).
|
339
|
+
|
340
|
+
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.
|
341
|
+
|
342
|
+
Currently, three algorithms are implemented, with different possibilities and trade-offs.
|
343
|
+
|
344
|
+
> [!NOTE]
|
345
|
+
> 💡 Complexity is listed for `n` dice with at most `m` sides and has not been rigorously proven.
|
346
|
+
|
347
|
+
### Kronecker substitution
|
348
|
+
|
349
|
+
An algorithm based on fast polynomial multiplication. This is the default algorithm, used for most reasonable dice.
|
350
|
+
|
351
|
+
- Limitations: only **natural** dice are allowed, including **regular** dice.
|
352
|
+
- Example: `dicey 5 3,4,1 '(0)'`
|
353
|
+
- Complexity: `O(m⋅n)` where `m` is the highest value
|
354
|
+
|
355
|
+
### Multinomial coefficients
|
356
|
+
|
357
|
+
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.
|
358
|
+
|
359
|
+
- Limitations: only *equal* **arithmetic** dice are allowed.
|
360
|
+
- Example: `dicey 1.5,3,4.5,6 1.5,3,4.5,6 1.5,3,4.5,6`
|
361
|
+
- Complexity: `O(m⋅n²)`
|
362
|
+
|
363
|
+
### Brute force
|
364
|
+
|
365
|
+
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.
|
366
|
+
|
367
|
+
- Limitations: objects on dice sides must be numbers.
|
368
|
+
- Example: `dicey 5 1,0.1,2 1,-1,1,-1,0`
|
369
|
+
- Complexity: `O(mⁿ)`
|
370
|
+
|
371
|
+
## Development
|
372
|
+
|
373
|
+
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.
|
374
|
+
|
375
|
+
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.
|
376
|
+
|
377
|
+
To install this gem onto your local machine, run `rake install`.
|
378
|
+
|
379
|
+
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](https://rubygems.org). After that, push the release commit and tags to the repository with `git push --follow-tags`.
|
380
|
+
|
381
|
+
## Contributing
|
382
|
+
|
383
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/trinistr/dicey.
|
384
|
+
|
385
|
+
### Checklist for a new or updated feature
|
386
|
+
|
387
|
+
- Running `rake spec` reports 100% coverage (unless it's impossible to achieve in one run).
|
388
|
+
- Running `rake rubocop` reports no offenses.
|
389
|
+
- Running `rake steep` reports no new warnings or errors.
|
390
|
+
- Tests cover the behavior and its interactions. 100% coverage *is not enough*, as it does not guarantee that all code paths are tested.
|
391
|
+
- Documentation is up-to-date: generate it with `rake docs` and read it.
|
392
|
+
- "*CHANGELOG.md*" lists the change if it has impact on users.
|
393
|
+
- "*README.md*" is updated if the feature should be visible there, including the Kanban board.
|
394
|
+
|
395
|
+
## License
|
396
|
+
|
397
|
+
This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT), see [LICENSE.txt](https://github.com/trinistr/dicey/blob/main/LICENSE.txt).
|
data/exe/dicey
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Generate an image from dicey results.
|
5
|
+
# Requires gnuplot to be installed and in PATH.
|
6
|
+
# Usage: dicey 6 6 6 -f gnuplot | gnuplot-for-dicey
|
7
|
+
|
8
|
+
require "tempfile"
|
9
|
+
|
10
|
+
data = ARGF.read
|
11
|
+
description = data[/(?<=\A# ).+$/] || "dice"
|
12
|
+
Tempfile.create do |file|
|
13
|
+
file << data
|
14
|
+
file.flush
|
15
|
+
Process.wait(
|
16
|
+
Process.spawn(
|
17
|
+
"gnuplot",
|
18
|
+
"-e", "set term pngcairo size 1000,600",
|
19
|
+
"-e", %(set output "#{description}.png"),
|
20
|
+
"-e", "set boxwidth 0.9 relative",
|
21
|
+
"-e", "set style fill solid 0.5",
|
22
|
+
"-e", "plot [][0:] \"#{file.path}\" " \
|
23
|
+
"using 1:2:xticlabels(1) with boxes title \"#{description}\", " \
|
24
|
+
"'' using 1:2:2 with labels notitle"
|
25
|
+
)
|
26
|
+
)
|
27
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dicey
|
4
|
+
# Asbtract die which may have an arbitrary list of sides,
|
5
|
+
# not even neccessarily numbers (but preferably so).
|
6
|
+
class AbstractDie
|
7
|
+
# rubocop:disable Style/ClassVars
|
8
|
+
|
9
|
+
# @api private
|
10
|
+
# Get a random value using a private instance of Random.
|
11
|
+
# @see Random#rand
|
12
|
+
def self.rand(...)
|
13
|
+
@@random.rand(...)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Reset internal randomizer using a new seed.
|
17
|
+
# @see Random.new
|
18
|
+
def self.srand(...)
|
19
|
+
@@random = Random.new(...)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Yes, class variable is actually useful here.
|
23
|
+
# TODO: Allow supplying a custom Random.
|
24
|
+
@@random = Random.new
|
25
|
+
|
26
|
+
# rubocop:enable Style/ClassVars
|
27
|
+
|
28
|
+
# Get a text representation of a list of dice.
|
29
|
+
#
|
30
|
+
# @param dice [Enumerable<AbstractDie>, AbstractDie]
|
31
|
+
# @return [String]
|
32
|
+
def self.describe(dice)
|
33
|
+
return dice.to_s if AbstractDie === dice
|
34
|
+
return dice.join(";") if Array === dice
|
35
|
+
|
36
|
+
dice.to_a.join(";")
|
37
|
+
end
|
38
|
+
|
39
|
+
# Create a bunch of different dice at once.
|
40
|
+
#
|
41
|
+
# @param definitions [Array<Enumerable<Any>>, Array<Any>]
|
42
|
+
# list of definitions suitable for the dice class
|
43
|
+
# @return [Array<AbstractDie>]
|
44
|
+
def self.from_list(*definitions)
|
45
|
+
definitions.map { new(_1) }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create a number of equal dice.
|
49
|
+
#
|
50
|
+
# @param count [Integer] number of dice to create
|
51
|
+
# @param definition [Enumerable<Any>, Any]
|
52
|
+
# definition suitable for the dice class
|
53
|
+
# @return [Array<AbstractDie>]
|
54
|
+
def self.from_count(count, definition)
|
55
|
+
Array.new(count) { new(definition) }
|
56
|
+
end
|
57
|
+
|
58
|
+
attr_reader :sides_list, :sides_num
|
59
|
+
|
60
|
+
# @param sides_list [Enumerable<Any>]
|
61
|
+
# @raise [DiceyError] if +sides_list+ is empty
|
62
|
+
def initialize(sides_list)
|
63
|
+
@sides_list = (Array === sides_list) ? sides_list.dup.freeze : sides_list.to_a.freeze
|
64
|
+
raise DiceyError, "dice must have at least one side!" if @sides_list.empty?
|
65
|
+
|
66
|
+
@sides_num = @sides_list.size
|
67
|
+
@current_side_index = 0
|
68
|
+
end
|
69
|
+
|
70
|
+
# Get current side of the die.
|
71
|
+
#
|
72
|
+
# @return [Any] current side
|
73
|
+
def current
|
74
|
+
@sides_list[@current_side_index]
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get next side of the die, advancing internal state.
|
78
|
+
# Starts from first side, wraps from last to first side.
|
79
|
+
#
|
80
|
+
# @return [Any] next side
|
81
|
+
def next
|
82
|
+
ret = current
|
83
|
+
@current_side_index = (@current_side_index + 1) % @sides_num
|
84
|
+
ret
|
85
|
+
end
|
86
|
+
|
87
|
+
# Move internal state to a random side.
|
88
|
+
#
|
89
|
+
# @return [Any] rolled side
|
90
|
+
def roll
|
91
|
+
@current_side_index = self.class.rand(0...@sides_num)
|
92
|
+
current
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [String]
|
96
|
+
def to_s
|
97
|
+
"(#{@sides_list.join(",")})"
|
98
|
+
end
|
99
|
+
|
100
|
+
# Determine if this die and the other one have the same list of sides.
|
101
|
+
# Be aware that differently ordered sides are not considered equal.
|
102
|
+
#
|
103
|
+
# @param other [AbstractDie, Any]
|
104
|
+
# @return [Boolean]
|
105
|
+
def ==(other)
|
106
|
+
AbstractDie === other && same_sides?(other)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Determine if this die and the other one are of the same class
|
110
|
+
# and have the same list of sides.
|
111
|
+
# Be aware that differently ordered sides are not considered equal.
|
112
|
+
#
|
113
|
+
# +die_1.eql?(die_2)+ implies +die_1.hash == die_2.hash+.
|
114
|
+
#
|
115
|
+
# @param other [AbstractDie, Any]
|
116
|
+
# @return [Boolean]
|
117
|
+
def eql?(other)
|
118
|
+
self.class === other && same_sides?(other)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Generates an Integer hash value for this object.
|
122
|
+
#
|
123
|
+
# @return [Integer]
|
124
|
+
def hash
|
125
|
+
[self.class, @sides_list].hash
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
# @param other [AbstractDie]
|
131
|
+
# @return [Boolean]
|
132
|
+
def same_sides?(other)
|
133
|
+
@sides_list == other.sides_list
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dicey
|
4
|
+
# Classes pertaining to CLI.
|
5
|
+
# NOT loaded by default, use +require "dicey/cli/blender"+ as needed.
|
6
|
+
module CLI
|
7
|
+
require_relative "../../dicey"
|
8
|
+
require_relative "options"
|
9
|
+
|
10
|
+
# Slice and dice everything in the Dicey module to produce a useful result.
|
11
|
+
# This is the entry point for the CLI.
|
12
|
+
class Blender
|
13
|
+
# List of calculators to use, ordered by efficiency.
|
14
|
+
ROLL_FREQUENCY_CALCULATORS = [
|
15
|
+
SumFrequencyCalculators::KroneckerSubstitution.new,
|
16
|
+
SumFrequencyCalculators::MultinomialCoefficients.new,
|
17
|
+
SumFrequencyCalculators::BruteForce.new,
|
18
|
+
].freeze
|
19
|
+
|
20
|
+
# How to transform option values from command-line arguments
|
21
|
+
# to internally significant objects.
|
22
|
+
OPTION_TRANSFORMATIONS = {
|
23
|
+
mode: lambda(&:to_sym),
|
24
|
+
result: lambda(&:to_sym),
|
25
|
+
format: {
|
26
|
+
"list" => OutputFormatters::ListFormatter.new,
|
27
|
+
"gnuplot" => OutputFormatters::GnuplotFormatter.new,
|
28
|
+
"yaml" => OutputFormatters::YAMLFormatter.new,
|
29
|
+
"json" => OutputFormatters::JSONFormatter.new,
|
30
|
+
}.freeze,
|
31
|
+
}.freeze
|
32
|
+
|
33
|
+
# What to run for every mode.
|
34
|
+
# Every runner must respond to `call(arguments, **options)`
|
35
|
+
# and return +true+, +false+ or a String.
|
36
|
+
RUNNERS = {
|
37
|
+
roll: Roller.new,
|
38
|
+
frequencies: SumFrequencyCalculators::Runner.new,
|
39
|
+
test: SumFrequencyCalculators::TestRunner.new,
|
40
|
+
}.freeze
|
41
|
+
|
42
|
+
# Run the program, blending everything together.
|
43
|
+
#
|
44
|
+
# @param argv [Array<String>] arguments for the program
|
45
|
+
# @return [Boolean]
|
46
|
+
# @raise [DiceyError] anything can happen
|
47
|
+
def call(argv = ARGV)
|
48
|
+
options, arguments = get_options_and_arguments(argv)
|
49
|
+
require_optional_libraries(options)
|
50
|
+
options[:roll_calculators] = ROLL_FREQUENCY_CALCULATORS
|
51
|
+
return_value = RUNNERS[options.delete(:mode)].call(arguments, **options)
|
52
|
+
print return_value if return_value.is_a?(String)
|
53
|
+
!!return_value
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def get_options_and_arguments(argv)
|
59
|
+
options = Options.new
|
60
|
+
arguments = options.read(argv)
|
61
|
+
options = options.to_h
|
62
|
+
options.each_pair do |k, v|
|
63
|
+
options[k] = OPTION_TRANSFORMATIONS[k][v] || v if OPTION_TRANSFORMATIONS[k]
|
64
|
+
end
|
65
|
+
[options, arguments]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Require libraries only when needed, to cut on run time.
|
69
|
+
def require_optional_libraries(options)
|
70
|
+
case options[:format]
|
71
|
+
when OutputFormatters::YAMLFormatter
|
72
|
+
require "yaml"
|
73
|
+
when OutputFormatters::JSONFormatter
|
74
|
+
require "json"
|
75
|
+
else
|
76
|
+
# No additional libraries needed
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|