games_dice 0.5.0 → 0.6.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +11 -1
  3. data/.gitignore +0 -1
  4. data/.rubocop.yml +13 -48
  5. data/CHANGELOG.md +19 -0
  6. data/Gemfile +5 -3
  7. data/README.md +46 -5
  8. data/Rakefile +12 -5
  9. data/ext/games_dice/extconf.rb +0 -3
  10. data/ext/games_dice/probabilities.c +293 -193
  11. data/ext/games_dice/probabilities.h +2 -12
  12. data/games_dice.gemspec +1 -1
  13. data/lib/games_dice/bunch.rb +50 -36
  14. data/lib/games_dice/bunch_helpers.rb +35 -14
  15. data/lib/games_dice/complex_die.rb +64 -22
  16. data/lib/games_dice/complex_die_helpers.rb +122 -144
  17. data/lib/games_dice/dice.rb +61 -17
  18. data/lib/games_dice/dice_helpers.rb +37 -0
  19. data/lib/games_dice/die_result.rb +6 -8
  20. data/lib/games_dice/marshal.rb +2 -7
  21. data/lib/games_dice/parser.rb +30 -0
  22. data/lib/games_dice/probability_budget.rb +90 -0
  23. data/lib/games_dice/probability_calculation.rb +95 -0
  24. data/lib/games_dice/probability_shortcuts.rb +65 -0
  25. data/lib/games_dice/recipe_cache.rb +155 -0
  26. data/lib/games_dice/version.rb +1 -1
  27. data/lib/games_dice.rb +4 -0
  28. data/script/check_documentation.rb +10 -0
  29. data/spec/games_dice/bunch_regressions_spec.rb +46 -0
  30. data/spec/{bunch_spec.rb → games_dice/bunch_spec.rb} +65 -112
  31. data/spec/games_dice/complex_die_budget_spec.rb +60 -0
  32. data/spec/games_dice/complex_die_limits_spec.rb +119 -0
  33. data/spec/games_dice/complex_die_spec.rb +228 -0
  34. data/spec/games_dice/dice_mutation_spec.rb +165 -0
  35. data/spec/games_dice/dice_regressions_spec.rb +50 -0
  36. data/spec/games_dice/dice_spec.rb +61 -0
  37. data/spec/games_dice/die_result_spec.rb +221 -0
  38. data/spec/{die_spec.rb → games_dice/die_spec.rb} +15 -14
  39. data/spec/{map_rule_spec.rb → games_dice/map_rule_spec.rb} +8 -1
  40. data/spec/{parser_spec.rb → games_dice/parser_spec.rb} +12 -2
  41. data/spec/games_dice/probabilities_boundaries_spec.rb +161 -0
  42. data/spec/games_dice/probabilities_marshal_spec.rb +62 -0
  43. data/spec/games_dice/probabilities_spec.rb +484 -0
  44. data/spec/games_dice/probability_budget_spec.rb +17 -0
  45. data/spec/{reroll_rule_spec.rb → games_dice/reroll_rule_spec.rb} +5 -1
  46. data/spec/games_dice_spec.rb +382 -0
  47. data/spec/helpers.rb +64 -2
  48. data/spec/simple_cov_spec.rb +42 -0
  49. data/spec/support/coverage.rb +12 -0
  50. data/spec/support/validation.rb +14 -0
  51. data/spec/yard/cli/stats_spec.rb +43 -0
  52. metadata +29 -12
  53. data/spec/complex_die_spec.rb +0 -314
  54. data/spec/dice_spec.rb +0 -33
  55. data/spec/die_result_spec.rb +0 -254
  56. data/spec/fixtures/probs_fair_die_6.dat +0 -2
  57. data/spec/probability_spec.rb +0 -584
  58. data/spec/readme_spec.rb +0 -410
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ccf1c8256d9cfa52fa5e99b06c0d0a32288ebdbf54e0e7d0d9f194e4fa21be8
4
- data.tar.gz: 0b0935a31996519bcd74a8b9fe865911bc5d5d1d68a288c46a2d9450588ecefa
3
+ metadata.gz: b413f80b11ad9ca9e552ffcf86f24d4405f8750bff01f231e4f0ab20be1963de
4
+ data.tar.gz: 9eae95cf6218d6ad7d047077d5541dd471707059f5ed325098d1031012ae4bcf
5
5
  SHA512:
