dicey 0.16.2 → 0.17.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 +4 -4
- data/README.md +50 -34
- 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 +3 -2
- 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: 44c61b75bbf67e966293a7f0f77eb1aef19add3f28989f2a04143fe12fdabfc8
|
|
4
|
+
data.tar.gz: 5d9c59008e82a2140ed01432f8e4d1e2dbc0e47e184c0fdc715cf3a77a8f4696
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2e54b27bc49ddb8586684ea4e9a71e5b7ec81b4436ee026c6aa5ab1340c72d92025bbb783bb8d754357c396debfc921e74cf4361fe0d5193c10f5c19f128279f
|
|
7
|
+
data.tar.gz: 7ae20ecc547a2a151bad2111b22ca323f68b0d09882b0b808c934551e1d36476fae7c265a549488179fcfb2db64f45cf8924a4306396c44c0eb2f05847710c6b
|
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
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
|
-
In seriousness, this program is mainly useful for calculating
|
|
13
|
+
In seriousness, this program is mainly useful for calculating distributions of weights (or probabilities) of all possible dice rolls for a given set of dice. Dice in such a set can be different, have arbitrary numbers, or even be non-numeric altogether. It can also be used to roll any dice that it supports.
|
|
14
14
|
|
|
15
15
|
## Table of contents
|
|
16
16
|
|
|
@@ -49,11 +49,11 @@ It does not provide quite all features, but it's easy to use and quick to get st
|
|
|
49
49
|
Thanks to the efforts of Ruby developers, you can run full **Dicey** online!
|
|
50
50
|
1. Head over to the prepared [RunRuby page](https://runruby.dev/gist/476679a55c24520782613d9ceb89d9a3).
|
|
51
51
|
2. Make sure that "*-main.rb*" is open.
|
|
52
|
-
3. Input arguments between "ARGUMENTS" lines, separated by spaces. Refer to [Usage
|
|
52
|
+
3. Input arguments between "ARGUMENTS" lines, separated by spaces. Refer to [Usage: CLI](#usage-cli-command-line) section.
|
|
53
53
|
4. Click "**Run code**" button below the editor.
|
|
54
54
|
5. Results will be printed to the "Logs" tab.
|
|
55
55
|
|
|
56
|
-
If familiar with Ruby, you can also use **RunRuby** to explore the API. Refer to [Usage
|
|
56
|
+
If familiar with Ruby, you can also use **RunRuby** to explore the API. Refer to [Usage: API](#usage-api) section for documentation.
|
|
57
57
|
|
|
58
58
|
## Installation
|
|
59
59
|
|
|
@@ -117,7 +117,7 @@ It should output the following:
|
|
|
117
117
|
8 => 1
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
First line is a comment telling you that calculation ran for two D4s. Every line after that has the form `
|
|
120
|
+
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
121
|
|
|
122
122
|
If probability is preferred, there is an option for that:
|
|
123
123
|
```sh
|
|
@@ -168,7 +168,7 @@ $ dicey 8 2d4 -f g | dicey-to-gnuplot
|
|
|
168
168
|
```
|
|
169
169
|
|
|
170
170
|
This will create a PNG image named "*D8+D4+D4.png*":
|
|
171
|
-

|
|
172
172
|
|
|
173
173
|
#### Example 2.2: JSON and YAML
|
|
174
174
|
|
|
@@ -217,7 +217,7 @@ $ dicey 1,2,4 4
|
|
|
217
217
|
8 => 1
|
|
218
218
|
```
|
|
219
219
|
|
|
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
|
|
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 weights to slightly dip in the middle. Good to know.
|
|
221
221
|
|
|
222
222
|
But what if you had TWO weird D4s?
|
|
223
223
|
```sh
|
|
@@ -273,7 +273,7 @@ There are four *main* ways to define dice:
|
|
|
273
273
|
- Lists can end in a comma, allowing single-number lists.
|
|
274
274
|
- *"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
275
|
- Lists can end in a comma, allowing single-string lists.
|
|
276
|
-
- Single (') or double (") quotes can be used to
|
|
276
|
+
- 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
277
|
- Quotes can also be used to treat numbers as strings.
|
|
278
278
|
|
|
279
279
|
*"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 +384,31 @@ die.roll
|
|
|
384
384
|
|
|
385
385
|
### Distribution calculators
|
|
386
386
|
|
|
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.
|
|
387
|
+
Distribution calculators live in `Dicey::DistributionCalculators` module. There are three main calculators currently.
|
|
388
|
+
- `Dicey::DistributionCalculators::PolynomialConvolution` is the recommended calculator, able to handle all `Dicey::RegularDie` and other integer dice.
|
|
389
|
+
- `Dicey::DistributionCalculators::MultinomialCoefficients` is specialized for repeated numeric dice. However, it is currently limited to dice with arithmetic sequences (this includes regular dice, however).
|
|
390
|
+
- `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
391
|
|
|
393
|
-
|
|
394
|
-
-
|
|
392
|
+
Additionally, there are three special calculators.
|
|
393
|
+
- `Dicey::DistributionCalculators::Binomial` is a fast calculator for collections of equal two-sided dice, like coins, including *abstract* dice.
|
|
394
|
+
- `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.
|
|
395
|
+
- `Dicey::DistributionCalculators::Trivial` is an extra-fast calculator for some trivial cases. There isn't much point in using it manually.
|
|
396
|
+
|
|
397
|
+
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.
|
|
398
|
+
|
|
399
|
+
Calculators inherit from `Dicey::DistributionCalculators::BaseCalculator` and provide the following public interface:
|
|
400
|
+
- `#call(dice, result_type: {:weights | :probabilities}, **options) : Hash`
|
|
395
401
|
- `#valid_for?(dice) : Boolean`
|
|
402
|
+
- `#heuristic_complexity(dice) : Integer`
|
|
396
403
|
|
|
397
404
|
See [Diving deeper](#diving-deeper) for more details on limitations and complexity considerations of different algorithms.
|
|
398
405
|
|
|
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
406
|
### Distribution properties
|
|
402
407
|
|
|
403
408
|
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
409
|
```rb
|
|
405
410
|
Dicey::DistributionPropertiesCalculator.new.call(
|
|
406
|
-
Dicey::
|
|
411
|
+
Dicey::DistributionCalculators::PolynomialConvolution.new.call(
|
|
407
412
|
Dicey::RegularDie.from_count(2, 3)
|
|
408
413
|
)
|
|
409
414
|
)
|
|
@@ -427,7 +432,7 @@ Dicey::DistributionPropertiesCalculator.new.call(
|
|
|
427
432
|
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
433
|
```rb
|
|
429
434
|
Dicey::DistributionPropertiesCalculator.new.call(
|
|
430
|
-
Dicey::
|
|
435
|
+
Dicey::DistributionCalculators::PolynomialConvolution.new.call(
|
|
431
436
|
[Dicey::RegularDie.new(4), Dicey::NumericDie.new([1,3,4])]
|
|
432
437
|
)
|
|
433
438
|
)
|
|
@@ -460,7 +465,7 @@ You can see that 11 and 12 are the most likely outcomes, coming from a larger pe
|
|
|
460
465
|
|
|
461
466
|
## Diving deeper
|
|
462
467
|
|
|
463
|
-
For a further discussion of calculations, it is important to understand which classes of dice
|
|
468
|
+
For a further discussion of calculations, it is important to understand which classes of dice are distinguished in **Dicey**.
|
|
464
469
|
- **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
470
|
|
|
466
471
|
> [!TIP]
|
|
@@ -474,45 +479,56 @@ For a further discussion of calculations, it is important to understand which cl
|
|
|
474
479
|
> [!NOTE]
|
|
475
480
|
> 💡 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
481
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
Currently, three algorithms for calculating frequencies are implemented, with different possibilities and trade-offs.
|
|
482
|
+
Currently, three algorithms for calculating distributions are implemented, with different possibilities and trade-offs.
|
|
480
483
|
|
|
481
484
|
> [!NOTE]
|
|
482
485
|
> 💡 Complexity is listed for **n** dice with at most **m** sides and is only an approximation.
|
|
483
486
|
|
|
484
|
-
###
|
|
487
|
+
### Polynomial convolution
|
|
485
488
|
|
|
486
|
-
An algorithm based on fast polynomial multiplication. This is the
|
|
489
|
+
An algorithm based on fast polynomial multiplication. This is the algorithm which probably will be used by auto-selector for most reasonable dice.
|
|
487
490
|
|
|
488
491
|
- Limitations: only **integer** dice are allowed, including **regular** dice.
|
|
489
492
|
- Example: `dicey 5 3,4,1 0,`
|
|
490
493
|
- Complexity: **O(n<sup>3</sup>⋅m<sup>2</sup>)**
|
|
491
494
|
- Running time examples:
|
|
492
495
|
- 6d1000 — 0.5 seconds
|
|
493
|
-
- 1000d6 —
|
|
496
|
+
- 1000d6 — 20 seconds
|
|
494
497
|
|
|
495
498
|
### Multinomial coefficients
|
|
496
499
|
|
|
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
|
|
500
|
+
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
501
|
|
|
499
502
|
- Limitations: only *equal* **arithmetic** dice are allowed.
|
|
500
503
|
- Example: `dicey 1.5,3,4.5,6 1.5,3,4.5,6 1.5,3,4.5,6`
|
|
501
504
|
- Complexity: **O(n<sup>2</sup>⋅m<sup>2</sup>)**
|
|
502
505
|
- Running time examples:
|
|
503
|
-
- 6d1000 — 1.
|
|
504
|
-
- 1000d6 — 10
|
|
506
|
+
- 6d1000 — 1.5 seconds
|
|
507
|
+
- 1000d6 — 10 seconds
|
|
505
508
|
|
|
506
|
-
###
|
|
509
|
+
### Binomial
|
|
507
510
|
|
|
508
|
-
|
|
511
|
+
This is a specialized alogorithm for coin-like dice of any kind. It is significantly faster than general algorithms.
|
|
512
|
+
|
|
513
|
+
- Limitations: only *equal* two-sided dice are allowed, **vector_number** allows non-numeric values.
|
|
514
|
+
- Example: `dicey 500d2`
|
|
515
|
+
- Complexity: **O(n<sup>2</sup>)**
|
|
516
|
+
- Running time examples:
|
|
517
|
+
- 1000d2 — 0.125 seconds
|
|
518
|
+
- 10000d2 — 3.5 seconds
|
|
519
|
+
|
|
520
|
+
### Iterative
|
|
521
|
+
|
|
522
|
+
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
523
|
|
|
510
524
|
- Limitations: without **vector_number** all values must be numbers, otherwise almost any values are viable.
|
|
511
525
|
- Example: `dicey 5 1,0.1,2 A,B,C`
|
|
512
|
-
- Complexity: **O(m<sup>
|
|
526
|
+
- Complexity: **O(n<sup>2</sup>⋅m<sup>2</sup>)**
|
|
513
527
|
- Running time examples:
|
|
514
|
-
-
|
|
515
|
-
-
|
|
528
|
+
- 6d1000 — 2.5 seconds
|
|
529
|
+
- 1000d6 — 20 seconds
|
|
530
|
+
- 10d(a,b,c,d,e,f) — 0.5 seconds
|
|
531
|
+
- 10d(a,b,c,d,e,f,g,h,i,j) — 15 seconds
|
|
516
532
|
|
|
517
533
|
## Development
|
|
518
534
|
|
|
@@ -540,4 +556,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/trinis
|
|
|
540
556
|
|
|
541
557
|
## License
|
|
542
558
|
|
|
543
|
-
This gem is available as open source under the terms of the
|
|
559
|
+
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
|
|
@@ -0,0 +1,14 @@
|
|
|
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 list of key => value pairs, similar to a Ruby Hash.
|
|
9
|
+
class ListFormatter < BaseListFormatter
|
|
10
|
+
SEPARATOR = " => "
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dicey
|
|
4
|
+
module CLI
|
|
5
|
+
module Formatters
|
|
6
|
+
# Formatter that doesn't format anything and always returns an empty string.
|
|
7
|
+
class NullFormatter
|
|
8
|
+
# @param hash [Hash{Object => Object}]
|
|
9
|
+
# @param description [String] text to add as a comment.
|
|
10
|
+
# @return [String] always an empty string
|
|
11
|
+
def call(hash, description = nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
12
|
+
""
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|