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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +50 -34
  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 +3 -2
  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: 44c61b75bbf67e966293a7f0f77eb1aef19add3f28989f2a04143fe12fdabfc8
4
+ data.tar.gz: 5d9c59008e82a2140ed01432f8e4d1e2dbc0e47e184c0fdc715cf3a77a8f4696
5
5
  SHA512:
6
- metadata.gz: 35e956615ff78b7007a7333172181f0c6666f06a5ab61cf2355c7744f1cff92e01fe9f1ad877c62b853bda9789de02d0a9a76ea13404e2e7a9b2711b7dc6c3df
7
- data.tar.gz: '0858e35f03ddd80ef754fde0276f33999124040fd499193c5f7eb2fe0c933c1c24677a2ea25e8ceea0259055469dd9076c6def50cde665267eb88386de8d7659'
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 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
+ 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 / CLI](#usage--cli-command-line) section.
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 / API](#usage--api) section for documentation.
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 `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.
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
- ![Graph of damage roll frequencies for Burning Sword](D8+D4+D4.png)
171
+ ![Graph of damage roll weights for Burning Sword](D8+D4+D4.png)
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 frequencies to slightly dip in the middle. Good to know.
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 use other quotes and round brackets in the string. Otherwise, they are prohibited. Commas are always prohibited.
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::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.
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
- Calculators inherit from `Dicey::SumFrequencyCalculators::BaseCalculator` and provide the following public interface:
394
- - `#call(dice, result_type: {:frequencies | :probabilities}, **options) : Hash`
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::SumFrequencyCalculators::KroneckerSubstitution.new.call(
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::SumFrequencyCalculators::KroneckerSubstitution.new.call(
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 exist.
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
- 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.
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
- ### Kronecker substitution
487
+ ### Polynomial convolution
485
488
 
486
- An algorithm based on fast polynomial multiplication. This is the default algorithm, used for most reasonable dice.
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 — 18 seconds
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 Kronecker substitution for many dice with few sides.
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.65 seconds
504
- - 1000d6 — 10.5 seconds
506
+ - 6d1000 — 1.5 seconds
507
+ - 1000d6 — 10 seconds
505
508
 
506
- ### Brute force
509
+ ### Binomial
507
510
 
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.
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>n</sup>)**
526
+ - Complexity: **O(n<sup>2</sup>⋅m<sup>2</sup>)**
513
527
  - Running time examples:
514
- - 6d100.25 seconds
515
- - 10d69.5 seconds
528
+ - 6d10002.5 seconds
529
+ - 1000d620 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 [MIT License](https://opensource.org/licenses/MIT), see [LICENSE.txt](https://github.com/trinistr/dicey/blob/main/LICENSE.txt).
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
@@ -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
@@ -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