6
- metadata.gz: ea6550cb8a23fd28d311ce1b9227e3a9d8b20cfc6ac57459f157e138be850fc543e2e0b17e3dbcee3a6c0ea070c1d126b2b467c1fcb927f13da77acad33dcdbf
7
- data.tar.gz: bb9b6efd51537a01de7ab325546431852ab96d1aa28042776481345e20e0ed60c64d9185b2161bc00e15d63aa0c8c859f33541cb34374fae736a3fa32a1fe3de
6
+ metadata.gz: 7394cf91a9a1c911340fcb3c8edee3ef7adb303172b5106eb0a3956e05be141be2ab874046b4cb99d8ec2f3d7208dc2f6428b10c5d8b12c2c72b3cb6f73af3d0
7
+ data.tar.gz: 9d8c8bd1337c26aa4c3203a62e359af2b180f21bf46831b8929f6f234d60f1e00a26e01f5822b13769c6f30a41b538b6dce20e8df35a3767909428bdb366b8a1
@@ -4,6 +4,7 @@ on:
4
4
  push:
5
5
  branches:
6
6
  - main
7
+ - maintenance
7
8
  pull_request:
8
9
  schedule:
9
10
  - cron: '0 6 8 * *'
@@ -25,7 +26,15 @@ jobs:
25
26
  with:
26
27
  ruby-version: ${{ matrix.ruby }}
27
28
  bundler-cache: true
28
- - run: bundle exec rake compile spec
29
+ - run: bundle exec rake
30
+ - name: Upload Ruby coverage report
31
+ if: always()
32
+ uses: actions/upload-artifact@v6
33
+ with:
34
+ name: ruby-coverage-${{ matrix.ruby }}
35
+ path: coverage/ruby
36
+ retention-days: 14
37
+ if-no-files-found: warn
29
38
 
30
39
  rubocop:
31
40
  name: RuboCop (Ruby ${{ matrix.ruby }})
@@ -41,6 +50,7 @@ jobs:
41
50
  ruby-version: ${{ matrix.ruby }}
42
51
  bundler-cache: true
43
52
  - run: bundle exec rubocop
53
+ - run: bundle exec ncs-rubocop-conf-audit
44
54
 
45
55
  audit:
46
56
  name: Dependency audit
data/.gitignore CHANGED
@@ -4,7 +4,6 @@
4
4
  *.bundle
5
5
  .config
6
6
  .yardoc
7
- Gemfile.lock
8
7
  InstalledFiles
9
8
  _yardoc
10
9
  coverage
data/.rubocop.yml CHANGED
@@ -1,53 +1,18 @@
1
+ inherit_gem:
2
+ ncs_rubocop_conf:
3
+ - config/base.yml
4
+ - config/rake.yml
5
+ - config/rspec.yml
6
+ - config/native_extension.yml
7
+
1
8
  plugins:
2
9
  - rubocop-performance
3
- - rubocop-rake
4
- - rubocop-rspec
5
-
6
- AllCops:
7
- TargetRubyVersion: 3.3
8
- NewCops: enable
9
- SuggestExtensions: false
10
-
11
- Layout/LineLength:
12
- Max: 120
13
-
14
- Metrics/BlockLength:
15
- AllowedMethods:
16
- - context
17
- - describe
18
- - shared_examples
19
- - define
20
-
21
- RSpec/ExampleLength:
22
- Max: 25
23
-
24
- RSpec/MultipleExpectations:
25
- Max: 21
26
-
27
- RSpec/NestedGroups:
28
- Max: 4
29
-
30
- # This project keeps specs flat so they are easy to run individually.
31
- RSpec/SpecFilePathFormat:
32
- Enabled: false
33
10
 
