dicey 0.16.2 → 0.17.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 +60 -37
- data/exe/dicey +2 -2
- data/lib/dicey/abstract_die.rb +24 -5
- data/lib/dicey/cli/blender.rb +19 -14
- data/lib/dicey/{sum_frequency_calculators/runner.rb → cli/calculator_runner.rb} +9 -15
- data/lib/dicey/{sum_frequency_calculators/test_runner.rb → cli/calculator_test_runner.rb} +37 -13
- data/lib/dicey/cli/formatters/base_list_formatter.rb +36 -0
- data/lib/dicey/cli/formatters/base_map_formatter.rb +38 -0
- data/lib/dicey/cli/formatters/gnuplot_formatter.rb +28 -0
- data/lib/dicey/cli/formatters/json_formatter.rb +14 -0
- data/lib/dicey/cli/formatters/list_formatter.rb +14 -0
- data/lib/dicey/cli/formatters/null_formatter.rb +17 -0
- data/lib/dicey/cli/formatters/yaml_formatter.rb +14 -0
- data/lib/dicey/cli/options.rb +15 -11
- data/lib/dicey/cli/roller.rb +47 -0
- data/lib/dicey/cli.rb +23 -0
- data/lib/dicey/die_foundry.rb +6 -5
- data/lib/dicey/distribution_calculators/auto_selector.rb +73 -0
- data/lib/dicey/{sum_frequency_calculators → distribution_calculators}/base_calculator.rb +44 -37
- data/lib/dicey/distribution_calculators/binomial.rb +62 -0
- data/lib/dicey/{sum_frequency_calculators → distribution_calculators}/empirical.rb +9 -10
- data/lib/dicey/distribution_calculators/iterative.rb +51 -0
- data/lib/dicey/{sum_frequency_calculators → distribution_calculators}/multinomial_coefficients.rb +21 -12
- data/lib/dicey/{sum_frequency_calculators/kronecker_substitution.rb → distribution_calculators/polynomial_convolution.rb} +15 -8
- data/lib/dicey/distribution_calculators/trivial.rb +56 -0
- data/lib/dicey/distribution_properties_calculator.rb +5 -2
- data/lib/dicey/mixins/missing_math.rb +44 -0
- data/lib/dicey/mixins/rational_to_integer.rb +1 -0
- data/lib/dicey/mixins/vectorize_dice.rb +17 -12
- data/lib/dicey/mixins/warn_about_vector_number.rb +19 -0
- data/lib/dicey/numeric_die.rb +1 -1
- data/lib/dicey/version.rb +1 -1
- data/lib/dicey.rb +26 -5
- metadata +30 -26
- data/lib/dicey/output_formatters/gnuplot_formatter.rb +0 -24
- data/lib/dicey/output_formatters/hash_formatter.rb +0 -36
- data/lib/dicey/output_formatters/json_formatter.rb +0 -12
- data/lib/dicey/output_formatters/key_value_formatter.rb +0 -34
- data/lib/dicey/output_formatters/list_formatter.rb +0 -12
- data/lib/dicey/output_formatters/null_formatter.rb +0 -15
- data/lib/dicey/output_formatters/yaml_formatter.rb +0 -12
- data/lib/dicey/roller.rb +0 -46
- data/lib/dicey/sum_frequency_calculators/auto_selector.rb +0 -41
- data/lib/dicey/sum_frequency_calculators/brute_force.rb +0 -41
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 530103d6f251d8f7c0b8400bde5b70c7c92bc6f79c6638aa99e0900f2af2129b
|
|
4
|
+
data.tar.gz: 943e0dd4655eb14c1e8b67bbbdf53deadec26856a864bfa5cb25b7f7d0b624e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 747b729c073e357d7f4016d40df42a1669d166c1056e91dcf4ad07308a92f6a09c9cc2e058bbf595930766e8319197be529daa5041b280bbb6c98ab1b0f1538f
|
|
7
|
+
data.tar.gz: c2cf10206752a9d0fff536b5d1e13291520d2af4355d468f51288d8f922de3c94dfca3cfefc20d6536c5aa957ec8d7e178724a7bdfabf544aaede4ab49cda8aa
|
data/README.md
CHANGED
|
@@ -10,7 +10,13 @@
|
|
|
10
10
|
|
|
11
11
|
The premier solution in total paradigm shift for resolving dicey problems of tomorrow, today, used by industry-leading professionals around the world!
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
*\*cough, cough\**
|
|
14
|
+
|
|
15
|
+
Dicey is a calculator of weight/probability distributions of all possible dice rolls for a given set of dice, with support for pretty much any dice you can imagine. It can also roll those dice, if you just need a roller!
|
|
16
|
+
|
|
17
|
+
Dicey was created due to frustration with online "calculators" that just calculate a single probability, and not even for interesting combinations of dice. Over time it has grown to support features that I've not seen anywhere else.
|
|
18
|
+
|
|
19
|
+
Check out Dicey online at [dicey.bulancov.tech](https://dicey.bulancov.tech)!
|
|
14
20
|
|
|
15
21
|
## Table of contents
|
|
16
22
|
|
|
@@ -42,18 +48,18 @@ In seriousness, this program is mainly useful for calculating total frequency (p
|
|
|
42
48
|
|
|
43
49
|
Use online version of **Dicey** on its own website: [dicey.bulancov.tech](https://dicey.bulancov.tech)!
|
|
44
50
|
|
|
45
|
-
It does not provide quite all features, but it's easy to use and quick to get started.
|
|
51
|
+
It does not provide quite all features, but it's easy to use and quick to get started. And it's installable as a webapp, so it can be used offline too!
|
|
46
52
|
|
|
47
53
|
### For those who want the full command line experience
|
|
48
54
|
|
|
49
55
|
Thanks to the efforts of Ruby developers, you can run full **Dicey** online!
|
|
50
56
|
1. Head over to the prepared [RunRuby page](https://runruby.dev/gist/476679a55c24520782613d9ceb89d9a3).
|
|
51
57
|
2. Make sure that "*-main.rb*" is open.
|
|
52
|
-
3. Input arguments between "ARGUMENTS" lines, separated by spaces. Refer to [Usage
|
|
58
|
+
3. Input arguments between "ARGUMENTS" lines, separated by spaces. Refer to [Usage: CLI](#usage-cli-command-line) section.
|
|
53
59
|
4. Click "**Run code**" button below the editor.
|
|
54
60
|
5. Results will be printed to the "Logs" tab.
|
|
55
61
|
|
|
56
|
-
If familiar with Ruby, you can also use **RunRuby** to explore the API. Refer to [Usage
|
|
62
|
+
If familiar with Ruby, you can also use **RunRuby** to explore the API. Refer to [Usage: API](#usage-api) section for documentation.
|
|
57
63
|
|
|
58
64
|
## Installation
|
|
59
65
|
|
|
@@ -117,7 +123,7 @@ It should output the following:
|
|
|
117
123
|
8 => 1
|
|
118
124
|
```
|
|
119
125
|
|
|
120
|
-
First line is a comment telling you that calculation ran for two D4s. Every line after that has the form `
|
|
126
|
+
First line is a comment telling you that calculation ran for two D4s. Every line after that has the form `outcome => weight`, where weight is the number of distinct rolls which result in this outcome. As can be seen, 5 is the most common result with 4 possible different rolls.
|
|
121
127
|
|
|
122
128
|
If probability is preferred, there is an option for that:
|
|
123
129
|
```sh
|
|
@@ -168,7 +174,7 @@ $ dicey 8 2d4 -f g | dicey-to-gnuplot
|
|
|
168
174
|
```
|
|
169
175
|
|
|
170
176
|
This will create a PNG image named "*D8+D4+D4.png*":
|
|
171
|
-

|
|
172
178
|
|
|
173
179
|
#### Example 2.2: JSON and YAML
|
|
174
180
|
|
|
@@ -217,7 +223,7 @@ $ dicey 1,2,4 4
|
|
|
217
223
|
8 => 1
|
|
218
224
|
```
|
|
219
225
|
|
|
220
|
-
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
|
|
226
|
+
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 weights to slightly dip in the middle. Good to know.
|
|
221
227
|
|
|
222
228
|
But what if you had TWO weird D4s?
|
|
223
229
|
```sh
|
|
@@ -242,7 +248,7 @@ You have a sudden urge to roll dice while only having boring integer dice at hom
|
|
|
242
248
|
|
|
243
249
|
Look no further than **roll** mode introduced in **Dicey** 0.12:
|
|
244
250
|
```sh
|
|
245
|
-
$ dicey
|
|
251
|
+
$ dicey 1/2,1,1.5,2,2.5 4 --mode roll # As always, can be abbreviated to -m r
|
|
246
252
|
# (1/2,1,3/2,2,5/2)+D4
|
|
247
253
|
roll => 7/2 # You probably will get a different value here.
|
|
248
254
|
```
|
|
@@ -269,11 +275,12 @@ There are four *main* ways to define dice:
|
|
|
269
275
|
- *"5", "25", or "525"*: a single positive integer makes a regular die (like a D20).
|
|
270
276
|
- *"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.
|
|
271
277
|
- Accepted separators: "-", "..", "...", "–" (en dash), "—" (em dash), "…" (ellipsis).
|
|
272
|
-
- *"1,2,4", "(-1.5,0,
|
|
278
|
+
- *"1,2,4", "(-1.5,0,3/2)", or "2,"*: a list of any numbers separated by commas, possibly in round brackets, makes a custom numeric die.
|
|
273
279
|
- Lists can end in a comma, allowing single-number lists.
|
|
280
|
+
- There is no difference between equal decimal and fractional representations of numbers.
|
|
274
281
|
- *"1,1.5,Two", "(💚,🧡,💙,💜)" or "('1','(bracket)')"*: a list of strings and numbers separated by commas, possibly in round brackets, makes an arbitrary die.
|
|
275
282
|
- Lists can end in a comma, allowing single-string lists.
|
|
276
|
-
- Single (') or double (") quotes can be used to
|
|
283
|
+
- Single (') or double (") quotes can be used to include other quotes and round brackets in the string. Otherwise, they are prohibited. Commas are always prohibited.
|
|
277
284
|
- Quotes can also be used to treat numbers as strings.
|
|
278
285
|
|
|
279
286
|
*"D6", "d(-1,3)", "d2..4", or "d💚,🧡"*: 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 "-".
|
|
@@ -384,26 +391,31 @@ die.roll
|
|
|
384
391
|
|
|
385
392
|
### Distribution calculators
|
|
386
393
|
|
|
387
|
-
Distribution calculators live in `Dicey::
|
|
388
|
-
- `Dicey::
|
|
389
|
-
- `Dicey::
|
|
390
|
-
- `Dicey::
|
|
391
|
-
- `Dicey::SumFrequencyCalculators::Empirical`... this is more of a tool than a calculator. It "calculates" probabilities by performing a large number of rolls and counting frequencies of outcomes. It can be interesting to play around with and see how practical results compare to theoretical ones. Due to its simplicity, it also works with *any* dice.
|
|
394
|
+
Distribution calculators live in `Dicey::DistributionCalculators` module. There are three main calculators currently.
|
|
395
|
+
- `Dicey::DistributionCalculators::PolynomialConvolution` is the recommended calculator, able to handle all `Dicey::RegularDie` and other integer dice.
|
|
396
|
+
- `Dicey::DistributionCalculators::MultinomialCoefficients` is specialized for repeated numeric dice. However, it is currently limited to dice with arithmetic sequences (this includes regular dice, however).
|
|
397
|
+
- `Dicey::DistributionCalculators::Iterative` is the most generic, able to work with *abstract* dice. It needs gem "**vector_number**" to be installed and available to work with non-numeric dice.
|
|
392
398
|
|
|
393
|
-
|
|
394
|
-
-
|
|
399
|
+
Additionally, there are three special calculators.
|
|
400
|
+
- `Dicey::DistributionCalculators::Binomial` is a fast calculator for collections of equal two-sided dice, like coins, including *abstract* dice.
|
|
401
|
+
- `Dicey::DistributionCalculators::Empirical` is more of a tool than a calculator. It "calculates" probabilities by performing a large number of rolls and counting frequency of outcomes. It can be interesting to play around with and see how practical results compare to theoretical ones. Due to its simplicity, it also works with *abstract* dice.
|
|
402
|
+
- `Dicey::DistributionCalculators::Trivial` is an extra-fast calculator for some trivial cases. There isn't much point in using it manually.
|
|
403
|
+
|
|
404
|
+
When in doubt which calculator to use (and if a given one *can* be used), use `Dicey::DistributionCalculators::AutoSelector`. Its `.call(dice)` method will return a valid calculator for the given dice or `nil` if none are acceptable.
|
|
405
|
+
|
|
406
|
+
Calculators inherit from `Dicey::DistributionCalculators::BaseCalculator` and provide the following public interface:
|
|
407
|
+
- `#call(dice, result_type: {:weights | :probabilities}, **options) : Hash`
|
|
395
408
|
- `#valid_for?(dice) : Boolean`
|
|
409
|
+
- `#heuristic_complexity(dice) : Integer`
|
|
396
410
|
|
|
397
411
|
See [Diving deeper](#diving-deeper) for more details on limitations and complexity considerations of different algorithms.
|
|
398
412
|
|
|
399
|
-
When in doubt which calculator to use (and if a given one *can* be used), use `Dicey::SumFrequencyCalculators::AutoSelector`. Its `#call(dice)` method will return a valid calculator for the given dice or `nil` if none are acceptable.
|
|
400
|
-
|
|
401
413
|
### Distribution properties
|
|
402
414
|
|
|
403
415
|
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` provides this functionality:
|
|
404
416
|
```rb
|
|
405
417
|
Dicey::DistributionPropertiesCalculator.new.call(
|
|
406
|
-
Dicey::
|
|
418
|
+
Dicey::DistributionCalculators::PolynomialConvolution.new.call(
|
|
407
419
|
Dicey::RegularDie.from_count(2, 3)
|
|
408
420
|
)
|
|
409
421
|
)
|
|
@@ -427,7 +439,7 @@ Dicey::DistributionPropertiesCalculator.new.call(
|
|
|
427
439
|
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)?
|
|
428
440
|
```rb
|
|
429
441
|
Dicey::DistributionPropertiesCalculator.new.call(
|
|
430
|
-
Dicey::
|
|
442
|
+
Dicey::DistributionCalculators::PolynomialConvolution.new.call(
|
|
431
443
|
[Dicey::RegularDie.new(4), Dicey::NumericDie.new([1,3,4])]
|
|
432
444
|
)
|
|
433
445
|
)
|
|
@@ -460,7 +472,7 @@ You can see that 11 and 12 are the most likely outcomes, coming from a larger pe
|
|
|
460
472
|
|
|
461
473
|
## Diving deeper
|
|
462
474
|
|
|
463
|
-
For a further discussion of calculations, it is important to understand which classes of dice
|
|
475
|
+
For a further discussion of calculations, it is important to understand which classes of dice are distinguished in **Dicey**.
|
|
464
476
|
- **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.
|
|
465
477
|
|
|
466
478
|
> [!TIP]
|
|
@@ -474,45 +486,56 @@ For a further discussion of calculations, it is important to understand which cl
|
|
|
474
486
|
> [!NOTE]
|
|
475
487
|
> 💡 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.
|
|
476
488
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
Currently, three algorithms for calculating frequencies are implemented, with different possibilities and trade-offs.
|
|
489
|
+
Currently, three algorithms for calculating distributions are implemented, with different possibilities and trade-offs.
|
|
480
490
|
|
|
481
491
|
> [!NOTE]
|
|
482
492
|
> 💡 Complexity is listed for **n** dice with at most **m** sides and is only an approximation.
|
|
483
493
|
|
|
484
|
-
###
|
|
494
|
+
### Polynomial convolution
|
|
485
495
|
|
|
486
|
-
An algorithm based on fast polynomial multiplication. This is the
|
|
496
|
+
An algorithm based on fast polynomial multiplication. This is the algorithm which probably will be used by auto-selector for most reasonable dice.
|
|
487
497
|
|
|
488
498
|
- Limitations: only **integer** dice are allowed, including **regular** dice.
|
|
489
499
|
- Example: `dicey 5 3,4,1 0,`
|
|
490
500
|
- Complexity: **O(n<sup>3</sup>⋅m<sup>2</sup>)**
|
|
491
501
|
- Running time examples:
|
|
492
502
|
- 6d1000 — 0.5 seconds
|
|
493
|
-
- 1000d6 —
|
|
503
|
+
- 1000d6 — 20 seconds
|
|
494
504
|
|
|
495
505
|
### Multinomial coefficients
|
|
496
506
|
|
|
497
|
-
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. It is usually faster than
|
|
507
|
+
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. It is usually faster than polynomial convolution for many dice with few sides.
|
|
498
508
|
|
|
499
509
|
- Limitations: only *equal* **arithmetic** dice are allowed.
|
|
500
510
|
- Example: `dicey 1.5,3,4.5,6 1.5,3,4.5,6 1.5,3,4.5,6`
|
|
501
511
|
- Complexity: **O(n<sup>2</sup>⋅m<sup>2</sup>)**
|
|
502
512
|
- Running time examples:
|
|
503
|
-
- 6d1000 — 1.
|
|
504
|
-
- 1000d6 — 10
|
|
513
|
+
- 6d1000 — 1.5 seconds
|
|
514
|
+
- 1000d6 — 10 seconds
|
|
515
|
+
|
|
516
|
+
### Binomial
|
|
517
|
+
|
|
518
|
+
This is a specialized alogorithm for coin-like dice of any kind. It is significantly faster than general algorithms.
|
|
519
|
+
|
|
520
|
+
- Limitations: only *equal* two-sided dice are allowed, **vector_number** allows non-numeric values.
|
|
521
|
+
- Example: `dicey 500d2`
|
|
522
|
+
- Complexity: **O(n<sup>2</sup>)**
|
|
523
|
+
- Running time examples:
|
|
524
|
+
- 1000d2 — 0.125 seconds
|
|
525
|
+
- 10000d2 — 3.5 seconds
|
|
505
526
|
|
|
506
|
-
###
|
|
527
|
+
### Iterative
|
|
507
528
|
|
|
508
|
-
|
|
529
|
+
At last, there is an iterative algorithm which goes through every possible dice roll and adds results together. Whil being inefficient, it has the largest input space, allowing to work with completely nonsensical dice, including complex numbers and altogether non-numeric values.
|
|
509
530
|
|
|
510
531
|
- Limitations: without **vector_number** all values must be numbers, otherwise almost any values are viable.
|
|
511
532
|
- Example: `dicey 5 1,0.1,2 A,B,C`
|
|
512
|
-
- Complexity: **O(m<sup>
|
|
533
|
+
- Complexity: **O(n<sup>2</sup>⋅m<sup>2</sup>)**
|
|
513
534
|
- Running time examples:
|
|
514
|
-
-
|
|
515
|
-
-
|
|
535
|
+
- 6d1000 — 2.5 seconds
|
|
536
|
+
- 1000d6 — 20 seconds
|
|
537
|
+
- 10d(a,b,c,d,e,f) — 0.5 seconds
|
|
538
|
+
- 10d(a,b,c,d,e,f,g,h,i,j) — 15 seconds
|
|
516
539
|
|
|
517
540
|
## Development
|
|
518
541
|
|
|
@@ -540,4 +563,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/trinis
|
|
|
540
563
|
|
|
541
564
|
## License
|
|
542
565
|
|
|
543
|
-
This gem is available as open source under the terms of the
|
|
566
|
+
This gem is available as open source under the terms of the MIT License, see [LICENSE.txt](https://github.com/trinistr/dicey/blob/main/LICENSE.txt).
|
data/exe/dicey
CHANGED
data/lib/dicey/abstract_die.rb
CHANGED
|
@@ -3,11 +3,29 @@
|
|
|
3
3
|
module Dicey
|
|
4
4
|
# Asbtract die which may have an arbitrary list of sides,
|
|
5
5
|
# not even neccessarily numbers but strings or other objects.
|
|
6
|
+
#
|
|
7
|
+
# As the base class for all dice, defines their API.
|
|
8
|
+
#
|
|
9
|
+
# Dice can be created through several methods:
|
|
10
|
+
# - basic +.new+ ({#initialize}), creating one die from an appropriate definition;
|
|
11
|
+
# - {.from_list}, creating an array of dice from a list of definitions;
|
|
12
|
+
# - {.from_count}, creating a number of equal dice from one definition.
|
|
13
|
+
#
|
|
14
|
+
# Rolling a die is done through {#roll}. {#current} returns the current side of the die.
|
|
15
|
+
#
|
|
16
|
+
# {.srand} can be used to (re)set the internal randomizer's state for all dice,
|
|
17
|
+
# allowing to reproduce the same sequence of rolls (if it was done with a known state).
|
|
6
18
|
class AbstractDie
|
|
19
|
+
# Yes, class variable is actually useful here.
|
|
20
|
+
# TODO: Allow supplying a custom Random.
|
|
7
21
|
# rubocop:disable Style/ClassVars
|
|
8
22
|
|
|
9
23
|
# @api private
|
|
10
24
|
# Get a random value using a private instance of Random.
|
|
25
|
+
#
|
|
26
|
+
# Do not use this method directly.
|
|
27
|
+
# Reproducible rolls depend on it being called only internally.
|
|
28
|
+
#
|
|
11
29
|
# @see Random#rand
|
|
12
30
|
def self.rand(...)
|
|
13
31
|
@@random.rand(...)
|
|
@@ -19,9 +37,6 @@ module Dicey
|
|
|
19
37
|
@@random = Random.new(...)
|
|
20
38
|
end
|
|
21
39
|
|
|
22
|
-
# Yes, class variable is actually useful here.
|
|
23
|
-
# TODO: Allow supplying a custom Random.
|
|
24
|
-
|
|
25
40
|
# Shared randomness source, accessed through {.rand} and {.srand}.
|
|
26
41
|
@@random = Random.new
|
|
27
42
|
|
|
@@ -37,7 +52,7 @@ module Dicey
|
|
|
37
52
|
dice.to_a.join("+")
|
|
38
53
|
end
|
|
39
54
|
|
|
40
|
-
# Create a bunch of different dice at once.
|
|
55
|
+
# Create a bunch of different dice at once from a list of definitions.
|
|
41
56
|
#
|
|
42
57
|
# @param definitions [Array<Enumerable<Any>>, Array<Any>]
|
|
43
58
|
# list of definitions suitable for the dice class
|
|
@@ -46,7 +61,7 @@ module Dicey
|
|
|
46
61
|
definitions.map { new(_1) }
|
|
47
62
|
end
|
|
48
63
|
|
|
49
|
-
# Create a number of equal dice.
|
|
64
|
+
# Create a number of equal dice from one definition.
|
|
50
65
|
#
|
|
51
66
|
# @param count [Integer] number of dice to create
|
|
52
67
|
# @param definition [Enumerable<Any>, Any]
|
|
@@ -115,6 +130,8 @@ module Dicey
|
|
|
115
130
|
# Determine if this die and the other one have the same list of sides.
|
|
116
131
|
# Be aware that differently ordered sides are not considered equal.
|
|
117
132
|
#
|
|
133
|
+
# @see #eql?
|
|
134
|
+
#
|
|
118
135
|
# @param other [AbstractDie, Any]
|
|
119
136
|
# @return [Boolean]
|
|
120
137
|
def ==(other)
|
|
@@ -127,6 +144,8 @@ module Dicey
|
|
|
127
144
|
#
|
|
128
145
|
# +die_1.eql?(die_2)+ implies +die_1.hash == die_2.hash+.
|
|
129
146
|
#
|
|
147
|
+
# @see #hash
|
|
148
|
+
#
|
|
130
149
|
# @param other [AbstractDie, Any]
|
|
131
150
|
# @return [Boolean]
|
|
132
151
|
def eql?(other)
|
data/lib/dicey/cli/blender.rb
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../../dicey"
|
|
4
|
+
|
|
5
|
+
require_relative "options"
|
|
6
|
+
|
|
7
|
+
require_relative "calculator_runner"
|
|
8
|
+
require_relative "calculator_test_runner"
|
|
9
|
+
require_relative "roller"
|
|
10
|
+
|
|
11
|
+
Dir["formatters/*.rb", base: __dir__].each { require_relative _1 }
|
|
12
|
+
|
|
3
13
|
module Dicey
|
|
4
|
-
# Classes pertaining to CLI.
|
|
5
|
-
# NOT loaded by default, use +require "dicey/cli/blender"+ as needed.
|
|
6
14
|
module CLI
|
|
7
|
-
require_relative "../../dicey"
|
|
8
|
-
require_relative "options"
|
|
9
|
-
|
|
10
15
|
# Slice and dice everything in the Dicey module to produce a useful result.
|
|
11
16
|
# This is the entry point for the CLI.
|
|
12
17
|
class Blender
|
|
@@ -16,11 +21,11 @@ module Dicey
|
|
|
16
21
|
mode: lambda(&:to_sym),
|
|
17
22
|
result: lambda(&:to_sym),
|
|
18
23
|
format: {
|
|
19
|
-
"list" =>
|
|
20
|
-
"gnuplot" =>
|
|
21
|
-
"yaml" =>
|
|
22
|
-
"json" =>
|
|
23
|
-
"null" =>
|
|
24
|
+
"list" => Formatters::ListFormatter.new,
|
|
25
|
+
"gnuplot" => Formatters::GnuplotFormatter.new,
|
|
26
|
+
"yaml" => Formatters::YAMLFormatter.new,
|
|
27
|
+
"json" => Formatters::JSONFormatter.new,
|
|
28
|
+
"null" => Formatters::NullFormatter.new,
|
|
24
29
|
}.freeze,
|
|
25
30
|
}.freeze
|
|
26
31
|
|
|
@@ -29,8 +34,8 @@ module Dicey
|
|
|
29
34
|
# and return +true+, +false+ or a String.
|
|
30
35
|
RUNNERS = {
|
|
31
36
|
roll: Roller.new,
|
|
32
|
-
|
|
33
|
-
test:
|
|
37
|
+
distribution: CLI::CalculatorRunner.new,
|
|
38
|
+
test: CLI::CalculatorTestRunner.new,
|
|
34
39
|
}.freeze
|
|
35
40
|
|
|
36
41
|
# Run the program, blending everything together.
|
|
@@ -61,9 +66,9 @@ module Dicey
|
|
|
61
66
|
# Require libraries only when needed, to cut on run time.
|
|
62
67
|
def require_optional_libraries(options)
|
|
63
68
|
case options[:format]
|
|
64
|
-
when
|
|
69
|
+
when Formatters::YAMLFormatter
|
|
65
70
|
require "yaml"
|
|
66
|
-
when
|
|
71
|
+
when Formatters::JSONFormatter
|
|
67
72
|
require "json"
|
|
68
73
|
else
|
|
69
74
|
# No additional libraries needed
|
|
@@ -2,31 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../die_foundry"
|
|
4
4
|
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative "kronecker_substitution"
|
|
7
|
-
require_relative "multinomial_coefficients"
|
|
5
|
+
Dir["../distribution_calculators/*.rb", base: __dir__].each { require_relative _1 }
|
|
8
6
|
|
|
9
7
|
module Dicey
|
|
10
|
-
module
|
|
11
|
-
# The defaultest runner which calculates roll
|
|
12
|
-
class
|
|
13
|
-
# Transform die definitions to roll
|
|
8
|
+
module CLI
|
|
9
|
+
# The defaultest runner which calculates roll distribution from command-line dice.
|
|
10
|
+
class CalculatorRunner
|
|
11
|
+
# Transform die definitions to roll distribution.
|
|
14
12
|
#
|
|
15
13
|
# @param arguments [Array<String>] die definitions
|
|
16
14
|
# @param format [#call] formatter for output
|
|
17
15
|
# @param result [Symbol] result type selector
|
|
18
|
-
# @return [
|
|
16
|
+
# @return [String]
|
|
19
17
|
# @raise [DiceyError]
|
|
20
18
|
def call(arguments, format:, result:, **)
|
|
21
19
|
raise DiceyError, "no dice!" if arguments.empty?
|
|
22
20
|
|
|
23
21
|
dice = arguments.flat_map { |definition| die_foundry.cast(definition) }
|
|
24
|
-
calculator =
|
|
22
|
+
calculator = DistributionCalculators::AutoSelector.call(dice)
|
|
25
23
|
raise DiceyError, "no calculator could handle these dice!" unless calculator
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
distribution = calculator.call(dice, result_type: result)
|
|
28
26
|
|
|
29
|
-
format.call(
|
|
27
|
+
format.call(distribution, AbstractDie.describe(dice))
|
|
30
28
|
end
|
|
31
29
|
|
|
32
30
|
private
|
|
@@ -34,10 +32,6 @@ module Dicey
|
|
|
34
32
|
def die_foundry
|
|
35
33
|
@die_foundry ||= DieFoundry.new
|
|
36
34
|
end
|
|
37
|
-
|
|
38
|
-
def calculator_selector
|
|
39
|
-
@calculator_selector ||= AutoSelector.new
|
|
40
|
-
end
|
|
41
35
|
end
|
|
42
36
|
end
|
|
43
37
|
end
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative "brute_force"
|
|
5
|
-
require_relative "kronecker_substitution"
|
|
6
|
-
require_relative "multinomial_coefficients"
|
|
3
|
+
Dir["../distribution_calculators/*.rb", base: __dir__].each { require_relative _1 }
|
|
7
4
|
|
|
8
5
|
module Dicey
|
|
9
|
-
module
|
|
10
|
-
# A simple testing facility for roll
|
|
11
|
-
class
|
|
12
|
-
AVAILABLE_CALCULATORS = AutoSelector::AVAILABLE_CALCULATORS
|
|
6
|
+
module CLI
|
|
7
|
+
# A simple testing facility for roll distribution calculators.
|
|
8
|
+
class CalculatorTestRunner
|
|
9
|
+
AVAILABLE_CALCULATORS = DistributionCalculators::AutoSelector::AVAILABLE_CALCULATORS
|
|
13
10
|
|
|
14
|
-
# These are manually calculated
|
|
11
|
+
# These are manually calculated weights,
|
|
15
12
|
# with test cases for pretty much all variations of what this program can handle.
|
|
16
13
|
TEST_DATA = [
|
|
17
14
|
[[1], { 1 => 1 }],
|
|
@@ -78,7 +75,7 @@ module Dicey
|
|
|
78
75
|
# @return [Boolean] whether there are no failing tests
|
|
79
76
|
def call(*, report_style:, **)
|
|
80
77
|
results = TEST_DATA.to_h { |test| run_test(test) }
|
|
81
|
-
|
|
78
|
+
output_report(results, report_style)
|
|
82
79
|
results.values.none? do |test_result|
|
|
83
80
|
test_result.values.any? { FAILURE_RESULTS.include?(_1) }
|
|
84
81
|
end
|
|
@@ -88,7 +85,7 @@ module Dicey
|
|
|
88
85
|
|
|
89
86
|
# @param test [Array(Array<Integer, Array<Numeric>>, Hash{Numeric => Integer})]
|
|
90
87
|
# pair of a dice list definition and expected results
|
|
91
|
-
# @return [Array(Array<
|
|
88
|
+
# @return [Array(Array<AbstractDie>, Hash{BaseCalculator => Symbol})]
|
|
92
89
|
# result of running the test in a format suitable for +#to_h+
|
|
93
90
|
def run_test(test)
|
|
94
91
|
dice = build_dice(test.first)
|
|
@@ -99,10 +96,10 @@ module Dicey
|
|
|
99
96
|
[dice, test_result]
|
|
100
97
|
end
|
|
101
98
|
|
|
102
|
-
# Build a list of
|
|
99
|
+
# Build a list of dice from a plain definition.
|
|
103
100
|
#
|
|
104
101
|
# @param definition [Array<Integer, Array<Integer>>]
|
|
105
|
-
# @return [Array<
|
|
102
|
+
# @return [Array<AbstractDie>]
|
|
106
103
|
def build_dice(definition)
|
|
107
104
|
definition.map do |die_def|
|
|
108
105
|
if die_def.is_a?(Integer)
|
|
@@ -124,6 +121,15 @@ module Dicey
|
|
|
124
121
|
:crash
|
|
125
122
|
end
|
|
126
123
|
|
|
124
|
+
# Output report based on the results.
|
|
125
|
+
def output_report(results, report_style)
|
|
126
|
+
if report_style == :full
|
|
127
|
+
full_report(results)
|
|
128
|
+
elsif report_style == :short
|
|
129
|
+
short_report(results)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
127
133
|
# Print results of running all tests.
|
|
128
134
|
def full_report(results)
|
|
129
135
|
results.each do |dice, test_result|
|
|
@@ -134,6 +140,24 @@ module Dicey
|
|
|
134
140
|
end
|
|
135
141
|
end
|
|
136
142
|
end
|
|
143
|
+
|
|
144
|
+
# Print only failing results of running tests.
|
|
145
|
+
def short_report(results)
|
|
146
|
+
results.each do |dice, test_result|
|
|
147
|
+
print "#{AbstractDie.describe(dice)}:"
|
|
148
|
+
if test_result.values.any? { FAILURE_RESULTS.include?(_1) }
|
|
149
|
+
puts
|
|
150
|
+
test_result.each do |calculator, result|
|
|
151
|
+
next unless FAILURE_RESULTS.include?(result)
|
|
152
|
+
|
|
153
|
+
print " #{calculator.class}: "
|
|
154
|
+
puts RESULT_TEXT[result]
|
|
155
|
+
end
|
|
156
|
+
else
|
|
157
|
+
print " ", RESULT_TEXT[:pass], "\n"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
137
161
|
end
|
|
138
162
|
end
|
|
139
163
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../mixins/rational_to_integer"
|
|
4
|
+
|
|
5
|
+
module Dicey
|
|
6
|
+
module CLI
|
|
7
|
+
module Formatters
|
|
8
|
+
# Base formatter for outputting lists of key-value pairs separated by newlines.
|
|
9
|
+
# Can add an optional description into the result.
|
|
10
|
+
# @abstract
|
|
11
|
+
class BaseListFormatter
|
|
12
|
+
include Mixins::RationalToInteger
|
|
13
|
+
|
|
14
|
+
# @param hash [Hash{Object => Object}]
|
|
15
|
+
# @param description [String] text to add as a comment.
|
|
16
|
+
# @return [String]
|
|
17
|
+
def call(hash, description = nil)
|
|
18
|
+
initial_string = description ? "# #{description}\n" : +""
|
|
19
|
+
hash.each_with_object(initial_string) do |(key, value), output|
|
|
20
|
+
output << line(transform(key, value)) << "\n"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def transform(key, value)
|
|
27
|
+
[rational_to_integer(key), rational_to_integer(value)]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def line((key, value))
|
|
31
|
+
"#{key}#{self.class::SEPARATOR}#{value}"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dicey
|
|
4
|
+
module CLI
|
|
5
|
+
# Processors which turn data to text.
|
|
6
|
+
module Formatters
|
|
7
|
+
# Base formatter for outputting in formats which are map- (or object-) like.
|
|
8
|
+
# Can add an optional description into the result.
|
|
9
|
+
# @abstract
|
|
10
|
+
class BaseMapFormatter
|
|
11
|
+
# @param hash [Hash{Object => Object}]
|
|
12
|
+
# @param description [String] text to add to result as an extra key
|
|
13
|
+
# @return [String]
|
|
14
|
+
def call(hash, description = nil)
|
|
15
|
+
hash = hash.transform_keys { to_primitive(_1) }
|
|
16
|
+
hash.transform_values! { to_primitive(_1) }
|
|
17
|
+
output = {}
|
|
18
|
+
output["description"] = description if description
|
|
19
|
+
output["results"] = hash
|
|
20
|
+
output.public_send(self.class::METHOD)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def to_primitive(value)
|
|
26
|
+
return value if primitive?(value)
|
|
27
|
+
return value.to_f if Numeric === value
|
|
28
|
+
|
|
29
|
+
value.to_s
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def primitive?(value)
|
|
33
|
+
value.is_a?(Integer) || value.is_a?(Float) || value.is_a?(String)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_list_formatter"
|
|
4
|
+
|
|
5
|
+
module Dicey
|
|
6
|
+
module CLI
|
|
7
|
+
module Formatters
|
|
8
|
+
# Formats a hash as a text file suitable for consumption by Gnuplot.
|
|
9
|
+
#
|
|
10
|
+
# Will transform Rational probabilities to Floats.
|
|
11
|
+
# Non-numeric dice inherently won't work in gnuplot,
|
|
12
|
+
# even though the formatter will process them.
|
|
13
|
+
class GnuplotFormatter < BaseListFormatter
|
|
14
|
+
SEPARATOR = " "
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def transform(key, value)
|
|
19
|
+
[derationalize(key), derationalize(value)]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def derationalize(value)
|
|
23
|
+
value.is_a?(Rational) ? value.to_f : value
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_map_formatter"
|
|
4
|
+
|
|
5
|
+
module Dicey
|
|
6
|
+
module CLI
|
|
7
|
+
module Formatters
|
|
8
|
+
# Formats a hash as a JSON document under +results+ key, with optional +description+ key.
|
|
9
|
+
class JSONFormatter < BaseMapFormatter
|
|
10
|
+
METHOD = :to_json
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|