dicey 0.16.0 ā 0.16.2
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 +23 -12
- data/lib/dicey/abstract_die.rb +1 -1
- data/lib/dicey/cli/blender.rb +1 -8
- data/lib/dicey/cli/options.rb +1 -1
- data/lib/dicey/mixins/rational_to_integer.rb +1 -1
- data/lib/dicey/mixins/vectorize_dice.rb +1 -0
- data/lib/dicey/output_formatters/null_formatter.rb +15 -0
- data/lib/dicey/roller.rb +0 -7
- data/lib/dicey/sum_frequency_calculators/auto_selector.rb +41 -0
- data/lib/dicey/sum_frequency_calculators/base_calculator.rb +27 -2
- data/lib/dicey/sum_frequency_calculators/brute_force.rb +7 -44
- data/lib/dicey/sum_frequency_calculators/empirical.rb +4 -7
- data/lib/dicey/sum_frequency_calculators/kronecker_substitution.rb +15 -8
- data/lib/dicey/sum_frequency_calculators/multinomial_coefficients.rb +7 -1
- data/lib/dicey/sum_frequency_calculators/runner.rb +9 -4
- data/lib/dicey/sum_frequency_calculators/test_runner.rb +7 -6
- data/lib/dicey/version.rb +1 -1
- data/lib/dicey.rb +7 -0
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5926d48370da981038bcc841fda2d3b1ea995d033f02f2373a097be5a510020a
|
4
|
+
data.tar.gz: 1692a73044f83c7fa8889b86f2ad812cb40e1f9faf26feb94522457872544108
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 35e956615ff78b7007a7333172181f0c6666f06a5ab61cf2355c7744f1cff92e01fe9f1ad877c62b853bda9789de02d0a9a76ea13404e2e7a9b2711b7dc6c3df
|
7
|
+
data.tar.gz: '0858e35f03ddd80ef754fde0276f33999124040fd499193c5f7eb2fe0c933c1c24677a2ea25e8ceea0259055469dd9076c6def50cde665267eb88386de8d7659'
|
data/README.md
CHANGED
@@ -385,8 +385,8 @@ die.roll
|
|
385
385
|
### Distribution calculators
|
386
386
|
|
387
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
|
389
|
-
- `Dicey::SumFrequencyCalculators::MultinomialCoefficients` is specialized for repeated numeric dice, with performance
|
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
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
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.
|
392
392
|
|
@@ -396,6 +396,8 @@ Calculators inherit from `Dicey::SumFrequencyCalculators::BaseCalculator` and pr
|
|
396
396
|
|
397
397
|
See [Diving deeper](#diving-deeper) for more details on limitations and complexity considerations of different algorithms.
|
398
398
|
|
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
|
+
|
399
401
|
### Distribution properties
|
400
402
|
|
401
403
|
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:
|
@@ -464,10 +466,10 @@ For a further discussion of calculations, it is important to understand which cl
|
|
464
466
|
> [!TIP]
|
465
467
|
> š” If you only need to roll **regular** dice, this section will not contain anything important.
|
466
468
|
|
467
|
-
- **
|
469
|
+
- **Integer** die has sides with only integers. For example, (1,2,3,4,5,6), (-5,0,5), (1,10000), (1,1,1,1,1,1,1,0).
|
468
470
|
- **Arithmetic** die's sides form an arithmetic sequence. For example, (1,2,3,4,5,6), (1,0,-1), (2.6,2.1,1.6,1.1).
|
469
471
|
- **Numeric** die is limited by having sides confined to ā (or ā if you are feeling particularly adventurous).
|
470
|
-
- **Abstract** die is
|
472
|
+
- **Abstract** die is unlimited!
|
471
473
|
|
472
474
|
> [!NOTE]
|
473
475
|
> š” 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.
|
@@ -477,31 +479,40 @@ Dicey is in principle able to handle any real numeric dice and some abstract dic
|
|
477
479
|
Currently, three algorithms for calculating frequencies are implemented, with different possibilities and trade-offs.
|
478
480
|
|
479
481
|
> [!NOTE]
|
480
|
-
> š” Complexity is listed for **n** dice with at most **m** sides and
|
482
|
+
> š” Complexity is listed for **n** dice with at most **m** sides and is only an approximation.
|
481
483
|
|
482
484
|
### Kronecker substitution
|
483
485
|
|
484
486
|
An algorithm based on fast polynomial multiplication. This is the default algorithm, used for most reasonable dice.
|
485
487
|
|
486
|
-
- Limitations: only **
|
488
|
+
- Limitations: only **integer** dice are allowed, including **regular** dice.
|
487
489
|
- Example: `dicey 5 3,4,1 0,`
|
488
|
-
- Complexity: **O(m
|
490
|
+
- Complexity: **O(n<sup>3</sup>ā
m<sup>2</sup>)**
|
491
|
+
- Running time examples:
|
492
|
+
- 6d1000 ā 0.5 seconds
|
493
|
+
- 1000d6 ā 18 seconds
|
489
494
|
|
490
495
|
### Multinomial coefficients
|
491
496
|
|
492
|
-
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.
|
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.
|
493
498
|
|
494
499
|
- Limitations: only *equal* **arithmetic** dice are allowed.
|
495
500
|
- Example: `dicey 1.5,3,4.5,6 1.5,3,4.5,6 1.5,3,4.5,6`
|
496
|
-
- Complexity: **O(m
|
501
|
+
- Complexity: **O(n<sup>2</sup>ā
m<sup>2</sup>)**
|
502
|
+
- Running time examples:
|
503
|
+
- 6d1000 ā 1.65 seconds
|
504
|
+
- 1000d6 ā 10.5 seconds
|
497
505
|
|
498
506
|
### Brute force
|
499
507
|
|
500
|
-
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, it has the largest input space, allowing to work with completely nonsensical dice, including
|
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.
|
501
509
|
|
502
510
|
- Limitations: without **vector_number** all values must be numbers, otherwise almost any values are viable.
|
503
|
-
- Example: `dicey 5 1,0.1,2
|
504
|
-
- Complexity: **O(m
|
511
|
+
- Example: `dicey 5 1,0.1,2 A,B,C`
|
512
|
+
- Complexity: **O(m<sup>n</sup>)**
|
513
|
+
- Running time examples:
|
514
|
+
- 6d10 ā 0.25 seconds
|
515
|
+
- 10d6 ā 9.5 seconds
|
505
516
|
|
506
517
|
## Development
|
507
518
|
|
data/lib/dicey/abstract_die.rb
CHANGED
data/lib/dicey/cli/blender.rb
CHANGED
@@ -10,13 +10,6 @@ module Dicey
|
|
10
10
|
# Slice and dice everything in the Dicey module to produce a useful result.
|
11
11
|
# This is the entry point for the CLI.
|
12
12
|
class Blender
|
13
|
-
# List of calculators to use, ordered by efficiency.
|
14
|
-
ROLL_FREQUENCY_CALCULATORS = [
|
15
|
-
SumFrequencyCalculators::KroneckerSubstitution.new,
|
16
|
-
SumFrequencyCalculators::MultinomialCoefficients.new,
|
17
|
-
SumFrequencyCalculators::BruteForce.new,
|
18
|
-
].freeze
|
19
|
-
|
20
13
|
# How to transform option values from command-line arguments
|
21
14
|
# to internally significant objects.
|
22
15
|
OPTION_TRANSFORMATIONS = {
|
@@ -27,6 +20,7 @@ module Dicey
|
|
27
20
|
"gnuplot" => OutputFormatters::GnuplotFormatter.new,
|
28
21
|
"yaml" => OutputFormatters::YAMLFormatter.new,
|
29
22
|
"json" => OutputFormatters::JSONFormatter.new,
|
23
|
+
"null" => OutputFormatters::NullFormatter.new,
|
30
24
|
}.freeze,
|
31
25
|
}.freeze
|
32
26
|
|
@@ -47,7 +41,6 @@ module Dicey
|
|
47
41
|
def call(argv = ARGV)
|
48
42
|
options, arguments = get_options_and_arguments(argv)
|
49
43
|
require_optional_libraries(options)
|
50
|
-
options[:roll_calculators] = ROLL_FREQUENCY_CALCULATORS
|
51
44
|
return_value = RUNNERS[options.delete(:mode)].call(arguments, **options)
|
52
45
|
print return_value if return_value.is_a?(String)
|
53
46
|
!!return_value
|
data/lib/dicey/cli/options.rb
CHANGED
@@ -11,7 +11,7 @@ module Dicey
|
|
11
11
|
# Allowed result types (--result).
|
12
12
|
RESULT_TYPES = %w[frequencies probabilities].freeze
|
13
13
|
# Allowed output formats (--format).
|
14
|
-
FORMATS = %w[list gnuplot json yaml].freeze
|
14
|
+
FORMATS = %w[list gnuplot json yaml null].freeze
|
15
15
|
|
16
16
|
# Default values for initial values of the options.
|
17
17
|
DEFAULT_OPTIONS = { mode: "frequencies", format: "list", result: "frequencies" }.freeze
|
@@ -11,7 +11,7 @@ module Dicey
|
|
11
11
|
# Convert +value+ to +Integer+ if it's a +Rational+ with denominator of 1.
|
12
12
|
# Otherwise, return +value+ as-is.
|
13
13
|
#
|
14
|
-
# @value [Numeric, Any]
|
14
|
+
# @param value [Numeric, Any]
|
15
15
|
# @return [Numeric, Integer, Any]
|
16
16
|
def rational_to_integer(value)
|
17
17
|
(Rational === value && value.denominator == 1) ? value.numerator : value
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dicey
|
4
|
+
module OutputFormatters
|
5
|
+
# Formatter that doesn't format anything and always returns an empty string.
|
6
|
+
class NullFormatter
|
7
|
+
# @param hash [Hash{Object => Object}]
|
8
|
+
# @param description [String] text to add as a comment.
|
9
|
+
# @return [String] always an empty string
|
10
|
+
def call(hash, description = nil) # rubocop:disable Lint/UnusedMethodArgument
|
11
|
+
""
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/dicey/roller.rb
CHANGED
@@ -1,12 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Try to load "vector_number" pre-emptively.
|
4
|
-
begin
|
5
|
-
require "vector_number"
|
6
|
-
rescue LoadError
|
7
|
-
# VectorNumber not available, sad
|
8
|
-
end
|
9
|
-
|
10
3
|
require_relative "die_foundry"
|
11
4
|
|
12
5
|
require_relative "mixins/rational_to_integer"
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "brute_force"
|
4
|
+
require_relative "kronecker_substitution"
|
5
|
+
require_relative "multinomial_coefficients"
|
6
|
+
|
7
|
+
module Dicey
|
8
|
+
module SumFrequencyCalculators
|
9
|
+
# Tool to automatically select a calculator for a given set of dice.
|
10
|
+
#
|
11
|
+
# Calculator is guaranteed to be compatible, with a strong chance of being the most performant.
|
12
|
+
#
|
13
|
+
# @see BaseCalculator#heuristic_complexity
|
14
|
+
class AutoSelector
|
15
|
+
# Calculators to consider when selecting a match.
|
16
|
+
AVAILABLE_CALCULATORS = [
|
17
|
+
KroneckerSubstitution.new,
|
18
|
+
MultinomialCoefficients.new,
|
19
|
+
BruteForce.new,
|
20
|
+
].freeze
|
21
|
+
|
22
|
+
# @param calculators [Array<BaseCalculator>]
|
23
|
+
# calculators which this instance will consider
|
24
|
+
def initialize(calculators = AVAILABLE_CALCULATORS)
|
25
|
+
@calculators = calculators
|
26
|
+
end
|
27
|
+
|
28
|
+
# Determine best (or adequate) calculator for a given set of dice
|
29
|
+
# based on heuristics from the list of available calculators.
|
30
|
+
#
|
31
|
+
# @param dice [Enumerable<NumericDie>]
|
32
|
+
# @return [BaseCalculator, nil] +nil+ if no calculator is compatible
|
33
|
+
def call(dice)
|
34
|
+
compatible = @calculators.select { _1.valid_for?(dice) }
|
35
|
+
return if compatible.empty?
|
36
|
+
|
37
|
+
compatible.min_by { _1.heuristic_complexity(dice) }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -25,6 +25,10 @@ module Dicey
|
|
25
25
|
# Possible values for +result_type+ argument in {#call}.
|
26
26
|
RESULT_TYPES = %i[frequencies probabilities].freeze
|
27
27
|
|
28
|
+
# Calculate distribution (probability mass function) for the list of dice.
|
29
|
+
#
|
30
|
+
# Returns empty hash for an empty list of dice.
|
31
|
+
#
|
28
32
|
# @param dice [Enumerable<AbstractDie>]
|
29
33
|
# @param result_type [Symbol] one of {RESULT_TYPES}
|
30
34
|
# @param options [Hash{Symbol => Any}] calculator-specific options,
|
@@ -55,6 +59,20 @@ module Dicey
|
|
55
59
|
dice.is_a?(Enumerable) && dice.all?(AbstractDie) && validate(dice)
|
56
60
|
end
|
57
61
|
|
62
|
+
# Heuristic complexity of the calculator, used to determine best calculator.
|
63
|
+
#
|
64
|
+
# Returns 0 for an empty list of dice.
|
65
|
+
#
|
66
|
+
# @see AutoSelector
|
67
|
+
#
|
68
|
+
# @param dice [Enumerable<AbstractDie>]
|
69
|
+
# @return [Integer]
|
70
|
+
def heuristic_complexity(dice)
|
71
|
+
return 0 if dice.empty?
|
72
|
+
|
73
|
+
calculate_heuristic(dice.length, dice.map(&:sides_num).max).to_i
|
74
|
+
end
|
75
|
+
|
58
76
|
private
|
59
77
|
|
60
78
|
# Do additional validation on the dice list.
|
@@ -63,12 +81,19 @@ module Dicey
|
|
63
81
|
true
|
64
82
|
end
|
65
83
|
|
84
|
+
# Calculate heuristic complexity of the calculator.
|
85
|
+
#
|
86
|
+
# @param dice_count [Integer]
|
87
|
+
# @param sides_count [Integer] maximum number of sides
|
88
|
+
# @return [Numeric]
|
89
|
+
def calculate_heuristic(dice_count, sides_count)
|
90
|
+
raise NotImplementedError
|
91
|
+
end
|
92
|
+
|
66
93
|
# Peform frequencies calculation.
|
67
94
|
# (see #call)
|
68
95
|
def calculate(dice, **nil)
|
69
|
-
# :nocov:
|
70
96
|
raise NotImplementedError
|
71
|
-
# :nocov:
|
72
97
|
end
|
73
98
|
|
74
99
|
# Check that resulting frequencies actually add up to what they are supposed to be.
|
@@ -1,12 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Try to load "vector_number" pre-emptively.
|
4
|
-
begin
|
5
|
-
require "vector_number"
|
6
|
-
rescue LoadError
|
7
|
-
# VectorNumber not available, sad
|
8
|
-
end
|
9
|
-
|
10
3
|
require_relative "base_calculator"
|
11
4
|
|
12
5
|
require_relative "../mixins/vectorize_dice"
|
@@ -33,46 +26,16 @@ module Dicey
|
|
33
26
|
end
|
34
27
|
end
|
35
28
|
|
36
|
-
def
|
37
|
-
|
38
|
-
combine_dice_enumerators(dice.map(&:sides_list)).map(&:sum).tally
|
29
|
+
def calculate_heuristic(dice_count, sides_count)
|
30
|
+
1000 * (sides_count**dice_count)
|
39
31
|
end
|
40
32
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
def combine_dice_enumerators(side_lists)
|
47
|
-
Enumerator::Product.new(*side_lists)
|
48
|
-
end
|
49
|
-
# :nocov:
|
50
|
-
else
|
51
|
-
# Get an enumerator which goes through all possible permutations of dice sides.
|
52
|
-
#
|
53
|
-
# @param side_lists [Enumerable<Enumerable<Any>>]
|
54
|
-
# @return [Enumerator<Array<Array<Any>>>]
|
55
|
-
def combine_dice_enumerators(side_lists)
|
56
|
-
product(side_lists)
|
57
|
-
end
|
58
|
-
|
59
|
-
# Simplified implementation of {Enumerator::Product}.
|
60
|
-
# Adapted from {https://bugs.ruby-lang.org/issues/18685#note-10}.
|
61
|
-
#
|
62
|
-
# @param enums [Enumerable<Enumerable<Any>>]
|
63
|
-
# @return [Enumerator<Array<Array<Any>>>]
|
64
|
-
def product(enums, &block)
|
65
|
-
return to_enum(__method__, enums) unless block_given?
|
66
|
-
|
67
|
-
enums
|
68
|
-
.reverse
|
69
|
-
.reduce(block) { |inner, enum|
|
70
|
-
->(values) { enum.each_entry { inner.call([*values, _1]) } }
|
71
|
-
}
|
72
|
-
.call([])
|
73
|
-
end
|
33
|
+
def calculate(dice, **nil)
|
34
|
+
dice = vectorize_dice(dice) if defined?(VectorNumber)
|
35
|
+
dice.map(&:sides_list).reduce { |result, die|
|
36
|
+
result.flat_map { |roll| die.map { |side| roll + side } }
|
37
|
+
}.tally
|
74
38
|
end
|
75
|
-
# :nocov:
|
76
39
|
end
|
77
40
|
end
|
78
41
|
end
|
@@ -1,12 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# Try to load "vector_number" pre-emptively.
|
4
|
-
begin
|
5
|
-
require "vector_number"
|
6
|
-
rescue LoadError
|
7
|
-
# VectorNumber not available, sad
|
8
|
-
end
|
9
|
-
|
10
3
|
require_relative "base_calculator"
|
11
4
|
|
12
5
|
require_relative "../mixins/vectorize_dice"
|
@@ -45,6 +38,10 @@ module Dicey
|
|
45
38
|
end
|
46
39
|
end
|
47
40
|
|
41
|
+
def calculate_heuristic(dice_count, sides_count)
|
42
|
+
N * dice_count * Math.log2(sides_count)
|
43
|
+
end
|
44
|
+
|
48
45
|
def calculate(dice, rolls: N)
|
49
46
|
dice = vectorize_dice(dice) if defined?(VectorNumber)
|
50
47
|
statistics = rolls.times.with_object(Hash.new(0)) { |_, hash| hash[dice.sum(&:roll)] += 1 }
|
@@ -4,9 +4,9 @@ require_relative "base_calculator"
|
|
4
4
|
|
5
5
|
module Dicey
|
6
6
|
module SumFrequencyCalculators
|
7
|
-
# Calculator for lists of dice with
|
7
|
+
# Calculator for lists of dice with integer sides (fast).
|
8
8
|
#
|
9
|
-
# Example dice: (1,2,3,4), (0,1
|
9
|
+
# Example dice: (1,2,3,4), (0,1,-5,6), (5,4,5,4,5).
|
10
10
|
#
|
11
11
|
# Based on Kronecker substitution method for polynomial multiplication.
|
12
12
|
# @see https://en.wikipedia.org/wiki/Kronecker_substitution
|
@@ -17,15 +17,19 @@ module Dicey
|
|
17
17
|
private
|
18
18
|
|
19
19
|
def validate(dice)
|
20
|
-
dice.all? { |die| die.sides_list.all?
|
20
|
+
dice.all? { |die| die.sides_list.all?(Integer) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def calculate_heuristic(dice_count, sides_count)
|
24
|
+
(dice_count**3.2) * 100 * (sides_count**1.9)
|
21
25
|
end
|
22
26
|
|
23
27
|
def calculate(dice, **nil)
|
24
|
-
polynomials = build_polynomials(dice)
|
28
|
+
polynomials, offset = build_polynomials(dice)
|
25
29
|
evaluation_point = find_evaluation_point(polynomials)
|
26
30
|
values = evaluate_polynomials(polynomials, evaluation_point)
|
27
31
|
product = values.reduce(:*)
|
28
|
-
extract_coefficients(product, evaluation_point)
|
32
|
+
extract_coefficients(product, evaluation_point, offset, polynomials.count)
|
29
33
|
end
|
30
34
|
|
31
35
|
# Turn dice into hashes where keys are side values and values are numbers of those sides,
|
@@ -35,7 +39,8 @@ module Dicey
|
|
35
39
|
# @param dice [Enumerable<NumericDie>]
|
36
40
|
# @return [Array<Hash{Integer => Integer}>]
|
37
41
|
def build_polynomials(dice)
|
38
|
-
dice.map {
|
42
|
+
minimum = dice.map { |die| die.sides_list.min }.min
|
43
|
+
[dice.map { |die| die.sides_list.map { _1 - minimum }.tally }, minimum]
|
39
44
|
end
|
40
45
|
|
41
46
|
# Find a power of 2 which is larger in magnitude than any resulting polynomial coefficients,
|
@@ -67,13 +72,15 @@ module Dicey
|
|
67
72
|
#
|
68
73
|
# @param product [Integer]
|
69
74
|
# @param evaluation_point [Integer]
|
75
|
+
# @param offset [Integer]
|
76
|
+
# @param number_of_dice [Integer]
|
70
77
|
# @return [Hash{Integer => Integer}]
|
71
|
-
def extract_coefficients(product, evaluation_point)
|
78
|
+
def extract_coefficients(product, evaluation_point, offset, number_of_dice)
|
72
79
|
window = evaluation_point - 1
|
73
80
|
window_shift = window.bit_length
|
74
81
|
(0..).each_with_object({}) do |power, result|
|
75
82
|
coefficient = product & window
|
76
|
-
result[power] = coefficient unless coefficient.zero?
|
83
|
+
result[power + (offset * number_of_dice)] = coefficient unless coefficient.zero?
|
77
84
|
product >>= window_shift
|
78
85
|
break result if product.zero?
|
79
86
|
end
|
@@ -34,6 +34,12 @@ module Dicey
|
|
34
34
|
true
|
35
35
|
end
|
36
36
|
|
37
|
+
def calculate_heuristic(dice_count, sides_count)
|
38
|
+
# Fitting shows both coefficients to be around 500,
|
39
|
+
# but empirical runtime doesn't agree, so 150 it is.
|
40
|
+
150 * (dice_count**2.2) * 500 * (sides_count**1.9)
|
41
|
+
end
|
42
|
+
|
37
43
|
def calculate(dice, **nil)
|
38
44
|
first_die = dice.first
|
39
45
|
number_of_sides = first_die.sides_num
|
@@ -72,7 +78,7 @@ module Dicey
|
|
72
78
|
# @return [Array<Integer>]
|
73
79
|
def next_row_of_coefficients(row_index, window_size, previous_row)
|
74
80
|
length = (row_index * window_size) + 1
|
75
|
-
(0
|
81
|
+
(0...length).map do |col_index|
|
76
82
|
# Have to clamp to 0 to prevent accessing array from the end.
|
77
83
|
# BUG: TruffleRuby can't handle endless range in #clamp (see https://github.com/oracle/truffleruby/issues/3945)
|
78
84
|
window_range = ((col_index - window_size).clamp(0..col_index)..col_index)
|
@@ -13,17 +13,18 @@ module Dicey
|
|
13
13
|
# Transform die definitions to roll frequencies.
|
14
14
|
#
|
15
15
|
# @param arguments [Array<String>] die definitions
|
16
|
-
# @param roll_calculators [Array<BaseCalculator>] list of calculators to use
|
17
16
|
# @param format [#call] formatter for output
|
18
17
|
# @param result [Symbol] result type selector
|
19
18
|
# @return [nil]
|
20
19
|
# @raise [DiceyError]
|
21
|
-
def call(arguments,
|
20
|
+
def call(arguments, format:, result:, **)
|
22
21
|
raise DiceyError, "no dice!" if arguments.empty?
|
23
22
|
|
24
23
|
dice = arguments.flat_map { |definition| die_foundry.cast(definition) }
|
25
|
-
|
26
|
-
raise DiceyError, "no calculator could handle these dice!" unless
|
24
|
+
calculator = calculator_selector.call(dice)
|
25
|
+
raise DiceyError, "no calculator could handle these dice!" unless calculator
|
26
|
+
|
27
|
+
frequencies = calculator.call(dice, result_type: result)
|
27
28
|
|
28
29
|
format.call(frequencies, AbstractDie.describe(dice))
|
29
30
|
end
|
@@ -33,6 +34,10 @@ module Dicey
|
|
33
34
|
def die_foundry
|
34
35
|
@die_foundry ||= DieFoundry.new
|
35
36
|
end
|
37
|
+
|
38
|
+
def calculator_selector
|
39
|
+
@calculator_selector ||= AutoSelector.new
|
40
|
+
end
|
36
41
|
end
|
37
42
|
end
|
38
43
|
end
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "auto_selector"
|
3
4
|
require_relative "brute_force"
|
4
5
|
require_relative "kronecker_substitution"
|
5
6
|
require_relative "multinomial_coefficients"
|
@@ -8,6 +9,8 @@ module Dicey
|
|
8
9
|
module SumFrequencyCalculators
|
9
10
|
# A simple testing facility for roll frequency calculators.
|
10
11
|
class TestRunner
|
12
|
+
AVAILABLE_CALCULATORS = AutoSelector::AVAILABLE_CALCULATORS
|
13
|
+
|
11
14
|
# These are manually calculated frequencies,
|
12
15
|
# with test cases for pretty much all variations of what this program can handle.
|
13
16
|
TEST_DATA = [
|
@@ -70,12 +73,11 @@ module Dicey
|
|
70
73
|
|
71
74
|
# Check all tests defined in {TEST_DATA} with every passed calculator.
|
72
75
|
#
|
73
|
-
# @param roll_calculators [Array<BaseCalculator>]
|
74
76
|
# @param report_style [Symbol] one of: +:full+, +:quiet+;
|
75
77
|
# +:quiet+ style does not output any text
|
76
78
|
# @return [Boolean] whether there are no failing tests
|
77
|
-
def call(*,
|
78
|
-
results = TEST_DATA.to_h { |test| run_test(test
|
79
|
+
def call(*, report_style:, **)
|
80
|
+
results = TEST_DATA.to_h { |test| run_test(test) }
|
79
81
|
full_report(results) if report_style == :full
|
80
82
|
results.values.none? do |test_result|
|
81
83
|
test_result.values.any? { FAILURE_RESULTS.include?(_1) }
|
@@ -86,13 +88,12 @@ module Dicey
|
|
86
88
|
|
87
89
|
# @param test [Array(Array<Integer, Array<Numeric>>, Hash{Numeric => Integer})]
|
88
90
|
# pair of a dice list definition and expected results
|
89
|
-
# @param calculators [Array<BaseCalculator>]
|
90
91
|
# @return [Array(Array<NumericDie>, Hash{BaseCalculator => Symbol})]
|
91
92
|
# result of running the test in a format suitable for +#to_h+
|
92
|
-
def run_test(test
|
93
|
+
def run_test(test)
|
93
94
|
dice = build_dice(test.first)
|
94
95
|
test_result =
|
95
|
-
|
96
|
+
AVAILABLE_CALCULATORS.each_with_object({}) do |calculator, hash|
|
96
97
|
hash[calculator] = run_test_on_calculator(calculator, dice, test.last)
|
97
98
|
end
|
98
99
|
[dice, test_result]
|
data/lib/dicey/version.rb
CHANGED
data/lib/dicey.rb
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# Try to load "vector_number" pre-emptively.
|
4
|
+
begin
|
5
|
+
require "vector_number"
|
6
|
+
rescue LoadError
|
7
|
+
# VectorNumber not available, sad
|
8
|
+
end
|
9
|
+
|
3
10
|
# A library for rolling dice and calculating roll frequencies.
|
4
11
|
module Dicey
|
5
12
|
# General error for Dicey.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dicey
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.16.
|
4
|
+
version: 0.16.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexandr Bulancov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-10-
|
11
|
+
date: 2025-10-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: vector_number
|
@@ -58,9 +58,11 @@ files:
|
|
58
58
|
- lib/dicey/output_formatters/json_formatter.rb
|
59
59
|
- lib/dicey/output_formatters/key_value_formatter.rb
|
60
60
|
- lib/dicey/output_formatters/list_formatter.rb
|
61
|
+
- lib/dicey/output_formatters/null_formatter.rb
|
61
62
|
- lib/dicey/output_formatters/yaml_formatter.rb
|
62
63
|
- lib/dicey/regular_die.rb
|
63
64
|
- lib/dicey/roller.rb
|
65
|
+
- lib/dicey/sum_frequency_calculators/auto_selector.rb
|
64
66
|
- lib/dicey/sum_frequency_calculators/base_calculator.rb
|
65
67
|
- lib/dicey/sum_frequency_calculators/brute_force.rb
|
66
68
|
- lib/dicey/sum_frequency_calculators/empirical.rb
|
@@ -75,9 +77,9 @@ licenses:
|
|
75
77
|
metadata:
|
76
78
|
homepage_uri: https://github.com/trinistr/dicey
|
77
79
|
bug_tracker_uri: https://github.com/trinistr/dicey/issues
|
78
|
-
documentation_uri: https://rubydoc.info/gems/dicey/0.16.
|
79
|
-
source_code_uri: https://github.com/trinistr/dicey/tree/v0.16.
|
80
|
-
changelog_uri: https://github.com/trinistr/dicey/blob/v0.16.
|
80
|
+
documentation_uri: https://rubydoc.info/gems/dicey/0.16.2
|
81
|
+
source_code_uri: https://github.com/trinistr/dicey/tree/v0.16.2
|
82
|
+
changelog_uri: https://github.com/trinistr/dicey/blob/v0.16.2/CHANGELOG.md
|
81
83
|
rubygems_mfa_required: 'true'
|
82
84
|
post_install_message:
|
83
85
|
rdoc_options:
|