34
- RSpec/IndexedLet:
35
- Enabled: false
11
+ inherit_mode:
12
+ merge:
13
+ - Exclude
36
14
 
37
- RSpec/InstanceVariable:
38
- Exclude:
39
- - spec/complex_die_spec.rb
40
-
41
- RSpec/MultipleDescribes:
42
- Exclude:
43
- - spec/readme_spec.rb
44
-
45
- RSpec/DescribeClass:
46
- Exclude:
47
- - spec/readme_spec.rb
48
-
49
- # Probabilities implements #each without mixing in Enumerable; these specs
50
- # intentionally collect yielded pairs manually.
51
- Style/MapIntoArray:
15
+ # RuboCop rationale: Local agent guidance and ad-hoc probes are not repository source.
16
+ AllCops:
52
17
  Exclude:
53
- - spec/probability_spec.rb
18
+ - 'docs/agent/**/*'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # GamesDice Changelog
2
2
 
3
+ ## 0.6.0 ( 14 October 2026 )
4
+
5
+ * Fix native repetition termination, keeper allocation leaks, and exception cleanup. Validate
6
+ finite probabilities and check integer endpoints and distribution spans before allocation.
7
+ * Fix bounds with negative multipliers, reject nonpositive keeper counts, and make explanations
8
+ reflect weighted results and the actual mapped keepers.
9
+ * Bound aggregate probability truncation error by `1e-10` before floating-point roundoff, use
10
+ cheap exact shortcuts, and raise `GamesDice::ProbabilityLimitError` for unsupported work.
11
+ Bounds return safe enclosing intervals when cheaply provable, otherwise they also raise.
12
+ * Cache combined probabilities and invalidate derived state when exposed recipe arrays or
13
+ mapping labels change. Previous results and explanations clear until the next roll.
14
+ * Clarify that shorthand replacement rerolls use the existing default limit of 1000.
15
+ * **Breaking serialization change:** remove the legacy `Probabilities._load` protocol and support
16
+ paired Marshal dump/load round trips. Previously serialized probabilities, including those
17
+ cached in dice, may no longer load. Recompute from recipes, or export hashes with the previous
18
+ version and reconstruct using `Probabilities.from_h`.
19
+ * Standardise development linting on `ncs_rubocop_conf` v0.2.0 with its base, Rake, RSpec, and native-extension profiles.
20
+ * Refactor the test suite to meet the shared policy without reducing its assertions or coverage.
21
+
3
22
  ## 0.5.0 ( 22 July 2026 )
4
23
 
5
24
  * Add support and CI coverage for Ruby 4.0.
data/Gemfile CHANGED
@@ -6,12 +6,14 @@ source 'https://rubygems.org'
6
6
  gemspec
7
7
 
8
8
  gem 'bundler-audit', '~> 0.9', require: false
9
+ gem 'ncs_rubocop_conf',
10
+ github: 'neilslater/ncs_rubocop_conf',
11
+ tag: 'v0.2.1',
12
+ require: false
9
13
  gem 'rake', '~> 13.2'
10
14
  gem 'rake-compiler', '~> 1.2'
11
15
  gem 'redcarpet', '~> 3.6'
12
16
  gem 'rspec', '~> 3.13'
13
- gem 'rubocop', '~> 1.75', require: false
14
17
  gem 'rubocop-performance', '~> 1.25', require: false
15
- gem 'rubocop-rake', '~> 0.7', require: false
16
- gem 'rubocop-rspec', '~> 3.6', require: false
18
+ gem 'simplecov', '~> 0.22.0', require: false
17
19
  gem 'yard', '~> 0.9'
data/README.md CHANGED
@@ -24,7 +24,7 @@ gem as-is, and add them as features within your project code.
24
24
 
25
25
  ## Supported Ruby Versions
26
26
 
