games_dice 0.4.2 → 0.5.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: 8e1494acd8aaa52a4d11f10bc8507c872be7eff6e77e6ad3d628d58d900a7e45
4
- data.tar.gz: fb92f1c294ec39199c7a98f52ffae82620f25dce69ee92afe08e630bcc273692
3
+ metadata.gz: 1ccf1c8256d9cfa52fa5e99b06c0d0a32288ebdbf54e0e7d0d9f194e4fa21be8
4
+ data.tar.gz: 0b0935a31996519bcd74a8b9fe865911bc5d5d1d68a288c46a2d9450588ecefa
5
5
  SHA512:
6
- metadata.gz: 3cf986407235b5bf5017695d61d2ae6dcde5287233d94f07b1c716763913bfb88c7e99c21bab98a684c1939e17cc2a827d5225b35808a241bf3c83c71730d351
7
- data.tar.gz: 5b5ec53e5140dfa03fd56b5e5c4115a25547f591305c9993ed1a1ebfb747d84e3441c0cd4a9eda394e1b76998230274441d3e9b20c39d115722008ede3637928
6
+ metadata.gz: ea6550cb8a23fd28d311ce1b9227e3a9d8b20cfc6ac57459f157e138be850fc543e2e0b17e3dbcee3a6c0ea070c1d126b2b467c1fcb927f13da77acad33dcdbf
7
+ data.tar.gz: bb9b6efd51537a01de7ab325546431852ab96d1aa28042776481345e20e0ed60c64d9185b2161bc00e15d63aa0c8c859f33541cb34374fae736a3fa32a1fe3de
@@ -2,6 +2,11 @@ name: CI
2
2
 
3
3
  on:
4
4
  push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ schedule:
9
+ - cron: '0 6 8 * *'
5
10
 
6
11
  permissions:
7
12
  contents: read
@@ -13,9 +18,9 @@ jobs:
13
18
  strategy:
14
19
  fail-fast: false
15
20
  matrix:
16
- ruby: ['3.3', '3.4']
21
+ ruby: ['3.3', '3.4', '4.0']
17
22
  steps:
18
- - uses: actions/checkout@v4
23
+ - uses: actions/checkout@v6
19
24
  - uses: ruby/setup-ruby@v1
20
25
  with:
21
26
  ruby-version: ${{ matrix.ruby }}
@@ -28,11 +33,60 @@ jobs:
28
33
  strategy:
29
34
  fail-fast: false
30
35
  matrix:
31
- ruby: ['3.3', '3.4']
36
+ ruby: ['3.3', '3.4', '4.0']
32
37
  steps:
33
- - uses: actions/checkout@v4
38
+ - uses: actions/checkout@v6
34
39
  - uses: ruby/setup-ruby@v1
35
40
  with:
36
41
  ruby-version: ${{ matrix.ruby }}
37
42
  bundler-cache: true
38
43
  - run: bundle exec rubocop
44
+
45
+ audit:
46
+ name: Dependency audit
47
+ runs-on: ubuntu-latest
48
+ steps:
49
+ - uses: actions/checkout@v6
50
+ - uses: ruby/setup-ruby@v1
51
+ with:
52
+ ruby-version: '3.3'
53
+ bundler-cache: true
54
+ - run: bundle exec bundle-audit check --update
55
+
56
+ native-quality:
57
+ name: C ${{ matrix.check }}
58
+ runs-on: ubuntu-latest
59
+ timeout-minutes: 20
60
+ strategy:
61
+ fail-fast: false
62
+ matrix:
63
+ check: [lint, coverage, sanitize]
64
+ steps:
65
+ - uses: actions/checkout@v6
66
+ - name: Install C coverage tools
67
+ if: matrix.check == 'coverage'
68
+ run: sudo apt-get update && sudo apt-get install -y gcovr
69
+ - uses: ruby/setup-ruby@v1
70
+ with:
71
+ ruby-version: '3.4'
72
+ bundler-cache: true
73
+ - name: Show Ruby extension compiler
74
+ run: |
75
+ ruby -rrbconfig -e 'puts RbConfig::CONFIG.fetch("CC")'
76
+ gcc --version
77
+ - name: Run native quality check
78
+ run: bundle exec rake c:${{ matrix.check }}
79
+ - name: Add C coverage summary to job summary
80
+ if: matrix.check == 'coverage' && always()
81
+ run: |
82
+ if test -f coverage/c/summary.txt; then
83
+ cat coverage/c/summary.txt >> "$GITHUB_STEP_SUMMARY"
84
+ fi
85
+ - name: Upload C coverage report
86
+ if: matrix.check == 'coverage' && always()
87
+ uses: actions/upload-artifact@v6
88
+ with:
89
+ name: c-coverage
90
+ path: coverage/c
91
+ retention-days: 14
92
+ if-no-files-found: warn
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # GamesDice Changelog
2
2
 
