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
@@ -0,0 +1,382 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'helpers'
4
+ # This spec demonstrates that documentation from the README.md works as intended
5
+
6
+ describe GamesDice, :aggregate_failures do
7
+ describe '#create' do
8
+ it "converts a string such as '3d6+6' into a GamesDice::Dice object" do
9
+ d = described_class.create '3d6+6'
10
+ expect(d).to be_a GamesDice::Dice
11
+ end
12
+
13
+ it "takes a parameter 'dice_description', which is a string such as '3d6' or '2d4-1'" do
14
+ d = described_class.create '3d6'
15
+ expect(d).to be_a GamesDice::Dice
16
+ d = described_class.create '2d4-1'
17
+ expect(d).to be_a GamesDice::Dice
18
+ end
19
+
20
+ it "takes an optional parameter 'prng', which should be an object that has a method 'rand( integer )'" do
21
+ prng = TestPRNG.new
22
+ d = described_class.create '3d6', prng
23
+ expect(d).to be_a GamesDice::Dice
24
+ expect_injected_prng(d, prng)
25
+ end
26
+ end
27
+
28
+ describe GamesDice::Dice do
29
+ before do
30
+ srand(67_809)
31
+ end
32
+
33
+ let(:dice) { GamesDice.create '3d6' }
34
+
35
+ describe '#roll' do
36
+ it 'simulates rolling the dice as they were described in the constructor' do
37
+ expected_results = [11, 15, 11, 12, 12, 9]
38
+ expected_results.each do |expected|
39
+ expect(dice.roll).to eql expected
40
+ end
41
+ end
42
+ end
43
+
44
+ describe '#result' do
45
+ it 'returns the value from the last call to roll' do
46
+ expected_results = [11, 15, 11, 12, 12, 9]
47
+ expected_results.each do |expected|
48
+ dice.roll
49
+ expect(dice.result).to eql expected
50
+ end
51
+ end
52
+
53
+ it 'is nil if no roll has been made yet' do
54
+ expect(dice.result).to be_nil
55
+ end
56
+ end
57
+
58
+ describe '#explain_result' do
59
+ it 'attempts to show how the result from the last call to roll was composed' do
60
+ expected_results = [
61
+ '3d6: 3 + 6 + 2 = 11',
62
+ '3d6: 4 + 5 + 6 = 15',
63
+ '3d6: 3 + 6 + 2 = 11',
64
+ '3d6: 5 + 6 + 1 = 12'
65
+ ]
66
+ expected_results.each do |expected|
67
+ dice.roll
68
+ expect(dice.explain_result).to eql expected
69
+ end
70
+ end
71
+
72
+ it 'is nil if no roll has been made yet' do
73
+ expect(dice.explain_result).to be_nil
74
+ end
75
+ end
76
+
77
+ describe '#max' do
78
+ it 'returns the maximum possible value from a roll of the dice' do
79
+ expect(dice.max).to be 18
80
+ end
81
+ end
82
+
83
+ describe '#min' do
84
+ it 'returns the minimum possible value from a roll of the dice' do
85
+ expect(dice.min).to be 3
86
+ end
87
+ end
88
+
89
+ describe '#minmax' do
90
+ it 'returns an array [ dice.min, dice.max ]' do
91
+ expect(dice.minmax).to eql [3, 18]
92
+ end
93
+ end
94
+
95
+ describe '#probabilities' do
96
+ it 'calculates probability distribution for the dice' do
97
+ pd = dice.probabilities
98
+ expect(pd).to be_a GamesDice::Probabilities
99
+ expect(pd.p_eql(3)).to be_within(1e-10).of 1.0 / 216
100
+ expect(pd.p_eql(11)).to be_within(1e-10).of 27.0 / 216
101
+ end
102
+ end
103
+ end
104
+
105
+ describe GamesDice::Probabilities do
106
+ let(:probs) { GamesDice.create('3d6').probabilities }
107
+
108
+ describe '#to_h' do
109
+ it 'returns a hash representation of the probability distribution' do
110
+ h = probs.to_h
111
+ expect(h).to be_valid_distribution
112
+ expect(h[3]).to be_within(1e-10).of 1.0 / 216
113
+ expect(h[11]).to be_within(1e-10).of 27.0 / 216
114
+ end
115
+ end
116
+
117
+ describe '#max' do
118
+ it 'returns maximum result in the probability distribution' do
119
+ expect(probs.max).to be 18
120
+ end
121
+ end
122
+
123
+ describe '#min' do
124
+ it 'returns minimum result in the probability distribution' do
125
+ expect(probs.min).to be 3
126
+ end
127
+ end
128
+
129
+ describe '#p_eql( n )' do
130
+ it 'returns the probability of a result equal to the integer n' do
131
+ expect(probs.p_eql(3)).to be_within(1e-10).of 1.0 / 216
132
+ expect(probs.p_eql(2)).to be 0.0
133
+ end
134
+ end
135
+
136
+ describe '#p_gt( n )' do
137
+ it 'returns the probability of a result greater than the integer n' do
138
+ expect(probs.p_gt(17)).to be_within(1e-10).of 1.0 / 216
139
+ expect(probs.p_gt(2)).to be 1.0
140
+ end
141
+ end
142
+
143
+ describe '#p_ge( n )' do
144
+ it 'returns the probability of a result greater than the integer n' do
145
+ expect(probs.p_ge(17)).to be_within(1e-10).of 4.0 / 216
146
+ expect(probs.p_ge(3)).to be 1.0
147
+ end
148
+ end
149
+
150
+ describe '#p_le( n )' do
151
+ it 'returns the probability of a result less than or equal to the integer n' do
152
+ expect(probs.p_le(17)).to be_within(1e-10).of 215.0 / 216
153
+ expect(probs.p_le(3)).to be_within(1e-10).of 1.0 / 216
154
+ end
155
+ end
156
+
157
+ describe '#p_lt( n )' do
158
+ it 'returns the probability of a result less than the integer n' do
159
+ expect(probs.p_lt(17)).to be_within(1e-10).of 212.0 / 216
160
+ expect(probs.p_lt(3)).to be 0.0
161
+ end
162
+ end
163
+ end
164
+
165
+ describe 'String Dice Description' do
166
+ before do
167
+ srand(35_241)
168
+ end
169
+
170
+ describe "'1d6'" do
171
+ it 'returns expected results from rolling' do
172
+ d = described_class.create '1d6'
173
+ expect((1..20).map { |_n| d.roll }).to eql [6, 3, 2, 3, 4, 6, 4, 2, 6, 3, 3, 5, 6, 6, 3, 6, 5, 2, 1, 4]
174
+ end
175
+ end
176
+
177
+ describe "'2d6 + 1d4'" do
178
+ it 'returns expected results from rolling' do
179
+ d = described_class.create '2d6 + 1d4'
180
+ expect((1..5).map { |_n| d.roll }).to eql [11, 10, 12, 12, 14]
181
+ end
182
+ end
183
+
184
+ describe "'1d100 + 1d20 - 5'" do
185
+ it 'returns expected results from rolling' do
186
+ d = described_class.create '1d100 + 1d20 - 5'
187
+ expect((1..5).map { |_n| d.roll }).to eql [75, 78, 24, 102, 32]
188
+ end
189
+ end
190
+
191
+ describe "'1d10x'" do
192
+ it 'returns expected results from rolling' do
193
+ d = described_class.create '1d10x'
194
+ expect((1..20).map { |_n| d.roll }).to eql [2, 3, 4, 7, 6, 7, 4, 2, 6, 3, 7, 5, 6, 7, 6, 6, 5, 19, 4, 19]
195
+ end
196
+ end
197
+
198
+ describe "'1d6r1'" do
199
+ it 'returns expected results from rolling' do
200
+ d = described_class.create '1d6r1'
201
+ expect((1..20).map { |_n| d.roll }).to eql [6, 3, 2, 3, 4, 6, 4, 2, 6, 3, 3, 5, 6, 6, 3, 6, 5, 2, 4, 2]
202
+ end
203
+ end
204
+
205
+ describe "'5d10r:10,add.k2'" do
206
+ it 'returns expected results from rolling' do
207
+ d = described_class.create '5d10r:10,add.k2'
208
+ expect((1..5).map { |_n| d.roll }).to eql [13, 13, 14, 38, 15]
209
+ end
210
+ end
211
+
212
+ describe "'3d10m6'" do
213
+ it 'returns expected results from rolling' do
214
+ d = described_class.create '3d10m6'
215
+ expect((1..6).map { |_n| d.roll }).to eql [0, 3, 1, 1, 3, 2]
216
+ end
217
+ end
218
+
219
+ describe "'5d10k2'" do
220
+ it 'returns expected results from rolling' do
221
+ d = described_class.create '5d10k2'
222
+ expect((1..5).map { |_n| d.roll }).to eql [13, 13, 14, 19, 19]
223
+ end
224
+ end
225
+
226
+ describe "'5d10x'" do
227
+ it "is the same as '5d10r:10,add.'" do
228
+ results1 = seeded_rolls(described_class, '5d10x', seed: 235_241, count: 50)
229
+ results2 = seeded_rolls(described_class, '5d10r:10,add.', seed: 235_241, count: 50)
230
+ expect(results1).to eql results2
231
+ end
232
+ end
233
+
234
+ describe "'1d6r:1.'" do
235
+ it "return same as '1d6r1'" do
236
+ results1 = seeded_rolls(described_class, '1d6r:1.', seed: 235_241, count: 50)
237
+ results2 = seeded_rolls(described_class, '1d6r1', seed: 235_241, count: 50)
238
+ expect(results1).to eql results2
239
+ end
240
+ end
241
+
242
+ describe "'1d10r:10,replace,1.'" do
243
+ it 'roll a 10-sided die, re-roll a result of 10 and take the value of the second roll' do
244
+ d = described_class.create '1d10r:10,replace,1.'
245
+ expect((1..27).map do
246
+ d.roll
247
+ end).to eql [2, 3, 4, 7, 6, 7, 4, 2, 6, 3, 7, 5, 6, 7, 6, 6, 5, 9, 4, 9, 8, 3, 1, 6, 7, 1, 1]
248
+ end
249
+ end
250
+
251
+ describe "'1d20r:<=10,use_best,1.'" do
252
+ it 'roll a 20-sided die, re-roll a result if 10 or lower, and use best result' do
253
+ d = described_class.create '1d20r:<=10,use_best,1.'
254
+ expect((1..20).map do
255
+ d.roll
256
+ end).to eql [18, 19, 20, 20, 3, 11, 7, 20, 15, 19, 6, 16, 17, 16, 15, 11, 9, 15, 20, 16]
257
+ end
258
+ end
259
+
260
+ describe "'5d10r:10,add.k2', '5d10xk2' and '5d10x.k2'" do
261
+ it 'all be equivalent' do
262
+ results1 = seeded_rolls(described_class, '5d10r:10,add.k2', seed: 135_241, count: 50)
263
+ results2 = seeded_rolls(described_class, '5d10xk2', seed: 135_241, count: 50)
264
+ results3 = seeded_rolls(described_class, '5d10x.k2', seed: 135_241, count: 50)
265
+ expect(results1).to eql results2
266
+ expect(results1).to eql results3
267
+ end
268
+ end
269
+
270
+ describe "'5d10r:>8,add.'" do
271
+ it 'returns expected results from rolling' do
272
+ d = described_class.create '5d10r:>8,add.'
273
+ expect((1..5).map { |_n| d.roll }).to eql [22, 22, 31, 64, 26]
274
+ end
275
+ end
276
+
277
+ describe "'9d6x.m:10.'" do
278
+ it 'returns expected results from rolling' do
279
+ d = described_class.create '9d6x.m:10.'
280
+ expect((1..5).map { |_n| d.roll }).to eql [1, 2, 1, 1, 1]
281
+ end
282
+
283
+ it 'can be explained as number of exploding dice scoring 10+' do
284
+ d = described_class.create '9d6x.m:10.'
285
+ expect((1..5).map do |_n|
286
+ d.roll
287
+ d.explain_result
288
+ end).to eql [
289
+ '9d6: [6+3] 9, 2, 3, 4, [6+4] 10, 2, [6+3] 9, 3, 5. Successes: 1',
290
+ '9d6: [6+6+3] 15, [6+5] 11, 2, 1, 4, 2, 1, 3, 5. Successes: 2',
291
+ '9d6: 1, [6+6+1] 13, 2, 1, 1, 3, [6+1] 7, 5, 4. Successes: 1',
292
+ '9d6: [6+4] 10, 3, 4, 5, 5, 1, [6+3] 9, 3, 5. Successes: 1',
293
+ '9d6: [6+3] 9, 3, [6+5] 11, 4, 2, 2, 1, 4, 5. Successes: 1'
294
+ ]
295
+ end
296
+ end
297
+
298
+ describe "'9d6x.m:10,1,S.'" do
299
+ it 'returns expected results from rolling' do
300
+ d = described_class.create '9d6x.m:10,1,S.'
301
+ expect((1..5).map { |_n| d.roll }).to eql [1, 2, 1, 1, 1]
302
+ end
303
+
304
+ it "includes the string 'S' next to each success" do
305
+ d = described_class.create '9d6x.m:10,1,S.'
306
+ expect((1..5).map do |_n|
307
+ d.roll
308
+ d.explain_result
309
+ end).to eql [
310
+ '9d6: [6+3] 9, 2, 3, 4, [6+4] 10 S, 2, [6+3] 9, 3, 5. Successes: 1',
311
+ '9d6: [6+6+3] 15 S, [6+5] 11 S, 2, 1, 4, 2, 1, 3, 5. Successes: 2',
312
+ '9d6: 1, [6+6+1] 13 S, 2, 1, 1, 3, [6+1] 7, 5, 4. Successes: 1',
313
+ '9d6: [6+4] 10 S, 3, 4, 5, 5, 1, [6+3] 9, 3, 5. Successes: 1',
314
+ '9d6: [6+3] 9, 3, [6+5] 11 S, 4, 2, 2, 1, 4, 5. Successes: 1'
315
+ ]
316
+ end
317
+ end
318
+
319
+ describe "'5d10m:>=6,1,S.m:==1,-1,F.'" do
320
+ it 'returns expected results from rolling' do
321
+ d = described_class.create '5d10m:>=6,1,S.m:==1,-1,F.'
322
+ expect((1..10).map { |_n| d.roll }).to eql [2, 2, 4, 3, 2, 1, 1, 3, 3, 0]
323
+ end
324
+
325
+ it "includes the string 'S' next to each success, and 'F' next to each 'fumble'" do
326
+ d = described_class.create '5d10m:>=6,1,S.m:==1,-1,F.'
327
+ expect((1..5).map do |_n|
328
+ d.roll
329
+ d.explain_result
330
+ end).to eql [
331
+ '5d10: 2, 3, 4, 7 S, 6 S. Successes: 2',
332
+ '5d10: 7 S, 4, 2, 6 S, 3. Successes: 2',
333
+ '5d10: 7 S, 5, 6 S, 7 S, 6 S. Successes: 4',
334
+ '5d10: 6 S, 5, 10 S, 9 S, 4. Successes: 3',
335
+ '5d10: 10 S, 9 S, 8 S, 3, 1 F. Successes: 2'
336
+ ]
337
+ end
338
+ end
339
+
340
+ describe "'4d6k:3.r:1,replace,1.'" do
341
+ it 'represents roll 4 six-sided dice, re-roll any 1s, and keep best 3.' do
342
+ d = described_class.create '4d6k:3.r:1,replace,1.'
343
+ expect((1..10).map { |_n| d.roll }).to eql [12, 14, 14, 18, 11, 17, 11, 15, 14, 14]
344
+ end
345
+
346
+ it 'includes re-rolls and keeper choice in explanations' do
347
+ d = described_class.create '4d6k:3.r:1,replace,1.'
348
+ expect((1..5).map do |_n|
349
+ d.roll
350
+ d.explain_result
351
+ end).to eql [
352
+ '4d6: 6, 3, 2, 3. Keep: 3 + 3 + 6 = 12',
353
+ '4d6: 4, 6, 4, 2. Keep: 4 + 4 + 6 = 14',
354
+ '4d6: 6, 3, 3, 5. Keep: 3 + 5 + 6 = 14',
355
+ '4d6: 6, 6, 3, 6. Keep: 6 + 6 + 6 = 18',
356
+ '4d6: 5, 2, [1|4] 4, 2. Keep: 2 + 4 + 5 = 11'
357
+ ]
358
+ end
359
+ end
360
+
361
+ describe "'2d20k:1,worst.'" do
362
+ it 'represents roll 2 twenty-sided dice, return lowest of the two results' do
363
+ d = described_class.create '2d20k:1,worst.'
364
+ expect((1..10).map { |_n| d.roll }).to eql [18, 6, 2, 3, 5, 10, 15, 1, 7, 10]
365
+ end
366
+
367
+ it 'includes keeper choice in explanations' do
368
+ d = described_class.create '2d20k:1,worst.'
369
+ expect((1..5).map do |_n|
370
+ d.roll
371
+ d.explain_result
372
+ end).to eql [
373
+ '2d20: 18, 19. Keep: 18',
374
+ '2d20: 20, 6. Keep: 6',
375
+ '2d20: 20, 2. Keep: 2',
376
+ '2d20: 3, 11. Keep: 3',
377
+ '2d20: 5, 7. Keep: 5'
378
+ ]
379
+ end
380
+ end
381
+ end
382
+ end
data/spec/helpers.rb CHANGED
@@ -1,12 +1,74 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # games_dice/spec/helpers.rb
4
+ require_relative 'support/coverage'
5
+
4
6
  require 'pathname'