27
- GamesDice 0.4.2 supports MRI Ruby 3.3 and 3.4. It uses a native extension and does not support JRuby.
27
+ GamesDice 0.6.0 supports MRI Ruby 3.3, 3.4, and 4.0. It uses a native extension and does not support JRuby.
28
28
 
29
29
  GamesDice 0.4.0 and 0.4.1 support MRI Ruby 2.6 and later. Versions before 0.4.0 also provide a
30
30
  pure-Ruby implementation of the probability calculations and can be used with JRuby.
@@ -132,7 +132,8 @@ Returns a GamesDice::Probabilities object that describes the probability distrib
132
132
  Note that some distributions, involving keeping a number best or worst results, can take
133
133
  significant time to calculate. If the theoretical distribution would contain a large number
134
134
  of very low probabilities due to a possibility of large numbers re-rolls, then the
135
- calculations cut short, typically approximating to the nearest 1.0e-10.
135
+ calculation may omit unfinished branches within the aggregate `1e-10` truncation bound described below,
136
+ or raise `GamesDice::ProbabilityLimitError` when its limits prevent that guarantee.
136
137
 
137
138
  ### GamesDice::Probabilities instance methods
138
139
 
@@ -166,7 +167,19 @@ Returns the probability of a result equal to the integer n.
166
167
  probabilities.p_eql( 3 ) # => 0.004629629629
167
168
  probabilities.p_eql( 2 ) # => 0.0
168
169
 
169
- Probabilities below 1e-10 due to requiring long sequences of re-rolls will calculate as 0.0
170
+ Very unlikely sequences of rerolls may be left out to keep calculation time reasonable.
171
+ This changes the probability of any result, or range of results, by at most `1e-10`
172
+ (one in ten billion), in addition to normal floating-point rounding. This limit applies
173
+ to all the dice together, including when keeping only the highest or lowest rolls.
174
+
175
+ GamesDice limits the amount of work spent calculating probabilities. If it cannot achieve
176
+ the accuracy described above within those limits, it raises `GamesDice::ProbabilityLimitError`.
177
+ These calculation limits do not change how the dice are rolled.
178
+
179
+ For complex dice, `dice.min` and `dice.max` may describe a wider range than can actually
180
+ be rolled. Every roll will fall within that range, but its endpoints may not be possible
181
+ results. These methods also raise `GamesDice::ProbabilityLimitError` if they cannot find
182
+ suitable bounds within the calculation limits.
170
183
 
171
184
  #### probabilities.p_gt( n )
172
185
 
@@ -200,7 +213,34 @@ Returns the probability of a result less than the integer n.
200
213
 
201
214
  Returns the mean result, weighted by the probability of each value.
202
215
 
203
- probabilities.expected # => 10.5 (rounded to nearest 1e-9)
216
+ probabilities.expected # => 10.5 (mean of the returned distribution)
217
+
218
+ If very unlikely reroll sequences were left out, this mean is also an approximation.
219
+ Rare but unusually high or low results can affect the mean more than their small
220
+ probabilities might suggest, so the `1e-10` accuracy limit for probabilities does
221
+ not apply to the mean.
222
+
223
+ ## Changing dice rules
224
+
225
+ You can change existing dice by editing their bunches, multipliers, reroll rules or mapping
226
+ rules. GamesDice recalculates affected probabilities and minimum/maximum values when next
227
+ needed. Previous results and explanations are cleared until you roll again.
228
+
229
+ Each entry in `dice.bunches` has a matching entry in `dice.bunch_multipliers`. If you add,
230
+ remove or reorder bunches, make the corresponding change to their multipliers before
231
+ using the dice.
232
+
233
+ If you supply custom rule callbacks, keep their behavior unchanged while the rules are
234
+ in use. Replace the rule object when changing its behavior, and do not change dice rules
235
+ from inside a callback.
236
+
237
+ ## Saving and loading dice
238
+
239
+ Breaking change in 0.6.0: legacy serialized `GamesDice::Probabilities` objects requiring the old
240
+ `_load` protocol are no longer supported. This also affects serialized dice containing such
241
+ cached probabilities. New objects support Marshal dump/load round trips. Recompute persisted
242
+ distributions from their recipes, or export hashes using the previous version and reconstruct
243
+ with `Probabilities.from_h`. Only deserialize trusted data.
204
244
 