3
+ ## 0.5.0 ( 22 July 2026 )
4
+
5
+ * Add support and CI coverage for Ruby 4.0.
6
+ * Add dependency auditing and native extension lint, sanitizer, and coverage checks to CI.
7
+ * Modernise native probability object memory management using Ruby's typed data API.
8
+ * Expand probability specifications to cover native extension edge cases.
9
+
3
10
  ## 0.4.2 ( 19 July 2026 )
4
11
 
5
12
  * Require MRI Ruby 3.3 or later and test against Ruby 3.3 and 3.4.
data/Gemfile CHANGED
@@ -5,6 +5,7 @@ source 'https://rubygems.org'
5
5
  # Dependencies are in games_dice.gemspec
6
6
  gemspec
7
7
 
8
+ gem 'bundler-audit', '~> 0.9', require: false
8
9
  gem 'rake', '~> 13.2'
9
10
  gem 'rake-compiler', '~> 1.2'
10
11
  gem 'redcarpet', '~> 3.6'
data/Rakefile CHANGED
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bundler/gem_tasks'
4
+ require 'fileutils'
5
+ require 'open3'
6
+ require 'rbconfig'
4
7
  require 'rspec/core/rake_task'
5
8
  require 'rake/extensiontask'
9
+ require 'shellwords'
6
10
  require 'yard'
7
11
 
8
12
  desc 'GamesDice unit tests'
@@ -25,3 +29,80 @@ Rake::ExtensionTask.new do |ext|
25
29
  end
26
30
 
27
31
  task default: %i[compile spec]
32
+
33
+ rebuild_and_test_native = lambda do |mode, test: true|
34
+ tasks = %w[clobber compile]
35
+ tasks << 'spec' if test
36
+
37
+ sh(
38
+ { 'GAMES_DICE_NATIVE_MODE' => mode },
39
+ RbConfig.ruby,
40
+ '-S',
41
+ 'bundle',
42
+ 'exec',
43
+ 'rake',
44
+ *tasks
45
+ )
46
+ end
47
+
48
+ # Native quality orchestration is kept together so each task shares the same rebuild contract.
49
+ # rubocop:disable Metrics/BlockLength
50
+ namespace :c do
51
+ desc 'Compile the C extension with strict warnings'
52
+ task :lint do
53
+ rebuild_and_test_native.call('lint', test: false)
54
+ end
55
+
56
+ desc 'Measure C coverage using the full Ruby spec suite'
57
+ task :coverage do
58
+ cc = RbConfig::CONFIG.fetch('CC')
59
+ compiler_version = Open3.capture2e(*Shellwords.split(cc), '--version').first
60
+
61
+ unless compiler_version.match?(/gcc/i) && !compiler_version.match?(/clang/i)
62
+ abort "c:coverage requires a GCC Ruby build (current compiler: #{cc})"
63
+ end
64
+
65
+ abort 'c:coverage requires gcovr on PATH' unless system('gcovr', '--version', out: File::NULL)
66
+
67
+ rebuild_and_test_native.call('coverage')
68
+
69
+ FileUtils.mkdir_p('coverage/c')
70
+ sh(
71
+ 'gcovr',
72
+ '--root', '.',
73
+ '--filter', 'ext/games_dice/',
74
+ '--html-details', 'coverage/c/index.html',
75
+ '--xml', 'coverage/c/cobertura.xml',
76
+ '--txt', 'coverage/c/summary.txt',
77
+ '--print-summary'
78
+ )
79
+ end
80
+
81
+ desc 'Run the Ruby specs with ASan and UBSan'
82
+ task :sanitize do
83
+ abort 'c:sanitize requires Linux' unless RUBY_PLATFORM.include?('linux')
84
+
85
+ cc = RbConfig::CONFIG.fetch('CC')
86
+ compiler_version = Open3.capture2e(*Shellwords.split(cc), '--version').first
87
+
88
+ unless compiler_version.match?(/gcc/i) && !compiler_version.match?(/clang/i)
89
+ abort "c:sanitize requires a GCC Ruby build (current compiler: #{cc})"
90
+ end
91
+
92
+ libasan = Open3.capture2e(*Shellwords.split(cc), '-print-file-name=libasan.so').first.strip
93
+ abort 'c:sanitize could not locate the GCC ASan runtime' if libasan.empty? || libasan == 'libasan.so'
94
+
95
+ rebuild_and_test_native.call('sanitize', test: false)
96
+
97
+ sh(
98
+ { 'ASAN_OPTIONS' => 'detect_leaks=0', 'LD_PRELOAD' => libasan },
99
+ RbConfig.ruby,
100
+ '-S',
101
+ 'bundle',
102
+ 'exec',
103
+ 'rake',
104
+ 'spec'
105
+ )
106
+ end
107
+ end
108
+ # rubocop:enable Metrics/BlockLength
@@ -3,5 +3,34 @@
3
3
  # ext/games_dice/extconf.rb
