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
@@ -1,6 +1,9 @@
1
1
  // ext/games_dice/probabilities.c
2
2
 
3
3
  #include "probabilities.h"
4
+ #include <limits.h>
5
+ #include <stdint.h>
6
+ #include <math.h>
4
7
 
5
8
  // Ruby 1.8.7 compatibility patch
6
9
  #ifndef DBL2NUM
@@ -21,22 +24,68 @@ VALUE Probabilities = Qnil;
21
24
  // General utils
22
25
  //
23
26
 
24
- static int max( int *a, int n ) {
25
- int m = -1000000000;
26
- int i;
27
- for ( i=0; i < n; i++ ) {
28
- m = a[i] > m ? a[i] : m;
27
+ /*
28
+ * Owns the temporary distributions created during one native calculation.
29
+ *
30
+ * A local C pointer does not automatically free its allocation when the
31
+ * function returns. Ruby exceptions can also leave a calculation before
32
+ * ordinary cleanup code runs, including when a later allocation fails.
33
+ *
34
+ * Run the calculation through rb_ensure with cleanup_operation as its
35
+ * cleanup callback. create_probability_list adds each new distribution
36
+ * to owned before allocating its buffers. Its fields start at zero, so
37
+ * cleanup can safely free a partially constructed distribution.
38
+ *
39
+ * owned is the head of a linked list. Each ProbabilityList stores next
40
+ * and owner_link, which points to the pointer that currently refers to
41
+ * that entry. This lets destroy_probability_list unlink an entry when
42
+ * it is freed early, preventing cleanup from freeing it again.
43
+ *
44
+ * When returning a distribution to Ruby, perform_operation wraps it in
45
+ * a Ruby object before detaching it from owned. Ruby then handles its
46
+ * lifetime. If wrapping fails, the operation still owns the distribution
47
+ * and cleanup frees it.
48
+ *
49
+ * Iteration uses the same owner for its snapshot, keeping that storage
50
+ * alive even if a Ruby callback replaces the original distribution.
51
+ * Cleanup frees the snapshot after iteration, including exceptions and
52
+ * early block exits.
53
+ */
54
+ typedef struct {
55
+ ProbabilityList *owned; /* Temporary distributions requiring cleanup. */
56
+ ProbabilityList *a, *b; /* Borrowed inputs, not owned by these pointers. */
57
+ int x, y, z; /* Arguments such as counts, weights and thresholds. */
58
+ int operation; /* Selects the calculation in perform_operation. */
59
+ } ProbabilityOperation;
60
+
61
+ static int checked_result( int64_t value ) {
62
+ if ( value < INT_MIN || value > INT_MAX ) {
63
+ rb_raise( rb_eRangeError, "Probability result is outside native integer range" );
29
64
  }
30
- return m;
65
+ return (int)value;
31
66
  }
32
67
 
33
- static int min( int *a, int n ) {
34
- int m = 1000000000;
35
- int i;
36
- for ( i=0; i < n; i++ ) {
37
- m = a[i] < m ? a[i] : m;
68
+ static int checked_slots( int64_t slots ) {
69
+ if ( slots < 1 || slots > 1000000 ) {
70
+ rb_raise( rb_eArgError, "Bad number of probability slots" );
71
+ }
72
+ return (int)slots;
73
+ }
74
+
75
+ static int weighted_result( int a, int x, int b, int y ) {
76
+ int64_t ax = (int64_t)a * x, by = (int64_t)b * y;
77
+ if ( (by > 0 && ax > INT64_MAX - by) || (by < 0 && ax < INT64_MIN - by) ) {
78
+ rb_raise( rb_eRangeError, "Probability result is outside native integer range" );
38
79
  }
39
- return m;
80
+ return checked_result( ax + by );
81
+ }
82
+
83
+ static void detach_probability_list( ProbabilityList *pl ) {
84
+ if ( !pl->owner_link ) return;
85
+ *pl->owner_link = pl->next;
86
+ if ( pl->next ) pl->next->owner_link = pl->owner_link;
87
+ pl->next = NULL;
88
+ pl->owner_link = NULL;
40
89
  }
41
90
 
42
91
  ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -110,20 +159,19 @@ double num_arrangements( int *args, int nargs ) {
110
159
  // Probability List basics - create, delete, copy
111
160
  //
112
161
 
113
- ProbabilityList *create_probability_list(void) {
114
- ProbabilityList *pl;
115
- pl = malloc (sizeof(ProbabilityList));
116
- if ( pl == NULL ) {
117
- rb_raise(rb_eRuntimeError, "Could not allocate memory for Probabilities");
118
- }
119
- pl->probs = NULL;
120
- pl->cumulative = NULL;
121
- pl->slots = 0;
122
- pl->offset = 0;
162
+ ProbabilityList *create_probability_list( ProbabilityOperation *op ) {
163
+ ProbabilityList *pl = ALLOC( ProbabilityList );
164
+ memset( pl, 0, sizeof(ProbabilityList) );
165
+ pl->next = op->owned;
166
+ pl->owner_link = &op->owned;
167
+ if ( op->owned ) op->owned->owner_link = &pl->next;
168
+ op->owned = pl;
123
169
  return pl;
124
170
  }
125
171
 
126
172
  void destroy_probability_list( ProbabilityList *pl ) {
173
+ if ( !pl ) return;
174
+ detach_probability_list( pl );
127
175
  xfree( pl->cumulative );
128
176
  xfree( pl->probs );
129
177
  xfree( pl );
@@ -173,8 +221,8 @@ double *alloc_probs_iv( ProbabilityList *pl, int slots, double iv ) {
173
221
  return pr;
174
222
  }
175
223
 
176
- ProbabilityList *copy_probability_list( ProbabilityList *orig ) {
177
- ProbabilityList *pl = create_probability_list();
224
+ ProbabilityList *copy_probability_list( ProbabilityOperation *op, ProbabilityList *orig ) {
225
+ ProbabilityList *pl = create_probability_list( op );
178
226
  double *pr;
179
227
  pr = alloc_probs( pl, orig->slots );
180
228
  pl->offset = orig->offset;
@@ -183,8 +231,8 @@ ProbabilityList *copy_probability_list( ProbabilityList *orig ) {
183
231
  return pl;
184
232
  }
185
233
 
186
- static ProbabilityList *new_basic_pl( int nslots, double iv, int o ) {
187
- ProbabilityList *pl = create_probability_list();
234
+ static ProbabilityList *new_basic_pl( ProbabilityOperation *op, int nslots, double iv, int o ) {
235
+ ProbabilityList *pl = create_probability_list( op );
188
236
  alloc_probs_iv( pl, nslots, iv );
189
237
  pl->offset = o;
190
238
  return pl;
@@ -202,16 +250,17 @@ static int pl_min( ProbabilityList *pl ) {
202
250
  }
203
251
 
204
252
  static int pl_max( ProbabilityList *pl ) {
205
- return pl->offset + pl->slots - 1;
253
+ return checked_result( (int64_t)pl->offset + pl->slots - 1 );
206
254
  }
207
255
 
208
- ProbabilityList *pl_add_distributions( ProbabilityList *pl_a, ProbabilityList *pl_b ) {
256
+ ProbabilityList *pl_add_distributions( ProbabilityOperation *op, ProbabilityList *pl_a, ProbabilityList *pl_b ) {
209
257
  double *pr;
210
- int s = pl_a->slots + pl_b->slots - 1;
211
- int o = pl_a->offset + pl_b->offset;
258
+ int s = checked_slots( (int64_t)pl_a->slots + pl_b->slots - 1 );
259
+ int o = checked_result( (int64_t)pl_a->offset + pl_b->offset );
260
+ checked_result( (int64_t)o + s - 1 );
212
261
  int i,j;
213
262
 
214
- ProbabilityList *pl = create_probability_list();
263
+ ProbabilityList *pl = create_probability_list( op );
215
264
  pl->offset = o;
216
265
  pr = alloc_probs_iv( pl, s, 0.0 );
217
266
  for ( i=0; i < pl_a->slots; i++ ) { for ( j=0; j < pl_b->slots; j++ ) {
@@ -221,32 +270,31 @@ ProbabilityList *pl_add_distributions( ProbabilityList *pl_a, ProbabilityList *p
221
270
  return pl;
222
271
  }
223
272
 
224
- ProbabilityList *pl_add_distributions_mult( int mul_a, ProbabilityList *pl_a, int mul_b, ProbabilityList *pl_b ) {
273
+ ProbabilityList *pl_add_distributions_mult( ProbabilityOperation *op, int mul_a, ProbabilityList *pl_a, int mul_b, ProbabilityList *pl_b ) {
225
274
  int pts[4] = {
226
- mul_a * pl_min( pl_a ) + mul_b * pl_min( pl_b ),
227
- mul_a * pl_max( pl_a ) + mul_b * pl_min( pl_b ),
228
- mul_a * pl_min( pl_a ) + mul_b * pl_max( pl_b ),
229
- mul_a * pl_max( pl_a ) + mul_b * pl_max( pl_b ) };
230
-
231
- double *pr;
232
- int combined_min = min( pts, 4 );
233
- int combined_max = max( pts, 4 );
234
- int s = 1 + combined_max - combined_min;
235
- int i,j;
236
-
237
- ProbabilityList *pl = create_probability_list();
238
- pl->offset = combined_min;
239
- pr = alloc_probs_iv( pl, s, 0.0 );
240
- for ( i=0; i < pl_a->slots; i++ ) { for ( j=0; j < pl_b->slots; j++ ) {
241
- int k = mul_a * (i + pl_a->offset) + mul_b * (j + pl_b->offset) - combined_min;
242
- pr[ k ] += (pl_a->probs)[i] * (pl_b->probs)[j];
243
- } }
275
+ weighted_result( mul_a, pl_min(pl_a), mul_b, pl_min(pl_b) ),
276
+ weighted_result( mul_a, pl_max(pl_a), mul_b, pl_min(pl_b) ),
277
+ weighted_result( mul_a, pl_min(pl_a), mul_b, pl_max(pl_b) ),
278
+ weighted_result( mul_a, pl_max(pl_a), mul_b, pl_max(pl_b) ) };
279
+ int low = pts[0], high = pts[0];
280
+ for ( int i = 1; i < 4; i++ ) {
281
+ if ( pts[i] < low ) low = pts[i];
282
+ if ( pts[i] > high ) high = pts[i];
283
+ }
284
+ int slots = checked_slots( 1 + (int64_t)high - low );
285
+ ProbabilityList *pl = new_basic_pl( op, slots, 0.0, low );
286
+ for ( int i = 0; i < pl_a->slots; i++ ) {
287
+ for ( int j = 0; j < pl_b->slots; j++ ) {
288
+ int64_t value = (int64_t)mul_a * (i + pl_a->offset) + (int64_t)mul_b * (j + pl_b->offset);
289
+ pl->probs[value - low] += pl_a->probs[i] * pl_b->probs[j];
290
+ }
291
+ }
244
292
  calc_cumulative( pl );
245
293
  return pl;
246
294
  }
247
295
 
248
296
  static double pl_p_eql( ProbabilityList *pl, int target ) {
249
- int idx = target - pl->offset;
297
+ int64_t idx = (int64_t)target - pl->offset;
250
298
  if ( idx < 0 || idx >= pl->slots ) {
251
299
  return 0.0;
252
300
  }
@@ -258,11 +306,11 @@ static double pl_p_gt( ProbabilityList *pl, int target ) {
258
306
  }
259
307
 
260
308
  static double pl_p_lt( ProbabilityList *pl, int target ) {
261
- return pl_p_le( pl, target - 1 );
309
+ return target == INT_MIN ? 0.0 : pl_p_le( pl, target - 1 );
262
310
  }
263
311
 
264
312
  static double pl_p_le( ProbabilityList *pl, int target ) {
265
- int idx = target - pl->offset;
313
+ int64_t idx = (int64_t)target - pl->offset;
266
314
  if ( idx < 0 ) {
267
315
  return 0.0;
268
316
  }
@@ -273,7 +321,7 @@ static double pl_p_le( ProbabilityList *pl, int target ) {
273
321
  }
274
322
 
275
323
  static double pl_p_ge( ProbabilityList *pl, int target ) {
276
- return 1.0 - pl_p_le( pl, target - 1 );
324
+ return 1.0 - pl_p_lt( pl, target );
277
325
  }
278
326
 
279
327
  static double pl_expected( ProbabilityList *pl ) {
@@ -288,7 +336,7 @@ static double pl_expected( ProbabilityList *pl ) {
288
336
  return t;
289
337
  }
290
338
 
291
- ProbabilityList *pl_given_ge( ProbabilityList *pl, int target ) {
339
+ ProbabilityList *pl_given_ge( ProbabilityOperation *op, ProbabilityList *pl, int target ) {
292
340
  int m = pl_min( pl );
293
341
  double p, mult;
294
342
  double *pr;
@@ -304,10 +352,10 @@ ProbabilityList *pl_given_ge( ProbabilityList *pl, int target ) {
304
352
  rb_raise( rb_eRuntimeError, "Cannot calculate given probabilities, divide by zero" );
305
353
  }
306
354
  mult = 1.0/p;
307
- s = pl->slots + pl->offset - target;
355
+ s = checked_slots( (int64_t)pl->slots + pl->offset - target );
308
356
  pr = pl->probs;
309
357
 
310
- new_pl = create_probability_list();
358
+ new_pl = create_probability_list( op );
311
359
  new_pl->offset = target;
312
360
  new_pr = alloc_probs( new_pl, s );
313
361
  o = target - pl->offset;
@@ -319,7 +367,7 @@ ProbabilityList *pl_given_ge( ProbabilityList *pl, int target ) {
319
367
  return new_pl;
320
368
  }
321
369
 
322
- ProbabilityList *pl_given_le( ProbabilityList *pl, int target ) {
370
+ ProbabilityList *pl_given_le( ProbabilityOperation *op, ProbabilityList *pl, int target ) {
323
371
  int m = pl_max( pl );
324
372
  double p, mult;
325
373
  double *pr;
@@ -338,7 +386,7 @@ ProbabilityList *pl_given_le( ProbabilityList *pl, int target ) {
338
386
  s = target - pl->offset + 1;
339
387
  pr = pl->probs;
340
388
 
341
- new_pl = create_probability_list();
389
+ new_pl = create_probability_list( op );
342
390
  new_pl->offset = pl->offset;
343
391
  new_pr = alloc_probs( new_pl, s );
344
392
 
@@ -349,34 +397,36 @@ ProbabilityList *pl_given_le( ProbabilityList *pl, int target ) {
349
397
  return new_pl;
350
398
  }
351
399
 
352
- ProbabilityList *pl_repeat_sum( ProbabilityList *pl, int n ) {
400
+ ProbabilityList *pl_repeat_sum( ProbabilityOperation *op, ProbabilityList *pl, int n ) {
353
401
  ProbabilityList *pd_power = NULL;
354
402
  ProbabilityList *pd_result = NULL;
355
403
  ProbabilityList *pd_next = NULL;
356
- int power = 1;
404
+ unsigned int remaining = (unsigned int)n;
357
405
 
358
406
  if ( n < 1 ) {
359
407
  rb_raise( rb_eRuntimeError, "Cannot calculate repeat_sum when n < 1" );
360
408
  }
361
- if ( n * pl->slots - n > 1000000 ) {
409
+ if ( (int64_t)n * (pl->slots - 1) >= 1000000 ) {
362
410
  rb_raise( rb_eRuntimeError, "Too many probability slots" );
363
411
  }
364
412
 
365
- pd_power = copy_probability_list( pl );
413
+ checked_result( (int64_t)n * pl_min(pl) );
414
+ checked_result( (int64_t)n * pl_max(pl) );
415
+ pd_power = copy_probability_list( op, pl );
366
416
 
367
417
  while ( 1 ) {
368
- if ( power & n ) {
418
+ if ( remaining & 1 ) {
369
419
  if ( pd_result ) {
370
- pd_next = pl_add_distributions( pd_result, pd_power );
420
+ pd_next = pl_add_distributions( op, pd_result, pd_power );
371
421
  destroy_probability_list( pd_result );
372
422
  pd_result = pd_next;
373
423
  } else {
374
- pd_result = copy_probability_list( pd_power );
424
+ pd_result = copy_probability_list( op, pd_power );
375
425
  }
376
426
  }
377
- power = power << 1;
378
- if ( power > n ) break;
379
- pd_next = pl_add_distributions( pd_power, pd_power );
427
+ remaining >>= 1;
428
+ if ( remaining == 0 ) break;
429
+ pd_next = pl_add_distributions( op, pd_power, pd_power );
380
430
  destroy_probability_list( pd_power );
381
431
  pd_power = pd_next;
382
432
  }
@@ -400,28 +450,30 @@ void calc_p_table( ProbabilityList *pl, int q, int kbest, double *buffer ) {
400
450
  }
401
451
 
402
452
  // Assigns a list of pl variants to a buffer
403
- void calc_keep_distributions( ProbabilityList *pl, int k, int q, int kbest, ProbabilityList **pl_array ) {
453
+ void calc_keep_distributions( ProbabilityOperation *op, ProbabilityList *pl, int k, int q, int kbest, ProbabilityList **pl_array ) {
404
454
  ProbabilityList *pl_kd;
405
455
 
406
456
  int n;
407
457
  for ( n=0; n<k; n++) { pl_array[n] = NULL; }
408
- pl_array[0] = new_basic_pl( 1, 1.0, q * k );
458
+ pl_array[0] = new_basic_pl( op, 1, 1.0, checked_result( (int64_t)q * k ) );
409
459
 
410
460
  if ( kbest ) {
411
461
  if ( pl_p_gt( pl, q ) > 0.0 && k > 1 ) {
412
- pl_kd = pl_given_ge( pl, q + 1 );
462
+ pl_kd = pl_given_ge( op, pl, q + 1 );
413
463
  for ( n = 1; n < k; n++ ) {
414
- pl_array[n] = pl_repeat_sum( pl_kd, n );
415
- (pl_array[n])->offset += q * ( k - n );
464
+ pl_array[n] = pl_repeat_sum( op, pl_kd, n );
465
+ (pl_array[n])->offset = checked_result( (int64_t)(pl_array[n])->offset + (int64_t)q * (k - n) );
416
466
  }
467
+ destroy_probability_list( pl_kd );
417
468
  }
418
469
  } else {
419
470
  if ( pl_p_lt( pl, q ) > 0.0 && k > 1 ) {
420
- pl_kd = pl_given_le( pl, q - 1 );
471
+ pl_kd = pl_given_le( op, pl, q - 1 );
421
472
  for ( n = 1; n < k; n++ ) {
422
- pl_array[n] = pl_repeat_sum( pl_kd, n );
423
- (pl_array[n])->offset += q * ( k - n );
473
+ pl_array[n] = pl_repeat_sum( op, pl_kd, n );
474
+ (pl_array[n])->offset = checked_result( (int64_t)(pl_array[n])->offset + (int64_t)q * (k - n) );
424
475
  }
476
+ destroy_probability_list( pl_kd );
425
477
  }
426
478
  }
427
479
 
@@ -438,17 +490,17 @@ static void clear_pl_array( int k, ProbabilityList **pl_array ) {
438
490
  return;
439
491
  }
440
492
 
441
- ProbabilityList *pl_repeat_n_sum_k( ProbabilityList *pl, int n, int k, int kbest ) {
493
+ ProbabilityList *pl_repeat_n_sum_k( ProbabilityOperation *op, ProbabilityList *pl, int n, int k, int kbest ) {
442
494
  // Table of probabilities ( reject, maybe, keep ) for each "pivot point"
443
495
  double p_table[3];
444
496
  int keep_combos[3];
445
497
  // Table of distributions for each count of > pivot point (vs == pivot point)
446
- ProbabilityList *keep_distributions[171];
498
+ ProbabilityList *keep_distributions[171] = { NULL };
447
499
  ProbabilityList *kd;
448
500
  ProbabilityList *pl_result = NULL;
449
501
 
450
502
  double *pr;
451
- int d = n - k;
503
+ int d;
452
504
  int i, j, q, dn, kn, mn, kdq;
453
505
  double p_sequence;
454
506
 
@@ -459,17 +511,21 @@ ProbabilityList *pl_repeat_n_sum_k( ProbabilityList *pl, int n, int k, int kbest
459
511
  rb_raise( rb_eRuntimeError, "Cannot calculate repeat_sum_k when k < 1" );
460
512
  }
461
513
  if ( k >= n ) {
462
- return pl_repeat_sum( pl, n );
514
+ return pl_repeat_sum( op, pl, n );
463
515
  }
464
- if ( k * pl->slots - k >= 1000000 ) {
516
+ if ( (int64_t)k * (pl->slots - 1) >= 1000000 ) {
465
517
  rb_raise( rb_eRuntimeError, "Too many probability slots" );
466
518
  }
467
519
  if ( n > 170 ) {
468
520
  rb_raise( rb_eRuntimeError, "Too many dice to calculate combinations" );
469
521
  }
470
522
 
523
+ d = n - k;
524
+ checked_result( (int64_t)k * pl_min(pl) );
525
+ checked_result( (int64_t)k * pl_max(pl) );
526
+
471
527
  // Init target
472
- pl_result = create_probability_list();
528
+ pl_result = create_probability_list( op );
473
529
  pr = alloc_probs_iv( pl_result, 1 + k * (pl->slots - 1), 0.0 );
474
530
  pl_result->offset = pl->offset * k;
475
531
 
@@ -477,7 +533,7 @@ ProbabilityList *pl_repeat_n_sum_k( ProbabilityList *pl, int n, int k, int kbest
477
533
  if ( pl->probs[i] <= 0.0 ) continue;
478
534
 
479
535
  q = i + pl->offset;
480
- calc_keep_distributions( pl, k, q, kbest, keep_distributions );
536
+ calc_keep_distributions( op, pl, k, q, kbest, keep_distributions );
481
537
  calc_p_table( pl, q, kbest, p_table );
482
538
 
483
539
  for ( kn = 0; kn < k; kn++ ) {
@@ -550,7 +606,9 @@ static inline VALUE pl_as_ruby_class( ProbabilityList *pl, VALUE klass ) {
550
606
  }
551
607
 
552
608
  VALUE pl_alloc(VALUE klass) {
553
- return pl_as_ruby_class( create_probability_list(), klass );
609
+ ProbabilityList *pl;
610
+ VALUE obj = TypedData_Make_Struct( klass, ProbabilityList, &probability_list_type, pl );
611
+ return obj;
554
612
  }
555
613
 
556
614
  static inline ProbabilityList *get_probability_list( VALUE obj ) {
@@ -565,36 +623,79 @@ void assert_value_wraps_pl( VALUE obj ) {
565
623
  }
566
624
  }
567
625
 
568
- // Validate key/value from hash, and adjust object properties as required
569
- int validate_key_value( VALUE key, VALUE val, VALUE obj ) {
570
- int k = NUM2INT( key );
571
- ProbabilityList *pl = get_probability_list( obj );
572
- // Not assigned (to avoid "unused" warning), but this throws execption if val cannot be coerced to double
573
- NUM2DBL( val );
626
+ enum { OP_ADD, OP_WEIGHTED, OP_GE, OP_LE, OP_REPEAT, OP_KEEP, OP_COPY };
574
627
 
575
- if ( k > 0x7fffffff ) {
576
- rb_raise( rb_eArgError, "Result too large" );
577
- }
628
+ static VALUE cleanup_operation( VALUE data ) {
629
+ ProbabilityOperation *op = (ProbabilityOperation *)data;
630
+ while ( op->owned ) destroy_probability_list( op->owned );
631
+ return Qnil;
632
+ }
578
633
 
579
- if ( k < pl->offset ) {
580
- if ( pl->slots < 1 ) {
581
- pl->slots = 1;
582
- } else {
583
- pl->slots = pl->slots - k + pl->offset;
584
- }
585
- pl->offset = k;
586
- } else if ( k - pl->offset >= pl->slots ) {
587
- pl->slots = 1 + k - pl->offset;
634
+ static VALUE perform_operation( VALUE data ) {
635
+ ProbabilityOperation *op = (ProbabilityOperation *)data;
636
+ ProbabilityList *result;
637
+ switch ( op->operation ) {
638
+ case OP_ADD: result = pl_add_distributions( op, op->a, op->b ); break;
639
+ case OP_WEIGHTED: result = pl_add_distributions_mult( op, op->x, op->a, op->y, op->b ); break;
640
+ case OP_GE: result = pl_given_ge( op, op->a, op->x ); break;
641
+ case OP_LE: result = pl_given_le( op, op->a, op->x ); break;
642
+ case OP_REPEAT: result = pl_repeat_sum( op, op->a, op->x ); break;
643
+ case OP_KEEP: result = pl_repeat_n_sum_k( op, op->a, op->x, op->y, op->z ); break;
644
+ default: result = copy_probability_list( op, op->a ); break;
588
645
  }
646
+ VALUE obj = pl_as_ruby_class( result, Probabilities );
647
+ detach_probability_list( result );
648
+ return obj;
649
+ }
650
+
651
+ static VALUE operate( int operation, ProbabilityList *a, ProbabilityList *b, int x, int y, int z ) {
652
+ ProbabilityOperation op = { .owned = NULL, .a = a, .b = b, .x = x, .y = y, .z = z, .operation = operation };
653
+ return rb_ensure( perform_operation, (VALUE)&op, cleanup_operation, (VALUE)&op );
654
+ }
655
+
656
+ /* A staging Ruby object owns buffers across coercion/allocation exceptions. */
657
+ static VALUE replace_distribution( VALUE receiver, VALUE staged ) {
658
+ ProbabilityList *old = get_probability_list( receiver );
659
+ RTYPEDDATA_DATA(receiver) = get_probability_list( staged );
660
+ RTYPEDDATA_DATA(staged) = NULL;
661
+ destroy_probability_list( old );
662
+ RB_GC_GUARD(staged);
663
+ return receiver;
664
+ }
665
+
666
+ static double probability_value( VALUE value ) {
667
+ double p = NUM2DBL(value);
668
+ if ( p < 0.0 ) rb_raise( rb_eArgError, "Negative probability not allowed" );
669
+ if ( p > 1.0 ) rb_raise( rb_eArgError, "Probability must be in range 0.0..1.0" );
670
+ if ( !isfinite(p) ) rb_raise( rb_eArgError, "Probability must be finite" );
671
+ return p;
672
+ }
673
+
674
+ static void validate_mass( ProbabilityList *pl ) {
675
+ double error = calc_cumulative( pl ) - 1.0;
676
+ if ( error < -1.0e-8 ) rb_raise( rb_eArgError, "Total probabilities are less than 1.0" );
677
+ if ( error > 1.0e-8 ) rb_raise( rb_eArgError, "Total probabilities are greater than 1.0" );
678
+ }
679
+
680
+ static int normalize_pair( VALUE key, VALUE value, VALUE normalized ) {
681
+ int k = NUM2INT(key);
682
+ double p = probability_value(value);
683
+ rb_hash_aset( normalized, INT2NUM(k), DBL2NUM(p) );
589
684
  return ST_CONTINUE;
590
685
  }
591
686
 
592
- // Copy key/value from hash
593
- int copy_key_value( VALUE key, VALUE val, VALUE obj ) {
594
- int k = NUM2INT( key );
595
- double v = NUM2DBL( val );
596
- ProbabilityList *pl = get_probability_list( obj );
597
- pl->probs[ k - pl->offset ] = v;
687
+ static int hash_bounds( VALUE key, VALUE value, VALUE data ) {
688
+ int *bounds = (int *)data;
689
+ int k = NUM2INT(key);
690
+ (void)value;
691
+ if ( k < bounds[0] ) bounds[0] = k;
692
+ if ( k > bounds[1] ) bounds[1] = k;
693
+ return ST_CONTINUE;
694
+ }
695
+
696
+ static int copy_pair( VALUE key, VALUE value, VALUE data ) {
697
+ ProbabilityList *pl = (ProbabilityList *)data;
698
+ pl->probs[NUM2INT(key) - pl->offset] = NUM2DBL(value);
598
699
  return ST_CONTINUE;
599
700
  }
600
701
 
@@ -611,33 +712,24 @@ int copy_key_value( VALUE key, VALUE val, VALUE obj ) {
611
712
  * @return [GamesDice::Probabilities]
612
713
  */
613
714
  VALUE probabilities_initialize( VALUE self, VALUE arr, VALUE offset ) {
614
- int i, o, s;
615
- double error, p_item;
616
- ProbabilityList *pl;
617
- double *pr;
618
-
619
- o = NUM2INT(offset);
715
+ rb_check_frozen(self);
716
+ int o = NUM2INT(offset);
620
717
  Check_Type( arr, T_ARRAY );
621
- s = FIX2INT( rb_funcall( arr, rb_intern("count"), 0 ) );
622
- pl = get_probability_list( self );
623
- pl->offset = o;
624
- pr = alloc_probs( pl, s );
625
- for(i=0; i<s; i++) {
626
- p_item = NUM2DBL( rb_ary_entry( arr, i ) );
627
- if ( p_item < 0.0 ) {
628
- rb_raise( rb_eArgError, "Negative probability not allowed" );
629
- } else if ( p_item > 1.0 ) {
630
- rb_raise( rb_eArgError, "Probability must be in range 0.0..1.0" );
631
- }
632
- pr[i] = p_item;
633
- }
634
- error = calc_cumulative( pl ) - 1.0;
635
- if ( error < -1.0e-8 ) {
636
- rb_raise( rb_eArgError, "Total probabilities are less than 1.0" );
637
- } else if ( error > 1.0e-8 ) {
638
- rb_raise( rb_eArgError, "Total probabilities are greater than 1.0" );
718
+ int slots = checked_slots( RARRAY_LEN(arr) );
719
+ checked_result( (int64_t)o + slots - 1 );
720
+ VALUE normalized = rb_ary_dup(arr);
721
+ for ( int i = 0; i < slots; i++ ) {
722
+ rb_ary_store( normalized, i, DBL2NUM(probability_value(rb_ary_entry(normalized, i))) );
639
723
  }
640
- return self;
724
+ VALUE staged = pl_alloc( Probabilities );
725
+ ProbabilityList *pl = get_probability_list( staged );
726
+ pl->offset = o;
727
+ alloc_probs( pl, slots );
728
+ for ( int i = 0; i < slots; i++ ) pl->probs[i] = NUM2DBL(rb_ary_entry(normalized, i));
729
+ validate_mass( pl );
730
+ RB_GC_GUARD(normalized);
731
+ rb_check_frozen(self);
732
+ return replace_distribution( self, staged );
641
733
  }
642
734
 
643
735
  /*
@@ -646,20 +738,11 @@ VALUE probabilities_initialize( VALUE self, VALUE arr, VALUE offset ) {
646
738
  * @return [GamesDice::Probabilities]
647
739
  */
648
740
  VALUE probabilities_initialize_copy( VALUE copy, VALUE orig ) {
649
- ProbabilityList *pl_copy;
650
- ProbabilityList *pl_orig;
651
- double *pr;
652
-
653
- if (copy == orig) return copy;
654
- pl_copy = get_probability_list( copy );
655
- pl_orig = get_probability_list( orig );
656
-
657
- pr = alloc_probs( pl_copy, pl_orig->slots );
658
- pl_copy->offset = pl_orig->offset;
659
- memcpy( pr, pl_orig->probs, pl_orig->slots * sizeof(double) );
660
- memcpy( pl_copy->cumulative, pl_orig->cumulative, pl_orig->slots * sizeof(double) );;
661
-
662
- return copy;
741
+ if ( copy == orig ) return copy;
742
+ rb_check_frozen(copy);
743
+ VALUE staged = operate( OP_COPY, get_probability_list(orig), NULL, 0, 0, 0 );
744
+ RB_GC_GUARD(orig);
745
+ return replace_distribution( copy, staged );
663
746
  }
664
747
 
665
748
  /*
@@ -709,7 +792,8 @@ VALUE probabilities_max( VALUE self ) {
709
792
  * @return [Float] in range (0.0..1.0)
710
793
  */
711
794
  VALUE probabilites_p_eql( VALUE self, VALUE target ) {
712
- return DBL2NUM( pl_p_eql( get_probability_list( self ), NUM2INT(target) ) );
795
+ int t = NUM2INT(target);
796
+ return DBL2NUM( pl_p_eql( get_probability_list( self ), t ) );
713
797
  }
714
798
 
715
799
  /*
@@ -718,7 +802,8 @@ VALUE probabilites_p_eql( VALUE self, VALUE target ) {
718
802
  * @return [Float] in range (0.0..1.0)
719
803
  */
720
804
  VALUE probabilites_p_gt( VALUE self, VALUE target ) {
721
- return DBL2NUM( pl_p_gt( get_probability_list( self ), NUM2INT(target) ) );
805
+ int t = NUM2INT(target);
806
+ return DBL2NUM( pl_p_gt( get_probability_list( self ), t ) );
722
807
  }
723
808
 
724
809
  /*
@@ -727,7 +812,8 @@ VALUE probabilites_p_gt( VALUE self, VALUE target ) {
727
812
  * @return [Float] in range (0.0..1.0)
728
813
  */
729
814
  VALUE probabilites_p_ge( VALUE self, VALUE target ) {
730
- return DBL2NUM( pl_p_ge( get_probability_list( self ), NUM2INT(target) ) );
815
+ int t = NUM2INT(target);
816
+ return DBL2NUM( pl_p_ge( get_probability_list( self ), t ) );
731
817
  }
732
818
 
733
819
  /*
@@ -736,7 +822,8 @@ VALUE probabilites_p_ge( VALUE self, VALUE target ) {
736
822
  * @return [Float] in range (0.0..1.0)
737
823
  */
738
824
  VALUE probabilites_p_le( VALUE self, VALUE target ) {
739
- return DBL2NUM( pl_p_le( get_probability_list( self ), NUM2INT(target) ) );
825
+ int t = NUM2INT(target);
826
+ return DBL2NUM( pl_p_le( get_probability_list( self ), t ) );
740
827
  }
741
828
 
742
829
  /*
@@ -745,7 +832,8 @@ VALUE probabilites_p_le( VALUE self, VALUE target ) {
745
832
  * @return [Float] in range (0.0..1.0)
746
833
  */
747
834
  VALUE probabilites_p_lt( VALUE self, VALUE target ) {
748
- return DBL2NUM( pl_p_lt( get_probability_list( self ), NUM2INT(target) ) );
835
+ int t = NUM2INT(target);
836
+ return DBL2NUM( pl_p_lt( get_probability_list( self ), t ) );
749
837
  }
750
838
 
751
839
  /*
@@ -767,7 +855,9 @@ VALUE probabilites_expected( VALUE self ) {
767
855
  VALUE probabilities_given_ge( VALUE self, VALUE target ) {
768
856
  int t = NUM2INT(target);
769
857
  ProbabilityList *pl = get_probability_list( self );
770
- return pl_as_ruby_class( pl_given_ge( pl, t ), Probabilities );
858
+ VALUE result = operate( OP_GE, pl, NULL, t, 0, 0 );
859
+ RB_GC_GUARD(self);
860
+ return result;
771
861
  }
772
862
 
773
863
  /*
@@ -779,7 +869,9 @@ VALUE probabilities_given_ge( VALUE self, VALUE target ) {
779
869
  VALUE probabilities_given_le( VALUE self, VALUE target ) {
780
870
  int t = NUM2INT(target);
781
871
  ProbabilityList *pl = get_probability_list( self );
782
- return pl_as_ruby_class( pl_given_le( pl, t ), Probabilities );
872
+ VALUE result = operate( OP_LE, pl, NULL, t, 0, 0 );
873
+ RB_GC_GUARD(self);
874
+ return result;
783
875
  }
784
876
 
785
877
  /*
@@ -792,7 +884,9 @@ VALUE probabilities_given_le( VALUE self, VALUE target ) {
792
884
  VALUE probabilities_repeat_sum( VALUE self, VALUE nsum ) {
793
885
  int n = NUM2INT(nsum);
794
886
  ProbabilityList *pl = get_probability_list( self );
795
- return pl_as_ruby_class( pl_repeat_sum( pl, n ), Probabilities );
887
+ VALUE result = operate( OP_REPEAT, pl, NULL, n, 0, 0 );
888
+ RB_GC_GUARD(self);
889
+ return result;
796
890
  }
797
891
 
798
892
  /*
@@ -822,17 +916,15 @@ VALUE probabilities_repeat_n_sum_k( int argc, VALUE* argv, VALUE self ) {
822
916
  n = NUM2INT(nsum);
823
917
  k = NUM2INT(nkeepers);
824
918
  pl = get_probability_list( self );
825
- return pl_as_ruby_class( pl_repeat_n_sum_k( pl, n, k, keep_best ), Probabilities );
919
+ VALUE result = operate( OP_KEEP, pl, NULL, n, k, keep_best );
920
+ RB_GC_GUARD(self);
921
+ return result;
826
922
  }
827
923
 
828
- /*
829
- * Iterates through value, probability pairs
830
- * @yieldparam [Integer] result A result that may be possible in the dice scheme
831
- * @yieldparam [Float] probability Probability of result, in range 0.0..1.0
832
- * @return [GamesDice::Probabilities] this object
833
- */
834
- VALUE probabilities_each( VALUE self ) {
835
- ProbabilityList *pl = get_probability_list( self );
924
+ static VALUE yield_probability_pairs( VALUE data ) {
925
+ ProbabilityOperation *op = (ProbabilityOperation *)data;
926
+ /* Keep the snapshot native-owned until every callback has finished. */
927
+ ProbabilityList *pl = copy_probability_list( op, op->a );
836
928
  int i;
837
929
  double *pr = pl->probs;
838
930
  int o = pl->offset;
@@ -844,6 +936,19 @@ VALUE probabilities_each( VALUE self ) {
844
936
  rb_yield( a );
845
937
  }
846
938
  }
939
+ return Qnil;
940
+ }
941
+
942
+ /*
943
+ * Iterates through value, probability pairs
944
+ * @yieldparam [Integer] result A result that may be possible in the dice scheme
945
+ * @yieldparam [Float] probability Probability of result, in range 0.0..1.0
946
+ * @return [GamesDice::Probabilities] this object
947
+ */
948
+ VALUE probabilities_each( VALUE self ) {
949
+ /* Ruby callbacks may replace self's storage through marshal_load/initialize. */
950
+ ProbabilityOperation op = { .owned = NULL, .a = get_probability_list(self) };
951
+ rb_ensure( yield_probability_pairs, (VALUE)&op, cleanup_operation, (VALUE)&op );
847
952
  return self;
848
953
  }
849
954
 
@@ -878,32 +983,21 @@ VALUE probabilities_for_fair_die( VALUE self, VALUE sides ) {
878
983
  * @return [GamesDice::Probabilities]
879
984
  */
880
985
  VALUE probabilities_from_h( VALUE self, VALUE hash ) {
881
- VALUE obj;
882
- ProbabilityList *pl;
883
- double error;
884
-
885
986
  Check_Type( hash, T_HASH );
886
-
887
- obj = pl_alloc( Probabilities );
888
- pl = get_probability_list( obj );
889
-
890
-
891
- // Set these up so that they get adjusted during hash iteration
892
- pl->offset = 0x7fffffff;
893
- pl->slots = 0;
894
- // First iteration establish min/max and validate all key/values
895
- rb_hash_foreach( hash, validate_key_value, obj );
896
-
897
- alloc_probs_iv( pl, pl->slots, 0.0 );
898
- // Second iteration copy key/value pairs into structure
899
- rb_hash_foreach( hash, copy_key_value, obj );
900
-
901
- error = calc_cumulative( pl ) - 1.0;
902
- if ( error < -1.0e-8 ) {
903
- rb_raise( rb_eArgError, "Total probabilities are less than 1.0" );
904
- } else if ( error > 1.0e-8 ) {
905
- rb_raise( rb_eArgError, "Total probabilities are greater than 1.0" );
906
- }
987
+ VALUE normalized = rb_hash_new();
988
+ VALUE input = rb_hash_dup(hash);
989
+ rb_hash_foreach( input, normalize_pair, normalized );
990
+ RB_GC_GUARD(input);
991
+ int bounds[2] = { INT_MAX, INT_MIN };
992
+ rb_hash_foreach( normalized, hash_bounds, (VALUE)bounds );
993
+ int slots = checked_slots( 1 + (int64_t)bounds[1] - bounds[0] );
994
+ VALUE obj = pl_alloc( Probabilities );
995
+ ProbabilityList *pl = get_probability_list( obj );
996
+ pl->offset = bounds[0];
997
+ alloc_probs_iv( pl, slots, 0.0 );
998
+ rb_hash_foreach( normalized, copy_pair, (VALUE)pl );
999
+ validate_mass( pl );
1000
+ RB_GC_GUARD(normalized);
907
1001
  return obj;
908
1002
  }
909
1003
 
@@ -923,7 +1017,10 @@ VALUE probabilities_add_distributions( VALUE self, VALUE gdpa, VALUE gdpb ) {
923
1017
  assert_value_wraps_pl( gdpb );
924
1018
  pl_a = get_probability_list( gdpa );
925
1019
  pl_b = get_probability_list( gdpb );
926
- return pl_as_ruby_class( pl_add_distributions( pl_a, pl_b ), Probabilities );
1020
+ VALUE result = operate( OP_ADD, pl_a, pl_b, 0, 0, 0 );
1021
+ RB_GC_GUARD(gdpa);
1022
+ RB_GC_GUARD(gdpb);
1023
+ return result;
927
1024
  }
928
1025
 
929
1026
  /*
@@ -944,10 +1041,13 @@ VALUE probabilities_add_distributions_mult( VALUE self, VALUE m_a, VALUE gdpa, V
944
1041
  assert_value_wraps_pl( gdpa );
945
1042
  assert_value_wraps_pl( gdpb );
946
1043
  mul_a = NUM2INT( m_a );
947
- pl_a = get_probability_list( gdpa );
948
1044
  mul_b = NUM2INT( m_b );
1045
+ pl_a = get_probability_list( gdpa );
949
1046
  pl_b = get_probability_list( gdpb );
950
- return pl_as_ruby_class( pl_add_distributions_mult( mul_a, pl_a, mul_b, pl_b ), Probabilities );
1047
+ VALUE result = operate( OP_WEIGHTED, pl_a, pl_b, mul_a, mul_b, 0 );
1048
+ RB_GC_GUARD(gdpa);
1049
+ RB_GC_GUARD(gdpb);
1050
+ return result;
951
1051
  }
952
1052
 
953
1053
  ///////////////////////////////////////////////////////////////////////////////////////////////////