205
245
  ## String Dice Descriptions
206
246
 
@@ -257,7 +297,8 @@ The simple form specifies a low value that will automatically trigger a re-roll
257
297
  1d6r1
258
298
 
259
299
  When rolled, this die will score from 1 to 6. If it rolls a 1, it will roll again automatically
260
- and use that result instead.
300
+ and use that result instead, repeating while it rolls 1, up to the default limit of 1000 rerolls.
301
+ Use `1d6r:1,replace,1.` for exactly one replacement attempt.
261
302
 
262
303
  The full version of this modifier, allows you to specify from 1 to 3 parameters:
263
304
 
data/Rakefile CHANGED
@@ -11,7 +11,7 @@ require 'yard'
11
11
 
12
12
  desc 'GamesDice unit tests'
13
13
  RSpec::Core::RakeTask.new(:spec) do |t|
14
- t.pattern = 'spec/*_spec.rb'
14
+ t.pattern = 'spec/**/*_spec.rb'
15
15
  t.verbose = false
16
16
  end
17
17
 
@@ -28,7 +28,14 @@ Rake::ExtensionTask.new do |ext|
28
28
  ext.gem_spec = gemspec
29
29
  end
30
30
 
31
- task default: %i[compile spec]
31
+ namespace :docs do
32
+ desc 'Check Ruby API documentation for YARD warnings and undocumented objects'
33
+ task :check do
34
+ ruby 'script/check_documentation.rb'
35
+ end
36
+ end
37
+
38
+ task default: %i[compile spec docs:check]
32
39
 
33
40
  rebuild_and_test_native = lambda do |mode, test: true|
34
41
  tasks = %w[clobber compile]
@@ -46,7 +53,6 @@ rebuild_and_test_native = lambda do |mode, test: true|
46
53
  end
47
54
 
48
55
  # Native quality orchestration is kept together so each task shares the same rebuild contract.
49
- # rubocop:disable Metrics/BlockLength
50
56
  namespace :c do
51
57
  desc 'Compile the C extension with strict warnings'
52
58
  task :lint do
@@ -94,8 +100,10 @@ namespace :c do
94
100
 
95
101
  rebuild_and_test_native.call('sanitize', test: false)
96
102
 
103
+ # MRI owns its threads' alternate signal stacks; ASan must not unmap them on thread exit.
104
+ # See https://bugs.ruby-lang.org/issues/20256 (also applies when preloading ASan into stock Ruby).
97
105
  sh(
98
- { 'ASAN_OPTIONS' => 'detect_leaks=0', 'LD_PRELOAD' => libasan },
106
+ { 'ASAN_OPTIONS' => 'detect_leaks=0:use_sigaltstack=0', 'LD_PRELOAD' => libasan },
99
107
  RbConfig.ruby,
100
108
  '-S',
101
109
  'bundle',
@@ -105,4 +113,3 @@ namespace :c do
105
113
  )
106
114
  end
107
115
  end
108
- # rubocop:enable Metrics/BlockLength
@@ -6,7 +6,6 @@ require 'mkmf'
6
6
  require 'rbconfig'
7
7
 
8
8
  # mkmf exposes compiler and linker flags through these globals.
9
- # rubocop:disable Style/GlobalVars
10
9
  case ENV.fetch('GAMES_DICE_NATIVE_MODE', 'release')
11
10
  when 'release'
12
11
  # Use Ruby's normal extension build flags.
@@ -31,6 +30,4 @@ when 'sanitize'
31
30
  else
32
31
  abort "Unknown GAMES_DICE_NATIVE_MODE: #{ENV.fetch('GAMES_DICE_NATIVE_MODE', nil)}"
33
32
  end
34
- # rubocop:enable Style/GlobalVars
35
-
36
33
  create_makefile('games_dice/games_dice')