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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -37
  3. data/exe/dicey +2 -2
  4. data/lib/dicey/abstract_die.rb +24 -5
  5. data/lib/dicey/cli/blender.rb +19 -14
  6. data/lib/dicey/{sum_frequency_calculators/runner.rb → cli/calculator_runner.rb} +9 -15
  7. data/lib/dicey/{sum_frequency_calculators/test_runner.rb → cli/calculator_test_runner.rb} +37 -13
  8. data/lib/dicey/cli/formatters/base_list_formatter.rb +36 -0
  9. data/lib/dicey/cli/formatters/base_map_formatter.rb +38 -0
  10. data/lib/dicey/cli/formatters/gnuplot_formatter.rb +28 -0
  11. data/lib/dicey/cli/formatters/json_formatter.rb +14 -0
  12. data/lib/dicey/cli/formatters/list_formatter.rb +14 -0
  13. data/lib/dicey/cli/formatters/null_formatter.rb +17 -0
  14. data/lib/dicey/cli/formatters/yaml_formatter.rb +14 -0
  15. data/lib/dicey/cli/options.rb +15 -11
  16. data/lib/dicey/cli/roller.rb +47 -0
  17. data/lib/dicey/cli.rb +23 -0
  18. data/lib/dicey/die_foundry.rb +6 -5
  19. data/lib/dicey/distribution_calculators/auto_selector.rb +73 -0
  20. data/lib/dicey/{sum_frequency_calculators → distribution_calculators}/base_calculator.rb +44 -37
  21. data/lib/dicey/distribution_calculators/binomial.rb +62 -0
  22. data/lib/dicey/{sum_frequency_calculators → distribution_calculators}/empirical.rb +9 -10
  23. data/lib/dicey/distribution_calculators/iterative.rb +51 -0
  24. data/lib/dicey/{sum_frequency_calculators → distribution_calculators}/multinomial_coefficients.rb +21 -12
  25. data/lib/dicey/{sum_frequency_calculators/kronecker_substitution.rb → distribution_calculators/polynomial_convolution.rb} +15 -8
  26. data/lib/dicey/distribution_calculators/trivial.rb +56 -0
  27. data/lib/dicey/distribution_properties_calculator.rb +5 -2
  28. data/lib/dicey/mixins/missing_math.rb +44 -0
  29. data/lib/dicey/mixins/rational_to_integer.rb +1 -0
  30. data/lib/dicey/mixins/vectorize_dice.rb +17 -12
  31. data/lib/dicey/mixins/warn_about_vector_number.rb +19 -0
  32. data/lib/dicey/numeric_die.rb +1 -1
  33. data/lib/dicey/version.rb +1 -1
  34. data/lib/dicey.rb +26 -5
  35. metadata +30 -26
  36. data/lib/dicey/output_formatters/gnuplot_formatter.rb +0 -24
  37. data/lib/dicey/output_formatters/hash_formatter.rb +0 -36
  38. data/lib/dicey/output_formatters/json_formatter.rb +0 -12
  39. data/lib/dicey/output_formatters/key_value_formatter.rb +0 -34
  40. data/lib/dicey/output_formatters/list_formatter.rb +0 -12
  41. data/lib/dicey/output_formatters/null_formatter.rb +0 -15
  42. data/lib/dicey/output_formatters/yaml_formatter.rb +0 -12
  43. data/lib/dicey/roller.rb +0 -46
  44. data/lib/dicey/sum_frequency_calculators/auto_selector.rb +0 -41
  45. 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: 5926d48370da981038bcc841fda2d3b1ea995d033f02f2373a097be5a510020a
4
- data.tar.gz: 1692a73044f83c7fa8889b86f2ad812cb40e1f9faf26feb94522457872544108
3
+ metadata.gz: 530103d6f251d8f7c0b8400bde5b70c7c92bc6f79c6638aa99e0900f2af2129b
4
+ data.tar.gz: 943e0dd4655eb14c1e8b67bbbdf53deadec26856a864bfa5cb25b7f7d0b624e6
5
5
  SHA512:
