dicey 0.14.0 → 0.15.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b23d3fe347080f349190417819aba5f2778a6196569ab39e38d79049f7557f02
4
- data.tar.gz: 5bd9295a6bb1a15fe33fe3b967fd1d2c0784a19e713977f57da7910589687ebe
3
+ metadata.gz: a269ac8baf4bf849781175b605d92c23ff95fae3e53baa540519ece9947cec0d
4
+ data.tar.gz: af8f74b92b35a2818d18dc91d9917d8af6e91e16f8674f02eebfb5e56b1759fd
5
5
  SHA512:
6
- metadata.gz: 778d6f36d896b5db19411bedf0bf272082310eb2c9a1403b57962932979ec0da863b6d408c9245b49bbfb914c99cc2bfa9e8443e0a02d732d2723f93f8b1f6bb
7
- data.tar.gz: 694960b274505249fc705e0a61bb1452b6efed062d8b183c154734a9677cc9c28309cf859917443fd997a60de70e00c7f45abc9f9e36e96007949c1bdbee7e0f
6
+ metadata.gz: 1b031d4ee67b9c3b1ebe0ba23ac2289f7d14536af4fda1b98b7f481fd9e027ef3b57d8911f7d3c78ca46da0554b63226312e7ae4fa136fc34431bf7f86b7f349
7
+ data.tar.gz: ac52115c59f846e4662ed055e871bf31f2e87b2080acb07042730698d5fa6d107daaf64792645752b47bced701ace913b58464a7a96e56dffe6d3aba3a0af2d8
data/README.md CHANGED
@@ -14,15 +14,18 @@ In seriousness, this program is mainly useful for calculating total frequency (p
14
14
 
15
15
  ## Table of contents
16
16
 
17
- - [No installation](#no-installation)
17
+ - [Online (no installation)](#online-no-installation)
18
+ - [Recommended](#recommended)
19
+ - [For those who want the full command line experience](#for-those-who-want-the-full-command-line-experience)
18
20
  - [Installation](#installation)
19
21
  - [Requirements](#requirements)
20
- - [Usage / CLI (command line interface)](#usage--cli-command-line-interface)
21
- - [Example 1 Basic distribution](#example-1--basic-distribution)
22
- - [Example 2 Complex distribution with different dice](#example-2--complex-distribution-with-different-dice)
23
- - [Example 3 Custom dice](#example-3--custom-dice)
24
- - [Example 4 Rolling even more custom dice](#example-4--rolling-even-more-custom-dice)
25
- - [Usage / API](#usage--api)
22
+ - [Usage: CLI (command line)](#usage-cli-command-line)
23
+ - [Example 1: Basic distribution](#example-1-basic-distribution)
24
+ - [Example 2: Complex distribution with different dice](#example-2-complex-distribution-with-different-dice)
25
+ - [Example 3: Custom dice](#example-3-custom-dice)
26
+ - [Example 4: Rolling even more custom dice](#example-4-rolling-even-more-custom-dice)
27
+ - [All ways to define dice](#all-ways-to-define-dice)
28
+ - [Usage: API](#usage-api)
26
29
  - [Dice](#dice)
27
30
  - [Rolling](#rolling)
28
31
  - [Calculators](#calculators)
@@ -31,13 +34,24 @@ In seriousness, this program is mainly useful for calculating total frequency (p
31
34
  - [Contributing](#contributing)
32
35
  - [License](#license)
33
36
 
34
- ## No installation
37
+ ## Online (no installation)
35
38
 
36
- Thanks to the efforts of Ruby developers, you can try **Dicey** online!
37
- 1. Head over to https://runruby.dev/gist/476679a55c24520782613d9ceb89d9a3
38
- 2. Make sure that "*-main.rb*" is open
39
- 3. Input arguments between "ARGUMENTS" lines, separated by spaces.
39
+ ### Recommended
40
+
41
+ Use online version of **Dicey** on its own website: [dicey.bulancov.tech](https://dicey.bulancov.tech)!
42
+
43
+ It does not provide quite all features, but it's easy to use and quick to get started.
44
+
45
+ ### For those who want the full command line experience
46
+
47
+ Thanks to the efforts of Ruby developers, you can run full **Dicey** online!
48
+ 1. Head over to the prepared [RunRuby page](https://runruby.dev/gist/476679a55c24520782613d9ceb89d9a3).
49
+ 2. Make sure that "*-main.rb*" is open.
50
+ 3. Input arguments between "ARGUMENTS" lines, separated by spaces. Refer to [Usage / CLI](#usage--cli-command-line) section.
40
51
  4. Click "**Run code**" button below the editor.
52
+ 5. Results will be printed to the "Logs" tab.
53
+
54
+ If familiar with Ruby, you can also use **RunRuby** to explore the API. Refer to [Usage / API](#usage--api) section for documentation.
41
55
 
42
56
  ## Installation
43
57
 
@@ -48,7 +62,7 @@ gem install dicey
48
62
 
49
63
  Or, if using Bundler, add it to your `Gemfile`:
50
64
  ```rb
51
- gem "dicey", "~> 0.13"
65
+ gem "dicey", "~> 0.14"
52
66
  ```
53
67
 
54
68
  > [!TIP]
@@ -65,14 +79,14 @@ gem "dicey", "~> 0.13"
65
79
 
66
80
  Otherwise, there are no direct dependencies.
67
81
 
68
- ## Usage / CLI (command line interface)
82
+ ## Usage: CLI (command line)
69
83
 
70
- Following examples assume that `dicey` (or `dicey-to-gnuplot`) is executable and is in `$PATH`. You can also run it with `ruby dicey` instead.
84
+ Following examples assume that `dicey` (or `dicey-to-gnuplot`) is executable and is in `$PATH`.
71
85
 
72
86
  > [!NOTE]
73
87
  > 💡 Run `dicey --help` to get a list of all possible options.
74
88
 
75
- ### Example 1 Basic distribution
89
+ ### Example 1: Basic distribution
76
90
 
77
91
  Let's start with something simple. Imagine that your Bard character has Vicious Mockery cantrip with 2d4 damage, and you would like to know the distribution of possible damage rolls. Run **Dicey** with two 4s as arguments:
78
92
  ```sh
@@ -81,7 +95,7 @@ $ dicey 4 4
81
95
 
82
96
  It should output the following:
83
97
  ```sh
84
- # ⚃;⚃
98
+ # D4+D4
85
99
  2 => 1
86
100
  3 => 2
87
101
  4 => 3
@@ -96,7 +110,7 @@ First line is a comment telling you that calculation ran for two D4s. Every line
96
110
  If probability is preferred, there is an option for that:
97
111
  ```sh
98
112
  $ dicey 4 4 --result probabilities # or -r p for short
99
- # ⚃;⚃
113
+ # D4+D4
100
114
  2 => 0.0625
101
115
  3 => 0.125
102
116
  4 => 0.1875
@@ -108,13 +122,13 @@ $ dicey 4 4 --result probabilities # or -r p for short
108
122
 
109
123
  This shows that 5 will probably be rolled a quarter of the time.
110
124
 
111
- ### Example 2 Complex distribution with different dice
125
+ ### Example 2: Complex distribution with different dice
112
126
 
113
127
  During your quest to end all ends you find a cool Burning Sword which deals 1d8 slashing damage and 2d4 fire damage on attack. You run **Dicey** with these dice:
114
128
  ```sh
115
129
  # Note the shorthand notation for two dice!
116
130
  $ dicey 8 2d4
117
- # [8];⚃;⚃
131
+ # D8+D4+D4
118
132
  3 => 1
119
133
  4 => 3
120
134
  5 => 6
@@ -133,30 +147,30 @@ $ dicey 8 2d4
133
147
 
134
148
  Results show that while the total range is 3–16, it is much more likely to roll numbers in the 6–13 range. That's pretty fire, huh?
135
149
 
136
- #### Example 2.1 Graph
150
+ #### Example 2.1: Graph
137
151
 
138
152
  If you downloaded `dicey-to-gnuplot` and have [gnuplot](http://gnuplot.info) installed, it is possible to turn these results into a graph with a somewhat clunky command:
139
153
  ```sh
140
- $ dicey 8 2d4 --format gnuplot | dicey-to-gnuplot
141
- # `--format gnuplot` can be abbreviated as `-f g`
154
+ $ dicey 8 2d4 -f g | dicey-to-gnuplot
155
+ # `--format gnuplot` is shortened to `-f g`
142
156
  ```
143
157
 
144
- This will create a PNG image named `[8];⚃;⚃.png`:
145
- ![Graph of damage roll frequencies for Burning Sword]([8];⚃;⚃.png)
158
+ This will create a PNG image named "*D8+D4+D4.png*":
159
+ ![Graph of damage roll frequencies for Burning Sword](D8+D4+D4.png)
146
160
 
147
- #### Example 2.2 JSON and YAML
161
+ #### Example 2.2: JSON and YAML
148
162
 
149
163
  If you find that you need to export results for further processing, it would be great if a common data interchange format was used. **Dicey** supports output as JSON and YAML with `--format json` (or `-f j`) and `--format yaml` (or `-f y`) respectively.
150
164
 
151
165
  JSON via `dicey 8 2d4 --format json`:
152
166
  ```json
153
- {"description":"[8];⚃;⚃","results":{"3":1,"4":3,"5":6,"6":10,"7":13,"8":15,"9":16,"10":16,"11":15,"12":13,"13":10,"14":6,"15":3,"16":1}}
167
+ {"description":"D8+D4+D4","results":{"3":1,"4":3,"5":6,"6":10,"7":13,"8":15,"9":16,"10":16,"11":15,"12":13,"13":10,"14":6,"15":3,"16":1}}
154
168
  ```
155
169
 
156
170
  YAML via `dicey 8 2d4 --format yaml`:
157
171
  ```yaml
158
172
  ---
159
- description: "[8];⚃;⚃"
173
+ description: D8+D4+D4
160
174
  results:
161
175
  3: 1
162
176
  4: 3
@@ -174,14 +188,14 @@ results:
174
188
  16: 1
175
189
  ```
176
190
 
177
- ### Example 3 Custom dice
191
+ ### Example 3: Custom dice
178
192
 
179
193
  While walking home from work you decide to take a shortcut through a dark alleyway. Suddenly, you notice a die lying on the ground. Looking closer, it turns out to be a D4, but its 3 side was erased from reality. You just have to learn what impact this has on a roll together with a normal D4. Thankfully, you know just the program for the job.
180
194
 
181
195
  Having ran to a computer as fast as you can, you sic **Dicey** on the problem:
182
196
  ```sh
183
197
  $ dicey 1,2,4 4
184
- # (1,2,4);⚃
198
+ # (1,2,4)+D4
185
199
  2 => 1
186
200
  3 => 2
187
201
  4 => 2
@@ -193,13 +207,10 @@ $ dicey 1,2,4 4
193
207
 
194
208
  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.
195
209
 
196
- > [!TIP]
197
- > 💡 A single positive integer argument N practically is a shorthand for listing every side from 1 to N.
198
-
199
210
  But what if you had TWO weird D4s?
200
211
  ```sh
201
212
  $ dicey 2d1,2,4
202
- # (1,2,4);(1,2,4)
213
+ # (1,2,4)+(1,2,4)
203
214
  2 => 1
204
215
  3 => 2
205
216
  4 => 1
@@ -210,45 +221,61 @@ $ dicey 2d1,2,4
210
221
 
211
222
  Hah, now this is a properly cursed distribution!
212
223
 
213
- ### Example 4 — Rolling even more custom dice
224
+ > [!TIP]
225
+ > 💡 A single positive integer argument N practically is a shorthand for listing every side from 1 to N.
226
+
227
+ ### Example 4: Rolling even more custom dice
214
228
 
215
229
  You have a sudden urge to roll dice while only having boring integer dice at home. Where to find *the cool* dice though?
216
230
 
217
231
  Look no further than **roll** mode introduced in **Dicey** 0.12:
218
232
  ```sh
219
233
  $ dicey 0.5,1.5,2.5 4 --mode roll # As always, can be abbreviated to -m r
220
- # (0.5e0,0.15e1,0.25e1);⚃
234
+ # (0.5e0,0.15e1,0.25e1)+D4
221
235
  roll => 0.35e1 # You probably will get a different value here.
222
236
  ```
223
237
 
224
238
  > [!NOTE]
225
- > 💡 Roll mode is compatible with `--format`, but not `--result`.
239
+ > 💡 Roll mode is compatible with `--format` option.
240
+
241
+ ### All ways to define dice
226
242
 
227
- ## Usage / API
243
+ There are three *main* ways to define dice:
244
+ - *"5", "25", or "525"*: a single positive integer makes a regular die (like a D20).
245
+ - *"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.
246
+ - Accepted separators: "-", "..", "...", "–" (en dash), "—" (em dash), "…" (ellipsis).
247
+ - *"1,2,4", "(-1.5,0,1.5)", or "2,"*: a list of any numbers separated by commas, possibly in round brackets, makes an arbitrary numeric die.
248
+ - Lists can end in a comma, allowing single-number lists.
249
+
250
+ *"D6", "d(-1,3)", or "d2..4"*: 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 "-".
251
+
252
+ *"2D6", "5d-1,3", or "277D(2..4)"*: any definitions can be prefixed with "*N*d" or "*N*D", where *N* is a positive integer. This creates *N* copies of the die.
253
+
254
+ ## Usage: API
228
255
 
229
256
  > [!Note]
230
- > - Latest documentation from `main` branch is automatically deployed to [GitHub Pages](https://trinistr.github.io/dicey).
257
+ > - Latest API documentation from `main` branch is automatically deployed to [GitHub Pages](https://trinistr.github.io/dicey).
231
258
  > - Documentation for published versions is available on [RubyDoc](https://rubydoc.info/gems/dicey).
232
259
 
233
260
  ### Dice
234
261
 
235
262
  There are 3 classes of dice currently:
236
263
  - `Dicey::AbstractDie` is the base class for other dice, but can be used on its own. It has no restrictions on values of sides. For now, it is *only* useful for rolling and can't be used for distribution calculations.
237
- - `Dicey::NumericDie` behaves much the same as `Dicey::AbstractDie`, except for checking that all values are instances of `Numeric`. It can be initialized with an Array or Range.
238
- - `Dicey::RegularDie` is a subclass of `Dicey::NumericDie`. It is defined by a single integer which is expanded to range (1..N).
264
+ - `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.
265
+ - `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*).
239
266
 
240
267
  All dice classes have constructor methods aside from `.new`:
241
268
  - `.from_list` takes a list of definitions and calls `.new` with each one;
242
269
  - `.from_count` takes a count and a definition and calls `.new` with it specified number of times.
243
270
 
244
- See [Diving deeper](#diving-deeper) for more information.
271
+ See [Diving deeper](#diving-deeper) for more theoretical information.
245
272
 
246
273
  > [!NOTE]
247
274
  > 💡 Using `Float` values is liable to cause precision issues. Due to in-built result verification, this **will** raise errors. Use `Rational` or `BigDecimal` instead.
248
275
 
249
276
  #### DieFoundry
250
277
 
251
- `Dicey::DieFoundry#call` provides the string interface for creating dice as available in CLI:
278
+ `Dicey::DieFoundry#call` provides the interface for creating dice from Strings as available in CLI:
252
279
  ```rb
253
280
  Dicey::DieFoundry.new.call("100")
254
281
  # same as Dicey::RegularDie.new(100)
@@ -262,7 +289,7 @@ It only takes a single argument and may return both an array of dice and a singl
262
289
  ```rb
263
290
  foundry = Dicey::DieFoundry.new
264
291
  %w[8 2d4].flat_map { foundry.call(_1) }
265
- # same as [Dicey::RegularDie.new(8), Dicey::RegularDie.new(4), Dicey::RegularDie.new(4)]
292
+ # same as [Dicey::RegularDie.new(8), *Dicey::RegularDie.from_count(2, 4)]
266
293
  ```
267
294
 
268
295
  ### Rolling
@@ -303,17 +330,19 @@ die.roll
303
330
  # => 1
304
331
  ```
305
332
 
306
- Randomness source is *global*, shared between all dice and probably not thread-safe.
333
+ > [!NOTE]
334
+ > 💡 Randomness source is *global*, shared between all dice and probably not thread-safe.
307
335
 
308
336
  ### Calculators
309
337
 
310
- Frequency calculators live in `Dicey::SumFrequencyCalculators` module. There are three implemented calculators:
338
+ Frequency calculators live in `Dicey::SumFrequencyCalculators` module. There are four calculators currently:
311
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 my laptop.
312
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.
313
- - `Dicey::SumFrequencyCalculators::BruteForce` is the most generic and slowest one, but can handle any dice. Currently, it is also limited to `Dicey::NumericDie`, as it's unclear how to handle other values.
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
+ - `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.
314
343
 
315
344
  Calculators inherit from `Dicey::SumFrequencyCalculators::BaseCalculator` and provide the following public interface:
316
- - `#call(dice, result_type: {:frequencies | :probabilities}) : Hash`
345
+ - `#call(dice, result_type: {:frequencies | :probabilities}, **options) : Hash`
317
346
  - `#valid_for?(dice) : Boolean`
318
347
 
319
348
  See [next section](#diving-deeper) for more details on limitations and complexity considerations.
@@ -321,10 +350,7 @@ See [next section](#diving-deeper) for more details on limitations and complexit
321
350
  ## Diving deeper
322
351
 
323
352
  For a further discussion of calculations, it is important to understand which classes of dice exist.
324
- - **Regular** die — a die with N sides with sequential integers from 1 to N,
325
- like a classic cubic D6, D20, or even a coin if you assume that it rolls 1 and 2.
326
- These are dice used for many tabletop games, including role-playing games.
327
- Most probably, you will only ever need these and not anything beyond.
353
+ - **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.
328
354
 
329
355
  > [!TIP]
330
356
  > 💡 If you only need to roll **regular** dice, this section will not contain anything important.
@@ -335,11 +361,11 @@ For a further discussion of calculations, it is important to understand which cl
335
361
  - **Abstract** die is not limited by anything other than not having partial sides (and how would that work anyway?).
336
362
 
337
363
  > [!NOTE]
338
- > 💡 If your die starts with a negative number or only has a single natural side, brackets can be employed to force treating it as a sides list, e.g. `dicey '(-1)'` (quotation is required due to shell processing).
364
+ > 💡 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.
339
365
 
340
- Dicey is in principle able to handle any 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.
366
+ 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.
341
367
 
342
- Currently, three algorithms are implemented, with different possibilities and trade-offs.
368
+ Currently, three algorithms for calculating frequencies are implemented, with different possibilities and trade-offs.
343
369
 
344
370
  > [!NOTE]
345
371
  > 💡 Complexity is listed for `n` dice with at most `m` sides and has not been rigorously proven.
@@ -349,7 +375,7 @@ Currently, three algorithms are implemented, with different possibilities and tr
349
375
  An algorithm based on fast polynomial multiplication. This is the default algorithm, used for most reasonable dice.
350
376
 
351
377
  - Limitations: only **natural** dice are allowed, including **regular** dice.
352
- - Example: `dicey 5 3,4,1 '(0)'`
378
+ - Example: `dicey 5 3,4,1 0,`
353
379
  - Complexity: `O(m⋅n)` where `m` is the highest value
354
380
 
355
381
  ### Multinomial coefficients
@@ -21,6 +21,8 @@ module Dicey
21
21
 
22
22
  # Yes, class variable is actually useful here.
23
23
  # TODO: Allow supplying a custom Random.
24
+
25
+ # Shared randomness source, accessed through {.rand} and {.srand}.
24
26
  @@random = Random.new
25
27
 
26
28
  # rubocop:enable Style/ClassVars
@@ -31,9 +33,8 @@ module Dicey
31
33
  # @return [String]
32
34
  def self.describe(dice)
33
35
  return dice.to_s if AbstractDie === dice
34
- return dice.join(";") if Array === dice
35
36
 
36
- dice.to_a.join(";")
37
+ dice.to_a.join("+")
37
38
  end
38
39
 
39
40
  # Create a bunch of different dice at once.
@@ -55,7 +56,15 @@ module Dicey
55
56
  Array.new(count) { new(definition) }
56
57
  end
57
58
 
58
- attr_reader :sides_list, :sides_num
59
+ # Die's list of sides.
60
+ #
61
+ # @return [Array<Any>]
62
+ attr_reader :sides_list
63
+
64
+ # Number of sides of the die.
65
+ #
66
+ # @return [Integer]
67
+ attr_reader :sides_num
59
68
 
60
69
  # @param sides_list [Enumerable<Any>]
61
70
  # @raise [DiceyError] if +sides_list+ is empty
@@ -92,9 +101,13 @@ module Dicey
92
101
  current
93
102
  end
94
103
 
104
+ # Return a string representing the die.
105
+ #
106
+ # Default representation is a list of sides in round brackets.
107
+ #
95
108
  # @return [String]
96
109
  def to_s
97
- "(#{@sides_list.join(",")})"
110
+ (@sides_list.size > 1) ? "(#{@sides_list.join(",")})" : "(#{@sides_list.first},)"
98
111
  end
99
112
 
100
113
  # Determine if this die and the other one have the same list of sides.
@@ -54,8 +54,10 @@ module Dicey
54
54
  def add_banner_and_version
55
55
  @parser.banner = <<~TEXT
56
56
  Usage: #{@parser.program_name} [options] <die> [<die> ...]
57
+ #{@parser.program_name} [options] -- <die> [<die> ...]
57
58
  #{@parser.program_name} --test [full|quiet]
58
59
  All option names and arguments can be abbreviated if abbreviation is unambiguous.
60
+ A lone "--" separates options and die definitions, allowing definitions to start with "-".
59
61
  TEXT
60
62
  end
61
63
 
@@ -6,18 +6,20 @@ require_relative "regular_die"
6
6
  module Dicey
7
7
  # Helper class to define die definitions and automatically select the best one.
8
8
  class DieFoundry
9
- # Regexp for matching a count.
10
- COUNT = /(?:(?<count>[1-9]\d*+)?d)?+/i
9
+ # Regexp for matching a possible count.
10
+ PREFIX = /(?>(?<count>[1-9]\d*+)?d)?+/i
11
11
 
12
12
  # Possible molds for the dice. They are matched in the order as written.
13
13
  MOLDS = [
14
14
  # Positive integer goes into the RegularDie mold.
15
- [/\A#{COUNT}(?<sides>[1-9]\d*+)\z/, :regular_mold],
15
+ [/\A#{PREFIX}(?<sides>[1-9]\d*+)\z/, :regular_mold].freeze,
16
+ # Integer range goes into the NumericDie mold.
17
+ [/\A#{PREFIX}\(?(?<begin>-?\d++)(?>[-–—…]|\.{2,3})(?<end>-?\d++)\)?\z/, :range_mold].freeze,
16
18
  # List of numbers goes into the NumericDie mold.
17
- [/\A#{COUNT}\(?(?<sides>-?\d++(?>,(?>-?\d++)?)*)\)?\z/, :weirdly_shaped_mold],
19
+ [/\A#{PREFIX}\(?(?<sides>-?\d++(?>,(?>-?\d++)?)+|,)\)?\z/, :weirdly_shaped_mold].freeze,
18
20
  # Non-integers require arbitrary precision arithmetic, which is not enabled by default.
19
- [/\A#{COUNT}\(?(?<sides>-?\d++(?>\.\d++)?(?>,(?>-?\d++(?>\.\d++)?)?)*)\)?\z/,
20
- :weirdly_precise_mold],
21
+ [/\A#{PREFIX}\(?(?<sides>-?\d++(?>\.\d++)?(?>,(?>-?\d++(?>\.\d++)?)?)+|,)\)?\z/,
22
+ :weirdly_precise_mold].freeze,
21
23
  # Anything else is spilled on the floor.
22
24
  ].freeze
23
25
 
@@ -25,15 +27,16 @@ module Dicey
25
27
  #
26
28
  # Following definitions are recognized:
27
29
  # - positive integer (like "6" or "20"), which produces a {RegularDie};
30
+ # - integer range (like "3-6" or "(-5..5)"), which produces a {NumericDie};
28
31
  # - list of integers (like "3,4,5", "(-1,0,1)", or "2,"), which produces a {NumericDie};
29
- # - list of decimal numbers (like "0.5,0.2,0.8" or "2.0"), which produces a {NumericDie},
32
+ # - list of decimal numbers (like "0.5,0.2,0.8" or "(2.0,)"), which produces a {NumericDie},
30
33
  # but uses +BigDecimal+ for values to maintain precise results.
31
34
  #
32
35
  # Any die definition can be prefixed with a count, like "2D6" or "1d1,3,5" to create an array.
33
36
  # A plain "d" without an explicit count is ignored instead, creating a single die.
34
37
  #
35
38
  # @param definition [String] die shape
36
- # @return [NumericDie, RegularDie, Array<NumericDie, RegularDie>]
39
+ # @return [NumericDie, RegularDie, Array<NumericDie>, Array<RegularDie>]
37
40
  # @raise [DiceyError] if no mold fits the definition
38
41
  def call(definition)
39
42
  matched, name =
@@ -54,6 +57,13 @@ module Dicey
54
57
  build_dice(RegularDie, definition[:count], definition[:sides].to_i)
55
58
  end
56
59
 
60
+ def range_mold(definition)
61
+ first = definition[:begin].to_i
62
+ last = definition[:end].to_i
63
+ first, last = last, first if first > last
64
+ build_dice(NumericDie, definition[:count], first..last)
65
+ end
66
+
57
67
  def weirdly_shaped_mold(definition)
58
68
  build_dice(NumericDie, definition[:count], definition[:sides].split(",").map(&:to_i))
59
69
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dicey
4
+ # Processors which turn data to text.
4
5
  module OutputFormatters
5
6
  # Base formatter for outputting in formats which can be converted from a Hash directly.
6
7
  # Can add an optional description into the result.
@@ -5,9 +5,6 @@ require_relative "numeric_die"
5
5
  module Dicey
6
6
  # Regular die, which has N sides with numbers from 1 to N.
7
7
  class RegularDie < NumericDie
8
- # Characters to use for small dice.
9
- D6 = "⚀⚁⚂⚃⚄⚅"
10
-
11
8
  # @param max [Integer] maximum side / number of sides
12
9
  def initialize(max)
13
10
  unless Integer === max && max.positive?
@@ -17,12 +14,13 @@ module Dicey
17
14
  super((1..max))
18
15
  end
19
16
 
20
- # Dice with 1–6 sides are displayed with a single character from {D6}.
21
- # More than that, and we get into the square bracket territory.
17
+ # Return a string representing the die.
18
+ #
19
+ # Regular dice are represented with a "D" followed by the number of sides.
22
20
  #
23
21
  # @return [String]
24
22
  def to_s
25
- (sides_num <= D6.size) ? D6[sides_num - 1] : "[#{sides_num}]"
23
+ "D#{sides_num}"
26
24
  end
27
25
  end
28
26
  end
@@ -1,8 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dicey
4
+ # Calculators for probability distributions of dice.
4
5
  module SumFrequencyCalculators
5
6
  # Base frequencies calculator.
7
+ #
8
+ # *Options:*
9
+ #
10
+ # Calculators may have calculator-specific options,
11
+ # passed as extra keyword arguments to {#call}.
12
+ # If present, they will be documented under *Options* heading
13
+ # on the class itself.
14
+ #
6
15
  # @abstract
7
16
  class BaseCalculator
8
17
  # Possible values for +result_type+ argument in {#call}.
@@ -10,11 +19,12 @@ module Dicey
10
19
 
11
20
  # @param dice [Enumerable<AbstractDie>]
12
21
  # @param result_type [Symbol] one of {RESULT_TYPES}
22
+ # @param options [Hash{Symbol => Any}] calculator-specific options
13
23
  # @return [Hash{Numeric => Numeric}] frequencies of each sum
14
24
  # @raise [DiceyError] if +result_type+ is invalid
15
25
  # @raise [DiceyError] if dice list is invalid for the calculator
16
26
  # @raise [DiceyError] if calculator returned obviously wrong results
17
- def call(dice, result_type: :frequencies)
27
+ def call(dice, result_type: :frequencies, **options)
18
28
  unless RESULT_TYPES.include?(result_type)
19
29
  raise DiceyError, "#{result_type} is not a valid result type!"
20
30
  end
@@ -22,7 +32,7 @@ module Dicey
22
32
  return {} if dice.empty?
23
33
  raise DiceyError, "#{self.class} can not handle these dice!" unless valid_for?(dice)
24
34
 
25
- frequencies = calculate(dice)
35
+ frequencies = calculate(dice, **options)
26
36
  verify_result(frequencies, dice)
27
37
  frequencies = sort_result(frequencies)
28
38
  transform_result(frequencies, result_type)
@@ -46,7 +56,7 @@ module Dicey
46
56
 
47
57
  # Peform frequencies calculation.
48
58
  # (see #call)
49
- def calculate(dice)
59
+ def calculate(dice, **nil)
50
60
  # :nocov:
51
61
  raise NotImplementedError
52
62
  # :nocov:
@@ -12,7 +12,7 @@ module Dicey
12
12
  dice.all?(NumericDie)
13
13
  end
14
14
 
15
- def calculate(dice)
15
+ def calculate(dice, **nil)
16
16
  combine_dice_enumerators(dice).map(&:sum).tally
17
17
  end
18
18
 
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_calculator"
4
+
5
+ module Dicey
6
+ module SumFrequencyCalculators
7
+ # "Calculator" for a collection of {NumericDie} using empirically-obtained statistics.
8
+ #
9
+ # @note This calculator is mostly a joke. It can be useful for educational purposes,
10
+ # or to verify results of {BruteForce} when in doubt. It is not used by default.
11
+ #
12
+ # Does a number of rolls and calculates approximate probabilities from that.
13
+ # Even if frequencies are requested, results are non-integer.
14
+ #
15
+ # *Options:*
16
+ # - *rolls* (Integer) (_defaults_ _to:_ _N_) — number of rolls to perform
17
+ class Empirical < BaseCalculator
18
+ # Default number of rolls to perform.
19
+ N = 10_000
20
+
21
+ private
22
+
23
+ def validate(dice)
24
+ dice.all?(NumericDie)
25
+ end
26
+
27
+ def calculate(dice, rolls: N)
28
+ statistics = rolls.times.with_object(Hash.new(0)) { |_, hash| hash[dice.sum(&:roll)] += 1 }
29
+ total_results = dice.map(&:sides_num).reduce(:*)
30
+ statistics.transform_values { (_1 * total_results).fdiv(rolls) }
31
+ end
32
+
33
+ def verify_result(*)
34
+ # Ignore verification, as this is inherently imprecise.
35
+ true
36
+ end
37
+ end
38
+ end
39
+ end
@@ -10,7 +10,9 @@ module Dicey
10
10
  #
11
11
  # Based on Kronecker substitution method for polynomial multiplication.
12
12
  # @see https://en.wikipedia.org/wiki/Kronecker_substitution
13
- # @see https://arxiv.org/pdf/0712.4046v1.pdf in particular section 3
13
+ # @see https://arxiv.org/pdf/0712.4046v1.pdf
14
+ # David Harvey, Faster polynomial multiplication via multi-point Kronecker substitution
15
+ # (in particular section 3)
14
16
  class KroneckerSubstitution < BaseCalculator
15
17
  private
16
18
 
@@ -18,7 +20,7 @@ module Dicey
18
20
  dice.all? { |die| die.sides_list.all? { _1.is_a?(Integer) && _1 >= 0 } }
19
21
  end
20
22
 
21
- def calculate(dice)
23
+ def calculate(dice, **nil)
22
24
  polynomials = build_polynomials(dice)
23
25
  evaluation_point = find_evaluation_point(polynomials)
24
26
  values = evaluate_polynomials(polynomials, evaluation_point)
@@ -9,7 +9,7 @@ module Dicey
9
9
  # Example dice: (1,2,3,4), (-2,-1,0,1,2), (0,0.2,0.4,0.6), (-1,-2,-3).
10
10
  #
11
11
  # Based on extension of Pascal's triangle for a higher number of coefficients.
12
- # @see https://en.wikipedia.org/wiki/Pascal%27s_triangle
12
+ # @see https://en.wikipedia.org/wiki/Pascal's_triangle
13
13
  # @see https://en.wikipedia.org/wiki/Trinomial_triangle
14
14
  class MultinomialCoefficients < BaseCalculator
15
15
  private
@@ -33,9 +33,7 @@ module Dicey
33
33
  true
34
34
  end
35
35
 
36
- # @param dice [Array<NumericDie>]
37
- # @return [Hash{Numeric => Integer}]
38
- def calculate(dice)
36
+ def calculate(dice, **nil)
39
37
  first_die = dice.first
40
38
  number_of_sides = first_die.sides_num
41
39
  number_of_dice = dice.size
data/lib/dicey/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dicey
4
- VERSION = "0.14.0"
4
+ VERSION = "0.15.0"
5
5
  end
data/lib/dicey.rb CHANGED
@@ -5,7 +5,7 @@ module Dicey
5
5
  # General error for Dicey.
6
6
  class DiceyError < StandardError; end
7
7
 
8
- Dir["#{__dir__}/dicey/*.rb"].each { require _1 }
9
- Dir["#{__dir__}/dicey/output_formatters/*.rb"].each { require _1 }
10
- Dir["#{__dir__}/dicey/sum_frequency_calculators/*.rb"].each { require _1 }
8
+ Dir["dicey/*.rb", base: __dir__].each { require_relative _1 }
9
+ Dir["dicey/output_formatters/*.rb", base: __dir__].each { require_relative _1 }
10
+ Dir["dicey/sum_frequency_calculators/*.rb", base: __dir__].each { require_relative _1 }
11
11
  end
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.14.0
4
+ version: 0.15.0
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-09-11 00:00:00.000000000 Z
11
+ date: 2025-09-22 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
@@ -46,6 +46,7 @@ files:
46
46
  - lib/dicey/roller.rb
47
47
  - lib/dicey/sum_frequency_calculators/base_calculator.rb
48
48
  - lib/dicey/sum_frequency_calculators/brute_force.rb
49
+ - lib/dicey/sum_frequency_calculators/empirical.rb
49
50
  - lib/dicey/sum_frequency_calculators/kronecker_substitution.rb
50
51
  - lib/dicey/sum_frequency_calculators/multinomial_coefficients.rb
51
52
  - lib/dicey/sum_frequency_calculators/runner.rb
@@ -57,9 +58,9 @@ licenses:
57
58
  metadata:
58
59
  homepage_uri: https://github.com/trinistr/dicey
59
60
  bug_tracker_uri: https://github.com/trinistr/dicey/issues
60
- documentation_uri: https://rubydoc.info/gems/dicey/0.14.0
61
- source_code_uri: https://github.com/trinistr/dicey/tree/v0.14.0
62
- changelog_uri: https://github.com/trinistr/dicey/blob/v0.14.0/CHANGELOG.md
61
+ documentation_uri: https://rubydoc.info/gems/dicey/0.15.0
62
+ source_code_uri: https://github.com/trinistr/dicey/tree/v0.15.0
63
+ changelog_uri: https://github.com/trinistr/dicey/blob/v0.15.0/CHANGELOG.md
63
64
  rubygems_mfa_required: 'true'
64
65
  post_install_message:
65
66
  rdoc_options: