dicey 0.14.0 → 0.15.1
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 +4 -4
- data/README.md +147 -71
- data/lib/dicey/abstract_die.rb +17 -4
- data/lib/dicey/cli/options.rb +2 -0
- data/lib/dicey/die_foundry.rb +25 -13
- data/lib/dicey/distribution_properties_calculator.rb +122 -0
- data/lib/dicey/numeric_die.rb +2 -2
- data/lib/dicey/output_formatters/gnuplot_formatter.rb +12 -0
- data/lib/dicey/output_formatters/hash_formatter.rb +5 -1
- data/lib/dicey/output_formatters/key_value_formatter.rb +15 -1
- data/lib/dicey/rational_to_integer.rb +15 -0
- data/lib/dicey/regular_die.rb +4 -6
- data/lib/dicey/roller.rb +5 -1
- data/lib/dicey/sum_frequency_calculators/base_calculator.rb +23 -5
- data/lib/dicey/sum_frequency_calculators/brute_force.rb +1 -1
- data/lib/dicey/sum_frequency_calculators/empirical.rb +39 -0
- data/lib/dicey/sum_frequency_calculators/kronecker_substitution.rb +4 -2
- data/lib/dicey/sum_frequency_calculators/multinomial_coefficients.rb +2 -4
- data/lib/dicey/version.rb +1 -1
- data/lib/dicey.rb +3 -3
- metadata +8 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 315d35b504558fad6ce08ebd3e648903e9f85e97dd5388c20afbcff896c40825
|
4
|
+
data.tar.gz: f4138742272906c291d44533391e3b63216c45f8bd99337f8c570e252bd0ce3b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 201115ad574e086ab99bf62d0129ae2dd3457cf2c06639d2e18f95c16699fa42f0ec72852aa13bdea408001a27fc366c1150d7cef2874f227957f5cd0e80bc40
|
7
|
+
data.tar.gz: abfec7efee1b05e607432650a6e5de444e2cf75d9c462e9b6a0211cb48888234ccf4e95ddb50d431017ddc01d5969d6ca4b6a130faefd31b7f852dc3c41a9200
|
data/README.md
CHANGED
@@ -14,30 +14,45 @@ In seriousness, this program is mainly useful for calculating total frequency (p
|
|
14
14
|
|
15
15
|
## Table of contents
|
16
16
|
|
17
|
-
- [
|
17
|
+
- [Online (no installation)](#online-no-installation)
|
18
|
+
- [Recommended](#recommended)
|
19
|
+
- [For those who want the full command line experience](#for-those-who-want-the-full-command-line-experience)
|
18
20
|
- [Installation](#installation)
|
19
21
|
- [Requirements](#requirements)
|
20
|
-
- [Usage
|
21
|
-
- [Example 1
|
22
|
-
- [Example 2
|
23
|
-
- [Example 3
|
24
|
-
- [Example 4
|
25
|
-
- [
|
22
|
+
- [Usage: CLI (command line)](#usage-cli-command-line)
|
23
|
+
- [Example 1: Basic distribution](#example-1-basic-distribution)
|
24
|
+
- [Example 2: Complex distribution with different dice](#example-2-complex-distribution-with-different-dice)
|
25
|
+
- [Example 3: Custom dice](#example-3-custom-dice)
|
26
|
+
- [Example 4: Rolling even more custom dice](#example-4-rolling-even-more-custom-dice)
|
27
|
+
- [All ways to define dice](#all-ways-to-define-dice)
|
28
|
+
- [Usage: API](#usage-api)
|
26
29
|
- [Dice](#dice)
|
27
30
|
- [Rolling](#rolling)
|
28
|
-
- [
|
31
|
+
- [Distribution calculators](#distribution-calculators)
|
32
|
+
- [Distribution properties](#distribution-properties)
|
29
33
|
- [Diving deeper](#diving-deeper)
|
30
34
|
- [Development](#development)
|
31
35
|
- [Contributing](#contributing)
|
32
36
|
- [License](#license)
|
33
37
|
|
34
|
-
##
|
38
|
+
## Online (no installation)
|
35
39
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
+
### Recommended
|
41
|
+
|
42
|
+
Use online version of **Dicey** on its own website: [dicey.bulancov.tech](https://dicey.bulancov.tech)!
|
43
|
+
|
44
|
+
It does not provide quite all features, but it's easy to use and quick to get started.
|
45
|
+
|
46
|
+
### For those who want the full command line experience
|
47
|
+
|
48
|
+
Thanks to the efforts of Ruby developers, you can run full **Dicey** online!
|
49
|
+
1. Head over to the prepared [RunRuby page](https://runruby.dev/gist/476679a55c24520782613d9ceb89d9a3).
|
50
|
+
2. Make sure that "*-main.rb*" is open.
|
51
|
+
3. Input arguments between "ARGUMENTS" lines, separated by spaces. Refer to [Usage / CLI](#usage--cli-command-line) section.
|
40
52
|
4. Click "**Run code**" button below the editor.
|
53
|
+
5. Results will be printed to the "Logs" tab.
|
54
|
+
|
55
|
+
If familiar with Ruby, you can also use **RunRuby** to explore the API. Refer to [Usage / API](#usage--api) section for documentation.
|
41
56
|
|
42
57
|
## Installation
|
43
58
|
|
@@ -48,7 +63,7 @@ gem install dicey
|
|
48
63
|
|
49
64
|
Or, if using Bundler, add it to your `Gemfile`:
|
50
65
|
```rb
|
51
|
-
gem "dicey", "~> 0.
|
66
|
+
gem "dicey", "~> 0.14"
|
52
67
|
```
|
53
68
|
|
54
69
|
> [!TIP]
|
@@ -61,18 +76,17 @@ gem "dicey", "~> 0.13"
|
|
61
76
|
|
62
77
|
**Dicey** is tested to work on CRuby 3.0+, latest JRuby and TruffleRuby. Compatible implementations should work too.
|
63
78
|
- JSON and YAML formatting require `json` and `yaml`.
|
64
|
-
- Decimal dice require `bigdecimal`.
|
65
79
|
|
66
80
|
Otherwise, there are no direct dependencies.
|
67
81
|
|
68
|
-
## Usage
|
82
|
+
## Usage: CLI (command line)
|
69
83
|
|
70
|
-
Following examples assume that `dicey` (or `dicey-to-gnuplot`) is executable and is in `$PATH`.
|
84
|
+
Following examples assume that `dicey` (or `dicey-to-gnuplot`) is executable and is in `$PATH`.
|
71
85
|
|
72
86
|
> [!NOTE]
|
73
87
|
> 💡 Run `dicey --help` to get a list of all possible options.
|
74
88
|
|
75
|
-
### Example 1
|
89
|
+
### Example 1: Basic distribution
|
76
90
|
|
77
91
|
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
92
|
```sh
|
@@ -81,7 +95,7 @@ $ dicey 4 4
|
|
81
95
|
|
82
96
|
It should output the following:
|
83
97
|
```sh
|
84
|
-
#
|
98
|
+
# D4+D4
|
85
99
|
2 => 1
|
86
100
|
3 => 2
|
87
101
|
4 => 3
|
@@ -96,25 +110,25 @@ First line is a comment telling you that calculation ran for two D4s. Every line
|
|
96
110
|
If probability is preferred, there is an option for that:
|
97
111
|
```sh
|
98
112
|
$ dicey 4 4 --result probabilities # or -r p for short
|
99
|
-
#
|
100
|
-
2 =>
|
101
|
-
3 =>
|
102
|
-
4 =>
|
103
|
-
5 =>
|
104
|
-
6 =>
|
105
|
-
7 =>
|
106
|
-
8 =>
|
113
|
+
# D4+D4
|
114
|
+
2 => 1/16
|
115
|
+
3 => 1/8
|
116
|
+
4 => 3/16
|
117
|
+
5 => 1/4
|
118
|
+
6 => 3/16
|
119
|
+
7 => 1/8
|
120
|
+
8 => 1/16
|
107
121
|
```
|
108
122
|
|
109
123
|
This shows that 5 will probably be rolled a quarter of the time.
|
110
124
|
|
111
|
-
### Example 2
|
125
|
+
### Example 2: Complex distribution with different dice
|
112
126
|
|
113
127
|
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
128
|
```sh
|
115
129
|
# Note the shorthand notation for two dice!
|
116
130
|
$ dicey 8 2d4
|
117
|
-
#
|
131
|
+
# D8+D4+D4
|
118
132
|
3 => 1
|
119
133
|
4 => 3
|
120
134
|
5 => 6
|
@@ -133,30 +147,30 @@ $ dicey 8 2d4
|
|
133
147
|
|
134
148
|
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
149
|
|
136
|
-
#### Example 2.1
|
150
|
+
#### Example 2.1: Graph
|
137
151
|
|
138
152
|
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
153
|
```sh
|
140
|
-
$ dicey 8 2d4
|
141
|
-
# `--format gnuplot`
|
154
|
+
$ dicey 8 2d4 -f g | dicey-to-gnuplot
|
155
|
+
# `--format gnuplot` is shortened to `-f g`
|
142
156
|
```
|
143
157
|
|
144
|
-
This will create a PNG image named
|
145
|
-

|
146
160
|
|
147
|
-
#### Example 2.2
|
161
|
+
#### Example 2.2: JSON and YAML
|
148
162
|
|
149
163
|
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
164
|
|
151
165
|
JSON via `dicey 8 2d4 --format json`:
|
152
166
|
```json
|
153
|
-
{"description":"
|
167
|
+
{"description":"D8+D4+D4","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
168
|
```
|
155
169
|
|
156
170
|
YAML via `dicey 8 2d4 --format yaml`:
|
157
171
|
```yaml
|
158
172
|
---
|
159
|
-
description:
|
173
|
+
description: D8+D4+D4
|
160
174
|
results:
|
161
175
|
3: 1
|
162
176
|
4: 3
|
@@ -174,14 +188,14 @@ results:
|
|
174
188
|
16: 1
|
175
189
|
```
|
176
190
|
|
177
|
-
### Example 3
|
191
|
+
### Example 3: Custom dice
|
178
192
|
|
179
193
|
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
194
|
|
181
195
|
Having ran to a computer as fast as you can, you sic **Dicey** on the problem:
|
182
196
|
```sh
|
183
197
|
$ dicey 1,2,4 4
|
184
|
-
# (1,2,4)
|
198
|
+
# (1,2,4)+D4
|
185
199
|
2 => 1
|
186
200
|
3 => 2
|
187
201
|
4 => 2
|
@@ -193,13 +207,10 @@ $ dicey 1,2,4 4
|
|
193
207
|
|
194
208
|
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
209
|
|
196
|
-
> [!TIP]
|
197
|
-
> 💡 A single positive integer argument N practically is a shorthand for listing every side from 1 to N.
|
198
|
-
|
199
210
|
But what if you had TWO weird D4s?
|
200
211
|
```sh
|
201
212
|
$ dicey 2d1,2,4
|
202
|
-
# (1,2,4)
|
213
|
+
# (1,2,4)+(1,2,4)
|
203
214
|
2 => 1
|
204
215
|
3 => 2
|
205
216
|
4 => 1
|
@@ -210,45 +221,61 @@ $ dicey 2d1,2,4
|
|
210
221
|
|
211
222
|
Hah, now this is a properly cursed distribution!
|
212
223
|
|
213
|
-
|
224
|
+
> [!TIP]
|
225
|
+
> 💡 A single positive integer argument N practically is a shorthand for listing every side from 1 to N.
|
226
|
+
|
227
|
+
### Example 4: Rolling even more custom dice
|
214
228
|
|
215
229
|
You have a sudden urge to roll dice while only having boring integer dice at home. Where to find *the cool* dice though?
|
216
230
|
|
217
231
|
Look no further than **roll** mode introduced in **Dicey** 0.12:
|
218
232
|
```sh
|
219
|
-
$ dicey 0.5,1.5,2.5 4 --mode roll # As always, can be abbreviated to -m r
|
220
|
-
# (
|
221
|
-
roll =>
|
233
|
+
$ dicey 0.5,1.0,1.5,2.0,2.5 4 --mode roll # As always, can be abbreviated to -m r
|
234
|
+
# (1/2,1,3/2,2,5/2)+D4
|
235
|
+
roll => 7/2 # You probably will get a different value here.
|
222
236
|
```
|
223
237
|
|
224
238
|
> [!NOTE]
|
225
|
-
> 💡 Roll mode is compatible with `--format
|
239
|
+
> 💡 Roll mode is compatible with `--format` option.
|
240
|
+
|
241
|
+
### All ways to define dice
|
226
242
|
|
227
|
-
|
243
|
+
There are three *main* ways to define dice:
|
244
|
+
- *"5", "25", or "525"*: a single positive integer makes a regular die (like a D20).
|
245
|
+
- *"3-6", "-5..5", "(0-1)"*: a pair of integers with a separator, possibly in round brackets, makes a numeric die with integers in the range.
|
246
|
+
- Accepted separators: "-", "..", "...", "–" (en dash), "—" (em dash), "…" (ellipsis).
|
247
|
+
- *"1,2,4", "(-1.5,0,1.5)", or "2,"*: a list of any numbers separated by commas, possibly in round brackets, makes an arbitrary numeric die.
|
248
|
+
- Lists can end in a comma, allowing single-number lists.
|
249
|
+
|
250
|
+
*"D6", "d(-1,3)", or "d2..4"*: any definitions can be prefixed with "d" or "D". While this doesn't do anything on its own, it can be useful to not start a definition with "-".
|
251
|
+
|
252
|
+
*"2D6", "5d-1,3", or "277D(2..4)"*: any definitions can be prefixed with "*N*d" or "*N*D", where *N* is a positive integer. This creates *N* copies of the die.
|
253
|
+
|
254
|
+
## Usage: API
|
228
255
|
|
229
256
|
> [!Note]
|
230
|
-
> - Latest documentation from `main` branch is automatically deployed to [GitHub Pages](https://trinistr.github.io/dicey).
|
257
|
+
> - Latest API documentation from `main` branch is automatically deployed to [GitHub Pages](https://trinistr.github.io/dicey).
|
231
258
|
> - Documentation for published versions is available on [RubyDoc](https://rubydoc.info/gems/dicey).
|
232
259
|
|
233
260
|
### Dice
|
234
261
|
|
235
262
|
There are 3 classes of dice currently:
|
236
263
|
- `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
|
238
|
-
- `Dicey::RegularDie` is a subclass of `Dicey::NumericDie`. It is defined by a single integer which is expanded to range (1
|
264
|
+
- `Dicey::NumericDie` behaves much the same as `Dicey::AbstractDie` (being its subclass), except for checking that all values are instances of `Numeric`. It can be initialized with an Array or Range.
|
265
|
+
- `Dicey::RegularDie` is a specialized subclass of `Dicey::NumericDie`. It is defined by a single integer *N* which is expanded to a range (1..*N*).
|
239
266
|
|
240
267
|
All dice classes have constructor methods aside from `.new`:
|
241
268
|
- `.from_list` takes a list of definitions and calls `.new` with each one;
|
242
269
|
- `.from_count` takes a count and a definition and calls `.new` with it specified number of times.
|
243
270
|
|
244
|
-
See [Diving deeper](#diving-deeper) for more information.
|
271
|
+
See [Diving deeper](#diving-deeper) for more theoretical information.
|
245
272
|
|
246
273
|
> [!NOTE]
|
247
274
|
> 💡 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
275
|
|
249
276
|
#### DieFoundry
|
250
277
|
|
251
|
-
`Dicey::DieFoundry#call` provides the
|
278
|
+
`Dicey::DieFoundry#call` provides the interface for creating dice from Strings as available in CLI:
|
252
279
|
```rb
|
253
280
|
Dicey::DieFoundry.new.call("100")
|
254
281
|
# same as Dicey::RegularDie.new(100)
|
@@ -262,7 +289,7 @@ It only takes a single argument and may return both an array of dice and a singl
|
|
262
289
|
```rb
|
263
290
|
foundry = Dicey::DieFoundry.new
|
264
291
|
%w[8 2d4].flat_map { foundry.call(_1) }
|
265
|
-
# same as [Dicey::RegularDie.new(8), Dicey::RegularDie.
|
292
|
+
# same as [Dicey::RegularDie.new(8), *Dicey::RegularDie.from_count(2, 4)]
|
266
293
|
```
|
267
294
|
|
268
295
|
### Rolling
|
@@ -303,28 +330,77 @@ die.roll
|
|
303
330
|
# => 1
|
304
331
|
```
|
305
332
|
|
306
|
-
|
333
|
+
> [!NOTE]
|
334
|
+
> 💡 Randomness source is *global*, shared between all dice and probably not thread-safe.
|
307
335
|
|
308
|
-
###
|
336
|
+
### Distribution calculators
|
309
337
|
|
310
|
-
|
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
|
338
|
+
Distribution calculators live in `Dicey::SumFrequencyCalculators` module. There are four calculators currently:
|
339
|
+
- `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 a laptop.
|
312
340
|
- `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
|
341
|
+
- `Dicey::SumFrequencyCalculators::BruteForce` is the most generic and slowest one, but can in principle work with any dice. Currently, it is also limited to `Dicey::NumericDie`, as it's unclear how to handle other values.
|
342
|
+
- `Dicey::SumFrequencyCalculators::Empirical`. This is more of a tool than a calculator. It can be interesting to play around with and see how practical results compare to theoretical ones.
|
314
343
|
|
315
344
|
Calculators inherit from `Dicey::SumFrequencyCalculators::BaseCalculator` and provide the following public interface:
|
316
|
-
- `#call(dice, result_type: {:frequencies | :probabilities}) : Hash`
|
345
|
+
- `#call(dice, result_type: {:frequencies | :probabilities}, **options) : Hash`
|
317
346
|
- `#valid_for?(dice) : Boolean`
|
318
347
|
|
319
|
-
See [
|
348
|
+
See [Diving deeper](#diving-deeper) for more details on limitations and complexity considerations.
|
349
|
+
|
350
|
+
### Distribution properties
|
351
|
+
|
352
|
+
While distribution itself is already enough in most cases (we are talking just dice here, after all). it may be of interest to calculate properties of it: mode, mean, expected value, standard deviation, etc. `Dicey::DistributionPropertiesCalculator` already provides this functionality:
|
353
|
+
```rb
|
354
|
+
Dicey::DistributionPropertiesCalculator.new.call(
|
355
|
+
Dicey::SumFrequencyCalculators::KroneckerSubstitution.new.call(
|
356
|
+
Dicey::RegularDie.from_count(2, 3)
|
357
|
+
)
|
358
|
+
)
|
359
|
+
# =>
|
360
|
+
# {:mode=>[4],
|
361
|
+
# :min=>2,
|
362
|
+
# :max=>6,
|
363
|
+
# :total_range=>4,
|
364
|
+
# :mid_range=>4,
|
365
|
+
# :median=>4,
|
366
|
+
# :arithmetic_mean=>4,
|
367
|
+
# :expected_value=>4,
|
368
|
+
# :variance=>(4/3),
|
369
|
+
# :standard_deviation=>1.1547005383792515,
|
370
|
+
# :skewness=>0.0,
|
371
|
+
# :kurtosis=>(9/4),
|
372
|
+
# :excess_kurtosis=>(-3/4)}
|
373
|
+
```
|
374
|
+
|
375
|
+
Of course, for regular dice most properties are quite simple and predicatable due to symmetricity of distribution. It becomes more interesting with unfair, lopsided dice. Remember [Example 3](#example-3-custom-dice)?
|
376
|
+
```rb
|
377
|
+
Dicey::DistributionPropertiesCalculator.new.call(
|
378
|
+
Dicey::SumFrequencyCalculators::KroneckerSubstitution.new.call(
|
379
|
+
[Dicey::RegularDie.new(4), Dicey::NumericDie.new([1,3,4])]
|
380
|
+
)
|
381
|
+
)
|
382
|
+
# =>
|
383
|
+
# {:mode=>[5],
|
384
|
+
# :min=>2,
|
385
|
+
# :max=>8,
|
386
|
+
# :total_range=>6,
|
387
|
+
# :mid_range=>5,
|
388
|
+
# :median=>5,
|
389
|
+
# :arithmetic_mean=>5,
|
390
|
+
# :expected_value=>(31/6),
|
391
|
+
# :variance=>(101/36),
|
392
|
+
# :standard_deviation=>1.674979270186815,
|
393
|
+
# :skewness=>-0.15762965389465178,
|
394
|
+
# :kurtosis=>(23145/10201),
|
395
|
+
# :excess_kurtosis=>(-7458/10201)}
|
396
|
+
```
|
397
|
+
|
398
|
+
This disitrubution is obviosuly skewed (as can be immediately seen from non-zero skewness), with expected value no longer equal to mean. This is a mild example. It is easily possible to create a distribution with multiple local maxima and high skewness.
|
320
399
|
|
321
400
|
## Diving deeper
|
322
401
|
|
323
402
|
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.
|
403
|
+
- **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.
|
328
404
|
|
329
405
|
> [!TIP]
|
330
406
|
> 💡 If you only need to roll **regular** dice, this section will not contain anything important.
|
@@ -335,11 +411,11 @@ For a further discussion of calculations, it is important to understand which cl
|
|
335
411
|
- **Abstract** die is not limited by anything other than not having partial sides (and how would that work anyway?).
|
336
412
|
|
337
413
|
> [!NOTE]
|
338
|
-
> 💡 If your die starts with a negative number
|
414
|
+
> 💡 If your die definition starts with a negative number, it can be bracketed, prefixed with "d", or put after "--" pseudo-argument to avoid processing as an option.
|
339
415
|
|
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.
|
416
|
+
Dicey is in principle able to handle any real 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
417
|
|
342
|
-
Currently, three algorithms are implemented, with different possibilities and trade-offs.
|
418
|
+
Currently, three algorithms for calculating frequencies are implemented, with different possibilities and trade-offs.
|
343
419
|
|
344
420
|
> [!NOTE]
|
345
421
|
> 💡 Complexity is listed for `n` dice with at most `m` sides and has not been rigorously proven.
|
@@ -349,7 +425,7 @@ Currently, three algorithms are implemented, with different possibilities and tr
|
|
349
425
|
An algorithm based on fast polynomial multiplication. This is the default algorithm, used for most reasonable dice.
|
350
426
|
|
351
427
|
- Limitations: only **natural** dice are allowed, including **regular** dice.
|
352
|
-
- Example: `dicey 5 3,4,1
|
428
|
+
- Example: `dicey 5 3,4,1 0,`
|
353
429
|
- Complexity: `O(m⋅n)` where `m` is the highest value
|
354
430
|
|
355
431
|
### Multinomial coefficients
|
@@ -390,7 +466,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/trinis
|
|
390
466
|
- Tests cover the behavior and its interactions. 100% coverage *is not enough*, as it does not guarantee that all code paths are tested.
|
391
467
|
- Documentation is up-to-date: generate it with `rake docs` and read it.
|
392
468
|
- "*CHANGELOG.md*" lists the change if it has impact on users.
|
393
|
-
- "*README.md*" is updated if the feature should be visible there
|
469
|
+
- "*README.md*" is updated if the feature should be visible there.
|
394
470
|
|
395
471
|
## License
|
396
472
|
|
data/lib/dicey/abstract_die.rb
CHANGED
@@ -21,6 +21,8 @@ module Dicey
|
|
21
21
|
|
22
22
|
# Yes, class variable is actually useful here.
|
23
23
|
# TODO: Allow supplying a custom Random.
|
24
|
+
|
25
|
+
# Shared randomness source, accessed through {.rand} and {.srand}.
|
24
26
|
@@random = Random.new
|
25
27
|
|
26
28
|
# rubocop:enable Style/ClassVars
|
@@ -31,9 +33,8 @@ module Dicey
|
|
31
33
|
# @return [String]
|
32
34
|
def self.describe(dice)
|
33
35
|
return dice.to_s if AbstractDie === dice
|
34
|
-
return dice.join(";") if Array === dice
|
35
36
|
|
36
|
-
dice.to_a.join("
|
37
|
+
dice.to_a.join("+")
|
37
38
|
end
|
38
39
|
|
39
40
|
# Create a bunch of different dice at once.
|
@@ -55,7 +56,15 @@ module Dicey
|
|
55
56
|
Array.new(count) { new(definition) }
|
56
57
|
end
|
57
58
|
|
58
|
-
|
59
|
+
# Die's list of sides.
|
60
|
+
#
|
61
|
+
# @return [Array<Any>]
|
62
|
+
attr_reader :sides_list
|
63
|
+
|
64
|
+
# Number of sides of the die.
|
65
|
+
#
|
66
|
+
# @return [Integer]
|
67
|
+
attr_reader :sides_num
|
59
68
|
|
60
69
|
# @param sides_list [Enumerable<Any>]
|
61
70
|
# @raise [DiceyError] if +sides_list+ is empty
|
@@ -92,9 +101,13 @@ module Dicey
|
|
92
101
|
current
|
93
102
|
end
|
94
103
|
|
104
|
+
# Return a string representing the die.
|
105
|
+
#
|
106
|
+
# Default representation is a list of sides in round brackets.
|
107
|
+
#
|
95
108
|
# @return [String]
|
96
109
|
def to_s
|
97
|
-
"(#{@sides_list.join(",")})"
|
110
|
+
(@sides_list.size > 1) ? "(#{@sides_list.join(",")})" : "(#{@sides_list.first},)"
|
98
111
|
end
|
99
112
|
|
100
113
|
# Determine if this die and the other one have the same list of sides.
|
data/lib/dicey/cli/options.rb
CHANGED
@@ -54,8 +54,10 @@ module Dicey
|
|
54
54
|
def add_banner_and_version
|
55
55
|
@parser.banner = <<~TEXT
|
56
56
|
Usage: #{@parser.program_name} [options] <die> [<die> ...]
|
57
|
+
#{@parser.program_name} [options] -- <die> [<die> ...]
|
57
58
|
#{@parser.program_name} --test [full|quiet]
|
58
59
|
All option names and arguments can be abbreviated if abbreviation is unambiguous.
|
60
|
+
A lone "--" separates options and die definitions, allowing definitions to start with "-".
|
59
61
|
TEXT
|
60
62
|
end
|
61
63
|
|
data/lib/dicey/die_foundry.rb
CHANGED
@@ -3,21 +3,27 @@
|
|
3
3
|
require_relative "numeric_die"
|
4
4
|
require_relative "regular_die"
|
5
5
|
|
6
|
+
require_relative "rational_to_integer"
|
7
|
+
|
6
8
|
module Dicey
|
7
9
|
# Helper class to define die definitions and automatically select the best one.
|
8
10
|
class DieFoundry
|
9
|
-
|
10
|
-
|
11
|
+
include RationalToInteger
|
12
|
+
|
13
|
+
# Regexp for matching a possible count.
|
14
|
+
PREFIX = /(?>(?<count>[1-9]\d*+)?d)?+/i
|
11
15
|
|
12
16
|
# Possible molds for the dice. They are matched in the order as written.
|
13
17
|
MOLDS = [
|
14
18
|
# Positive integer goes into the RegularDie mold.
|
15
|
-
[/\A#{
|
19
|
+
[/\A#{PREFIX}(?<sides>[1-9]\d*+)\z/, :regular_mold].freeze,
|
20
|
+
# Integer range goes into the NumericDie mold.
|
21
|
+
[/\A#{PREFIX}\(?(?<begin>-?\d++)(?>[-–—…]|\.{2,3})(?<end>-?\d++)\)?\z/, :range_mold].freeze,
|
16
22
|
# List of numbers goes into the NumericDie mold.
|
17
|
-
[/\A#{
|
18
|
-
# Non-integers require
|
19
|
-
[/\A#{
|
20
|
-
:weirdly_precise_mold],
|
23
|
+
[/\A#{PREFIX}\(?(?<sides>-?\d++(?>,(?>-?\d++)?)+|,)\)?\z/, :weirdly_shaped_mold].freeze,
|
24
|
+
# Non-integers require special handling for precision.
|
25
|
+
[/\A#{PREFIX}\(?(?<sides>-?\d++(?>\.\d++)?(?>,(?>-?\d++(?>\.\d++)?)?)+|,)\)?\z/,
|
26
|
+
:weirdly_precise_mold].freeze,
|
21
27
|
# Anything else is spilled on the floor.
|
22
28
|
].freeze
|
23
29
|
|
@@ -25,15 +31,16 @@ module Dicey
|
|
25
31
|
#
|
26
32
|
# Following definitions are recognized:
|
27
33
|
# - positive integer (like "6" or "20"), which produces a {RegularDie};
|
34
|
+
# - integer range (like "3-6" or "(-5..5)"), which produces a {NumericDie};
|
28
35
|
# - list of integers (like "3,4,5", "(-1,0,1)", or "2,"), which produces a {NumericDie};
|
29
|
-
# - list of decimal numbers (like "0.5,0.2,0.8" or "2.0"), which produces a {NumericDie},
|
30
|
-
# but uses +
|
36
|
+
# - list of decimal numbers (like "0.5,0.2,0.8" or "(2.0,)"), which produces a {NumericDie},
|
37
|
+
# but uses +Rational+ for values to maintain precise results.
|
31
38
|
#
|
32
39
|
# Any die definition can be prefixed with a count, like "2D6" or "1d1,3,5" to create an array.
|
33
40
|
# A plain "d" without an explicit count is ignored instead, creating a single die.
|
34
41
|
#
|
35
42
|
# @param definition [String] die shape
|
36
|
-
# @return [NumericDie, RegularDie, Array<NumericDie
|
43
|
+
# @return [NumericDie, RegularDie, Array<NumericDie>, Array<RegularDie>]
|
37
44
|
# @raise [DiceyError] if no mold fits the definition
|
38
45
|
def call(definition)
|
39
46
|
matched, name =
|
@@ -54,14 +61,19 @@ module Dicey
|
|
54
61
|
build_dice(RegularDie, definition[:count], definition[:sides].to_i)
|
55
62
|
end
|
56
63
|
|
64
|
+
def range_mold(definition)
|
65
|
+
first = definition[:begin].to_i
|
66
|
+
last = definition[:end].to_i
|
67
|
+
first, last = last, first if first > last
|
68
|
+
build_dice(NumericDie, definition[:count], first..last)
|
69
|
+
end
|
70
|
+
|
57
71
|
def weirdly_shaped_mold(definition)
|
58
72
|
build_dice(NumericDie, definition[:count], definition[:sides].split(",").map(&:to_i))
|
59
73
|
end
|
60
74
|
|
61
75
|
def weirdly_precise_mold(definition)
|
62
|
-
|
63
|
-
|
64
|
-
sides = definition[:sides].split(",").map { BigDecimal(_1) }
|
76
|
+
sides = definition[:sides].split(",").map { rational_to_integer(Rational(_1)) }
|
65
77
|
build_dice(NumericDie, definition[:count], sides)
|
66
78
|
end
|
67
79
|
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rational_to_integer"
|
4
|
+
|
5
|
+
module Dicey
|
6
|
+
# Calculates distribution properties,
|
7
|
+
# also known as descriptive statistics when applied to a population sample.
|
8
|
+
#
|
9
|
+
# These are well-known properties such as:
|
10
|
+
# - min, max, mid-range;
|
11
|
+
# - mode, median, arithmetic mean;
|
12
|
+
# - important moments (expected value, variance, skewness, kurtosis).
|
13
|
+
#
|
14
|
+
# It is notable that most dice create symmetric distributions,
|
15
|
+
# which means that skewness is 0, while properties denoting center in some way
|
16
|
+
# (median, mean, ...) are all equal.
|
17
|
+
# Mode is often not unique, but includes this center.
|
18
|
+
class DistributionPropertiesCalculator
|
19
|
+
include RationalToInteger
|
20
|
+
|
21
|
+
# Calculate properties for a given distribution.
|
22
|
+
#
|
23
|
+
# Depending on values in the distribution, some properties may be undefined.
|
24
|
+
# In such cases, only mode is guaranteed to be present.
|
25
|
+
#
|
26
|
+
# @param distribution [Hash{Numeric => Numeric}
|
27
|
+
# numeric distribution with pre-sorted keys
|
28
|
+
# @return [Hash{Symbol => Numeric, Array<Numeric>}]
|
29
|
+
def call(distribution)
|
30
|
+
return {} if distribution.empty?
|
31
|
+
|
32
|
+
calculate_properties(distribution)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def calculate_properties(distribution)
|
38
|
+
outcomes = distribution.keys
|
39
|
+
weights = distribution.values
|
40
|
+
|
41
|
+
{
|
42
|
+
mode: mode(outcomes, weights),
|
43
|
+
**range_characteristics(outcomes),
|
44
|
+
**median(outcomes),
|
45
|
+
**means(outcomes, weights),
|
46
|
+
**moments(distribution),
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def mode(outcomes, weights)
|
51
|
+
max_weight = weights.max
|
52
|
+
outcomes.select.with_index { |_, index| weights[index] == max_weight }
|
53
|
+
end
|
54
|
+
|
55
|
+
def range_characteristics(outcomes)
|
56
|
+
min = outcomes.min
|
57
|
+
max = outcomes.max
|
58
|
+
{
|
59
|
+
min: min,
|
60
|
+
max: max,
|
61
|
+
total_range: max - min,
|
62
|
+
mid_range: rational_to_integer(Rational(min + max, 2)),
|
63
|
+
}
|
64
|
+
rescue ArgumentError, TypeError, NoMethodError
|
65
|
+
# Outcomes are not comparable with each other, so a range can not be determined.
|
66
|
+
{}
|
67
|
+
end
|
68
|
+
|
69
|
+
def means(outcomes, _weights)
|
70
|
+
{
|
71
|
+
arithmetic_mean: rational_to_integer(Rational(outcomes.sum, outcomes.size)),
|
72
|
+
}
|
73
|
+
rescue ArgumentError, TypeError
|
74
|
+
# Outcomes are not summable with each other, means are meaningless.
|
75
|
+
{}
|
76
|
+
end
|
77
|
+
|
78
|
+
def median(outcomes)
|
79
|
+
outcomes = outcomes.sort
|
80
|
+
value =
|
81
|
+
if outcomes.size.odd?
|
82
|
+
outcomes[outcomes.size / 2]
|
83
|
+
else
|
84
|
+
Rational(outcomes[(outcomes.size / 2) - 1] + outcomes[outcomes.size / 2], 2)
|
85
|
+
end
|
86
|
+
{ median: value }
|
87
|
+
rescue ArgumentError, TypeError, NoMethodError
|
88
|
+
# Outcomes are not compatible with each other, so a median can not be determined.
|
89
|
+
{}
|
90
|
+
end
|
91
|
+
|
92
|
+
def moments(distribution)
|
93
|
+
total_weight = distribution.values.sum
|
94
|
+
expected_value = rational_to_integer(moment(distribution, total_weight, 1))
|
95
|
+
variance = rational_to_integer(moment(distribution, total_weight, 2) - (expected_value**2))
|
96
|
+
skewness =
|
97
|
+
rational_to_integer(moment(distribution, total_weight, 3, expected_value, variance))
|
98
|
+
kurtosis =
|
99
|
+
rational_to_integer(moment(distribution, total_weight, 4, expected_value, variance))
|
100
|
+
|
101
|
+
{
|
102
|
+
expected_value: expected_value,
|
103
|
+
variance: variance,
|
104
|
+
standard_deviation: Math.sqrt(variance),
|
105
|
+
skewness: skewness,
|
106
|
+
kurtosis: kurtosis,
|
107
|
+
excess_kurtosis: kurtosis ? kurtosis - 3 : nil,
|
108
|
+
}
|
109
|
+
rescue ArgumentError, TypeError, NoMethodError
|
110
|
+
# Outcomes are not compatible with each other, moments are fleeing.
|
111
|
+
{}
|
112
|
+
end
|
113
|
+
|
114
|
+
def moment(distribution, total_weight, degree, center = 0, variance = nil)
|
115
|
+
# With 0 variance, normalized moments are undefined.
|
116
|
+
return nil if variance == 0 # rubocop:disable Style/NumericPredicate
|
117
|
+
|
118
|
+
unnormalized = distribution.sum { |r, w| ((r - center)**degree) * Rational(w, total_weight) }
|
119
|
+
variance ? (unnormalized / (variance**(degree/2r))) : unnormalized
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
data/lib/dicey/numeric_die.rb
CHANGED
@@ -9,8 +9,8 @@ module Dicey
|
|
9
9
|
# @raise [DiceyError] if +sides_list+ contains non-numerical values or is empty
|
10
10
|
def initialize(sides_list)
|
11
11
|
if Range === sides_list
|
12
|
-
unless
|
13
|
-
raise DiceyError, "`#{sides_list.inspect}` is not a
|
12
|
+
unless Integer === sides_list.begin && Integer === sides_list.end
|
13
|
+
raise DiceyError, "`#{sides_list.inspect}` is not a valid range!"
|
14
14
|
end
|
15
15
|
else
|
16
16
|
sides_list.each do |value|
|
@@ -5,8 +5,20 @@ require_relative "key_value_formatter"
|
|
5
5
|
module Dicey
|
6
6
|
module OutputFormatters
|
7
7
|
# Formats a hash as a text file suitable for consumption by Gnuplot.
|
8
|
+
#
|
9
|
+
# Will transform Rational probabilities to Floats.
|
8
10
|
class GnuplotFormatter < KeyValueFormatter
|
9
11
|
SEPARATOR = " "
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def transform(key, value)
|
16
|
+
[derationalize(key), derationalize(value)]
|
17
|
+
end
|
18
|
+
|
19
|
+
def derationalize(value)
|
20
|
+
value.is_a?(Rational) ? value.to_f : value
|
21
|
+
end
|
10
22
|
end
|
11
23
|
end
|
12
24
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Dicey
|
4
|
+
# Processors which turn data to text.
|
4
5
|
module OutputFormatters
|
5
6
|
# Base formatter for outputting in formats which can be converted from a Hash directly.
|
6
7
|
# Can add an optional description into the result.
|
@@ -21,7 +22,10 @@ module Dicey
|
|
21
22
|
private
|
22
23
|
|
23
24
|
def to_primitive(value)
|
24
|
-
primitive?(value)
|
25
|
+
return value if primitive?(value)
|
26
|
+
return value.to_f if Numeric === value
|
27
|
+
|
28
|
+
value.to_s
|
25
29
|
end
|
26
30
|
|
27
31
|
def primitive?(value)
|
@@ -1,20 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "../rational_to_integer"
|
4
|
+
|
3
5
|
module Dicey
|
4
6
|
module OutputFormatters
|
5
7
|
# Base formatter for outputting lists of key-value pairs separated by newlines.
|
6
8
|
# Can add an optional description into the result.
|
7
9
|
# @abstract
|
8
10
|
class KeyValueFormatter
|
11
|
+
include RationalToInteger
|
12
|
+
|
9
13
|
# @param hash [Hash{Object => Object}]
|
10
14
|
# @param description [String] text to add as a comment.
|
11
15
|
# @return [String]
|
12
16
|
def call(hash, description = nil)
|
13
17
|
initial_string = description ? "# #{description}\n" : +""
|
14
18
|
hash.each_with_object(initial_string) do |(key, value), output|
|
15
|
-
output <<
|
19
|
+
output << line(transform(key, value)) << "\n"
|
16
20
|
end
|
17
21
|
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def transform(key, value)
|
26
|
+
[rational_to_integer(key), rational_to_integer(value)]
|
27
|
+
end
|
28
|
+
|
29
|
+
def line((key, value))
|
30
|
+
"#{key}#{self.class::SEPARATOR}#{value}"
|
31
|
+
end
|
18
32
|
end
|
19
33
|
end
|
20
34
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dicey
|
4
|
+
# Mix-in for converting rationals with denominator of 1 to integers.
|
5
|
+
module RationalToInteger
|
6
|
+
# Convert +value+ to +Integer+ if it's a +Rational+ with denominator of 1.
|
7
|
+
# Otherwise, return +value+ as-is.
|
8
|
+
#
|
9
|
+
# @value [Numeric, Any]
|
10
|
+
# @return [Numeric, Integer, Any]
|
11
|
+
def rational_to_integer(value)
|
12
|
+
(Rational === value && value.denominator == 1) ? value.numerator : value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/dicey/regular_die.rb
CHANGED
@@ -5,9 +5,6 @@ require_relative "numeric_die"
|
|
5
5
|
module Dicey
|
6
6
|
# Regular die, which has N sides with numbers from 1 to N.
|
7
7
|
class RegularDie < NumericDie
|
8
|
-
# Characters to use for small dice.
|
9
|
-
D6 = "⚀⚁⚂⚃⚄⚅"
|
10
|
-
|
11
8
|
# @param max [Integer] maximum side / number of sides
|
12
9
|
def initialize(max)
|
13
10
|
unless Integer === max && max.positive?
|
@@ -17,12 +14,13 @@ module Dicey
|
|
17
14
|
super((1..max))
|
18
15
|
end
|
19
16
|
|
20
|
-
#
|
21
|
-
#
|
17
|
+
# Return a string representing the die.
|
18
|
+
#
|
19
|
+
# Regular dice are represented with a "D" followed by the number of sides.
|
22
20
|
#
|
23
21
|
# @return [String]
|
24
22
|
def to_s
|
25
|
-
|
23
|
+
"D#{sides_num}"
|
26
24
|
end
|
27
25
|
end
|
28
26
|
end
|
data/lib/dicey/roller.rb
CHANGED
@@ -2,9 +2,13 @@
|
|
2
2
|
|
3
3
|
require_relative "die_foundry"
|
4
4
|
|
5
|
+
require_relative "rational_to_integer"
|
6
|
+
|
5
7
|
module Dicey
|
6
8
|
# Let the dice roll!
|
7
9
|
class Roller
|
10
|
+
include RationalToInteger
|
11
|
+
|
8
12
|
# @param arguments [Array<String>] die definitions
|
9
13
|
# @param format [#call] formatter for output
|
10
14
|
# @return [nil]
|
@@ -15,7 +19,7 @@ module Dicey
|
|
15
19
|
dice = arguments.flat_map { |definition| die_foundry.cast(definition) }
|
16
20
|
result = dice.sum(&:roll)
|
17
21
|
|
18
|
-
format.call({ "roll" => result }, AbstractDie.describe(dice))
|
22
|
+
format.call({ "roll" => rational_to_integer(result) }, AbstractDie.describe(dice))
|
19
23
|
end
|
20
24
|
|
21
25
|
private
|
@@ -1,8 +1,25 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Dicey
|
4
|
+
# Calculators for probability distributions of dice.
|
4
5
|
module SumFrequencyCalculators
|
5
6
|
# Base frequencies calculator.
|
7
|
+
#
|
8
|
+
# *Result types:*
|
9
|
+
# - +:frequencies+ (default)
|
10
|
+
# - +:probabilities+
|
11
|
+
#
|
12
|
+
# By default, returns frequencies as they are easier to calculate and
|
13
|
+
# can be represented with integers.
|
14
|
+
# Probabilities are calculated using +Rational+ numbers to return exact results.
|
15
|
+
#
|
16
|
+
# *Options:*
|
17
|
+
#
|
18
|
+
# Calculators may have calculator-specific options,
|
19
|
+
# passed as extra keyword arguments to {#call}.
|
20
|
+
# If present, they will be documented under *Options* heading
|
21
|
+
# on the class itself.
|
22
|
+
#
|
6
23
|
# @abstract
|
7
24
|
class BaseCalculator
|
8
25
|
# Possible values for +result_type+ argument in {#call}.
|
@@ -10,11 +27,12 @@ module Dicey
|
|
10
27
|
|
11
28
|
# @param dice [Enumerable<AbstractDie>]
|
12
29
|
# @param result_type [Symbol] one of {RESULT_TYPES}
|
13
|
-
# @
|
30
|
+
# @param options [Hash{Symbol => Any}] calculator-specific options
|
31
|
+
# @return [Hash{Numeric => Numeric}] frequencies or probabilities for each outcome
|
14
32
|
# @raise [DiceyError] if +result_type+ is invalid
|
15
33
|
# @raise [DiceyError] if dice list is invalid for the calculator
|
16
34
|
# @raise [DiceyError] if calculator returned obviously wrong results
|
17
|
-
def call(dice, result_type: :frequencies)
|
35
|
+
def call(dice, result_type: :frequencies, **options)
|
18
36
|
unless RESULT_TYPES.include?(result_type)
|
19
37
|
raise DiceyError, "#{result_type} is not a valid result type!"
|
20
38
|
end
|
@@ -22,7 +40,7 @@ module Dicey
|
|
22
40
|
return {} if dice.empty?
|
23
41
|
raise DiceyError, "#{self.class} can not handle these dice!" unless valid_for?(dice)
|
24
42
|
|
25
|
-
frequencies = calculate(dice)
|
43
|
+
frequencies = calculate(dice, **options)
|
26
44
|
verify_result(frequencies, dice)
|
27
45
|
frequencies = sort_result(frequencies)
|
28
46
|
transform_result(frequencies, result_type)
|
@@ -46,7 +64,7 @@ module Dicey
|
|
46
64
|
|
47
65
|
# Peform frequencies calculation.
|
48
66
|
# (see #call)
|
49
|
-
def calculate(dice)
|
67
|
+
def calculate(dice, **nil)
|
50
68
|
# :nocov:
|
51
69
|
raise NotImplementedError
|
52
70
|
# :nocov:
|
@@ -82,7 +100,7 @@ module Dicey
|
|
82
100
|
frequencies
|
83
101
|
else
|
84
102
|
total = frequencies.values.sum
|
85
|
-
frequencies.transform_values { _1
|
103
|
+
frequencies.transform_values { Rational(_1, total) }
|
86
104
|
end
|
87
105
|
end
|
88
106
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base_calculator"
|
4
|
+
|
5
|
+
module Dicey
|
6
|
+
module SumFrequencyCalculators
|
7
|
+
# "Calculator" for a collection of {NumericDie} using empirically-obtained statistics.
|
8
|
+
#
|
9
|
+
# @note This calculator is mostly a joke. It can be useful for educational purposes,
|
10
|
+
# or to verify results of {BruteForce} when in doubt. It is not used by default.
|
11
|
+
#
|
12
|
+
# Does a number of rolls and calculates approximate probabilities from that.
|
13
|
+
# Even if frequencies are requested, results are non-integer.
|
14
|
+
#
|
15
|
+
# *Options:*
|
16
|
+
# - *rolls* (Integer) (_defaults_ _to:_ _N_) — number of rolls to perform
|
17
|
+
class Empirical < BaseCalculator
|
18
|
+
# Default number of rolls to perform.
|
19
|
+
N = 10_000
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def validate(dice)
|
24
|
+
dice.all?(NumericDie)
|
25
|
+
end
|
26
|
+
|
27
|
+
def calculate(dice, rolls: N)
|
28
|
+
statistics = rolls.times.with_object(Hash.new(0)) { |_, hash| hash[dice.sum(&:roll)] += 1 }
|
29
|
+
total_results = dice.map(&:sides_num).reduce(:*)
|
30
|
+
statistics.transform_values { Rational(_1 * total_results, rolls) }
|
31
|
+
end
|
32
|
+
|
33
|
+
def verify_result(*)
|
34
|
+
# Ignore verification, as this is inherently imprecise.
|
35
|
+
true
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -10,7 +10,9 @@ module Dicey
|
|
10
10
|
#
|
11
11
|
# Based on Kronecker substitution method for polynomial multiplication.
|
12
12
|
# @see https://en.wikipedia.org/wiki/Kronecker_substitution
|
13
|
-
# @see https://arxiv.org/pdf/0712.4046v1.pdf
|
13
|
+
# @see https://arxiv.org/pdf/0712.4046v1.pdf
|
14
|
+
# David Harvey, Faster polynomial multiplication via multi-point Kronecker substitution
|
15
|
+
# (in particular section 3)
|
14
16
|
class KroneckerSubstitution < BaseCalculator
|
15
17
|
private
|
16
18
|
|
@@ -18,7 +20,7 @@ module Dicey
|
|
18
20
|
dice.all? { |die| die.sides_list.all? { _1.is_a?(Integer) && _1 >= 0 } }
|
19
21
|
end
|
20
22
|
|
21
|
-
def calculate(dice)
|
23
|
+
def calculate(dice, **nil)
|
22
24
|
polynomials = build_polynomials(dice)
|
23
25
|
evaluation_point = find_evaluation_point(polynomials)
|
24
26
|
values = evaluate_polynomials(polynomials, evaluation_point)
|
@@ -9,7 +9,7 @@ module Dicey
|
|
9
9
|
# Example dice: (1,2,3,4), (-2,-1,0,1,2), (0,0.2,0.4,0.6), (-1,-2,-3).
|
10
10
|
#
|
11
11
|
# Based on extension of Pascal's triangle for a higher number of coefficients.
|
12
|
-
# @see https://en.wikipedia.org/wiki/Pascal
|
12
|
+
# @see https://en.wikipedia.org/wiki/Pascal's_triangle
|
13
13
|
# @see https://en.wikipedia.org/wiki/Trinomial_triangle
|
14
14
|
class MultinomialCoefficients < BaseCalculator
|
15
15
|
private
|
@@ -33,9 +33,7 @@ module Dicey
|
|
33
33
|
true
|
34
34
|
end
|
35
35
|
|
36
|
-
|
37
|
-
# @return [Hash{Numeric => Integer}]
|
38
|
-
def calculate(dice)
|
36
|
+
def calculate(dice, **nil)
|
39
37
|
first_die = dice.first
|
40
38
|
number_of_sides = first_die.sides_num
|
41
39
|
number_of_dice = dice.size
|
data/lib/dicey/version.rb
CHANGED
data/lib/dicey.rb
CHANGED
@@ -5,7 +5,7 @@ module Dicey
|
|
5
5
|
# General error for Dicey.
|
6
6
|
class DiceyError < StandardError; end
|
7
7
|
|
8
|
-
Dir["
|
9
|
-
Dir["
|
10
|
-
Dir["
|
8
|
+
Dir["dicey/*.rb", base: __dir__].each { require_relative _1 }
|
9
|
+
Dir["dicey/output_formatters/*.rb", base: __dir__].each { require_relative _1 }
|
10
|
+
Dir["dicey/sum_frequency_calculators/*.rb", base: __dir__].each { require_relative _1 }
|
11
11
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dicey
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.15.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexandr Bulancov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-10-07 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |
|
14
14
|
Dicey provides a CLI executable and a Ruby API for fast calculation of
|
@@ -35,6 +35,7 @@ files:
|
|
35
35
|
- lib/dicey/cli/blender.rb
|
36
36
|
- lib/dicey/cli/options.rb
|
37
37
|
- lib/dicey/die_foundry.rb
|
38
|
+
- lib/dicey/distribution_properties_calculator.rb
|
38
39
|
- lib/dicey/numeric_die.rb
|
39
40
|
- lib/dicey/output_formatters/gnuplot_formatter.rb
|
40
41
|
- lib/dicey/output_formatters/hash_formatter.rb
|
@@ -42,10 +43,12 @@ files:
|
|
42
43
|
- lib/dicey/output_formatters/key_value_formatter.rb
|
43
44
|
- lib/dicey/output_formatters/list_formatter.rb
|
44
45
|
- lib/dicey/output_formatters/yaml_formatter.rb
|
46
|
+
- lib/dicey/rational_to_integer.rb
|
45
47
|
- lib/dicey/regular_die.rb
|
46
48
|
- lib/dicey/roller.rb
|
47
49
|
- lib/dicey/sum_frequency_calculators/base_calculator.rb
|
48
50
|
- lib/dicey/sum_frequency_calculators/brute_force.rb
|
51
|
+
- lib/dicey/sum_frequency_calculators/empirical.rb
|
49
52
|
- lib/dicey/sum_frequency_calculators/kronecker_substitution.rb
|
50
53
|
- lib/dicey/sum_frequency_calculators/multinomial_coefficients.rb
|
51
54
|
- lib/dicey/sum_frequency_calculators/runner.rb
|
@@ -57,9 +60,9 @@ licenses:
|
|
57
60
|
metadata:
|
58
61
|
homepage_uri: https://github.com/trinistr/dicey
|
59
62
|
bug_tracker_uri: https://github.com/trinistr/dicey/issues
|
60
|
-
documentation_uri: https://rubydoc.info/gems/dicey/0.
|
61
|
-
source_code_uri: https://github.com/trinistr/dicey/tree/v0.
|
62
|
-
changelog_uri: https://github.com/trinistr/dicey/blob/v0.
|
63
|
+
documentation_uri: https://rubydoc.info/gems/dicey/0.15.1
|
64
|
+
source_code_uri: https://github.com/trinistr/dicey/tree/v0.15.1
|
65
|
+
changelog_uri: https://github.com/trinistr/dicey/blob/v0.15.1/CHANGELOG.md
|
63
66
|
rubygems_mfa_required: 'true'
|
64
67
|
post_install_message:
|
65
68
|
rdoc_options:
|