dicey 0.15.0 → 0.15.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 +67 -17
- data/lib/dicey/die_foundry.rb +7 -5
- data/lib/dicey/distribution_properties_calculator.rb +147 -0
- data/lib/dicey/numeric_die.rb +2 -2
- data/lib/dicey/output_formatters/gnuplot_formatter.rb +12 -0
- data/lib/dicey/output_formatters/hash_formatter.rb +4 -1
- data/lib/dicey/output_formatters/key_value_formatter.rb +15 -1
- data/lib/dicey/rational_to_integer.rb +18 -0
- data/lib/dicey/roller.rb +5 -1
- data/lib/dicey/sum_frequency_calculators/base_calculator.rb +10 -2
- data/lib/dicey/sum_frequency_calculators/empirical.rb +1 -1
- data/lib/dicey/version.rb +1 -1
- 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: c18dbf421c9c19d438194ec9f63a9a22bfd18abb50d1f3b491a6e8067e3a04ae
|
4
|
+
data.tar.gz: e01eabaa58523f5fb2e1610ec671fc41a31f4aee7a583b64aa52d5bb9f083462
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 48759bc7919e8dbf699777a92735c586a8365f517c2c12d48cb725f12ada1fcfd6904e2c08e54ec7159267ad79003e7a1725a1e6e97694d7d4cbfb32198b2566
|
7
|
+
data.tar.gz: fa0cd4f20b6262cf927eb98cb5c296db2473910a27e2500323f80aeb5d2ac3224de143f67e0990fe555890535ff395d8514996934759984316d9fe22c4fb0e17
|
data/README.md
CHANGED
@@ -28,7 +28,8 @@ In seriousness, this program is mainly useful for calculating total frequency (p
|
|
28
28
|
- [Usage: API](#usage-api)
|
29
29
|
- [Dice](#dice)
|
30
30
|
- [Rolling](#rolling)
|
31
|
-
- [
|
31
|
+
- [Distribution calculators](#distribution-calculators)
|
32
|
+
- [Distribution properties](#distribution-properties)
|
32
33
|
- [Diving deeper](#diving-deeper)
|
33
34
|
- [Development](#development)
|
34
35
|
- [Contributing](#contributing)
|
@@ -75,7 +76,6 @@ gem "dicey", "~> 0.14"
|
|
75
76
|
|
76
77
|
**Dicey** is tested to work on CRuby 3.0+, latest JRuby and TruffleRuby. Compatible implementations should work too.
|
77
78
|
- JSON and YAML formatting require `json` and `yaml`.
|
78
|
-
- Decimal dice require `bigdecimal`.
|
79
79
|
|
80
80
|
Otherwise, there are no direct dependencies.
|
81
81
|
|
@@ -111,13 +111,13 @@ If probability is preferred, there is an option for that:
|
|
111
111
|
```sh
|
112
112
|
$ dicey 4 4 --result probabilities # or -r p for short
|
113
113
|
# D4+D4
|
114
|
-
2 =>
|
115
|
-
3 =>
|
116
|
-
4 =>
|
117
|
-
5 =>
|
118
|
-
6 =>
|
119
|
-
7 =>
|
120
|
-
8 =>
|
114
|
+
2 => 1/16
|
115
|
+
3 => 1/8
|
116
|
+
4 => 3/16
|
117
|
+
5 => 1/4
|
118
|
+
6 => 3/16
|
119
|
+
7 => 1/8
|
120
|
+
8 => 1/16
|
121
121
|
```
|
122
122
|
|
123
123
|
This shows that 5 will probably be rolled a quarter of the time.
|
@@ -230,9 +230,9 @@ You have a sudden urge to roll dice while only having boring integer dice at hom
|
|
230
230
|
|
231
231
|
Look no further than **roll** mode introduced in **Dicey** 0.12:
|
232
232
|
```sh
|
233
|
-
$ dicey 0.5,1.5,2.5 4 --mode roll # As always, can be abbreviated to -m r
|
234
|
-
# (
|
235
|
-
roll =>
|
233
|
+
$ dicey 0.5,1.0,1.5,2.0,2.5 4 --mode roll # As always, can be abbreviated to -m r
|
234
|
+
# (1/2,1,3/2,2,5/2)+D4
|
235
|
+
roll => 7/2 # You probably will get a different value here.
|
236
236
|
```
|
237
237
|
|
238
238
|
> [!NOTE]
|
@@ -333,10 +333,10 @@ die.roll
|
|
333
333
|
> [!NOTE]
|
334
334
|
> 💡 Randomness source is *global*, shared between all dice and probably not thread-safe.
|
335
335
|
|
336
|
-
###
|
336
|
+
### Distribution calculators
|
337
337
|
|
338
|
-
|
339
|
-
- `Dicey::SumFrequencyCalculators::KroneckerSubstitution` is the recommended calculator, able to handle all `Dicey::RegularDie`. It is very fast, calculating distribution for *100d6* in about 0.1 seconds on
|
338
|
+
Distribution calculators live in `Dicey::SumFrequencyCalculators` module. There are four calculators currently:
|
339
|
+
- `Dicey::SumFrequencyCalculators::KroneckerSubstitution` is the recommended calculator, able to handle all `Dicey::RegularDie`. It is very fast, calculating distribution for *100d6* in about 0.1 seconds on a laptop.
|
340
340
|
- `Dicey::SumFrequencyCalculators::MultinomialCoefficients` is specialized for repeated numeric dice, with performance only slightly worse. However, it is currently limited to dice with arithmetic sequences.
|
341
341
|
- `Dicey::SumFrequencyCalculators::BruteForce` is the most generic and slowest one, but can in principle work with any dice. Currently, it is also limited to `Dicey::NumericDie`, as it's unclear how to handle other values.
|
342
342
|
- `Dicey::SumFrequencyCalculators::Empirical`. This is more of a tool than a calculator. It can be interesting to play around with and see how practical results compare to theoretical ones.
|
@@ -345,7 +345,57 @@ Calculators inherit from `Dicey::SumFrequencyCalculators::BaseCalculator` and pr
|
|
345
345
|
- `#call(dice, result_type: {:frequencies | :probabilities}, **options) : Hash`
|
346
346
|
- `#valid_for?(dice) : Boolean`
|
347
347
|
|
348
|
-
See [
|
348
|
+
See [Diving deeper](#diving-deeper) for more details on limitations and complexity considerations.
|
349
|
+
|
350
|
+
### Distribution properties
|
351
|
+
|
352
|
+
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` already provides this functionality:
|
353
|
+
```rb
|
354
|
+
Dicey::DistributionPropertiesCalculator.new.call(
|
355
|
+
Dicey::SumFrequencyCalculators::KroneckerSubstitution.new.call(
|
356
|
+
Dicey::RegularDie.from_count(2, 3)
|
357
|
+
)
|
358
|
+
)
|
359
|
+
# =>
|
360
|
+
# {:mode=>[4],
|
361
|
+
# :min=>2,
|
362
|
+
# :max=>6,
|
363
|
+
# :total_range=>4,
|
364
|
+
# :mid_range=>4,
|
365
|
+
# :median=>4,
|
366
|
+
# :arithmetic_mean=>4,
|
367
|
+
# :expected_value=>4,
|
368
|
+
# :variance=>(4/3),
|
369
|
+
# :standard_deviation=>1.1547005383792515,
|
370
|
+
# :skewness=>0.0,
|
371
|
+
# :kurtosis=>(9/4),
|
372
|
+
# :excess_kurtosis=>(-3/4)}
|
373
|
+
```
|
374
|
+
|
375
|
+
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)?
|
376
|
+
```rb
|
377
|
+
Dicey::DistributionPropertiesCalculator.new.call(
|
378
|
+
Dicey::SumFrequencyCalculators::KroneckerSubstitution.new.call(
|
379
|
+
[Dicey::RegularDie.new(4), Dicey::NumericDie.new([1,3,4])]
|
380
|
+
)
|
381
|
+
)
|
382
|
+
# =>
|
383
|
+
# {:mode=>[5],
|
384
|
+
# :min=>2,
|
385
|
+
# :max=>8,
|
386
|
+
# :total_range=>6,
|
387
|
+
# :mid_range=>5,
|
388
|
+
# :median=>5,
|
389
|
+
# :arithmetic_mean=>5,
|
390
|
+
# :expected_value=>(31/6),
|
391
|
+
# :variance=>(101/36),
|
392
|
+
# :standard_deviation=>1.674979270186815,
|
393
|
+
# :skewness=>-0.15762965389465178,
|
394
|
+
# :kurtosis=>(23145/10201),
|
395
|
+
# :excess_kurtosis=>(-7458/10201)}
|
396
|
+
```
|
397
|
+
|
398
|
+
This disitrubution is obviosuly skewed (as can be immediately seen from non-zero skewness), with expected value no longer equal to mean. This is a mild example. It is easily possible to create a distribution with multiple local maxima and high skewness.
|
349
399
|
|
350
400
|
## Diving deeper
|
351
401
|
|
@@ -416,7 +466,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/trinis
|
|
416
466
|
- Tests cover the behavior and its interactions. 100% coverage *is not enough*, as it does not guarantee that all code paths are tested.
|
417
467
|
- Documentation is up-to-date: generate it with `rake docs` and read it.
|
418
468
|
- "*CHANGELOG.md*" lists the change if it has impact on users.
|
419
|
-
- "*README.md*" is updated if the feature should be visible there
|
469
|
+
- "*README.md*" is updated if the feature should be visible there.
|
420
470
|
|
421
471
|
## License
|
422
472
|
|
data/lib/dicey/die_foundry.rb
CHANGED
@@ -3,9 +3,13 @@
|
|
3
3
|
require_relative "numeric_die"
|
4
4
|
require_relative "regular_die"
|
5
5
|
|
6
|
+
require_relative "rational_to_integer"
|
7
|
+
|
6
8
|
module Dicey
|
7
9
|
# Helper class to define die definitions and automatically select the best one.
|
8
10
|
class DieFoundry
|
11
|
+
include RationalToInteger
|
12
|
+
|
9
13
|
# Regexp for matching a possible count.
|
10
14
|
PREFIX = /(?>(?<count>[1-9]\d*+)?d)?+/i
|
11
15
|
|
@@ -17,7 +21,7 @@ module Dicey
|
|
17
21
|
[/\A#{PREFIX}\(?(?<begin>-?\d++)(?>[-–—…]|\.{2,3})(?<end>-?\d++)\)?\z/, :range_mold].freeze,
|
18
22
|
# List of numbers goes into the NumericDie mold.
|
19
23
|
[/\A#{PREFIX}\(?(?<sides>-?\d++(?>,(?>-?\d++)?)+|,)\)?\z/, :weirdly_shaped_mold].freeze,
|
20
|
-
# Non-integers require
|
24
|
+
# Non-integers require special handling for precision.
|
21
25
|
[/\A#{PREFIX}\(?(?<sides>-?\d++(?>\.\d++)?(?>,(?>-?\d++(?>\.\d++)?)?)+|,)\)?\z/,
|
22
26
|
:weirdly_precise_mold].freeze,
|
23
27
|
# Anything else is spilled on the floor.
|
@@ -30,7 +34,7 @@ module Dicey
|
|
30
34
|
# - integer range (like "3-6" or "(-5..5)"), which produces a {NumericDie};
|
31
35
|
# - list of integers (like "3,4,5", "(-1,0,1)", or "2,"), which produces a {NumericDie};
|
32
36
|
# - list of decimal numbers (like "0.5,0.2,0.8" or "(2.0,)"), which produces a {NumericDie},
|
33
|
-
# but uses +
|
37
|
+
# but uses +Rational+ for values to maintain precise results.
|
34
38
|
#
|
35
39
|
# Any die definition can be prefixed with a count, like "2D6" or "1d1,3,5" to create an array.
|
36
40
|
# A plain "d" without an explicit count is ignored instead, creating a single die.
|
@@ -69,9 +73,7 @@ module Dicey
|
|
69
73
|
end
|
70
74
|
|
71
75
|
def weirdly_precise_mold(definition)
|
72
|
-
|
73
|
-
|
74
|
-
sides = definition[:sides].split(",").map { BigDecimal(_1) }
|
76
|
+
sides = definition[:sides].split(",").map { rational_to_integer(Rational(_1)) }
|
75
77
|
build_dice(NumericDie, definition[:count], sides)
|
76
78
|
end
|
77
79
|
|
@@ -0,0 +1,147 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rational_to_integer"
|
4
|
+
|
5
|
+
module Dicey
|
6
|
+
# Calculates distribution properties,
|
7
|
+
# also known as descriptive statistics when applied to a population sample.
|
8
|
+
#
|
9
|
+
# These are well-known properties such as:
|
10
|
+
# - min, max, mid-range;
|
11
|
+
# - mode(s), median, arithmetic mean;
|
12
|
+
# - important moments (expected value, variance, skewness, kurtosis).
|
13
|
+
#
|
14
|
+
# It is notable that most dice create symmetric distributions,
|
15
|
+
# which means that skewness is 0, while properties denoting center in some way
|
16
|
+
# (median, mean, ...) are all equal.
|
17
|
+
# Mode is often not unique, but includes this center.
|
18
|
+
class DistributionPropertiesCalculator
|
19
|
+
include RationalToInteger
|
20
|
+
|
21
|
+
# Calculate properties for a given distribution.
|
22
|
+
#
|
23
|
+
# Depending on values in the distribution, some properties may be undefined.
|
24
|
+
# In such cases, only mode(s) are guaranteed to be present.
|
25
|
+
#
|
26
|
+
# On empty distribution, returns an empty hash.
|
27
|
+
#
|
28
|
+
# @param distribution [Hash{Numeric => Numeric}, Hash{Any => Numeric}]
|
29
|
+
# distribution with pre-sorted keys
|
30
|
+
# @return [Hash{Symbol => Numeric, Array<Numeric>, Array<Array<Numeric>>}]
|
31
|
+
def call(distribution)
|
32
|
+
return {} if distribution.empty?
|
33
|
+
|
34
|
+
calculate_properties(distribution)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def calculate_properties(distribution)
|
40
|
+
outcomes = distribution.keys
|
41
|
+
weights = distribution.values
|
42
|
+
|
43
|
+
{
|
44
|
+
mode: mode(outcomes, weights),
|
45
|
+
modes: modes(distribution),
|
46
|
+
**range_characteristics(outcomes),
|
47
|
+
**median(outcomes),
|
48
|
+
**means(outcomes, weights),
|
49
|
+
**moments(distribution),
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def mode(outcomes, weights)
|
54
|
+
max_weight = weights.max
|
55
|
+
outcomes.select.with_index { |_, index| weights[index] == max_weight }
|
56
|
+
end
|
57
|
+
|
58
|
+
def modes(distribution)
|
59
|
+
# Split into chunks with different weights,
|
60
|
+
# then select those with higher weights than their neighbors.
|
61
|
+
chunks = distribution.chunk_while { |(_, w_1), (_, w_2)| w_1 == w_2 }.to_a
|
62
|
+
return [chunks.first.map(&:first)] if chunks.size == 1
|
63
|
+
|
64
|
+
modes = []
|
65
|
+
add_local_mode(modes, nil, chunks[0], chunks[1])
|
66
|
+
chunks.each_cons(3).each do |chunk_before, chunk, chunk_after|
|
67
|
+
add_local_mode(modes, chunk_before, chunk, chunk_after)
|
68
|
+
end
|
69
|
+
add_local_mode(modes, chunks[-2], chunks[-1], nil)
|
70
|
+
modes
|
71
|
+
end
|
72
|
+
|
73
|
+
def add_local_mode(modes, chunk_before, chunk, chunk_after)
|
74
|
+
if (!chunk_before || chunk_before.first.last < chunk.first.last) &&
|
75
|
+
(!chunk_after || chunk_after.first.last < chunk.first.last)
|
76
|
+
modes << chunk.map(&:first)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def range_characteristics(outcomes)
|
81
|
+
min = outcomes.min
|
82
|
+
max = outcomes.max
|
83
|
+
{
|
84
|
+
min: min,
|
85
|
+
max: max,
|
86
|
+
range_length: max - min,
|
87
|
+
mid_range: rational_to_integer(Rational(min + max, 2)),
|
88
|
+
}
|
89
|
+
rescue ArgumentError, TypeError, NoMethodError
|
90
|
+
# Outcomes are not comparable with each other, so a range can not be determined.
|
91
|
+
{}
|
92
|
+
end
|
93
|
+
|
94
|
+
def means(outcomes, _weights)
|
95
|
+
{
|
96
|
+
arithmetic_mean: rational_to_integer(Rational(outcomes.sum, outcomes.size)),
|
97
|
+
}
|
98
|
+
rescue ArgumentError, TypeError
|
99
|
+
# Outcomes are not summable with each other, means are meaningless.
|
100
|
+
{}
|
101
|
+
end
|
102
|
+
|
103
|
+
def median(outcomes)
|
104
|
+
outcomes = outcomes.sort
|
105
|
+
value =
|
106
|
+
if outcomes.size.odd?
|
107
|
+
outcomes[outcomes.size / 2]
|
108
|
+
else
|
109
|
+
Rational(outcomes[(outcomes.size / 2) - 1] + outcomes[outcomes.size / 2], 2)
|
110
|
+
end
|
111
|
+
{ median: value }
|
112
|
+
rescue ArgumentError, TypeError, NoMethodError
|
113
|
+
# Outcomes are not compatible with each other, so a median can not be determined.
|
114
|
+
{}
|
115
|
+
end
|
116
|
+
|
117
|
+
def moments(distribution)
|
118
|
+
total_weight = distribution.values.sum
|
119
|
+
expected_value = rational_to_integer(moment(distribution, total_weight, 1))
|
120
|
+
variance = rational_to_integer(moment(distribution, total_weight, 2) - (expected_value**2))
|
121
|
+
skewness =
|
122
|
+
rational_to_integer(moment(distribution, total_weight, 3, expected_value, variance))
|
123
|
+
kurtosis =
|
124
|
+
rational_to_integer(moment(distribution, total_weight, 4, expected_value, variance))
|
125
|
+
|
126
|
+
{
|
127
|
+
expected_value: expected_value,
|
128
|
+
variance: variance,
|
129
|
+
standard_deviation: Math.sqrt(variance),
|
130
|
+
skewness: skewness,
|
131
|
+
kurtosis: kurtosis,
|
132
|
+
excess_kurtosis: kurtosis ? kurtosis - 3 : nil,
|
133
|
+
}
|
134
|
+
rescue ArgumentError, TypeError, NoMethodError
|
135
|
+
# Outcomes are not compatible with each other, moments are fleeing.
|
136
|
+
{}
|
137
|
+
end
|
138
|
+
|
139
|
+
def moment(distribution, total_weight, degree, center = 0, variance = nil)
|
140
|
+
# With 0 variance, normalized moments are undefined.
|
141
|
+
return nil if variance == 0 # rubocop:disable Style/NumericPredicate
|
142
|
+
|
143
|
+
unnormalized = distribution.sum { |r, w| ((r - center)**degree) * Rational(w, total_weight) }
|
144
|
+
variance ? (unnormalized / (variance**(degree/2r))) : unnormalized
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
data/lib/dicey/numeric_die.rb
CHANGED
@@ -9,8 +9,8 @@ module Dicey
|
|
9
9
|
# @raise [DiceyError] if +sides_list+ contains non-numerical values or is empty
|
10
10
|
def initialize(sides_list)
|
11
11
|
if Range === sides_list
|
12
|
-
unless
|
13
|
-
raise DiceyError, "`#{sides_list.inspect}` is not a
|
12
|
+
unless Integer === sides_list.begin && Integer === sides_list.end
|
13
|
+
raise DiceyError, "`#{sides_list.inspect}` is not a valid range!"
|
14
14
|
end
|
15
15
|
else
|
16
16
|
sides_list.each do |value|
|
@@ -5,8 +5,20 @@ require_relative "key_value_formatter"
|
|
5
5
|
module Dicey
|
6
6
|
module OutputFormatters
|
7
7
|
# Formats a hash as a text file suitable for consumption by Gnuplot.
|
8
|
+
#
|
9
|
+
# Will transform Rational probabilities to Floats.
|
8
10
|
class GnuplotFormatter < KeyValueFormatter
|
9
11
|
SEPARATOR = " "
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def transform(key, value)
|
16
|
+
[derationalize(key), derationalize(value)]
|
17
|
+
end
|
18
|
+
|
19
|
+
def derationalize(value)
|
20
|
+
value.is_a?(Rational) ? value.to_f : value
|
21
|
+
end
|
10
22
|
end
|
11
23
|
end
|
12
24
|
end
|
@@ -1,20 +1,34 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "../rational_to_integer"
|
4
|
+
|
3
5
|
module Dicey
|
4
6
|
module OutputFormatters
|
5
7
|
# Base formatter for outputting lists of key-value pairs separated by newlines.
|
6
8
|
# Can add an optional description into the result.
|
7
9
|
# @abstract
|
8
10
|
class KeyValueFormatter
|
11
|
+
include RationalToInteger
|
12
|
+
|
9
13
|
# @param hash [Hash{Object => Object}]
|
10
14
|
# @param description [String] text to add as a comment.
|
11
15
|
# @return [String]
|
12
16
|
def call(hash, description = nil)
|
13
17
|
initial_string = description ? "# #{description}\n" : +""
|
14
18
|
hash.each_with_object(initial_string) do |(key, value), output|
|
15
|
-
output <<
|
19
|
+
output << line(transform(key, value)) << "\n"
|
16
20
|
end
|
17
21
|
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def transform(key, value)
|
26
|
+
[rational_to_integer(key), rational_to_integer(value)]
|
27
|
+
end
|
28
|
+
|
29
|
+
def line((key, value))
|
30
|
+
"#{key}#{self.class::SEPARATOR}#{value}"
|
31
|
+
end
|
18
32
|
end
|
19
33
|
end
|
20
34
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dicey
|
4
|
+
# @api private
|
5
|
+
# Mix-in for converting rationals with denominator of 1 to integers.
|
6
|
+
module RationalToInteger
|
7
|
+
private
|
8
|
+
|
9
|
+
# Convert +value+ to +Integer+ if it's a +Rational+ with denominator of 1.
|
10
|
+
# Otherwise, return +value+ as-is.
|
11
|
+
#
|
12
|
+
# @value [Numeric, Any]
|
13
|
+
# @return [Numeric, Integer, Any]
|
14
|
+
def rational_to_integer(value)
|
15
|
+
(Rational === value && value.denominator == 1) ? value.numerator : value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/dicey/roller.rb
CHANGED
@@ -2,9 +2,13 @@
|
|
2
2
|
|
3
3
|
require_relative "die_foundry"
|
4
4
|
|
5
|
+
require_relative "rational_to_integer"
|
6
|
+
|
5
7
|
module Dicey
|
6
8
|
# Let the dice roll!
|
7
9
|
class Roller
|
10
|
+
include RationalToInteger
|
11
|
+
|
8
12
|
# @param arguments [Array<String>] die definitions
|
9
13
|
# @param format [#call] formatter for output
|
10
14
|
# @return [nil]
|
@@ -15,7 +19,7 @@ module Dicey
|
|
15
19
|
dice = arguments.flat_map { |definition| die_foundry.cast(definition) }
|
16
20
|
result = dice.sum(&:roll)
|
17
21
|
|
18
|
-
format.call({ "roll" => result }, AbstractDie.describe(dice))
|
22
|
+
format.call({ "roll" => rational_to_integer(result) }, AbstractDie.describe(dice))
|
19
23
|
end
|
20
24
|
|
21
25
|
private
|
@@ -5,6 +5,14 @@ module Dicey
|
|
5
5
|
module SumFrequencyCalculators
|
6
6
|
# Base frequencies calculator.
|
7
7
|
#
|
8
|
+
# *Result types:*
|
9
|
+
# - +:frequencies+ (default)
|
10
|
+
# - +:probabilities+
|
11
|
+
#
|
12
|
+
# By default, returns frequencies as they are easier to calculate and
|
13
|
+
# can be represented with integers.
|
14
|
+
# Probabilities are calculated using +Rational+ numbers to return exact results.
|
15
|
+
#
|
8
16
|
# *Options:*
|
9
17
|
#
|
10
18
|
# Calculators may have calculator-specific options,
|
@@ -20,7 +28,7 @@ module Dicey
|
|
20
28
|
# @param dice [Enumerable<AbstractDie>]
|
21
29
|
# @param result_type [Symbol] one of {RESULT_TYPES}
|
22
30
|
# @param options [Hash{Symbol => Any}] calculator-specific options
|
23
|
-
# @return [Hash{Numeric => Numeric}] frequencies
|
31
|
+
# @return [Hash{Numeric => Numeric}] frequencies or probabilities for each outcome
|
24
32
|
# @raise [DiceyError] if +result_type+ is invalid
|
25
33
|
# @raise [DiceyError] if dice list is invalid for the calculator
|
26
34
|
# @raise [DiceyError] if calculator returned obviously wrong results
|
@@ -92,7 +100,7 @@ module Dicey
|
|
92
100
|
frequencies
|
93
101
|
else
|
94
102
|
total = frequencies.values.sum
|
95
|
-
frequencies.transform_values { _1
|
103
|
+
frequencies.transform_values { Rational(_1, total) }
|
96
104
|
end
|
97
105
|
end
|
98
106
|
end
|
@@ -27,7 +27,7 @@ module Dicey
|
|
27
27
|
def calculate(dice, rolls: N)
|
28
28
|
statistics = rolls.times.with_object(Hash.new(0)) { |_, hash| hash[dice.sum(&:roll)] += 1 }
|
29
29
|
total_results = dice.map(&:sides_num).reduce(:*)
|
30
|
-
statistics.transform_values { (_1 * total_results
|
30
|
+
statistics.transform_values { Rational(_1 * total_results, rolls) }
|
31
31
|
end
|
32
32
|
|
33
33
|
def verify_result(*)
|
data/lib/dicey/version.rb
CHANGED
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.15.
|
4
|
+
version: 0.15.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-
|
11
|
+
date: 2025-10-08 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: |
|
14
14
|
Dicey provides a CLI executable and a Ruby API for fast calculation of
|
@@ -35,6 +35,7 @@ files:
|
|
35
35
|
- lib/dicey/cli/blender.rb
|
36
36
|
- lib/dicey/cli/options.rb
|
37
37
|
- lib/dicey/die_foundry.rb
|
38
|
+
- lib/dicey/distribution_properties_calculator.rb
|
38
39
|
- lib/dicey/numeric_die.rb
|
39
40
|
- lib/dicey/output_formatters/gnuplot_formatter.rb
|
40
41
|
- lib/dicey/output_formatters/hash_formatter.rb
|
@@ -42,6 +43,7 @@ files:
|
|
42
43
|
- lib/dicey/output_formatters/key_value_formatter.rb
|
43
44
|
- lib/dicey/output_formatters/list_formatter.rb
|
44
45
|
- lib/dicey/output_formatters/yaml_formatter.rb
|
46
|
+
- lib/dicey/rational_to_integer.rb
|
45
47
|
- lib/dicey/regular_die.rb
|
46
48
|
- lib/dicey/roller.rb
|
47
49
|
- lib/dicey/sum_frequency_calculators/base_calculator.rb
|
@@ -58,9 +60,9 @@ licenses:
|
|
58
60
|
metadata:
|
59
61
|
homepage_uri: https://github.com/trinistr/dicey
|
60
62
|
bug_tracker_uri: https://github.com/trinistr/dicey/issues
|
61
|
-
documentation_uri: https://rubydoc.info/gems/dicey/0.15.
|
62
|
-
source_code_uri: https://github.com/trinistr/dicey/tree/v0.15.
|
63
|
-
changelog_uri: https://github.com/trinistr/dicey/blob/v0.15.
|
63
|
+
documentation_uri: https://rubydoc.info/gems/dicey/0.15.2
|
64
|
+
source_code_uri: https://github.com/trinistr/dicey/tree/v0.15.2
|
65
|
+
changelog_uri: https://github.com/trinistr/dicey/blob/v0.15.2/CHANGELOG.md
|
64
66
|
rubygems_mfa_required: 'true'
|
65
67
|
post_install_message:
|
66
68
|
rdoc_options:
|