5
7
 
6
8
  require 'games_dice'
7
9
 
8
- def fixture(name)
9
- "#{__dir__}/fixtures/#{name}"
10
+ def marshal_round_trip(value)
11
+ payload = Marshal.dump(value)
12
+ # RuboCop rationale: round-trip compatibility requires loading data generated by this test helper.
13
+ # rubocop:disable Security/MarshalLoad
14
+ Marshal.load(payload)
15
+ # rubocop:enable Security/MarshalLoad
16
+ end
17
+
18
+ def expect_probability_values(actual, expected, tolerance: 1e-10, tolerances: {}, total_tolerance: 1e-9)
19
+ expected.each do |result, probability|
20
+ matcher = probability.nil? ? be_nil : be_within(tolerances.fetch(result, tolerance)).of(probability)
21
+ expect(actual[result]).to matcher
22
+ end
23
+ expect(actual.values.sum).to be_within(total_tolerance).of(1.0) if total_tolerance
24
+ end
25
+
26
+ def expect_aggregate_probabilities(actual, expected, tolerance: 1e-10)
27
+ expected.each do |query, cases|
28
+ cases.each do |target, probability|
29
+ boundary_probability = probability.zero? || (1.0 - probability).zero?
30
+ matcher = boundary_probability ? eq(probability) : be_within(tolerance).of(probability)
31
+ expect(actual.public_send(query, target)).to matcher
32
+ end
33
+ end
34
+ end
35
+
36
+ def expect_rule_arguments(die_class, keyword, valid:, invalid:)
37
+ valid.each { |value| expect { die_class.new(10, **{ keyword => value }) }.not_to raise_error }
38
+ invalid.each { |value, error| expect { die_class.new(10, **{ keyword => value }) }.to raise_error(error) }
39
+ end
40
+
41
+ def expect_die_result_state(actual, value:, rolls:, reasons:)
42
+ expect(actual.value).to eq(value)
43
+ expect(actual.rolls).to eq(rolls)
44
+ expect(actual.roll_reasons).to eq(reasons)
45
+ end
46
+
47
+ def expect_fair_die(probability_class, sides)
48
+ probability = probability_class.for_fair_die(sides)
49
+ expect(probability).to be_a(probability_class)
50
+ expect_fair_distribution(probability.to_h, sides)
51
+ end
52
+
53
+ def expect_fair_distribution(distribution, sides)
54
+ expected = (1..sides).to_h { |result| [result, 1.0 / sides] }
55
+ expect(distribution).to be_valid_distribution
56
+ expect(distribution.keys).to match_array(expected.keys)
57
+ expect_probability_values(distribution, expected, total_tolerance: nil)
58
+ end
59
+
60
+ def expect_injected_prng(die, prng)
61
+ (0..5).each do |die_result|
62
+ allow(prng).to receive(:rand).and_return(die_result)
63
+ expect(die.roll).to eq((die_result + 1) * 3)
64
+ expect(prng).to have_received(:rand).with(6).at_least(:once)
65
+ end
66
+ end
67
+
68
+ def seeded_rolls(factory, notation, seed:, count:)
69
+ srand(seed)
70
+ die = factory.create(notation)
71
+ Array.new(count) { die.roll }
10
72
  end