4
4
 
5
5
  require 'mkmf'
6
+ require 'rbconfig'
7
+
8
+ # mkmf exposes compiler and linker flags through these globals.
9
+ # rubocop:disable Style/GlobalVars
10
+ case ENV.fetch('GAMES_DICE_NATIVE_MODE', 'release')
11
+ when 'release'
12
+ # Use Ruby's normal extension build flags.
13
+ when 'lint'
14
+ $CFLAGS << ' -std=gnu2x -O0 -g'
15
+ $CFLAGS << ' -Wall -Wextra -Wpedantic -Wformat=2 -Werror'
16
+
17
+ cc = RbConfig::CONFIG.fetch('CC')
18
+ host_os = RbConfig::CONFIG.fetch('host_os')
19
+ if cc.include?('clang') || host_os.include?('darwin')
20
+ # Ruby callback signatures legitimately leave some parameters unused, and
21
+ # Ruby 4 headers use standard attributes that Clang treats as C23 syntax.
22
+ $CFLAGS << ' -Wno-strict-prototypes -Wno-unused-parameter -Wno-c23-extensions'
23
+ end
24
+ when 'coverage'
25
+ $CFLAGS << ' -O0 -g --coverage'
26
+ $LDFLAGS << ' --coverage'
27
+ when 'sanitize'
28
+ $CFLAGS << ' -O1 -g -fsanitize=address,undefined'
29
+ $CFLAGS << ' -fno-omit-frame-pointer'
30
+ $LDFLAGS << ' -fsanitize=address,undefined'
31
+ else
32
+ abort "Unknown GAMES_DICE_NATIVE_MODE: #{ENV.fetch('GAMES_DICE_NATIVE_MODE', nil)}"
33
+ end
34
+ # rubocop:enable Style/GlobalVars
6
35
 
7
36
  create_makefile('games_dice/games_dice')
@@ -6,7 +6,7 @@
6
6
  // To hold the module object
7
7
  VALUE GamesDice = Qnil;
8
8
 
