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 +4 -4
- data/.github/workflows/ci.yml +58 -4
- data/CHANGELOG.md +7 -0
- data/Gemfile +1 -0
- data/Rakefile +81 -0
- data/ext/games_dice/extconf.rb +29 -0
- data/ext/games_dice/games_dice.c +1 -1
- data/ext/games_dice/probabilities.c +52 -22
- data/ext/games_dice/probabilities.h +1 -17
- data/lib/games_dice/version.rb +1 -1
- data/spec/probability_spec.rb +62 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ccf1c8256d9cfa52fa5e99b06c0d0a32288ebdbf54e0e7d0d9f194e4fa21be8
|
|
4
|
+
data.tar.gz: 0b0935a31996519bcd74a8b9fe865911bc5d5d1d68a288c46a2d9450588ecefa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ea6550cb8a23fd28d311ce1b9227e3a9d8b20cfc6ac57459f157e138be850fc543e2e0b17e3dbcee3a6c0ea070c1d126b2b467c1fcb927f13da77acad33dcdbf
|
|
7
|
+
data.tar.gz: bb9b6efd51537a01de7ab325546431852ab96d1aa28042776481345e20e0ed60c64d9185b2161bc00e15d63aa0c8c859f33541cb34374fae736a3fa32a1fe3de
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -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@
|
|
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@
|
|
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
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
|
data/ext/games_dice/extconf.rb
CHANGED
|
@@ -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')
|
data/ext/games_dice/games_dice.c
CHANGED
|
@@ -21,7 +21,7 @@ VALUE Probabilities = Qnil;
|
|
|
21
21
|
// General utils
|
|
22
22
|
//
|
|
23
23
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
+
static double pl_p_lt( ProbabilityList *pl, int target ) {
|
|
259
261
|
return pl_p_le( pl, target - 1 );
|
|
260
262
|
}
|
|
261
263
|
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
891
|
-
ProbabilityList *pl_b
|
|
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 );
|
data/lib/games_dice/version.rb
CHANGED
data/spec/probability_spec.rb
CHANGED
|
@@ -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
|
+
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.
|
|
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".
|