11
73
 
12
74
  # TestPRNG tests short predictable series
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'helpers'
4
+ require 'open3'
5
+ require 'rbconfig'
6
+ require_relative 'support/validation'
7
+
8
+ describe SimpleCov, :aggregate_failures do
9
+ include_context 'with temporary Ruby source'
10
+
11
+ let(:coverage_setup) { File.expand_path('support/coverage.rb', __dir__) }
12
+
13
+ def coverage_result(source, load_source: true)
14
+ with_source(source) do |directory|
15
+ code = load_source ? 'require "./lib/probe"' : ''
16
+ Open3.capture2e(RbConfig.ruby, '-r', coverage_setup, '-e', code, chdir: directory)
17
+ end
18
+ end
19
+
20
+ it 'accepts fully covered Ruby code' do
21
+ output, status = coverage_result('[true, false].each { |flag| flag ? 1 : 2 }')
22
+ expect(status).to be_success, output
23
+ end
24
+
25
+ it 'rejects a line coverage shortfall' do
26
+ output, status = coverage_result("def uncalled\n 1\n 2\nend\n")
27
+ expect(status.exitstatus).to eq(2)
28
+ expect(output).to include('Line coverage', 'below the expected minimum coverage (95.00%)')
29
+ end
30
+
31
+ it 'rejects a branch shortfall even when every line runs' do
32
+ output, status = coverage_result('[true].each { |flag| flag ? 1 : 2 }')
33
+ expect(status.exitstatus).to eq(2)
34
+ expect(output).to include('Line Coverage: 100.0%', 'Branch coverage', 'expected minimum coverage (95.00%)')
35
+ end
36
+
37
+ it 'counts maintained Ruby files that were never loaded' do
38
+ output, status = coverage_result("class Unloaded\n def value\n 1\n end\nend\n", load_source: false)
39
+ expect(status.exitstatus).to eq(2)
40
+ expect(output).to include('Line Coverage: 0.0%')
41
+ end
42
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'simplecov'
4
+
5
+ SimpleCov.start do
6
+ enable_coverage :branch
7
+ track_files 'lib/**/*.rb'
8
+ add_filter { |source| !source.filename.start_with?(File.join(SimpleCov.root, 'lib/')) }
9
+ coverage_dir 'coverage/ruby'
10
+ use_merging false
11
+ minimum_coverage line: 95, branch: 95
12
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+
6
+ shared_context 'with temporary Ruby source' do
7
+ def with_source(source)
8
+ Dir.mktmpdir('games-dice-validation') do |directory|
9
+ FileUtils.mkdir_p(File.join(directory, 'lib'))
10
+ File.write(File.join(directory, 'lib/probe.rb'), source)
11
+ yield directory
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'helpers'
4
+ require 'open3'
5
+ require 'rbconfig'
6
+ require 'yard'
7
+ require_relative '../../support/validation'
8
+
9
+ describe YARD::CLI::Stats, :aggregate_failures do
10
+ include_context 'with temporary Ruby source'
11
+
12
+ let(:documentation_check) { File.expand_path('../../../script/check_documentation.rb', __dir__) }
13
+
14
+ def documentation_result(source)
15
+ with_source(source) do |directory|
16
+ Open3.capture2e(RbConfig.ruby, documentation_check, chdir: directory)
17
+ end
18
+ end
19
+
20
+ it 'accepts documented source' do
21
+ output, status = documentation_result("# A documented class.\nclass Probe\nend\n")
22
+ expect(status).to be_success, output
23
+ expect(output).to include('100.00% documented')
24
+ end
25
+
26
+ it 'rejects undocumented objects' do
27
+ output, status = documentation_result("class Probe\nend\n")
28
+ expect(status).not_to be_success
29
+ expect(output).to include('Documentation check found undocumented objects', 'Probe')
30
+ end
31
+
32
+ it 'rejects YARD warnings even when the object is documented' do
33
+ output, status = documentation_result("# A documented class.\n# @unknown_tag invalid\nclass Probe\nend\n")
34
+ expect(status).not_to be_success
35
+ expect(output).to include('Unknown tag @unknown_tag', '100.00% documented')
36
+ end
37
+
38
+ it 'rejects an empty documentation source boundary' do
39
+ output, status = documentation_result('# No API objects here.')
40
+ expect(status).not_to be_success
41
+ expect(output).to include('Documentation check found no objects')
42
+ end
43
+ end
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Neil Slater
@@ -55,25 +55,42 @@ files:
55
55
  - lib/games_dice/complex_die_helpers.rb