9
- void Init_games_dice() {
9
+ void Init_games_dice(void) {
10
10
  GamesDice = rb_define_module("GamesDice");
11
11
  init_probabilities_class();
12
12
  }
@@ -21,7 +21,7 @@ VALUE Probabilities = Qnil;
21
21
  // General utils
22
22
  //
23
23
 
24
- inline int max( int *a, int n ) {
24
+ static int max( int *a, int n ) {
25
25
  int m = -1000000000;
26
26
  int i;
27
27
  for ( i=0; i < n; i++ ) {
@@ -30,7 +30,7 @@ inline int max( int *a, int n ) {
30
30
  return m;
31
31
  }
32
32
 
33
- inline int min( int *a, int n ) {
33
+ static int min( int *a, int n ) {
34
34
  int m = 1000000000;
35
35
  int i;
36
36
  for ( i=0; i < n; i++ ) {
@@ -110,7 +110,7 @@ double num_arrangements( int *args, int nargs ) {
110
110
  // Probability List basics - create, delete, copy
111
111
  //
112
112
 
113
- ProbabilityList *create_probability_list() {
113
+ ProbabilityList *create_probability_list(void) {
114
114
  ProbabilityList *pl;
115
115
  pl = malloc (sizeof(ProbabilityList));
116
116
  if ( pl == NULL ) {
@@ -183,7 +183,7 @@ ProbabilityList *copy_probability_list( ProbabilityList *orig ) {
183
183
  return pl;
184
184
  }
185
185
 
186
- inline ProbabilityList *new_basic_pl( int nslots, double iv, int o ) {
186
+ static ProbabilityList *new_basic_pl( int nslots, double iv, int o ) {
187
187
  ProbabilityList *pl = create_probability_list();
188
188
  alloc_probs_iv( pl, nslots, iv );
189
189
  pl->offset = o;
@@ -195,11 +195,13 @@ inline ProbabilityList *new_basic_pl( int nslots, double iv, int o ) {
195
195
  // Probability List core "native" methods
196
196
  //
197
197
 
198
- inline int pl_min( ProbabilityList *pl ) {
198
+ static double pl_p_le( ProbabilityList *pl, int target );
199
+
200
+ static int pl_min( ProbabilityList *pl ) {
199
201
  return pl->offset;
200
202
  }
201
203
 
202
- inline int pl_max( ProbabilityList *pl ) {
204
+ static int pl_max( ProbabilityList *pl ) {
203
205
  return pl->offset + pl->slots - 1;
204
206
  }
205
207
 
@@ -243,7 +245,7 @@ ProbabilityList *pl_add_distributions_mult( int mul_a, ProbabilityList *pl_a, in
243
245
  return pl;
244
246
  }
245
247
 
246
- inline double pl_p_eql( ProbabilityList *pl, int target ) {
248
+ static double pl_p_eql( ProbabilityList *pl, int target ) {
247
249
  int idx = target - pl->offset;
248
250
  if ( idx < 0 || idx >= pl->slots ) {
249
251
  return 0.0;
@@ -251,15 +253,15 @@ inline double pl_p_eql( ProbabilityList *pl, int target ) {
251
253
  return (pl->probs)[idx];
252
254
  }
253
255
 
254
- inline double pl_p_gt( ProbabilityList *pl, int target ) {
256
+ static double pl_p_gt( ProbabilityList *pl, int target ) {
255
257
  return 1.0 - pl_p_le( pl, target );
256
258
  }
257
259
 
258
- inline double pl_p_lt( ProbabilityList *pl, int target ) {
260
+ static double pl_p_lt( ProbabilityList *pl, int target ) {
259
261
  return pl_p_le( pl, target - 1 );
260
262
  }
261
263
 
262
- inline double pl_p_le( ProbabilityList *pl, int target ) {
264
+ static double pl_p_le( ProbabilityList *pl, int target ) {
263
265
  int idx = target - pl->offset;
264
266
  if ( idx < 0 ) {
265
267
  return 0.0;
@@ -270,11 +272,11 @@ inline double pl_p_le( ProbabilityList *pl, int target ) {
270
272
  return (pl->cumulative)[idx];
271
273
  }
272
274
 
273
- inline double pl_p_ge( ProbabilityList *pl, int target ) {
275
+ static double pl_p_ge( ProbabilityList *pl, int target ) {
274
276
  return 1.0 - pl_p_le( pl, target - 1 );
275
277
  }
276
278
 
277
- inline double pl_expected( ProbabilityList *pl ) {
279
+ static double pl_expected( ProbabilityList *pl ) {
278
280
  double t = 0.0;
279
281
  int o = pl->offset;
280
282
  int s = pl->slots;
@@ -426,7 +428,7 @@ void calc_keep_distributions( ProbabilityList *pl, int k, int q, int kbest, Prob
426
428
  return;
427
429
  }
428
430
 
429
- inline void clear_pl_array( int k, ProbabilityList **pl_array ) {
431
+ static void clear_pl_array( int k, ProbabilityList **pl_array ) {
430
432
  int n;
431
433
  for ( n=0; n<k; n++) {
432
434
  if ( pl_array[n] != NULL ) {
@@ -515,23 +517,50 @@ ProbabilityList *pl_repeat_n_sum_k( ProbabilityList *pl, int n, int k, int kbest
515
517
  // Ruby integration
516
518
  //
517
519
 
518
- inline VALUE pl_as_ruby_class( ProbabilityList *pl, VALUE klass ) {
519
- return Data_Wrap_Struct( klass, 0, destroy_probability_list, pl );
520
+ static void free_probability_list( void *ptr ) {
521
+ destroy_probability_list( ptr );
522
+ }
523
+
524
+ static size_t probability_list_memsize( const void *ptr ) {
525
+ const ProbabilityList *pl = ptr;
526
+ size_t size = sizeof(ProbabilityList);
527
+
528
+ if ( pl == NULL ) return 0;
529
+ if ( pl->probs != NULL ) size += pl->slots * sizeof(double);
530
+ if ( pl->cumulative != NULL ) size += pl->slots * sizeof(double);
531
+ return size;
532
+ }
533
+
534
+ static const rb_data_type_t probability_list_type = {
535
+ .wrap_struct_name = "GamesDice::Probabilities",
536
+ .function = {
537
+ .dmark = NULL,
538
+ .dfree = free_probability_list,
539
+ .dsize = probability_list_memsize,
540
+ .dcompact = NULL,
541
+ .reserved = { NULL }
542
+ },
543
+ .parent = NULL,
544
+ .data = NULL,
545
+ .flags = 0
546
+ };
547
+
548
+ static inline VALUE pl_as_ruby_class( ProbabilityList *pl, VALUE klass ) {
549
+ return TypedData_Wrap_Struct( klass, &probability_list_type, pl );
520
550
  }
521
551
 
522
552
  VALUE pl_alloc(VALUE klass) {
523
553
  return pl_as_ruby_class( create_probability_list(), klass );
524
554
  }
525
555
 
526
- inline ProbabilityList *get_probability_list( VALUE obj ) {
556
+ static inline ProbabilityList *get_probability_list( VALUE obj ) {
527
557
  ProbabilityList *pl;
528
- Data_Get_Struct( obj, ProbabilityList, pl );
558
+ TypedData_Get_Struct( obj, ProbabilityList, &probability_list_type, pl );
529
559
  return pl;
530
560
  }
531
561
 
532
562
  void assert_value_wraps_pl( VALUE obj ) {
533
- if ( TYPE(obj) != T_DATA ||
534
- RDATA(obj)->dfree != (RUBY_DATA_FUNC)destroy_probability_list) {
563
+ if ( ! rb_typeddata_is_kind_of( obj, &probability_list_type ) ) {
535
564
  rb_raise( rb_eTypeError, "Expected a Probabilities object, but got something else" );
536
565
  }
537
566
  }
@@ -887,8 +916,9 @@ VALUE probabilities_from_h( VALUE self, VALUE hash ) {
887
916
  * @return [GamesDice::Probabilities]
888
917
  */
889
918
  VALUE probabilities_add_distributions( VALUE self, VALUE gdpa, VALUE gdpb ) {
890
- ProbabilityList *pl_a = get_probability_list( gdpa );
891
- ProbabilityList *pl_b = get_probability_list( gdpb );
919
+ ProbabilityList *pl_a;
920
+ ProbabilityList *pl_b;
921
+
892
922
  assert_value_wraps_pl( gdpa );
893
923
  assert_value_wraps_pl( gdpb );
894
924
  pl_a = get_probability_list( gdpa );
@@ -925,7 +955,7 @@ VALUE probabilities_add_distributions_mult( VALUE self, VALUE m_a, VALUE gdpa, V
925
955
  // Setup Probabilities class for Ruby interpretter
926
956
  //
927
957
 
928
- void init_probabilities_class() {
958
+ void init_probabilities_class(void) {
929
959
  VALUE GamesDice = rb_define_module("GamesDice");
930
960
  Probabilities = rb_define_class_under( GamesDice, "Probabilities", rb_cObject );
931
961
  rb_define_alloc_func( Probabilities, pl_alloc );
@@ -7,7 +7,7 @@
7
7
 
8
8
  #include <ruby.h>
9
9
 
10
- void init_probabilities_class();
10
+ void init_probabilities_class(void);
11
11
 
12
12
  typedef struct _pd {
13
13
  int offset;
@@ -16,26 +16,10 @@ typedef struct _pd {
16
16
  double *cumulative;
17
17
  } ProbabilityList;
18
18
 
19
- inline int pl_min( ProbabilityList *pl );
20
-
21
- inline int pl_max( ProbabilityList *pl );
22
-
23
19
  ProbabilityList *pl_add_distributions( ProbabilityList *pl_a, ProbabilityList *pl_b );
24
20
 
25
21
  ProbabilityList *pl_add_distributions_mult( int mul_a, ProbabilityList *pl_a, int mul_b, ProbabilityList *pl_b );
26
22
 
27
- inline double pl_p_eql( ProbabilityList *pl, int target );
28
-
29
- inline double pl_p_gt( ProbabilityList *pl, int target );
30
-
31
- inline double pl_p_lt( ProbabilityList *pl, int target );
32
-
33
- inline double pl_p_le( ProbabilityList *pl, int target );
34
-
35
- inline double pl_p_ge( ProbabilityList *pl, int target );
36
-
37
- inline double pl_expected( ProbabilityList *pl );
38
-
39
23
  ProbabilityList *pl_given_ge( ProbabilityList *pl, int target );
40
24
 
41
25
  ProbabilityList *pl_given_le( ProbabilityList *pl, int target );
@@ -2,5 +2,5 @@
2
2
 
3
3
  module GamesDice
4
4
  # Current version of the gem.
5
- VERSION = '0.4.2'
5
+ VERSION = '0.5.0'
6
6
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'helpers'
4
+ require 'objspace'
4
5
 
5
6
  describe GamesDice::Probabilities do
6
7
  describe 'class methods' do
@@ -23,6 +24,7 @@ describe GamesDice::Probabilities do
23
24
  expect { described_class.new([], 1) }.to raise_error ArgumentError
24
25
  expect { described_class.new([0.9], 1) }.to raise_error ArgumentError
25
26
  expect { described_class.new([-0.9, 0.2, 0.9], 1) }.to raise_error ArgumentError
27
+ expect { described_class.new([1.1], 1) }.to raise_error ArgumentError
26
28
  end
27
29
  end
28
30
 
@@ -143,8 +145,14 @@ describe GamesDice::Probabilities do
143
145
  expect(pr).to be_a described_class
144
146
  end
145
147
 
148
+ it 'create the same distribution when hash keys descend' do
149
+ pr = described_class.from_h({ 9 => 0.5, 7 => 0.5 })
150
+ expect(pr.to_h).to eql({ 7 => 0.5, 9 => 0.5 })
151
+ end
152
+
146
153
  it 'raise an ArgumentError when called with a non-valid hash' do
147
154
  expect { described_class.from_h({ 7 => 0.5, 9 => 0.6 }) }.to raise_error ArgumentError
155
+ expect { described_class.from_h({ 7 => 0.4, 9 => 0.5 }) }.to raise_error ArgumentError
148
156
  end
149
157
 
150
158
  it 'raise an TypeError when called with data that is not a hash' do
@@ -169,6 +177,21 @@ describe GamesDice::Probabilities do
169
177
  let(:pr10) { described_class.for_fair_die(10) }
170
178
  let(:pra) { described_class.new([0.4, 0.2, 0.4], -1) }
171
179
 
180
+ describe '#clone' do
181
+ it 'create an independent object with the same distribution' do
182
+ copy = pra.clone
183
+ expect(copy).not_to equal(pra)
184
+ expect(copy.to_h).to eql(pra.to_h)
185
+ end
186
+ end
187
+
188
+ describe 'native memory accounting' do
189
+ it 'include allocated probability arrays' do
190
+ empty = described_class.allocate
191
+ expect(ObjectSpace.memsize_of(pr10)).to be > ObjectSpace.memsize_of(empty)
192
+ end
193
+ end
194
+
172
195
  describe '#each' do
173
196
  it 'iterate through all result/probability pairs' do
174
197
  yielded = []
@@ -375,6 +398,11 @@ describe GamesDice::Probabilities do
375
398
  it 'raise a TypeError if asked for probability of non-Integer' do
376
399
  expect { pr10.given_ge([]) }.to raise_error TypeError
377
400
  end
401
+
402
+ it 'clamp targets below the minimum and reject targets above the maximum' do
403
+ expect(pr10.given_ge(0).to_h).to eql(pr10.to_h)
404
+ expect { pr10.given_ge(11) }.to raise_error(RuntimeError, /divide by zero/)
405
+ end
378
406
  end
379
407
 
380
408
  describe '#given_le' do
@@ -390,6 +418,11 @@ describe GamesDice::Probabilities do
390
418
  it 'raise a TypeError if asked for probability of non-Integer' do
391
419
  expect { pr10.given_le({}) }.to raise_error TypeError
392
420
  end
421
+
422
+ it 'clamp targets above the maximum and reject targets below the minimum' do
423
+ expect(pr10.given_le(11).to_h).to eql(pr10.to_h)
424
+ expect { pr10.given_le(0) }.to raise_error(RuntimeError, /divide by zero/)
425
+ end
393
426
  end
394
427
 
395
428
  describe '#repeat_sum' do
@@ -407,6 +440,10 @@ describe GamesDice::Probabilities do
407
440
  expect { d6.repeat_sum({}) }.to raise_error TypeError
408
441
  end
409
442
 
443
+ it 'raise an error if repetitions are not positive' do
444
+ expect { pr6.repeat_sum(0) }.to raise_error(RuntimeError, /n < 1/)
445
+ end
446
+
410
447
  it 'raise an error if distribution would have more than a million results' do
411
448
  d1000 = described_class.for_fair_die(1000)
412
449
  expect { d1000.repeat_sum(11_000) }.to raise_error(RuntimeError, /Too many probability slots/)
@@ -452,6 +489,24 @@ describe GamesDice::Probabilities do
452
489
  expect { d6.repeat_n_sum_k(10, {}) }.to raise_error TypeError
453
490
  end
454
491
 
492
+ it 'raise an error if repetitions or keepers are not positive' do
493
+ expect { pr6.repeat_n_sum_k(0, 1) }.to raise_error(RuntimeError, /n < 1/)
494
+ expect { pr6.repeat_n_sum_k(2, 0) }.to raise_error(RuntimeError, /k < 1/)
495
+ end
496
+
497
+ it 'sum every repetition when the keeper count is at least the repetition count' do
498
+ expect(pr6.repeat_n_sum_k(3, 3).to_h).to eql(pr6.repeat_sum(3).to_h)
499
+ end
500
+
501
+ it 'raise an error if the kept distribution would have more than a million results' do
502
+ d1000 = described_class.for_fair_die(1000)
503
+ expect { d1000.repeat_n_sum_k(1003, 1002) }.to raise_error(RuntimeError, /Too many probability slots/)
504
+ end
505
+
506
+ it 'raise an error for an unknown keep mode' do
507
+ expect { pr6.repeat_n_sum_k(3, 2, :middle) }.to raise_error(ArgumentError, /Keep mode/)
508
+ end
509
+
455
510
  it 'raise an error if n is greater than 170' do
456
511
  d6 = described_class.for_fair_die(6)
457
512
  expect { d6.repeat_n_sum_k(171, 10) }.to raise_error(RuntimeError, /Too many dice/)
@@ -506,6 +561,13 @@ describe GamesDice::Probabilities do
506
561
  expect(h[19]).to be_within(1e-10).of 3 / 400.0
507
562
  expect(h[20]).to be_within(1e-10).of 1 / 400.0
508
563
  end
564
+
565
+ it "calculate a '4d6 keep worst 3' distribution accurately at its bounds" do
566
+ h = pr6.repeat_n_sum_k(4, 3, :keep_worst).to_h
567
+ expect(h).to be_valid_distribution
568
+ expect(h[3]).to be_within(1e-10).of 21 / 1296.0
569
+ expect(h[18]).to be_within(1e-10).of 1 / 1296.0
570
+ end
509
571
  end
510
572
  end
511
573
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: games_dice
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neil Slater
@@ -93,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
93
  - !ruby/object:Gem::Version
94
94
  version: '0'
95
95
  requirements: []
96
- rubygems_version: 4.0.1
96
+ rubygems_version: 4.0.16
97
97
  specification_version: 4
98
98
  summary: Simulates and explains dice rolls from simple "1d6" to complex "roll 7 ten-sided
99
99
  dice, take best 3, results of 10 roll again and add on".