6
- metadata.gz: 35e956615ff78b7007a7333172181f0c6666f06a5ab61cf2355c7744f1cff92e01fe9f1ad877c62b853bda9789de02d0a9a76ea13404e2e7a9b2711b7dc6c3df
7
- data.tar.gz: '0858e35f03ddd80ef754fde0276f33999124040fd499193c5f7eb2fe0c933c1c24677a2ea25e8ceea0259055469dd9076c6def50cde665267eb88386de8d7659'
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
- In seriousness, this program is mainly useful for calculating total frequency (probability) distributions of all possible dice rolls for a given set of dice. Dice in such a set can be different or even have arbitrary numbers on the sides. It can also be used to roll any dice that it supports.
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 / CLI](#usage--cli-command-line) section.
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 / API](#usage--api) section for documentation.
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 `roll sum => frequency`, where frequency is the number of different rolls which result in this sum. As can be seen, 5 is the most common result with 4 possible different rolls.
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
- ![Graph of damage roll frequencies for Burning Sword](D8+D4+D4.png)
177
+ ![Graph of damage roll weights for Burning Sword](D8+D4+D4.png)
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 frequencies to slightly dip in the middle. Good to know.
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 0.5,1.0,1.5,2.0,2.5 4 --mode roll # As always, can be abbreviated to -m r
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,1.5)", or "2,"*: a list of any numbers separated by commas, possibly in round brackets, makes a custom numeric die.
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 use other quotes and round brackets in the string. Otherwise, they are prohibited. Commas are always prohibited.
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::SumFrequencyCalculators` module. There are four calculators currently:
388
- - `Dicey::SumFrequencyCalculators::KroneckerSubstitution` is the recommended calculator, able to handle all `Dicey::RegularDie` and more. It is very fast, though sometimes slower than the next one.
389
- - `Dicey::SumFrequencyCalculators::MultinomialCoefficients` is specialized for repeated numeric dice, with performance on par with the previous one, depending on exact parameters. However, it is currently limited to dice with arithmetic sequences (this includes regular dice, however).
390
- - `Dicey::SumFrequencyCalculators::BruteForce` is the most generic and slowest one, but can work with *any* dice. It needs gem "**vector_number**" to be installed and available to work with non-numeric dice.
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
- Calculators inherit from `Dicey::SumFrequencyCalculators::BaseCalculator` and provide the following public interface:
394
- - `#call(dice, result_type: {:frequencies | :probabilities}, **options) : Hash`
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::SumFrequencyCalculators::KroneckerSubstitution.new.call(
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::SumFrequencyCalculators::KroneckerSubstitution.new.call(
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 exist.
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
- 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.
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
- ### Kronecker substitution
494
+ ### Polynomial convolution
485
495
 
486
- An algorithm based on fast polynomial multiplication. This is the default algorithm, used for most reasonable dice.
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 — 18 seconds
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 Kronecker substitution for many dice with few sides.
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.65 seconds
504
- - 1000d6 — 10.5 seconds
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
- ### Brute force
527
+ ### Iterative
507
528
 
508
- As a last resort, there is a brute force algorithm which goes through every possible dice roll and adds results together. While quickly growing terrible in performace (and memory usage), it has the largest input space, allowing to work with completely nonsensical dice, including complex numbers and altogether non-numeric values.
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>n</sup>)**
533
+ - Complexity: **O(n<sup>2</sup>⋅m<sup>2</sup>)**
513
534
  - Running time examples:
514
- - 6d100.25 seconds
515
- - 10d69.5 seconds
535
+ - 6d10002.5 seconds
536
+ - 1000d620 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 [MIT License](https://opensource.org/licenses/MIT), see [LICENSE.txt](https://github.com/trinistr/dicey/blob/main/LICENSE.txt).
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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require "dicey/cli/blender"
4
+ require "dicey/cli"
5
5
 
6
- exit Dicey::CLI::Blender.new.call
6
+ exit Dicey::CLI.call
@@ -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)
@@ -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" => OutputFormatters::ListFormatter.new,
20
- "gnuplot" => OutputFormatters::GnuplotFormatter.new,
21
- "yaml" => OutputFormatters::YAMLFormatter.new,
22
- "json" => OutputFormatters::JSONFormatter.new,
23
- "null" => OutputFormatters::NullFormatter.new,
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
- frequencies: SumFrequencyCalculators::Runner.new,
33
- test: SumFrequencyCalculators::TestRunner.new,
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 OutputFormatters::YAMLFormatter
69
+ when Formatters::YAMLFormatter
65
70
  require "yaml"
66
- when OutputFormatters::JSONFormatter
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 "brute_force"
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 SumFrequencyCalculators
11
- # The defaultest runner which calculates roll frequencies from command-line dice.
12
- class Runner
13
- # Transform die definitions to roll frequencies.
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 [nil]
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 = calculator_selector.call(dice)
22
+ calculator = DistributionCalculators::AutoSelector.call(dice)
25
23
  raise DiceyError, "no calculator could handle these dice!" unless calculator
26
24
 
27
- frequencies = calculator.call(dice, result_type: result)
25
+ distribution = calculator.call(dice, result_type: result)
28
26
 
29
- format.call(frequencies, AbstractDie.describe(dice))
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 "auto_selector"
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 SumFrequencyCalculators
10
- # A simple testing facility for roll frequency calculators.
11
- class TestRunner
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 frequencies,
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
- full_report(results) if report_style == :full
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<NumericDie>, Hash{BaseCalculator => Symbol})]
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 {NumericDie} objects from a plain definition.
99
+ # Build a list of dice from a plain definition.
103
100
  #
104
101
  # @param definition [Array<Integer, Array<Integer>>]
105
- # @return [Array<NumericDie>]
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