dicey 0.15.2 โ†’ 0.16.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: c18dbf421c9c19d438194ec9f63a9a22bfd18abb50d1f3b491a6e8067e3a04ae
4
- data.tar.gz: e01eabaa58523f5fb2e1610ec671fc41a31f4aee7a583b64aa52d5bb9f083462
3
+ metadata.gz: 1b590b1e43f4e265f3a8fab12a949de1cdd20cc39b1f4334d997520cdbc9efc4
4
+ data.tar.gz: 2c0e3c4577a3f22483296b3a6c238c54937c8b5a360b21c53ca2d0b9aa432fcb
5
5
  SHA512:
6
- metadata.gz: 48759bc7919e8dbf699777a92735c586a8365f517c2c12d48cb725f12ada1fcfd6904e2c08e54ec7159267ad79003e7a1725a1e6e97694d7d4cbfb32198b2566
7
- data.tar.gz: fa0cd4f20b6262cf927eb98cb5c296db2473910a27e2500323f80aeb5d2ac3224de143f67e0990fe555890535ff395d8514996934759984316d9fe22c4fb0e17
6
+ metadata.gz: bed55910f504316de9f078f938f2a9a56e8dfbe65c3552eee330bc58f7559f699a0906837f4bda30fe098feed58469e8c7039e78864e79f4d263918729222ebb
7
+ data.tar.gz: 392b0c5c3b5e547d497092358769260b7b8e1c99bd98f1eb8759c389757386452720a173e619aea5bd2cebe996348c99f314bfc3b1e54fa068583fc083525cd9
data/README.md CHANGED
@@ -24,6 +24,7 @@ In seriousness, this program is mainly useful for calculating total frequency (p
24
24
  - [Example 2: Complex distribution with different dice](#example-2-complex-distribution-with-different-dice)
25
25
  - [Example 3: Custom dice](#example-3-custom-dice)
26
26
  - [Example 4: Rolling even more custom dice](#example-4-rolling-even-more-custom-dice)
27
+ - [Example 5: Non-numeric dice](#example-5-non-numeric-dice)
27
28
  - [All ways to define dice](#all-ways-to-define-dice)
28
29
  - [Usage: API](#usage-api)
29
30
  - [Dice](#dice)
@@ -63,7 +64,17 @@ gem install dicey
63
64
 
64
65
  Or, if using Bundler, add it to your `Gemfile`:
65
66
  ```rb
66
- gem "dicey", "~> 0.14"
67
+ gem "dicey", "~> 0.16"
68
+ ```
69
+
70
+ (Optional) If intending to work with non-numeric dice, install **vector_number** too:
71
+ ```sh
72
+ gem install vector_number
73
+ ```
74
+
75
+ or add it to your `Gemfile`:
76
+ ```rb
77
+ gem "vector_number"
67
78
  ```
68
79
 
69
80
  > [!TIP]
@@ -76,6 +87,7 @@ gem "dicey", "~> 0.14"
76
87
 
77
88
  **Dicey** is tested to work on CRuby 3.0+, latest JRuby and TruffleRuby. Compatible implementations should work too.
78
89
  - JSON and YAML formatting require `json` and `yaml`.
90
+ - Non-numeric dice require gem `vector_number` to be installed.
79
91
 
80
92
  Otherwise, there are no direct dependencies.
81
93
 
@@ -238,18 +250,35 @@ roll => 7/2 # You probably will get a different value here.
238
250
  > [!NOTE]
239
251
  > ๐Ÿ’ก Roll mode is compatible with `--format` option.
240
252
 
253
+ ### Example 5: Non-numeric dice
254
+
255
+ You are a wizard and you have a spellbook with an elemental vortex spell that deals three instances of random elemental damage. Let's find out what you have for your enemies in store today:
256
+ ```sh
257
+ $ dicey 3dโ„๏ธ,๐Ÿ”ฅ,โšก๏ธ,๐ŸŒช,๐ŸŒฒ -m r
258
+ # (โ„๏ธ,๐Ÿ”ฅ,โšก๏ธ,๐ŸŒช,๐ŸŒฒ)+(โ„๏ธ,๐Ÿ”ฅ,โšก๏ธ,๐ŸŒช,๐ŸŒฒ)+(โ„๏ธ,๐Ÿ”ฅ,โšก๏ธ,๐ŸŒช,๐ŸŒฒ)
259
+ roll => 1โ‹…๐ŸŒช + 1โ‹…๐ŸŒฒ + 1โ‹…โšก๏ธ
260
+ ```
261
+
262
+ Wind, wood and lightning in equal proportion it is! Your enemies will tremble!
263
+
264
+ Regrettably, it is not possible to use elemental dice without installing **vector_number** gem first.
265
+
241
266
  ### All ways to define dice
242
267
 
243
- There are three *main* ways to define dice:
268
+ There are four *main* ways to define dice:
244
269
  - *"5", "25", or "525"*: a single positive integer makes a regular die (like a D20).
245
270
  - *"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
271
  - 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.
272
+ - *"1,2,4", "(-1.5,0,1.5)", or "2,"*: a list of any numbers separated by commas, possibly in round brackets, makes a custom numeric die.
248
273
  - Lists can end in a comma, allowing single-number lists.
274
+ - *"1,1.5,Two", "(๐Ÿ’š,๐Ÿงก,๐Ÿ’™,๐Ÿ’œ)" or "('1','(bracket)')"*: a list of strings and numbers separated by commas, possibly in round brackets, makes an arbitrary die.
275
+ - Lists can end in a comma, allowing single-string lists.
276
+ - Single (') or double (") quotes can be used to use other quotes and round brackets in the string. Otherwise, they are prohibited. Commas are always prohibited.
277
+ - Quotes can also be used to treat numbers as strings.
249
278
 
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 "-".
279
+ *"D6", "d(-1,3)", "d2..4", or "d๐Ÿ’š,๐Ÿงก"*: any definitions can be prefixed with "d" or "D". While this doesn't do anything on its own, it can be useful to not start a definition with "-".
251
280
 
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.
281
+ *"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.
253
282
 
254
283
  ## Usage: API
255
284
 
@@ -260,7 +289,7 @@ There are three *main* ways to define dice:
260
289
  ### Dice
261
290
 
262
291
  There are 3 classes of dice currently:
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.
292
+ - `Dicey::AbstractDie` is the base class for other dice, but can be used on its own. It has no restrictions on values of sides.
264
293
  - `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
294
  - `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*).
266
295
 
@@ -282,7 +311,7 @@ Dicey::DieFoundry.new.call("100")
282
311
  Dicey::DieFoundry.new.call("2d6")
283
312
  # same as Dicey::RegularDie.from_count(2, 6)
284
313
  Dicey::DieFoundry.new.call("1d1,2,4")
285
- # same as Dicey::NumericDie.from_list([1,2,4])
314
+ # same as Dicey::NumericDie.from_count(1, [1,2,4])
286
315
  ```
287
316
 
288
317
  It only takes a single argument and may return both an array of dice and a single die. You will probably want to use `Enumerable#flat_map`:
@@ -292,6 +321,26 @@ foundry = Dicey::DieFoundry.new
292
321
  # same as [Dicey::RegularDie.new(8), *Dicey::RegularDie.from_count(2, 4)]
293
322
  ```
294
323
 
324
+ #### Really custom dice
325
+
326
+ It is easy enough to create numeric dice or dice with distinct symbols. However, what if a symbolic die is needed, but one which also has custom counts of symbols?
327
+
328
+ For example, a game may have a die which can roll 1-3 ๐Ÿ’š or a โ™ฅ๏ธ. You could just use completely different strings for the different numbers of hearts, but they would not be summable (what if you need to roll two such dice and add them together?). In this case, you can directly use `VectorNumber` to create summable strings:
329
+ ```rb
330
+ # Using Symbols is not required, but they look nicer in output.
331
+ # `DieFoundry` uses Symbols for this reason.
332
+ heal = VectorNumber[:"๐Ÿ’š"]
333
+ regen = VectorNumber[:"โ™ฅ๏ธ"]
334
+ die = Dicey::AbstractDie.new([heal, heal * 2, heal * 3, regen])
335
+ # => #<Dicey::AbstractDie:0x00007f4a7c95efe8 @current_side_index=0, @sides_list=[(1โ‹…๐Ÿ’š), (2โ‹…๐Ÿ’š), (3โ‹…๐Ÿ’š), (1โ‹…โ™ฅ๏ธ)], @sides_num=4>
336
+ ```
337
+
338
+ Now such dice can easily be rolled together and results summed:
339
+ ```rb
340
+ die.roll + die.roll
341
+ # => (5โ‹…๐Ÿ’š)
342
+ ```
343
+
295
344
  ### Rolling
296
345
 
297
346
  `Dicey::AbstractDie#roll` implements the rolling:
@@ -338,18 +387,18 @@ die.roll
338
387
  Distribution calculators live in `Dicey::SumFrequencyCalculators` module. There are four calculators currently:
339
388
  - `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
389
  - `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
- - `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.
390
+ - `Dicey::SumFrequencyCalculators::BruteForce` is the most generic and slowest one, but can work with *any* dice. It needs gem "**vector_number**" to be installed and available to work with non-numeric dice.
391
+ - `Dicey::SumFrequencyCalculators::Empirical`... this is more of a tool than a calculator. It "calculates" probabilities by performing a large number of rolls and counting frequencies of outcomes. It can be interesting to play around with and see how practical results compare to theoretical ones. Due to its simplicity, it also works with *any* dice.
343
392
 
344
393
  Calculators inherit from `Dicey::SumFrequencyCalculators::BaseCalculator` and provide the following public interface:
345
394
  - `#call(dice, result_type: {:frequencies | :probabilities}, **options) : Hash`
346
395
  - `#valid_for?(dice) : Boolean`
347
396
 
348
- See [Diving deeper](#diving-deeper) for more details on limitations and complexity considerations.
397
+ See [Diving deeper](#diving-deeper) for more details on limitations and complexity considerations of different algorithms.
349
398
 
350
399
  ### Distribution properties
351
400
 
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:
401
+ While distribution itself is already enough in most cases (we are talking just dice here, after all). it may be of interest to calculate properties of it: mode, mean, expected value, standard deviation, etc. `Dicey::DistributionPropertiesCalculator` provides this functionality:
353
402
  ```rb
354
403
  Dicey::DistributionPropertiesCalculator.new.call(
355
404
  Dicey::SumFrequencyCalculators::KroneckerSubstitution.new.call(
@@ -358,18 +407,19 @@ Dicey::DistributionPropertiesCalculator.new.call(
358
407
  )
359
408
  # =>
360
409
  # {: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)}
410
+ # :modes=>[[4]]
411
+ # :min=>2,
412
+ # :max=>6,
413
+ # :total_range=>4,
414
+ # :mid_range=>4,
415
+ # :median=>4,
416
+ # :arithmetic_mean=>4,
417
+ # :expected_value=>4,
418
+ # :variance=>(4/3),
419
+ # :standard_deviation=>1.1547005383792515,
420
+ # :skewness=>0.0,
421
+ # :kurtosis=>(9/4),
422
+ # :excess_kurtosis=>(-3/4)}
373
423
  ```
374
424
 
375
425
  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)?
@@ -381,21 +431,30 @@ Dicey::DistributionPropertiesCalculator.new.call(
381
431
  )
382
432
  # =>
383
433
  # {: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)}
434
+ # :modes=>[[5]],
435
+ # :min=>2,
436
+ # :max=>8,
437
+ # :total_range=>6,
438
+ # :mid_range=>5,
439
+ # :median=>5,
440
+ # :arithmetic_mean=>5,
441
+ # :expected_value=>(31/6),
442
+ # :variance=>(101/36),
443
+ # :standard_deviation=>1.674979270186815,
444
+ # :skewness=>-0.15762965389465178,
445
+ # :kurtosis=>(23145/10201),
446
+ # :excess_kurtosis=>(-7458/10201)}
447
+ ```
448
+
449
+ 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 or high skewness. For example, let's take two D2 and a weighted die to create a distribution with two peaks:
450
+ ```rb
451
+ [*Dicey::RegularDie.from_count(2, 2), Dicey::NumericDie.new([1,8,9])]
452
+ # =>
453
+ # {:mode=>[11, 12],
454
+ # :modes=>[[4], [11, 12]],
396
455
  ```
397
456
 
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.
457
+ You can see that 11 and 12 are the most likely outcomes, coming from a larger peak, but a smaller peak (with lower probability) is placed at 4.
399
458
 
400
459
  ## Diving deeper
401
460
 
@@ -418,7 +477,7 @@ Dicey is in principle able to handle any real numeric dice and some abstract dic
418
477
  Currently, three algorithms for calculating frequencies are implemented, with different possibilities and trade-offs.
419
478
 
420
479
  > [!NOTE]
421
- > ๐Ÿ’ก Complexity is listed for `n` dice with at most `m` sides and has not been rigorously proven.
480
+ > ๐Ÿ’ก Complexity is listed for **n** dice with at most **m** sides and has not been rigorously proven.
422
481
 
423
482
  ### Kronecker substitution
424
483
 
@@ -426,7 +485,7 @@ An algorithm based on fast polynomial multiplication. This is the default algori
426
485
 
427
486
  - Limitations: only **natural** dice are allowed, including **regular** dice.
428
487
  - Example: `dicey 5 3,4,1 0,`
429
- - Complexity: `O(mโ‹…n)` where `m` is the highest value
488
+ - Complexity: **O(mโ‹…n)** where **m** is the highest value
430
489
 
431
490
  ### Multinomial coefficients
432
491
 
@@ -434,15 +493,15 @@ This algorithm is based on raising a univariate polynomial to a power and using
434
493
 
435
494
  - Limitations: only *equal* **arithmetic** dice are allowed.
436
495
  - Example: `dicey 1.5,3,4.5,6 1.5,3,4.5,6 1.5,3,4.5,6`
437
- - Complexity: `O(mโ‹…nยฒ)`
496
+ - Complexity: **O(mโ‹…nยฒ)**
438
497
 
439
498
  ### Brute force
440
499
 
441
500
  As a last resort, there is a brute force algorithm which goes through every possible dice roll and adds results together. While quickly growing terrible in performace, it has the largest input space, allowing to work with completely nonsensical dice, including aforementioned dice with complex numbers.
442
501
 
443
- - Limitations: objects on dice sides must be numbers.
502
+ - Limitations: without **vector_number** all values must be numbers, otherwise almost any values are viable.
444
503
  - Example: `dicey 5 1,0.1,2 1,-1,1,-1,0`
445
- - Complexity: `O(mโฟ)`
504
+ - Complexity: **O(mโฟ)**
446
505
 
447
506
  ## Development
448
507
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Dicey
4
4
  # Asbtract die which may have an arbitrary list of sides,
5
- # not even neccessarily numbers (but preferably so).
5
+ # not even neccessarily numbers, but strings or other objects.
6
6
  class AbstractDie
7
7
  # rubocop:disable Style/ClassVars
8
8
 
@@ -69,9 +69,11 @@ module Dicey
69
69
  # @param sides_list [Enumerable<Any>]
70
70
  # @raise [DiceyError] if +sides_list+ is empty
71
71
  def initialize(sides_list)
72
- @sides_list = (Array === sides_list) ? sides_list.dup.freeze : sides_list.to_a.freeze
72
+ @sides_list = sides_list.to_a
73
+ @sides_list = @sides_list.dup if @sides_list.equal?(sides_list)
73
74
  raise DiceyError, "dice must have at least one side!" if @sides_list.empty?
74
75
 
76
+ @sides_list.freeze
75
77
  @sides_num = @sides_list.size
76
78
  @current_side_index = 0
77
79
  end
@@ -97,7 +99,7 @@ module Dicey
97
99
  #
98
100
  # @return [Any] rolled side
99
101
  def roll
100
- @current_side_index = self.class.rand(0...@sides_num)
102
+ @current_side_index = self.class.rand(@sides_num)
101
103
  current
102
104
  end
103
105
 
@@ -3,27 +3,37 @@
3
3
  require_relative "numeric_die"
4
4
  require_relative "regular_die"
5
5
 
6
- require_relative "rational_to_integer"
6
+ require_relative "mixins/rational_to_integer"
7
7
 
8
8
  module Dicey
9
9
  # Helper class to define die definitions and automatically select the best one.
10
10
  class DieFoundry
11
- include RationalToInteger
11
+ include Mixins::RationalToInteger
12
12
 
13
13
  # Regexp for matching a possible count.
14
- PREFIX = /(?>(?<count>[1-9]\d*+)?d)?+/i
14
+ PREFIX = /(?:(?<count>[1-9]\d*+)?+d)?+/i
15
+ # Regexp for an integer number.
16
+ INTEGER = /(?:-?\d++)/
17
+ # Regexp for a (possibly) fractional number.
18
+ DECIMAL = /(?:-?\d++(?:\.\d++)?)/
19
+ # Regexp for an "arbitrary" string.
20
+ STRING = /(?:(?<side>[^"',()]++)|"(?<side>[^",]++)"|'(?<side>[^',]++)')/
15
21
 
16
22
  # Possible molds for the dice. They are matched in the order as written.
17
23
  MOLDS = [
18
24
  # Positive integer goes into the RegularDie mold.
19
25
  [/\A#{PREFIX}(?<sides>[1-9]\d*+)\z/, :regular_mold].freeze,
20
26
  # Integer range goes into the NumericDie mold.
21
- [/\A#{PREFIX}\(?(?<begin>-?\d++)(?>[-โ€“โ€”โ€ฆ]|\.{2,3})(?<end>-?\d++)\)?\z/, :range_mold].freeze,
27
+ [/\A#{PREFIX}\(?(?<begin>#{INTEGER})(?:[-โ€“โ€”โ€ฆ]|\.{2,3})(?<end>#{INTEGER})\)?\z/,
28
+ :range_mold].freeze,
22
29
  # List of numbers goes into the NumericDie mold.
23
- [/\A#{PREFIX}\(?(?<sides>-?\d++(?>,(?>-?\d++)?)+|,)\)?\z/, :weirdly_shaped_mold].freeze,
30
+ [/\A#{PREFIX}\(?(?<sides>#{INTEGER}(?:(?:,#{INTEGER})++,?+|,))\)?\z/,
31
+ :weirdly_shaped_mold].freeze,
24
32
  # Non-integers require special handling for precision.
25
- [/\A#{PREFIX}\(?(?<sides>-?\d++(?>\.\d++)?(?>,(?>-?\d++(?>\.\d++)?)?)+|,)\)?\z/,
33
+ [/\A#{PREFIX}\(?(?<sides>#{DECIMAL}(?:(?:,#{DECIMAL})++,?+|,))\)?\z/,
26
34
  :weirdly_precise_mold].freeze,
35
+ # Lists of stuff are broken into AbstractDie.
36
+ [/\A#{PREFIX}\(?(?<sides>#{STRING}(?:(?:,#{STRING})++,?+|,))\)?\z/, :cursed_mold].freeze,
27
37
  # Anything else is spilled on the floor.
28
38
  ].freeze
29
39
 
@@ -34,13 +44,16 @@ module Dicey
34
44
  # - integer range (like "3-6" or "(-5..5)"), which produces a {NumericDie};
35
45
  # - list of integers (like "3,4,5", "(-1,0,1)", or "2,"), which produces a {NumericDie};
36
46
  # - list of decimal numbers (like "0.5,0.2,0.8" or "(2.0,)"), which produces a {NumericDie},
37
- # but uses +Rational+ for values to maintain precise results.
47
+ # but uses +Rational+ for values to maintain precise results;
48
+ # - list of strings, possibly mixed with numbers (like "0.5,asdf" or "(๐Ÿ‘‘,โ™ ๏ธ,โ™ฅ๏ธ,โ™ฃ๏ธ,โ™ฆ๏ธ,โš“๏ธ)"),
49
+ # which produces an {AbstractDie} with strings converted to Symbols
50
+ # and numbers treated the same as in previous cases.
38
51
  #
39
52
  # Any die definition can be prefixed with a count, like "2D6" or "1d1,3,5" to create an array.
40
53
  # A plain "d" without an explicit count is ignored instead, creating a single die.
41
54
  #
42
55
  # @param definition [String] die shape
43
- # @return [NumericDie, RegularDie, Array<NumericDie>, Array<RegularDie>]
56
+ # @return [AbstractDie, Array<AbstractDie>]
44
57
  # @raise [DiceyError] if no mold fits the definition
45
58
  def call(definition)
46
59
  matched, name =
@@ -77,6 +90,21 @@ module Dicey
77
90
  build_dice(NumericDie, definition[:count], sides)
78
91
  end
79
92
 
93
+ def cursed_mold(definition)
94
+ sides = definition[:sides].split(",")
95
+ sides.map! do |side|
96
+ case side
97
+ when /\A#{INTEGER}\z/o
98
+ side.to_i
99
+ when /\A#{DECIMAL}\z/o
100
+ rational_to_integer(Rational(side))
101
+ else
102
+ side.match(STRING)[:side].to_sym
103
+ end
104
+ end
105
+ build_dice(AbstractDie, definition[:count], sides)
106
+ end
107
+
80
108
  def build_dice(die_class, count, sides)
81
109
  if count
82
110
  die_class.from_count(count.to_i, sides)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "rational_to_integer"
3
+ require_relative "mixins/rational_to_integer"
4
4
 
5
5
  module Dicey
6
6
  # Calculates distribution properties,
@@ -16,7 +16,7 @@ module Dicey
16
16
  # (median, mean, ...) are all equal.
17
17
  # Mode is often not unique, but includes this center.
18
18
  class DistributionPropertiesCalculator
19
- include RationalToInteger
19
+ include Mixins::RationalToInteger
20
20
 
21
21
  # Calculate properties for a given distribution.
22
22
  #
@@ -27,7 +27,7 @@ module Dicey
27
27
  #
28
28
  # @param distribution [Hash{Numeric => Numeric}, Hash{Any => Numeric}]
29
29
  # distribution with pre-sorted keys
30
- # @return [Hash{Symbol => Numeric, Array<Numeric>, Array<Array<Numeric>>}]
30
+ # @return [Hash{Symbol => Any}]
31
31
  def call(distribution)
32
32
  return {} if distribution.empty?
33
33
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dicey
4
+ # @api private
5
+ # Various mixins with shared methods.
6
+ module Mixins
7
+ # Mix-in for converting rationals with denominator of 1 to integers.
8
+ module RationalToInteger
9
+ private
10
+
11
+ # Convert +value+ to +Integer+ if it's a +Rational+ with denominator of 1.
12
+ # Otherwise, return +value+ as-is.
13
+ #
14
+ # @value [Numeric, Any]
15
+ # @return [Numeric, Integer, Any]
16
+ def rational_to_integer(value)
17
+ (Rational === value && value.denominator == 1) ? value.numerator : value
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dicey
4
+ module Mixins
5
+ # Mix-in for converting dice with non-numeric sides into dice with +VectorNumber+ sides.
6
+ module VectorizeDice
7
+ private
8
+
9
+ # Vectorize non-numeric sides for AbstractDie instances,
10
+ # leaving NumericDie instances unchanged.
11
+ #
12
+ # Check for VectorNumber availability *before* calling.
13
+ #
14
+ # @param dice [Array<AbstractDie>]
15
+ # @return [Array<AbstractDie>] a new array of dice
16
+ def vectorize_dice(dice)
17
+ dice.map do |die|
18
+ next die if NumericDie === die
19
+
20
+ sides =
21
+ die.sides_list.map do |side|
22
+ (Numeric === side || VectorNumber === side) ? side : VectorNumber.new([side])
23
+ end
24
+ die.class.new(sides)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -4,8 +4,12 @@ require_relative "abstract_die"
4
4
 
5
5
  module Dicey
6
6
  # A die which only has numeric sides, with no shenanigans.
7
+ #
8
+ # The only inherent difference in behavior compared to {AbstractDie} is
9
+ # that this class checks values for sides on initialization.
10
+ # However, other classes may reject {AbstractDie} even with all numeric sides.
7
11
  class NumericDie < AbstractDie
8
- # @param sides_list [Enumerable<Numeric>]
12
+ # @param sides_list [Array<Numeric>, Range<Numeric>, Enumerable<Numeric>]
9
13
  # @raise [DiceyError] if +sides_list+ contains non-numerical values or is empty
10
14
  def initialize(sides_list)
11
15
  if Range === sides_list
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../rational_to_integer"
3
+ require_relative "../mixins/rational_to_integer"
4
4
 
5
5
  module Dicey
6
6
  module OutputFormatters
@@ -8,7 +8,7 @@ module Dicey
8
8
  # Can add an optional description into the result.
9
9
  # @abstract
10
10
  class KeyValueFormatter
11
- include RationalToInteger
11
+ include Mixins::RationalToInteger
12
12
 
13
13
  # @param hash [Hash{Object => Object}]
14
14
  # @param description [String] text to add as a comment.
@@ -4,6 +4,8 @@ require_relative "numeric_die"
4
4
 
5
5
  module Dicey
6
6
  # Regular die, which has N sides with numbers from 1 to N.
7
+ #
8
+ # As a subclass of {NumericDie}, enjoys its treatment.
7
9
  class RegularDie < NumericDie
8
10
  # @param max [Integer] maximum side / number of sides
9
11
  def initialize(max)
data/lib/dicey/roller.rb CHANGED
@@ -1,13 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Try to load "vector_number" pre-emptively.
4
+ begin
5
+ require "vector_number"
6
+ rescue LoadError
7
+ # VectorNumber not available, sad
8
+ end
9
+
3
10
  require_relative "die_foundry"
4
11
 
5
- require_relative "rational_to_integer"
12
+ require_relative "mixins/rational_to_integer"
13
+ require_relative "mixins/vectorize_dice"
6
14
 
7
15
  module Dicey
8
16
  # Let the dice roll!
17
+ #
18
+ # This is the implementation of roll mode for the CLI.
9
19
  class Roller
10
- include RationalToInteger
20
+ include Mixins::RationalToInteger
21
+ include Mixins::VectorizeDice
11
22
 
12
23
  # @param arguments [Array<String>] die definitions
13
24
  # @param format [#call] formatter for output
@@ -17,9 +28,15 @@ module Dicey
17
28
  raise DiceyError, "no dice!" if arguments.empty?
18
29
 
19
30
  dice = arguments.flat_map { |definition| die_foundry.cast(definition) }
20
- result = dice.sum(&:roll)
31
+ result = roll_dice(dice)
21
32
 
22
33
  format.call({ "roll" => rational_to_integer(result) }, AbstractDie.describe(dice))
34
+ rescue TypeError
35
+ warn <<~TEXT
36
+ Dice with non-numeric sides need gem "vector_number" to be present and available.
37
+ If this is intended, please install the gem.
38
+ TEXT
39
+ raise DiceyError, "can not roll dice with non-numeric sides!"
23
40
  end
24
41
 
25
42
  private
@@ -27,5 +44,10 @@ module Dicey
27
44
  def die_foundry
28
45
  @die_foundry ||= DieFoundry.new
29
46
  end
47
+
48
+ def roll_dice(dice)
49
+ dice = vectorize_dice(dice) if defined?(VectorNumber)
50
+ dice.sum(&:roll)
51
+ end
30
52
  end
31
53
  end
@@ -27,7 +27,8 @@ module Dicey
27
27
 
28
28
  # @param dice [Enumerable<AbstractDie>]
29
29
  # @param result_type [Symbol] one of {RESULT_TYPES}
30
- # @param options [Hash{Symbol => Any}] calculator-specific options
30
+ # @param options [Hash{Symbol => Any}] calculator-specific options,
31
+ # refer to the calculator's documentation to see what it accepts
31
32
  # @return [Hash{Numeric => Numeric}] frequencies or probabilities for each outcome
32
33
  # @raise [DiceyError] if +result_type+ is invalid
33
34
  # @raise [DiceyError] if dice list is invalid for the calculator
@@ -1,44 +1,66 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Try to load "vector_number" pre-emptively.
4
+ begin
5
+ require "vector_number"
6
+ rescue LoadError
7
+ # VectorNumber not available, sad
8
+ end
9
+
3
10
  require_relative "base_calculator"
4
11
 
12
+ require_relative "../mixins/vectorize_dice"
13
+
5
14
  module Dicey
6
15
  module SumFrequencyCalculators
7
- # Calculator for a collection of {NumericDie} using exhaustive search (very slow).
16
+ # Calculator for a collection of {AbstractDie} using exhaustive search (very slow).
17
+ #
18
+ # If dice include non-numeric sides, gem +vector_number+ has to be installed.
8
19
  class BruteForce < BaseCalculator
20
+ include Mixins::VectorizeDice
21
+
9
22
  private
10
23
 
11
24
  def validate(dice)
12
- dice.all?(NumericDie)
25
+ if defined?(VectorNumber) || dice.all?(NumericDie)
26
+ true
27
+ else
28
+ warn <<~TEXT
29
+ Dice with non-numeric sides need gem "vector_number" to be present and available.
30
+ If this is intended, please install the gem.
31
+ TEXT
32
+ false
33
+ end
13
34
  end
14
35
 
15
36
  def calculate(dice, **nil)
16
- combine_dice_enumerators(dice).map(&:sum).tally
37
+ dice = vectorize_dice(dice) if defined?(VectorNumber)
38
+ combine_dice_enumerators(dice.map(&:sides_list)).map(&:sum).tally
17
39
  end
18
40
 
19
41
  if defined?(Enumerator::Product)
20
42
  # Get an enumerator which goes through all possible permutations of dice sides.
21
43
  #
22
- # @param dice [Enumerable<NumericDie>]
23
- # @return [Enumerator<Array<Numeric>>]
24
- def combine_dice_enumerators(dice)
25
- Enumerator::Product.new(*dice.map(&:sides_list))
44
+ # @param side_lists [Enumerable<Enumerable<Any>>]
45
+ # @return [Enumerator<Enumerable<Enumerable<Any>>>]
46
+ def combine_dice_enumerators(side_lists)
47
+ Enumerator::Product.new(*side_lists)
26
48
  end
27
49
  # :nocov:
28
50
  else
29
51
  # Get an enumerator which goes through all possible permutations of dice sides.
30
52
  #
31
- # @param dice [Enumerable<NumericDie>]
32
- # @return [Enumerator<Array<Numeric>>]
33
- def combine_dice_enumerators(dice)
34
- product(dice.map(&:sides_list))
53
+ # @param side_lists [Enumerable<Enumerable<Any>>]
54
+ # @return [Enumerator<Array<Array<Any>>>]
55
+ def combine_dice_enumerators(side_lists)
56
+ product(side_lists)
35
57
  end
36
58
 
37
59
  # Simplified implementation of {Enumerator::Product}.
38
60
  # Adapted from {https://bugs.ruby-lang.org/issues/18685#note-10}.
39
61
  #
40
- # @param enums [Enumerable<Enumerable<Numeric>>]
41
- # @return [Enumerator<Array<Numeric>>]
62
+ # @param enums [Enumerable<Enumerable<Any>>]
63
+ # @return [Enumerator<Array<Array<Any>>>]
42
64
  def product(enums, &block)
43
65
  return to_enum(__method__, enums) unless block_given?
44
66
 
@@ -1,10 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Try to load "vector_number" pre-emptively.
4
+ begin
5
+ require "vector_number"
6
+ rescue LoadError
7
+ # VectorNumber not available, sad
8
+ end
9
+
3
10
  require_relative "base_calculator"
4
11
 
12
+ require_relative "../mixins/vectorize_dice"
13
+
5
14
  module Dicey
6
15
  module SumFrequencyCalculators
7
- # "Calculator" for a collection of {NumericDie} using empirically-obtained statistics.
16
+ # "Calculator" for a collection of {AbstractDie} using empirically-obtained statistics.
8
17
  #
9
18
  # @note This calculator is mostly a joke. It can be useful for educational purposes,
10
19
  # or to verify results of {BruteForce} when in doubt. It is not used by default.
@@ -12,19 +21,32 @@ module Dicey
12
21
  # Does a number of rolls and calculates approximate probabilities from that.
13
22
  # Even if frequencies are requested, results are non-integer.
14
23
  #
24
+ # If dice include non-numeric sides, gem +vector_number+ has to be installed.
25
+ #
15
26
  # *Options:*
16
27
  # - *rolls* (Integer) (_defaults_ _to:_ _N_) โ€” number of rolls to perform
17
28
  class Empirical < BaseCalculator
29
+ include Mixins::VectorizeDice
30
+
18
31
  # Default number of rolls to perform.
19
32
  N = 10_000
20
33
 
21
34
  private
22
35
 
23
36
  def validate(dice)
24
- dice.all?(NumericDie)
37
+ if defined?(VectorNumber) || dice.all?(NumericDie)
38
+ true
39
+ else
40
+ warn <<~TEXT
41
+ Dice with non-numeric sides need gem "vector_number" to be present and available.
42
+ If this is intended, please install the gem.
43
+ TEXT
44
+ false
45
+ end
25
46
  end
26
47
 
27
48
  def calculate(dice, rolls: N)
49
+ dice = vectorize_dice(dice) if defined?(VectorNumber)
28
50
  statistics = rolls.times.with_object(Hash.new(0)) { |_, hash| hash[dice.sum(&:roll)] += 1 }
29
51
  total_results = dice.map(&:sides_num).reduce(:*)
30
52
  statistics.transform_values { Rational(_1 * total_results, rolls) }
@@ -11,6 +11,7 @@ module Dicey
11
11
  # Based on extension of Pascal's triangle for a higher number of coefficients.
12
12
  # @see https://en.wikipedia.org/wiki/Pascal's_triangle
13
13
  # @see https://en.wikipedia.org/wiki/Trinomial_triangle
14
+ # @see https://en.wikipedia.org/wiki/Multinomial_distribution
14
15
  class MultinomialCoefficients < BaseCalculator
15
16
  private
16
17
 
@@ -42,6 +42,25 @@ module Dicey
42
42
  { Complex(1, 1) => 1, Complex(2, 1) => 1, Complex(3, 1) => 1,
43
43
  Complex(1, 2) => 1, Complex(2, 2) => 1, Complex(3, 2) => 1,
44
44
  Complex(1, 3) => 1, Complex(2, 3) => 1, Complex(3, 3) => 1 }],
45
+ *(
46
+ # :nocov:
47
+ if defined?(VectorNumber)
48
+ # :nocov:
49
+ [
50
+ [[["s", "a", "d", 33]],
51
+ { VectorNumber["s"] => 1, VectorNumber["a"] => 1, VectorNumber["d"] => 1, 33 => 1 }],
52
+ [[%w[s a d], [0, 1, 2]],
53
+ { VectorNumber["s"] => 1, VectorNumber["s", 1] => 1, VectorNumber["s", 2] => 1,
54
+ VectorNumber["a"] => 1, VectorNumber["a", 1] => 1, VectorNumber["a", 2] => 1,
55
+ VectorNumber["d"] => 1, VectorNumber["d", 1] => 1, VectorNumber["d", 2] => 1 }],
56
+ [Array.new(2) { ["s", "a", 4] },
57
+ {
58
+ VectorNumber["s"] * 2 => 1, VectorNumber["a"] * 2 => 1, 8 => 1,
59
+ VectorNumber["s", "a"] => 2, VectorNumber["s", 4] => 2, VectorNumber["a", 4] => 2,
60
+ }],
61
+ ]
62
+ end
63
+ ),
45
64
  ].freeze
46
65
 
47
66
  # Strings for displaying test results.
@@ -84,7 +103,15 @@ module Dicey
84
103
  # @param definition [Array<Integer, Array<Integer>>]
85
104
  # @return [Array<NumericDie>]
86
105
  def build_dice(definition)
87
- definition.map { _1.is_a?(Integer) ? RegularDie.new(_1) : NumericDie.new(_1) }
106
+ definition.map do |die_def|
107
+ if die_def.is_a?(Integer)
108
+ RegularDie.new(die_def)
109
+ elsif die_def.all?(Numeric)
110
+ NumericDie.new(die_def)
111
+ else
112
+ AbstractDie.new(die_def)
113
+ end
114
+ end
88
115
  end
89
116
 
90
117
  # Determine test result for the selected calculator.
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.15.2"
4
+ VERSION = "0.16.0"
5
5
  end
data/lib/dicey.rb CHANGED
@@ -5,6 +5,7 @@ module Dicey
5
5
  # General error for Dicey.
6
6
  class DiceyError < StandardError; end
7
7
 
8
+ Dir["dicey/mixins/*.rb", base: __dir__].each { require_relative _1 }
8
9
  Dir["dicey/*.rb", base: __dir__].each { require_relative _1 }
9
10
  Dir["dicey/output_formatters/*.rb", base: __dir__].each { require_relative _1 }
10
11
  Dir["dicey/sum_frequency_calculators/*.rb", base: __dir__].each { require_relative _1 }
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dicey
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.15.2
4
+ version: 0.16.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-10-08 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2025-10-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: vector_number
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 0.4.3
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 0.4.3
13
27
  description: |
14
28
  Dicey provides a CLI executable and a Ruby API for fast calculation of
15
29
  frequency/probability distributions of dice rolls,
@@ -36,6 +50,8 @@ files:
36
50
  - lib/dicey/cli/options.rb
37
51
  - lib/dicey/die_foundry.rb
38
52
  - lib/dicey/distribution_properties_calculator.rb
53
+ - lib/dicey/mixins/rational_to_integer.rb
54
+ - lib/dicey/mixins/vectorize_dice.rb
39
55
  - lib/dicey/numeric_die.rb
40
56
  - lib/dicey/output_formatters/gnuplot_formatter.rb
41
57
  - lib/dicey/output_formatters/hash_formatter.rb
@@ -43,7 +59,6 @@ files:
43
59
  - lib/dicey/output_formatters/key_value_formatter.rb
44
60
  - lib/dicey/output_formatters/list_formatter.rb
45
61
  - lib/dicey/output_formatters/yaml_formatter.rb
46
- - lib/dicey/rational_to_integer.rb
47
62
  - lib/dicey/regular_die.rb
48
63
  - lib/dicey/roller.rb
49
64
  - lib/dicey/sum_frequency_calculators/base_calculator.rb
@@ -60,9 +75,9 @@ licenses:
60
75
  metadata:
61
76
  homepage_uri: https://github.com/trinistr/dicey
62
77
  bug_tracker_uri: https://github.com/trinistr/dicey/issues
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
78
+ documentation_uri: https://rubydoc.info/gems/dicey/0.16.0
79
+ source_code_uri: https://github.com/trinistr/dicey/tree/v0.16.0
80
+ changelog_uri: https://github.com/trinistr/dicey/blob/v0.16.0/CHANGELOG.md
66
81
  rubygems_mfa_required: 'true'
67
82
  post_install_message:
68
83
  rdoc_options:
@@ -1,18 +0,0 @@
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