56
56
  - lib/games_dice/constants.rb
57
57
  - lib/games_dice/dice.rb
58
+ - lib/games_dice/dice_helpers.rb
58
59
  - lib/games_dice/die.rb
59
60
  - lib/games_dice/die_result.rb
60
61
  - lib/games_dice/map_rule.rb
61
62
  - lib/games_dice/marshal.rb
62
63
  - lib/games_dice/parser.rb
64
+ - lib/games_dice/probability_budget.rb
65
+ - lib/games_dice/probability_calculation.rb
66
+ - lib/games_dice/probability_shortcuts.rb
67
+ - lib/games_dice/recipe_cache.rb
63
68
  - lib/games_dice/reroll_rule.rb
64
69
  - lib/games_dice/version.rb
65
- - spec/bunch_spec.rb
66
- - spec/complex_die_spec.rb
67
- - spec/dice_spec.rb
68
- - spec/die_result_spec.rb
69
- - spec/die_spec.rb
70
- - spec/fixtures/probs_fair_die_6.dat
70
+ - script/check_documentation.rb
71
+ - spec/games_dice/bunch_regressions_spec.rb
72
+ - spec/games_dice/bunch_spec.rb
73
+ - spec/games_dice/complex_die_budget_spec.rb
74
+ - spec/games_dice/complex_die_limits_spec.rb
75
+ - spec/games_dice/complex_die_spec.rb
76
+ - spec/games_dice/dice_mutation_spec.rb
77
+ - spec/games_dice/dice_regressions_spec.rb
78
+ - spec/games_dice/dice_spec.rb
79
+ - spec/games_dice/die_result_spec.rb
80
+ - spec/games_dice/die_spec.rb
81
+ - spec/games_dice/map_rule_spec.rb
82
+ - spec/games_dice/parser_spec.rb
83
+ - spec/games_dice/probabilities_boundaries_spec.rb
84
+ - spec/games_dice/probabilities_marshal_spec.rb
85
+ - spec/games_dice/probabilities_spec.rb
86
+ - spec/games_dice/probability_budget_spec.rb
87
+ - spec/games_dice/reroll_rule_spec.rb
88
+ - spec/games_dice_spec.rb
71
89
  - spec/helpers.rb
72
- - spec/map_rule_spec.rb
73
- - spec/parser_spec.rb
74
- - spec/probability_spec.rb
75
- - spec/readme_spec.rb
76
- - spec/reroll_rule_spec.rb
90
+ - spec/simple_cov_spec.rb
91
+ - spec/support/coverage.rb
92
+ - spec/support/validation.rb
93
+ - spec/yard/cli/stats_spec.rb
77
94
  homepage: https://github.com/neilslater/games_dice
78
95
  licenses:
79
96
  - MIT