dicey 0.18.0 โ 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +23 -13
- data/lib/dicey/abstract_die.rb +41 -3
- data/lib/dicey/cli/calculator_test_runner.rb +14 -6
- data/lib/dicey/die_foundry.rb +63 -31
- data/lib/dicey/distribution_calculators/base_calculator.rb +51 -9
- data/lib/dicey/distribution_calculators/binomial.rb +2 -3
- data/lib/dicey/distribution_calculators/empirical.rb +1 -5
- data/lib/dicey/distribution_calculators/iterative.rb +2 -6
- data/lib/dicey/distribution_calculators/multinomial_coefficients.rb +2 -2
- data/lib/dicey/distribution_calculators/trivial.rb +1 -3
- data/lib/dicey/distribution_properties_calculator.rb +15 -12
- data/lib/dicey/mixins/missing_math.rb +2 -0
- data/lib/dicey/mixins/rational_to_integer.rb +1 -3
- data/lib/dicey/mixins/vectorize_dice.rb +11 -6
- data/lib/dicey/numeric_die.rb +21 -0
- data/lib/dicey/static_die.rb +42 -0
- data/lib/dicey/version.rb +1 -1
- data/lib/dicey.rb +3 -1
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c3e35746de9367cc5f314d235013036e23347addba832d108612e96a5d0b556
|
|
4
|
+
data.tar.gz: 7c269e9c93c22370780a7dec18e8439a433c07c6f67dad5d2574453196f3b82e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ac83738ab8b68e73ce66003c524d38020e1cd6157d833de26567c9c0fb89e84ae036833cdce81747e4ef672432a36c62c12802b27f6646f1f860d3565d28ead
|
|
7
|
+
data.tar.gz: 6131b2bd5b4d1fadb7a9a9fbb458cbea0c7e63839ac6a0819c0ff29c58449a873626c2acc3d54e04ddd9ae48fd6084d461ac4f0518c723cc5288ded82aa416c1
|
data/README.md
CHANGED
|
@@ -70,7 +70,7 @@ gem install dicey
|
|
|
70
70
|
|
|
71
71
|
Or, if using Bundler, add it to your `Gemfile`:
|
|
72
72
|
```rb
|
|
73
|
-
gem "dicey", "~> 0.
|
|
73
|
+
gem "dicey", "~> 0.19"
|
|
74
74
|
```
|
|
75
75
|
|
|
76
76
|
> [!TIP]
|
|
@@ -87,7 +87,7 @@ gem "dicey", "~> 0.17"
|
|
|
87
87
|
|
|
88
88
|
## Usage: CLI (command line)
|
|
89
89
|
|
|
90
|
-
Following examples assume that `dicey` (or `dicey-to-gnuplot`) is executable and is in `$PATH`.
|
|
90
|
+
Following examples assume that `dicey` (or `dicey-to-gnuplot`) is executable and is in `$PATH`. This should be the case if installed through RubyGems or Bundler.
|
|
91
91
|
|
|
92
92
|
> [!NOTE]
|
|
93
93
|
> ๐ก Run `dicey --help` to get a list of all possible options.
|
|
@@ -259,19 +259,28 @@ Wind, wood and lightning in equal proportion it is! Your enemies will tremble!
|
|
|
259
259
|
|
|
260
260
|
There are four *main* ways to define dice:
|
|
261
261
|
- *"5", "25", or "525"*: a single positive integer makes a regular die (like a D20).
|
|
262
|
-
- *"3
|
|
263
|
-
- Accepted separators: "
|
|
262
|
+
- *"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.
|
|
263
|
+
- Accepted separators: "..", "...", "โ" (en dash), "โ" (em dash), "โฆ" (ellipsis).
|
|
264
264
|
- *"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.
|
|
265
265
|
- Lists can end in a comma, allowing single-number lists.
|
|
266
266
|
- There is no difference between equal decimal and fractional representations of numbers.
|
|
267
|
-
- *"1,1.5,Two", "(๐,๐งก,๐,๐)" or "('1','(bracket)')"*: a list of strings and numbers separated by commas, possibly in round brackets, makes an arbitrary die.
|
|
267
|
+
- *"1,1.5,Two", "(๐,๐งก,๐,๐)", or "('1','(bracket)')"*: a list of strings and numbers separated by commas, possibly in round brackets, makes an arbitrary die.
|
|
268
268
|
- Lists can end in a comma, allowing single-string lists.
|
|
269
|
-
- Single (') or double (") quotes can be used to include other quotes
|
|
269
|
+
- Single (') or double (") quotes can be used to include other quotes, round brackets and ambigious characters in a string. Otherwise, they are prohibited. Commas are always prohibited.
|
|
270
270
|
- Quotes can also be used to treat numbers as strings.
|
|
271
|
+
- *"+5", "+WIZ", or "-2.5"*: a single value prefixed with a sign makes a die with exactly that value.
|
|
272
|
+
- Accepted signs: "+", "-", "โ" (minus).
|
|
273
|
+
- Single (') or double (") quotes can be used to include other quotes, round brackets and ambigious characters in a string. Otherwise, they are prohibited. Commas are always prohibited.
|
|
274
|
+
- Non-numeric values are only accepted with a "+".
|
|
271
275
|
|
|
272
|
-
*"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 "-".
|
|
276
|
+
*"D6", "d(-1,3)", "d2..4", "d+5", 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 "-".
|
|
273
277
|
|
|
274
|
-
*"2D6", "5d-1,3", "277D(2..4)", or "3d๐,โ ๏ธ,โฅ๏ธ,โฃ๏ธ,โฆ๏ธ,โ๏ธ"*: any definitions can be prefixed with "*N*d" or "*N*D", where *N* is a positive integer. This creates *N* copies of the die.
|
|
278
|
+
*"2D6", "5d-1,3", "277D(2..4)", "3d+5", or "3d๐,โ ๏ธ,โฅ๏ธ,โฃ๏ธ,โฆ๏ธ,โ๏ธ"*: any definitions can be prefixed with "*N*d" or "*N*D", where *N* is a positive integer. This creates *N* copies of the die.
|
|
279
|
+
|
|
280
|
+
*"5+3", "2d6+4", "2D3โ5-1", or "d(๐,๐งก)+โฅ๏ธ"*: any definitions can be suffixed with a single signed value (see above), except for a single signed value itself. This adds the constant factor to result once.
|
|
281
|
+
|
|
282
|
+
> [!NOTE]
|
|
283
|
+
> ๐ก If your die definition starts with a negative number, it can be bracketed, prefixed with "d", or put after "--" pseudo-argument to avoid being processed as an option.
|
|
275
284
|
|
|
276
285
|
## Usage: API
|
|
277
286
|
|
|
@@ -281,10 +290,11 @@ There are four *main* ways to define dice:
|
|
|
281
290
|
|
|
282
291
|
### Dice
|
|
283
292
|
|
|
284
|
-
There are
|
|
293
|
+
There are 4 classes of dice currently:
|
|
285
294
|
- `Dicey::AbstractDie` is the base class for other dice, but can be used on its own. It has no restrictions on values of sides.
|
|
286
295
|
- `Dicey::NumericDie` behaves much the same as `Dicey::AbstractDie` (being its subclass), except for checking that all values are instances of `Numeric`. It can be initialized with an Array or Range.
|
|
287
296
|
- `Dicey::RegularDie` is a specialized subclass of `Dicey::NumericDie`. It is defined by a single integer *N* which is expanded to a range (1..*N*).
|
|
297
|
+
- `Dicey::StaticDie` is a specialized subclass of `Dicey::AbstractDie`. It accepts any single value and always produces that value.
|
|
288
298
|
|
|
289
299
|
All dice classes have constructor methods aside from `.new`:
|
|
290
300
|
- `.from_list` takes a list of definitions and calls `.new` with each one;
|
|
@@ -293,7 +303,7 @@ All dice classes have constructor methods aside from `.new`:
|
|
|
293
303
|
See [Diving deeper](#diving-deeper) for more theoretical information.
|
|
294
304
|
|
|
295
305
|
> [!NOTE]
|
|
296
|
-
> ๐ก Using `Float` values is liable to cause precision issues. Due to in
|
|
306
|
+
> ๐ก Using `Float` values is liable to cause precision issues. Due to built-in result verification, this **will** raise errors. Use `Rational` or `BigDecimal` instead.
|
|
297
307
|
|
|
298
308
|
#### DieFoundry
|
|
299
309
|
|
|
@@ -373,6 +383,9 @@ die.roll
|
|
|
373
383
|
> [!NOTE]
|
|
374
384
|
> ๐ก Randomness source is *global*, shared between all dice and probably not thread-safe.
|
|
375
385
|
|
|
386
|
+
> [!NOTE]
|
|
387
|
+
> ๐ก `Dicey::StaticDie` has no impact on roll randomness, unlike other single-sided dice.
|
|
388
|
+
|
|
376
389
|
### Distribution calculators
|
|
377
390
|
|
|
378
391
|
Distribution calculators live in `Dicey::DistributionCalculators` module. There are three main calculators currently.
|
|
@@ -467,9 +480,6 @@ For a further discussion of calculations, it is important to understand which cl
|
|
|
467
480
|
- **Numeric** die is limited by having sides confined to โ (or โ if you are feeling particularly adventurous).
|
|
468
481
|
- **Abstract** die is unlimited!
|
|
469
482
|
|
|
470
|
-
> [!NOTE]
|
|
471
|
-
> ๐ก 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.
|
|
472
|
-
|
|
473
483
|
Currently, three algorithms for calculating distributions are implemented, with different possibilities and trade-offs.
|
|
474
484
|
|
|
475
485
|
> [!NOTE]
|
data/lib/dicey/abstract_die.rb
CHANGED
|
@@ -16,6 +16,10 @@ module Dicey
|
|
|
16
16
|
# {.srand} can be used to (re)set the internal randomizer's state for all dice,
|
|
17
17
|
# allowing to reproduce the same sequence of rolls (if it was done with a known state).
|
|
18
18
|
class AbstractDie
|
|
19
|
+
# Matcher to check whether string needs quoting.
|
|
20
|
+
# @see DieFoundry::SPECIAL
|
|
21
|
+
STRING_TO_QUOTE = /["',()+โ-]/
|
|
22
|
+
|
|
19
23
|
# Yes, class variable is actually useful here.
|
|
20
24
|
# TODO: Allow supplying a custom Random.
|
|
21
25
|
# rubocop:disable Style/ClassVars
|
|
@@ -49,7 +53,12 @@ module Dicey
|
|
|
49
53
|
def self.describe(dice)
|
|
50
54
|
return dice.to_s if AbstractDie === dice
|
|
51
55
|
|
|
52
|
-
dice.
|
|
56
|
+
dice.map(&:to_s).reduce { |string, die|
|
|
57
|
+
die_string = die.to_s
|
|
58
|
+
string << "+" unless die_string.match?(/\A[+-]/)
|
|
59
|
+
string << die_string
|
|
60
|
+
string
|
|
61
|
+
}.to_s
|
|
53
62
|
end
|
|
54
63
|
|
|
55
64
|
# Create a bunch of different dice at once from a list of definitions.
|
|
@@ -85,7 +94,7 @@ module Dicey
|
|
|
85
94
|
# @raise [DiceyError] if +sides_list+ is empty
|
|
86
95
|
def initialize(sides_list)
|
|
87
96
|
@sides_list = sides_list.to_a
|
|
88
|
-
@sides_list = @sides_list.dup if @sides_list.equal?(sides_list)
|
|
97
|
+
@sides_list = @sides_list.dup if @sides_list.equal?(sides_list) && !@sides_list.frozen?
|
|
89
98
|
raise DiceyError, "dice must have at least one side!" if @sides_list.empty?
|
|
90
99
|
|
|
91
100
|
@sides_list.freeze
|
|
@@ -121,10 +130,12 @@ module Dicey
|
|
|
121
130
|
# Return a string representing the die.
|
|
122
131
|
#
|
|
123
132
|
# Default representation is a list of sides in round brackets.
|
|
133
|
+
# Strings are quoted.
|
|
124
134
|
#
|
|
125
135
|
# @return [String]
|
|
126
136
|
def to_s
|
|
127
|
-
|
|
137
|
+
sides = @sides_list.map { |side| side_to_s(side) }
|
|
138
|
+
(@sides_list.size > 1) ? "(#{sides.join(",")})" : "(#{sides.first},)"
|
|
128
139
|
end
|
|
129
140
|
|
|
130
141
|
# Determine if this die and the other one have the same list of sides.
|
|
@@ -159,8 +170,35 @@ module Dicey
|
|
|
159
170
|
[self.class, @sides_list].hash
|
|
160
171
|
end
|
|
161
172
|
|
|
173
|
+
# Whether all sides of this die are +Numeric+.
|
|
174
|
+
#
|
|
175
|
+
# @return [Boolean]
|
|
176
|
+
def numeric?
|
|
177
|
+
return @numeric if defined?(@numeric)
|
|
178
|
+
|
|
179
|
+
@numeric = @sides_list.all?(Numeric)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Freezes +self+ (if not already frozen); returns +self+.
|
|
183
|
+
#
|
|
184
|
+
# Performs computations that memoize results before freezing.
|
|
185
|
+
def freeze
|
|
186
|
+
numeric? unless frozen?
|
|
187
|
+
super
|
|
188
|
+
end
|
|
189
|
+
|
|
162
190
|
private
|
|
163
191
|
|
|
192
|
+
# @param side [#to_s]
|
|
193
|
+
# @return [String]
|
|
194
|
+
def side_to_s(side)
|
|
195
|
+
if String === side && side.match?(STRING_TO_QUOTE)
|
|
196
|
+
side.include?('"') ? "'#{side}'" : "\"#{side}\""
|
|
197
|
+
else
|
|
198
|
+
side.to_s
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
164
202
|
# @param other [AbstractDie]
|
|
165
203
|
# @return [Boolean]
|
|
166
204
|
def same_sides?(other)
|
|
@@ -24,7 +24,10 @@ module Dicey
|
|
|
24
24
|
[[2, 2, 2, 2], { 4 => 1, 5 => 4, 6 => 6, 7 => 4, 8 => 1 }],
|
|
25
25
|
[[1, 2, 3], { 3 => 1, 4 => 2, 5 => 2, 6 => 1 }],
|
|
26
26
|
[[3, 2, 1], { 3 => 1, 4 => 2, 5 => 2, 6 => 1 }],
|
|
27
|
+
[["+5"], { 5 => 1 }],
|
|
28
|
+
[["+5", "-12.2"], { -7.2r => 1 }],
|
|
27
29
|
[[[0], 1], { 1 => 1 }],
|
|
30
|
+
[[[2, 3, 4], "-3"], { -1 => 1, 0 => 1, 1 => 1 }],
|
|
28
31
|
[[4, 6], { 2 => 1, 3 => 2, 4 => 3, 5 => 4, 6 => 4, 7 => 4, 8 => 3, 9 => 2, 10 => 1 }],
|
|
29
32
|
[[[3, 17, 21]], { 3 => 1, 17 => 1, 21 => 1 }],
|
|
30
33
|
[[[3, 3, 3, 3, 3, 5, 5, 5]], { 3 => 5, 5 => 3 }],
|
|
@@ -42,10 +45,9 @@ module Dicey
|
|
|
42
45
|
{ Complex(1, 1) => 1, Complex(2, 1) => 1, Complex(3, 1) => 1,
|
|
43
46
|
Complex(1, 2) => 1, Complex(2, 2) => 1, Complex(3, 2) => 1,
|
|
44
47
|
Complex(1, 3) => 1, Complex(2, 3) => 1, Complex(3, 3) => 1 }],
|
|
48
|
+
[[2, 2, "+2"], { 4 => 1, 5 => 2, 6 => 1 }],
|
|
45
49
|
*(
|
|
46
|
-
# :
|
|
47
|
-
if defined?(VectorNumber)
|
|
48
|
-
# :nocov:
|
|
50
|
+
if defined?(VectorNumber) # simplecov:disable
|
|
49
51
|
[
|
|
50
52
|
[[["s", "a", "d", 33]],
|
|
51
53
|
{ VectorNumber["s"] => 1, VectorNumber["a"] => 1, VectorNumber["d"] => 1, 33 => 1 }],
|
|
@@ -58,6 +60,10 @@ module Dicey
|
|
|
58
60
|
VectorNumber["s"] * 2 => 1, VectorNumber["a"] * 2 => 1, 8 => 1,
|
|
59
61
|
VectorNumber["s", "a"] => 2, VectorNumber["s", 4] => 2, VectorNumber["a", 4] => 2,
|
|
60
62
|
}],
|
|
63
|
+
[[[1, 3], "+d"], { VectorNumber[1, "d"] => 1, VectorNumber[3, "d"] => 1 }],
|
|
64
|
+
[[%w[A B], %w[A B], "+A"],
|
|
65
|
+
{ VectorNumber["A"] * 3 => 1, VectorNumber["A", "B", "A"] => 2,
|
|
66
|
+
VectorNumber["B", "B", "A"] => 1 }],
|
|
61
67
|
]
|
|
62
68
|
end
|
|
63
69
|
),
|
|
@@ -93,8 +99,8 @@ module Dicey
|
|
|
93
99
|
def run_test(test)
|
|
94
100
|
dice = build_dice(test.first)
|
|
95
101
|
test_result =
|
|
96
|
-
AVAILABLE_CALCULATORS.
|
|
97
|
-
|
|
102
|
+
AVAILABLE_CALCULATORS.to_h do |calculator|
|
|
103
|
+
[calculator, run_test_on_calculator(calculator, dice, test.last)]
|
|
98
104
|
end
|
|
99
105
|
[dice, test_result]
|
|
100
106
|
end
|
|
@@ -104,9 +110,11 @@ module Dicey
|
|
|
104
110
|
# @param definition [Array<Integer, Array<Integer>>]
|
|
105
111
|
# @return [Array<AbstractDie>]
|
|
106
112
|
def build_dice(definition)
|
|
107
|
-
definition.
|
|
113
|
+
definition.flat_map do |die_def|
|
|
108
114
|
if die_def.is_a?(Integer)
|
|
109
115
|
RegularDie.new(die_def)
|
|
116
|
+
elsif die_def.is_a?(String)
|
|
117
|
+
DieFoundry.new.cast(die_def)
|
|
110
118
|
elsif die_def.all?(Numeric)
|
|
111
119
|
NumericDie.new(die_def)
|
|
112
120
|
else
|
data/lib/dicey/die_foundry.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "abstract_die"
|
|
3
4
|
require_relative "numeric_die"
|
|
4
5
|
require_relative "regular_die"
|
|
6
|
+
require_relative "static_die"
|
|
5
7
|
|
|
6
8
|
require_relative "mixins/rational_to_integer"
|
|
7
9
|
|
|
@@ -11,60 +13,81 @@ module Dicey
|
|
|
11
13
|
class DieFoundry
|
|
12
14
|
include Mixins::RationalToInteger
|
|
13
15
|
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
#
|
|
19
|
-
|
|
20
|
-
#
|
|
21
|
-
|
|
16
|
+
# Special characters disallowed in unquoted strings.
|
|
17
|
+
# @see AbstractDie::STRING_TO_QUOTE
|
|
18
|
+
SPECIAL = %{"',()+โ-}
|
|
19
|
+
|
|
20
|
+
# Pattern for an integer number.
|
|
21
|
+
INTEGER = "(?:-?\\d++)"
|
|
22
|
+
# Pattern for a possibly fractional number.
|
|
23
|
+
NUMBER = "(?:-?\\d++(?:/\\d++|\\.\\d++)?)"
|
|
24
|
+
# Pattern for an "arbitrary" string or number.
|
|
25
|
+
STRING = %{(?:(?<string>[^#{SPECIAL}]++)|"(?<string>[^",]++)"|'(?<string>[^',]++)')}.freeze
|
|
26
|
+
# Pattern for a number or string (allowing negative numbers).
|
|
27
|
+
VALUE = "(?:#{NUMBER}(?=[,)+โ-]|\\z)|#{STRING})".freeze
|
|
28
|
+
|
|
29
|
+
# Pattern for matching a possible count.
|
|
30
|
+
COUNT = "(?:(?<count>[1-9]\\d*+)?+[Dd])?+"
|
|
31
|
+
# Pattern for matching an optional constant factor.
|
|
32
|
+
CONSTANT = "(?<constant>(?<sign>[+โ-])(?<constant_value>#{NUMBER})|" \
|
|
33
|
+
"(?<sign>\\+)(?<constant_value>#{STRING}))".freeze
|
|
34
|
+
|
|
35
|
+
molder = ->(pattern) { /\A#{COUNT}(?:#{pattern}|\(#{pattern}\))#{CONSTANT}?\z/ }
|
|
22
36
|
|
|
23
37
|
# Possible molds for the dice. They are matched in the order as written.
|
|
24
38
|
MOLDS = [
|
|
25
39
|
# Positive integer goes into the RegularDie mold.
|
|
26
|
-
[/\A#{
|
|
40
|
+
[/\A#{COUNT}(?<sides>[1-9]\d*+)#{CONSTANT}?\z/, :regular_mold],
|
|
27
41
|
# Integer range goes into the NumericDie mold.
|
|
28
|
-
[
|
|
29
|
-
:range_mold].freeze,
|
|
42
|
+
[molder.("(?<begin>#{INTEGER})(?:[โโโฆ]|\\.{2,3})(?<end>#{INTEGER})"), :range_mold],
|
|
30
43
|
# List of numbers goes into the NumericDie mold.
|
|
31
|
-
[
|
|
32
|
-
:weirdly_shaped_mold].freeze,
|
|
44
|
+
[molder.("(?<sides>#{INTEGER}(?:(?:,#{INTEGER})++,?+|,))"), :weirdly_shaped_mold],
|
|
33
45
|
# Non-integers require special handling for precision.
|
|
34
|
-
[
|
|
35
|
-
:weirdly_precise_mold].freeze,
|
|
46
|
+
[molder.("(?<sides>#{NUMBER}(?:(?:,#{NUMBER})++,?+|,))"), :weirdly_precise_mold],
|
|
36
47
|
# Lists of stuff are broken into AbstractDie.
|
|
37
|
-
[
|
|
48
|
+
[molder.("(?<sides>#{VALUE}(?:(?:,#{VALUE})++,?+|,))"), :cursed_mold],
|
|
49
|
+
# Sign-prefixed value goes into the StaticDie mold.
|
|
50
|
+
[/\A#{COUNT}(?:#{CONSTANT}|\(#{CONSTANT}\))\z/, :static_mold],
|
|
38
51
|
# Anything else is spilled on the floor.
|
|
39
|
-
].freeze
|
|
52
|
+
].each(&:freeze).freeze
|
|
40
53
|
|
|
41
54
|
# Cast a die definition into a mold to make a die.
|
|
42
55
|
#
|
|
43
56
|
# Following definitions are recognized:
|
|
44
57
|
# - positive integer (like "6" or "20"), which produces a {RegularDie};
|
|
45
|
-
# - integer range (like "3
|
|
58
|
+
# - integer range (like "3โ6" or "(-5..5)"), which produces a {NumericDie};
|
|
46
59
|
# - list of integers (like "(3,4,5)", "-1,0,1", or "2,"), which produces a {NumericDie};
|
|
47
60
|
# - list of decimal numbers (like "0.5,0.2,0.8" or "(2.0,)"), which produces a {NumericDie},
|
|
48
61
|
# but uses +Rational+ for values to maintain precise results;
|
|
49
62
|
# - list of strings, possibly mixed with numbers (like "0.5,asdf" or "(๐,โ ๏ธ,โฅ๏ธ,โฃ๏ธ,โฆ๏ธ,โ๏ธ)"),
|
|
50
63
|
# which produces an {AbstractDie} with numbers treated the same as in previous cases,
|
|
51
64
|
# and other or quoted values treated as Strings.
|
|
65
|
+
# - signed value (like "+3", "-3.6" or "(+ABC)"), which produces a {StaticDie},
|
|
66
|
+
# non-numeric values are only allowed as positive values;
|
|
52
67
|
#
|
|
53
68
|
# Any die definition can be prefixed with a count, like "2D6" or "1d1,3,5" to create an array.
|
|
54
|
-
# A plain "d" without an explicit count is ignored instead, creating a single die.
|
|
69
|
+
# A plain "d"/"D" without an explicit count is ignored instead, creating a single die.
|
|
70
|
+
#
|
|
71
|
+
# All die definitions (aside from plain signed value) can be suffixed with a signed value
|
|
72
|
+
# to add or subtract from the result, like "2D6+3" or "5dA,B,C+C".
|
|
73
|
+
# Only numbers can be subtracted.
|
|
55
74
|
#
|
|
56
75
|
# @param definition [String] die shape
|
|
57
76
|
# @return [AbstractDie, Array<AbstractDie>]
|
|
58
77
|
# @raise [DiceyError] if no mold fits the definition
|
|
59
78
|
def call(definition)
|
|
60
79
|
matched, name =
|
|
61
|
-
MOLDS.
|
|
80
|
+
MOLDS.find do |(shape, mold)|
|
|
62
81
|
match = shape.match(definition)
|
|
63
82
|
break [match, mold] if match
|
|
64
83
|
end
|
|
65
84
|
raise DiceyError, "can not cast die from `#{definition}`!" unless name
|
|
66
85
|
|
|
67
|
-
|
|
86
|
+
if matched[:constant] && name != :static_mold
|
|
87
|
+
[__send__(name, matched), static_mold(matched, ignore_count: true)].flatten
|
|
88
|
+
else
|
|
89
|
+
__send__(name, matched)
|
|
90
|
+
end
|
|
68
91
|
end
|
|
69
92
|
|
|
70
93
|
alias cast call
|
|
@@ -93,19 +116,28 @@ module Dicey
|
|
|
93
116
|
|
|
94
117
|
def cursed_mold(definition)
|
|
95
118
|
sides = definition[:sides].split(",")
|
|
96
|
-
sides.map!
|
|
97
|
-
case side
|
|
98
|
-
when /\A#{INTEGER}\z/o
|
|
99
|
-
side.to_i
|
|
100
|
-
when /\A#{FRACTION}\z/o
|
|
101
|
-
rational_to_integer(Rational(side))
|
|
102
|
-
else
|
|
103
|
-
side.match(STRING)[:side]
|
|
104
|
-
end
|
|
105
|
-
end
|
|
119
|
+
sides.map! { |side| parse_value(side) }
|
|
106
120
|
build_dice(AbstractDie, definition[:count], sides)
|
|
107
121
|
end
|
|
108
122
|
|
|
123
|
+
def static_mold(definition, ignore_count: false)
|
|
124
|
+
value = parse_value(definition[:constant_value])
|
|
125
|
+
value = -value if definition[:sign] != "+"
|
|
126
|
+
|
|
127
|
+
build_dice(StaticDie, ignore_count ? nil : definition[:count], value)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def parse_value(side)
|
|
131
|
+
case side
|
|
132
|
+
when /\A#{INTEGER}\z/o
|
|
133
|
+
side.to_i
|
|
134
|
+
when /\A#{NUMBER}\z/o
|
|
135
|
+
rational_to_integer(Rational(side))
|
|
136
|
+
else
|
|
137
|
+
side.match(STRING)[:string]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
109
141
|
def build_dice(die_class, count, sides)
|
|
110
142
|
if count
|
|
111
143
|
die_class.from_count(count.to_i, sides)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../mixins/vectorize_dice"
|
|
4
|
+
|
|
3
5
|
module Dicey
|
|
4
6
|
module DistributionCalculators
|
|
5
7
|
# Base class for implementing distribution calculators.
|
|
@@ -26,6 +28,8 @@ module Dicey
|
|
|
26
28
|
#
|
|
27
29
|
# @abstract
|
|
28
30
|
class BaseCalculator
|
|
31
|
+
include Mixins::VectorizeDice
|
|
32
|
+
|
|
29
33
|
# Possible values for +result_type+ argument in {#call}.
|
|
30
34
|
RESULT_TYPES = %i[weights probabilities].freeze
|
|
31
35
|
|
|
@@ -33,6 +37,10 @@ module Dicey
|
|
|
33
37
|
#
|
|
34
38
|
# Returns empty hash for an empty list of dice.
|
|
35
39
|
#
|
|
40
|
+
# @note Calculation is supposed to return exact results.
|
|
41
|
+
# Using dice with +Float+ values can break this promise and raise errors.
|
|
42
|
+
# Please use +Integer+, +Rational+ or +BigDecimal+ instead.
|
|
43
|
+
#
|
|
36
44
|
# @param dice [Enumerable<AbstractDie>]
|
|
37
45
|
# @param result_type [Symbol] one of {RESULT_TYPES}
|
|
38
46
|
# @param options [Hash{Symbol => Any}] calculator-specific options,
|
|
@@ -47,23 +55,31 @@ module Dicey
|
|
|
47
55
|
unless RESULT_TYPES.include?(result_type)
|
|
48
56
|
raise DiceyError, "#{result_type} is not a valid result type!"
|
|
49
57
|
end
|
|
50
|
-
raise DiceyError, "
|
|
51
|
-
|
|
58
|
+
raise DiceyError, "dice must be an Enumerable!" unless Enumerable === dice
|
|
52
59
|
# Short-circuit for a degenerate case.
|
|
53
60
|
return {} if dice.empty?
|
|
54
61
|
|
|
55
|
-
|
|
62
|
+
static_dice, normal_dice = dice.partition { StaticDie === _1 }
|
|
63
|
+
raise DiceyError, "#{self.class} can not handle these dice!" unless valid_for?(normal_dice)
|
|
64
|
+
|
|
65
|
+
distribution = prepare_distribution(normal_dice, static_dice, options)
|
|
56
66
|
verify_result(distribution, dice)
|
|
57
|
-
distribution = sort_result(distribution)
|
|
58
67
|
transform_result(distribution, result_type)
|
|
59
68
|
end
|
|
60
69
|
|
|
61
70
|
# Whether this calculator can be used for the list of dice.
|
|
62
71
|
#
|
|
72
|
+
# {StaticDie} instances are always ignored.
|
|
73
|
+
#
|
|
63
74
|
# @param dice [Enumerable<AbstractDie>]
|
|
64
75
|
# @return [Boolean]
|
|
65
76
|
def valid_for?(dice)
|
|
66
|
-
|
|
77
|
+
return false if !(Enumerable === dice) || !dice.all?(AbstractDie)
|
|
78
|
+
|
|
79
|
+
normal_dice = dice.grep_v(StaticDie)
|
|
80
|
+
return true if normal_dice.none?
|
|
81
|
+
|
|
82
|
+
validate(normal_dice)
|
|
67
83
|
end
|
|
68
84
|
|
|
69
85
|
# Heuristic complexity of the calculator, used to determine best calculator.
|
|
@@ -77,7 +93,7 @@ module Dicey
|
|
|
77
93
|
def heuristic_complexity(dice)
|
|
78
94
|
return 0 if dice.empty?
|
|
79
95
|
|
|
80
|
-
calculate_heuristic(dice.length, dice.map(&:sides_num).max).to_i
|
|
96
|
+
calculate_heuristic(dice.grep_v(StaticDie).length, dice.map(&:sides_num).max).to_i
|
|
81
97
|
end
|
|
82
98
|
|
|
83
99
|
private
|
|
@@ -103,9 +119,35 @@ module Dicey
|
|
|
103
119
|
raise NotImplementedError
|
|
104
120
|
end
|
|
105
121
|
|
|
122
|
+
# Prepare distribution by calculating it for normal dice and adding static dice to it.
|
|
123
|
+
#
|
|
124
|
+
# @param normal_dice [Enumerable<AbstractDie>]
|
|
125
|
+
# @param static_dice [Enumerable<StaticDie>]
|
|
126
|
+
# @param options [Hash]
|
|
127
|
+
# @return [Hash{Any => Integer}]
|
|
128
|
+
def prepare_distribution(normal_dice, static_dice, options)
|
|
129
|
+
if normal_dice.any?
|
|
130
|
+
distribution = calculate(normal_dice, **options)
|
|
131
|
+
distribution = sort_result(distribution)
|
|
132
|
+
else
|
|
133
|
+
# We can't get to this point if there are no dice at all,
|
|
134
|
+
# so prepare a "nothing" distribution to add static dice to it.
|
|
135
|
+
distribution = { 0 => 1 }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
if static_dice.any?
|
|
139
|
+
c = vectorize_dice(static_dice).sum(&:value)
|
|
140
|
+
# This is done via `+=` because different `k + c` can produce the same key.
|
|
141
|
+
distribution = distribution.each_with_object(Hash.new(0)) { |(k, v), h| h[k + c] += v }
|
|
142
|
+
distribution.default = nil
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
distribution
|
|
146
|
+
end
|
|
147
|
+
|
|
106
148
|
# Check that resulting weights actually add up to what they are supposed to be.
|
|
107
149
|
#
|
|
108
|
-
# @param distribution [Hash{
|
|
150
|
+
# @param distribution [Hash{Any => Integer}]
|
|
109
151
|
# @param dice [Enumerable<AbstractDie>]
|
|
110
152
|
# @return [void]
|
|
111
153
|
# @raise [DiceyError] if result is wrong
|
|
@@ -125,9 +167,9 @@ module Dicey
|
|
|
125
167
|
|
|
126
168
|
# Transform calculated weights to requested result type, if needed.
|
|
127
169
|
#
|
|
128
|
-
# @param distribution [Hash{
|
|
170
|
+
# @param distribution [Hash{Any => Integer}]
|
|
129
171
|
# @param result_type [Symbol] one of {RESULT_TYPES}
|
|
130
|
-
# @return [Hash{
|
|
172
|
+
# @return [Hash{Any => Numeric}]
|
|
131
173
|
def transform_result(distribution, result_type)
|
|
132
174
|
if result_type == :weights
|
|
133
175
|
distribution
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require_relative "base_calculator"
|
|
4
4
|
|
|
5
5
|
require_relative "../mixins/missing_math"
|
|
6
|
-
require_relative "../mixins/vectorize_dice"
|
|
7
6
|
|
|
8
7
|
module Dicey
|
|
9
8
|
module DistributionCalculators
|
|
@@ -13,12 +12,12 @@ module Dicey
|
|
|
13
12
|
# If dice include non-numeric sides, gem +vector_number+ has to be installed.
|
|
14
13
|
class Binomial < BaseCalculator
|
|
15
14
|
include Mixins::MissingMath
|
|
16
|
-
include Mixins::VectorizeDice
|
|
17
15
|
|
|
18
16
|
private
|
|
19
17
|
|
|
20
18
|
def validate(dice)
|
|
21
|
-
|
|
19
|
+
die = dice.first
|
|
20
|
+
die.sides_num == 2 && dice.all? { _1 == die }
|
|
22
21
|
end
|
|
23
22
|
|
|
24
23
|
def calculate_heuristic(dice_count, _sides_count)
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "base_calculator"
|
|
4
4
|
|
|
5
|
-
require_relative "../mixins/vectorize_dice"
|
|
6
|
-
|
|
7
5
|
module Dicey
|
|
8
6
|
module DistributionCalculators
|
|
9
7
|
# "Calculator" for a collection of {AbstractDie} using empirically-obtained statistics.
|
|
@@ -19,15 +17,13 @@ module Dicey
|
|
|
19
17
|
# *Options:*
|
|
20
18
|
# - *rolls* (Integer) (_defaults_ _to:_ _N_) โ number of rolls to perform
|
|
21
19
|
class Empirical < BaseCalculator
|
|
22
|
-
include Mixins::VectorizeDice
|
|
23
|
-
|
|
24
20
|
# Default number of rolls to perform.
|
|
25
21
|
N = 10_000
|
|
26
22
|
|
|
27
23
|
private
|
|
28
24
|
|
|
29
25
|
def validate(dice)
|
|
30
|
-
!!defined?(VectorNumber) || dice.all?(
|
|
26
|
+
!!defined?(VectorNumber) || dice.all?(&:numeric?)
|
|
31
27
|
end
|
|
32
28
|
|
|
33
29
|
def calculate_heuristic(dice_count, sides_count)
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "base_calculator"
|
|
4
4
|
|
|
5
|
-
require_relative "../mixins/vectorize_dice"
|
|
6
|
-
|
|
7
5
|
module Dicey
|
|
8
6
|
module DistributionCalculators
|
|
9
7
|
# Calculator for a collection of {AbstractDie} which goes through
|
|
@@ -11,12 +9,10 @@ module Dicey
|
|
|
11
9
|
#
|
|
12
10
|
# If dice include non-numeric sides, gem +vector_number+ has to be available.
|
|
13
11
|
class Iterative < BaseCalculator
|
|
14
|
-
include Mixins::VectorizeDice
|
|
15
|
-
|
|
16
12
|
private
|
|
17
13
|
|
|
18
14
|
def validate(dice)
|
|
19
|
-
!!defined?(VectorNumber) || dice.all?(
|
|
15
|
+
!!defined?(VectorNumber) || dice.all?(&:numeric?)
|
|
20
16
|
end
|
|
21
17
|
|
|
22
18
|
def calculate_heuristic(dice_count, sides_count)
|
|
@@ -24,7 +20,7 @@ module Dicey
|
|
|
24
20
|
end
|
|
25
21
|
|
|
26
22
|
def calculate(dice, **nil)
|
|
27
|
-
dice = vectorize_dice(dice)
|
|
23
|
+
dice = vectorize_dice(dice).sort_by! { _1.numeric? ? 0 : _1.sides_num }
|
|
28
24
|
|
|
29
25
|
dice[1..].reduce(dice.first.sides_list.tally) do |previous_distribution, die|
|
|
30
26
|
convolve_with_die(previous_distribution, die.sides_list.tally)
|
|
@@ -28,7 +28,7 @@ module Dicey
|
|
|
28
28
|
|
|
29
29
|
def validate(dice)
|
|
30
30
|
first_die = dice.first
|
|
31
|
-
return false unless first_die.
|
|
31
|
+
return false unless first_die.numeric?
|
|
32
32
|
return false unless dice.all? { _1 == first_die }
|
|
33
33
|
return true if first_die.sides_num == 1
|
|
34
34
|
|
|
@@ -36,7 +36,7 @@ module Dicey
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# @param sides_list [Array<Numeric>]
|
|
39
|
-
# @return [
|
|
39
|
+
# @return [Boolean]
|
|
40
40
|
def arithmetic_sequence?(sides_list)
|
|
41
41
|
increment = sides_list[1] - sides_list[0]
|
|
42
42
|
return false if increment.zero?
|
|
@@ -47,9 +47,7 @@ module Dicey
|
|
|
47
47
|
# Simplest multinomial distribution: two regular dice.
|
|
48
48
|
def bimultinomial(die)
|
|
49
49
|
middle = die.sides_num
|
|
50
|
-
(1...(die.sides_num * 2)).
|
|
51
|
-
hash[i + 1] = middle - (middle - i).abs
|
|
52
|
-
end
|
|
50
|
+
(1...(die.sides_num * 2)).to_h { |i| [i + 1, middle - (middle - i).abs] }
|
|
53
51
|
end
|
|
54
52
|
end
|
|
55
53
|
end
|
|
@@ -5,6 +5,11 @@ require_relative "mixins/rational_to_integer"
|
|
|
5
5
|
module Dicey
|
|
6
6
|
# @note This class is considered experimental. It may be changed at any point.
|
|
7
7
|
#
|
|
8
|
+
# @note Almost all distribution properties only really make sense
|
|
9
|
+
# for ordered values in a single dimension.
|
|
10
|
+
# This calculator assumes that only distributions of real numbers
|
|
11
|
+
# satisfy these conditions.
|
|
12
|
+
#
|
|
8
13
|
# Calculates distribution properties,
|
|
9
14
|
# also known as descriptive statistics when applied to a population sample.
|
|
10
15
|
#
|
|
@@ -36,7 +41,7 @@ module Dicey
|
|
|
36
41
|
def call(distribution)
|
|
37
42
|
return {} if distribution.empty?
|
|
38
43
|
|
|
39
|
-
calculate_properties(distribution)
|
|
44
|
+
calculate_properties(distribution).transform_values! { rational_to_integer(_1) }
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
private
|
|
@@ -50,7 +55,7 @@ module Dicey
|
|
|
50
55
|
modes: modes(distribution),
|
|
51
56
|
**range_characteristics(outcomes),
|
|
52
57
|
**median(outcomes),
|
|
53
|
-
**means(outcomes
|
|
58
|
+
**means(outcomes),
|
|
54
59
|
**moments(distribution),
|
|
55
60
|
}
|
|
56
61
|
end
|
|
@@ -89,16 +94,16 @@ module Dicey
|
|
|
89
94
|
min: min,
|
|
90
95
|
max: max,
|
|
91
96
|
range_length: max - min,
|
|
92
|
-
mid_range:
|
|
97
|
+
mid_range: Rational(min + max, 2),
|
|
93
98
|
}
|
|
94
99
|
rescue ArgumentError, TypeError, NoMethodError
|
|
95
100
|
# Outcomes are not comparable with each other, so a range can not be determined.
|
|
96
101
|
{}
|
|
97
102
|
end
|
|
98
103
|
|
|
99
|
-
def means(outcomes
|
|
104
|
+
def means(outcomes)
|
|
100
105
|
{
|
|
101
|
-
arithmetic_mean:
|
|
106
|
+
arithmetic_mean: Rational(outcomes.sum, outcomes.size),
|
|
102
107
|
}
|
|
103
108
|
rescue ArgumentError, TypeError
|
|
104
109
|
# Outcomes are not summable with each other, means are meaningless.
|
|
@@ -121,12 +126,10 @@ module Dicey
|
|
|
121
126
|
|
|
122
127
|
def moments(distribution)
|
|
123
128
|
total_weight = distribution.values.sum
|
|
124
|
-
expected_value =
|
|
125
|
-
variance =
|
|
126
|
-
skewness =
|
|
127
|
-
|
|
128
|
-
kurtosis =
|
|
129
|
-
rational_to_integer(moment(distribution, total_weight, 4, expected_value, variance))
|
|
129
|
+
expected_value = moment(distribution, total_weight, 1)
|
|
130
|
+
variance = moment(distribution, total_weight, 2) - (expected_value**2)
|
|
131
|
+
skewness = moment(distribution, total_weight, 3, expected_value, variance)
|
|
132
|
+
kurtosis = moment(distribution, total_weight, 4, expected_value, variance)
|
|
130
133
|
|
|
131
134
|
{
|
|
132
135
|
expected_value: expected_value,
|
|
@@ -137,7 +140,7 @@ module Dicey
|
|
|
137
140
|
excess_kurtosis: kurtosis ? kurtosis - 3 : nil,
|
|
138
141
|
}
|
|
139
142
|
rescue ArgumentError, TypeError, NoMethodError
|
|
140
|
-
# Outcomes are not compatible with each other, moments are
|
|
143
|
+
# Outcomes are not compatible with each other, moments are fleeting.
|
|
141
144
|
{}
|
|
142
145
|
end
|
|
143
146
|
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Dicey
|
|
4
|
-
# @api private
|
|
5
|
-
# Various mixins with shared methods.
|
|
6
4
|
module Mixins
|
|
7
5
|
# @api private
|
|
8
6
|
# Mix-in for converting rationals with denominator of 1 to integers.
|
|
@@ -13,7 +11,7 @@ module Dicey
|
|
|
13
11
|
# Otherwise, return +value+ as-is.
|
|
14
12
|
#
|
|
15
13
|
# @param value [Numeric, Any]
|
|
16
|
-
# @return [
|
|
14
|
+
# @return [Rational, Integer, Any]
|
|
17
15
|
def rational_to_integer(value)
|
|
18
16
|
(Rational === value && value.denominator == 1) ? value.numerator : value
|
|
19
17
|
end
|
|
@@ -22,13 +22,18 @@ module Dicey
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def vectorize_die_sides(die)
|
|
25
|
-
|
|
25
|
+
case die
|
|
26
|
+
when NumericDie
|
|
27
|
+
die
|
|
28
|
+
when StaticDie
|
|
29
|
+
die.class.new(vectorize_one_die_side(die.value))
|
|
30
|
+
else
|
|
31
|
+
die.class.new(die.sides_list.map { vectorize_one_die_side(_1) })
|
|
32
|
+
end
|
|
33
|
+
end
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
(Numeric === side || VectorNumber === side) ? side : VectorNumber.new([side])
|
|
30
|
-
end
|
|
31
|
-
)
|
|
35
|
+
def vectorize_one_die_side(side)
|
|
36
|
+
(Numeric === side || VectorNumber === side) ? side : VectorNumber.new([side])
|
|
32
37
|
end
|
|
33
38
|
end
|
|
34
39
|
end
|
data/lib/dicey/numeric_die.rb
CHANGED
|
@@ -16,6 +16,8 @@ module Dicey
|
|
|
16
16
|
unless Integer === sides_list.begin && Integer === sides_list.end
|
|
17
17
|
raise DiceyError, "`#{sides_list.inspect}` is not a valid range!"
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
@range = sides_list
|
|
19
21
|
else
|
|
20
22
|
sides_list.each do |value|
|
|
21
23
|
raise DiceyError, "`#{value.inspect}` is not a number!" unless Numeric === value
|
|
@@ -24,5 +26,24 @@ module Dicey
|
|
|
24
26
|
|
|
25
27
|
super
|
|
26
28
|
end
|
|
29
|
+
|
|
30
|
+
# Whether all sides of this die are +Numeric+.
|
|
31
|
+
#
|
|
32
|
+
# @return [true]
|
|
33
|
+
def numeric?
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Return a string representing the die.
|
|
38
|
+
#
|
|
39
|
+
# Default representation is a list of sides in round brackets.
|
|
40
|
+
# If the die was initialized with a +Range+, string will be in the form of +begin..end+.
|
|
41
|
+
#
|
|
42
|
+
# @return [String]
|
|
43
|
+
def to_s
|
|
44
|
+
return "#{@range.begin}..#{@range.end}" if @range
|
|
45
|
+
|
|
46
|
+
super
|
|
47
|
+
end
|
|
27
48
|
end
|
|
28
49
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "abstract_die"
|
|
4
|
+
|
|
5
|
+
module Dicey
|
|
6
|
+
# Static die has only one side and always returns the same value.
|
|
7
|
+
# Useful to model constants in dice expressions.
|
|
8
|
+
#
|
|
9
|
+
# @note Unlike other dice, rolling a static die does not advance
|
|
10
|
+
# shared randomness generator, thus it does not have any impact on
|
|
11
|
+
# rolling other dice.
|
|
12
|
+
class StaticDie < AbstractDie
|
|
13
|
+
# Die's only value.
|
|
14
|
+
#
|
|
15
|
+
# @return [Any]
|
|
16
|
+
attr_reader :value
|
|
17
|
+
|
|
18
|
+
# @param value [Any]
|
|
19
|
+
def initialize(value)
|
|
20
|
+
@value = value
|
|
21
|
+
super([@value].freeze)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
alias current value
|
|
25
|
+
alias next value
|
|
26
|
+
alias roll value
|
|
27
|
+
|
|
28
|
+
# Return a string representing the die.
|
|
29
|
+
#
|
|
30
|
+
# Static dice are represented with a "+" or "-" followed by the absolute value
|
|
31
|
+
# (0 is represented as +0). Strings are quoted.
|
|
32
|
+
#
|
|
33
|
+
# @return [String]
|
|
34
|
+
def to_s
|
|
35
|
+
if @value.respond_to?(:negative?) && @value.negative?
|
|
36
|
+
side_to_s(@value)
|
|
37
|
+
else
|
|
38
|
+
"+#{side_to_s(@value)}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/dicey/version.rb
CHANGED
data/lib/dicey.rb
CHANGED
|
@@ -12,7 +12,8 @@ end
|
|
|
12
12
|
# Includes several classes of dice:
|
|
13
13
|
# - {AbstractDie}, the base and most generic class;
|
|
14
14
|
# - {NumericDie}, a subclass for strictly numeric dice;
|
|
15
|
-
# - {RegularDie}, for the most common dice
|
|
15
|
+
# - {RegularDie}, for the most common dice;
|
|
16
|
+
# - {StaticDie}, a constant offset pseudo-die.
|
|
16
17
|
#
|
|
17
18
|
# See {AbstractDie} for API and more information.
|
|
18
19
|
#
|
|
@@ -30,6 +31,7 @@ module Dicey
|
|
|
30
31
|
require_relative "dicey/abstract_die"
|
|
31
32
|
require_relative "dicey/numeric_die"
|
|
32
33
|
require_relative "dicey/regular_die"
|
|
34
|
+
require_relative "dicey/static_die"
|
|
33
35
|
|
|
34
36
|
require_relative "dicey/die_foundry"
|
|
35
37
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dicey
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alexander Bulancov
|
|
@@ -79,6 +79,7 @@ files:
|
|
|
79
79
|
- lib/dicey/mixins/vectorize_dice.rb
|
|
80
80
|
- lib/dicey/numeric_die.rb
|
|
81
81
|
- lib/dicey/regular_die.rb
|
|
82
|
+
- lib/dicey/static_die.rb
|
|
82
83
|
- lib/dicey/version.rb
|
|
83
84
|
homepage: https://github.com/trinistr/dicey
|
|
84
85
|
licenses:
|
|
@@ -86,9 +87,9 @@ licenses:
|
|
|
86
87
|
metadata:
|
|
87
88
|
homepage_uri: https://github.com/trinistr/dicey
|
|
88
89
|
bug_tracker_uri: https://github.com/trinistr/dicey/issues
|
|
89
|
-
documentation_uri: https://rubydoc.info/gems/dicey/0.
|
|
90
|
-
source_code_uri: https://github.com/trinistr/dicey/tree/v0.
|
|
91
|
-
changelog_uri: https://github.com/trinistr/dicey/blob/v0.
|
|
90
|
+
documentation_uri: https://rubydoc.info/gems/dicey/0.19.0
|
|
91
|
+
source_code_uri: https://github.com/trinistr/dicey/tree/v0.19.0
|
|
92
|
+
changelog_uri: https://github.com/trinistr/dicey/blob/v0.19.0/CHANGELOG.md
|
|
92
93
|
rubygems_mfa_required: 'true'
|
|
93
94
|
rdoc_options:
|
|
94
95
|
- "--main"
|