games_dice 0.3.12 → 0.4.2

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 (43) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +38 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +53 -0
  5. data/.yardopts +1 -1
  6. data/CHANGELOG.md +18 -0
  7. data/Gemfile +12 -0
  8. data/README.md +11 -33
  9. data/Rakefile +10 -23
  10. data/ext/games_dice/extconf.rb +4 -22
  11. data/ext/games_dice/games_dice.c +1 -1
  12. data/ext/games_dice/probabilities.c +128 -9
  13. data/ext/games_dice/probabilities.h +1 -1
  14. data/games_dice.gemspec +22 -36
  15. data/lib/games_dice/bunch.rb +203 -247
  16. data/lib/games_dice/bunch_helpers.rb +68 -0
  17. data/lib/games_dice/complex_die.rb +151 -270
  18. data/lib/games_dice/complex_die_helpers.rb +260 -60
  19. data/lib/games_dice/constants.rb +10 -10
  20. data/lib/games_dice/dice.rb +153 -143
  21. data/lib/games_dice/die.rb +101 -97
  22. data/lib/games_dice/die_result.rb +206 -189
  23. data/lib/games_dice/map_rule.rb +72 -70
  24. data/lib/games_dice/marshal.rb +41 -13
  25. data/lib/games_dice/parser.rb +245 -218
  26. data/lib/games_dice/reroll_rule.rb +76 -77
  27. data/lib/games_dice/version.rb +4 -1
  28. data/lib/games_dice.rb +23 -25
  29. data/spec/bunch_spec.rb +399 -420
  30. data/spec/complex_die_spec.rb +314 -305
  31. data/spec/dice_spec.rb +33 -34
  32. data/spec/die_result_spec.rb +174 -181
  33. data/spec/die_spec.rb +81 -81
  34. data/spec/helpers.rb +25 -25
  35. data/spec/map_rule_spec.rb +40 -44
  36. data/spec/parser_spec.rb +106 -82
  37. data/spec/probability_spec.rb +522 -526
  38. data/spec/readme_spec.rb +410 -390
  39. data/spec/reroll_rule_spec.rb +40 -44
  40. metadata +17 -129
  41. data/.travis.yml +0 -14
  42. data/lib/games_dice/prob_helpers.rb +0 -259
  43. data/lib/games_dice/probabilities.rb +0 -244
@@ -1,305 +1,314 @@
1
- require 'helpers'
2
-
3
- describe GamesDice::ComplexDie do
4
-
5
- before do
6
- # Set state of default PRNG
7
- srand(4567)
8
- end
9
-
10
- it "should represent a basic die as an object" do
11
- die = GamesDice::ComplexDie.new(6)
12
- die.min.should == 1
13
- die.max.should == 6
14
- die.sides.should == 6
15
- end
16
-
17
- it "should return results based on Ruby's internal rand() by default" do
18
- die = GamesDice::ComplexDie.new(10)
19
- [5,4,10,4,7,8,1,9].each do |expected|
20
- die.roll.should == expected
21
- die.result.should == expected
22
- end
23
- end
24
-
25
- it "should use any object with a rand(Integer) method" do
26
- prng = TestPRNG.new()
27
- die = GamesDice::ComplexDie.new(20, :prng => prng)
28
- [16,7,3,11,16,18,20,7].each do |expected|
29
- die.roll.should == expected
30
- die.result.should == expected
31
- end
32
- end
33
-
34
- it "should optionally accept a rerolls param" do
35
- GamesDice::ComplexDie.new( 10, :rerolls => [] )
36
- GamesDice::ComplexDie.new( 10, :rerolls => [GamesDice::RerollRule.new(6, :<=, :reroll_add)] )
37
- GamesDice::ComplexDie.new( 10, :rerolls => [GamesDice::RerollRule.new(6, :<=, :reroll_add),GamesDice::RerollRule.new(1, :>=, :reroll_subtract)] )
38
- GamesDice::ComplexDie.new( 10, :rerolls => [[6, :<=, :reroll_add]] )
39
- GamesDice::ComplexDie.new( 10, :rerolls => [[6, :<=, :reroll_add],[1, :>=, :reroll_subtract]] )
40
-
41
- lambda do
42
- GamesDice::ComplexDie.new( 10, :rerolls => 7 )
43
- end.should raise_error( TypeError )
44
-
45
- lambda do
46
- GamesDice::ComplexDie.new( 10, :rerolls => ['hello'] )
47
- end.should raise_error( TypeError )
48
-
49
- lambda do
50
- GamesDice::ComplexDie.new( 10, :rerolls => [GamesDice::RerollRule.new(6, :<=, :reroll_add), :reroll_add] )
51
- end.should raise_error( TypeError )
52
-
53
- lambda do
54
- GamesDice::ComplexDie.new( 10, :rerolls => [7] )
55
- end.should raise_error( TypeError )
56
-
57
- lambda do
58
- GamesDice::ComplexDie.new( 10, :rerolls => [['hello']] )
59
- end.should raise_error( ArgumentError )
60
-
61
- lambda do
62
- GamesDice::ComplexDie.new( 10, :rerolls => [ [6, :<=, :reroll_add ], :reroll_add] )
63
- end.should raise_error( TypeError )
64
-
65
- end
66
-
67
- it "should optionally accept a maps param" do
68
- GamesDice::ComplexDie.new( 10, :maps => [] )
69
- GamesDice::ComplexDie.new( 10, :maps => [GamesDice::MapRule.new(7, :<=, 1)] )
70
- GamesDice::ComplexDie.new( 10, :maps => [GamesDice::MapRule.new(7, :<=, 1), GamesDice::MapRule.new(1, :>, -1) ] )
71
- GamesDice::ComplexDie.new( 10, :maps => [ [7, :<=, 1] ] )
72
- GamesDice::ComplexDie.new( 10, :maps => [ [7, :<=, 1], [1, :>, -1] ] )
73
-
74
- lambda do
75
- GamesDice::ComplexDie.new( 10, :maps => 7 )
76
- end.should raise_error( TypeError )
77
-
78
- lambda do
79
- GamesDice::ComplexDie.new( 10, :maps => [7] )
80
- end.should raise_error( TypeError )
81
-
82
- lambda do
83
- GamesDice::ComplexDie.new( 10, :maps => [ [7] ] )
84
- end.should raise_error( ArgumentError )
85
-
86
- lambda do
87
- GamesDice::ComplexDie.new( 10, :maps => ['hello'] )
88
- end.should raise_error( TypeError )
89
-
90
- lambda do
91
- GamesDice::ComplexDie.new( 10, :maps => [GamesDice::MapRule.new(7, :<=, 1),GamesDice::RerollRule.new(6, :<=, :reroll_add)] )
92
- end.should raise_error( TypeError )
93
- end
94
-
95
- describe "with rerolls" do
96
- it "should calculate correct minimum and maximum results" do
97
- die = GamesDice::ComplexDie.new( 10, :rerolls => [GamesDice::RerollRule.new(10, :<=, :reroll_add, 3)] )
98
- die.min.should == 1
99
- die.max.should == 40
100
-
101
- die = GamesDice::ComplexDie.new( 10, :rerolls => [[1, :>=, :reroll_subtract]] )
102
- die.min.should == -9
103
- die.max.should == 10
104
-
105
- die = GamesDice::ComplexDie.new( 10, :rerolls => [GamesDice::RerollRule.new(10, :<=, :reroll_add)] )
106
- die.min.should == 1
107
- die.max.should == 10_010
108
- end
109
-
110
- it "should simulate a d10 that rerolls and adds on a result of 10" do
111
- die = GamesDice::ComplexDie.new(10, :rerolls => [[10, :<=, :reroll_add]] )
112
- [5,4,14,7,8,1,9].each do |expected|
113
- die.roll.should == expected
114
- die.result.should == expected
115
- end
116
- end
117
-
118
- it "should explain how it got results outside range 1 to 10 on a d10" do
119
- die = GamesDice::ComplexDie.new(10, :rerolls => [[10, :<=, :reroll_add],[1, :>=, :reroll_subtract]] )
120
- ["5","4","[10+4] 14","7","8","[1-9] -8"].each do |expected|
121
- die.roll
122
- die.explain_result.should == expected
123
- end
124
- end
125
-
126
- it "should calculate an expected result" do
127
- die = GamesDice::ComplexDie.new(10, :rerolls => [[10, :<=, :reroll_add],[1, :>=, :reroll_subtract]] )
128
- die.probabilities.expected.should be_within(1e-10).of 5.5
129
-
130
- die = GamesDice::ComplexDie.new(10, :rerolls => [GamesDice::RerollRule.new(1, :<=, :reroll_use_best, 1)] )
131
- die.probabilities.expected.should be_within(1e-10).of 7.15
132
-
133
- die = GamesDice::ComplexDie.new(10, :rerolls => [GamesDice::RerollRule.new(1, :<=, :reroll_use_worst, 2)] )
134
- die.probabilities.expected.should be_within(1e-10).of 3.025
135
-
136
- die = GamesDice::ComplexDie.new(6, :rerolls => [GamesDice::RerollRule.new(6, :<=, :reroll_add)] )
137
- die.probabilities.expected.should be_within(1e-10).of 4.2
138
-
139
- die = GamesDice::ComplexDie.new(8, :rerolls => [GamesDice::RerollRule.new(1, :>=, :reroll_use_best)] )
140
- die.probabilities.expected.should be_within(1e-10).of 5.0
141
-
142
- die = GamesDice::ComplexDie.new(4, :rerolls => [GamesDice::RerollRule.new(1, :>=, :reroll_replace, 1)] )
143
- die.probabilities.expected.should be_within(1e-10).of 2.875
144
- end
145
-
146
- it "should calculate probabilities of each possible result" do
147
- die = GamesDice::ComplexDie.new(6, :rerolls => [GamesDice::RerollRule.new(7, :>, :reroll_add, 1)] )
148
- probs = die.probabilities.to_h
149
- probs[11].should be_within(1e-10).of 2/36.0
150
- probs[8].should be_within(1e-10).of 5/36.0
151
- probs.values.inject(:+).should be_within(1e-9).of 1.0
152
-
153
- die = GamesDice::ComplexDie.new(10, :rerolls => [GamesDice::RerollRule.new(10, :<=, :reroll_add)] )
154
- probs = die.probabilities.to_h
155
- probs[8].should be_within(1e-10).of 0.1
156
- probs[10].should be_nil
157
- probs[13].should be_within(1e-10).of 0.01
158
- probs[27].should be_within(1e-10).of 0.001
159
- probs.values.inject(:+).should be_within(1e-9).of 1.0
160
-
161
- die = GamesDice::ComplexDie.new(6, :rerolls => [GamesDice::RerollRule.new(1, :>=, :reroll_replace, 1)] )
162
- probs = die.probabilities.to_h
163
- probs[1].should be_within(1e-10).of 1/36.0
164
- probs[2].should be_within(1e-10).of 7/36.0
165
- probs.values.inject(:+).should be_within(1e-9).of 1.0
166
- end
167
-
168
- it "should calculate aggregate probabilities" do
169
- die = GamesDice::ComplexDie.new(6, :rerolls => [GamesDice::RerollRule.new(7, :>, :reroll_add, 1)] )
170
- probs = die.probabilities
171
- probs.p_gt(7).should be_within(1e-10).of 15/36.0
172
- probs.p_gt(-10).should == 1.0
173
- probs.p_gt(12).should == 0.0
174
-
175
- probs.p_ge(7).should be_within(1e-10).of 21/36.0
176
- probs.p_ge(2).should == 1.0
177
- probs.p_ge(15).should == 0.0
178
-
179
- probs.p_lt(7).should be_within(1e-10).of 15/36.0
180
- probs.p_lt(-10).should == 0.0
181
- probs.p_lt(13).should == 1.0
182
-
183
- probs.p_le(7).should be_within(1e-10).of 21/36.0
184
- probs.p_le(1).should == 0.0
185
- probs.p_le(12).should == 1.0
186
- end
187
- end
188
-
189
- describe "with maps" do
190
- it "should calculate correct minimum and maximum results" do
191
- die = GamesDice::ComplexDie.new( 10, :maps => [GamesDice::MapRule.new(7, :<=, 1, 'S')] )
192
- die.min.should == 0
193
- die.max.should == 1
194
- end
195
-
196
- it "should simulate a d10 that scores 1 for success on a value of 7 or more" do
197
- die = GamesDice::ComplexDie.new( 10, :maps => [ [ 7, :<=, 1, 'S' ] ] )
198
- [0,0,1,0,1,1,0,1].each do |expected|
199
- die.roll.should == expected
200
- die.result.should == expected
201
- end
202
- end
203
-
204
- it "should label the mappings applied with the provided names" do
205
- die = GamesDice::ComplexDie.new( 10, :maps => [ [7, :<=, 1, 'S'], [1, :>=, -1, 'F'] ] )
206
- ["5", "4", "10 S", "4", "7 S", "8 S", "1 F", "9 S"].each do |expected|
207
- die.roll
208
- die.explain_result.should == expected
209
- end
210
- end
211
-
212
- it "should calculate an expected result" do
213
- die = GamesDice::ComplexDie.new(10, :maps => [GamesDice::MapRule.new(7, :<=, 1, 'S'),GamesDice::MapRule.new(1, :>=, -1, 'F')] )
214
- die.probabilities.expected.should be_within(1e-10).of 0.3
215
- end
216
-
217
- it "should calculate probabilities of each possible result" do
218
- die = GamesDice::ComplexDie.new(10, :maps => [GamesDice::MapRule.new(7, :<=, 1, 'S'),GamesDice::MapRule.new(1, :>=, -1, 'F')] )
219
- probs_hash = die.probabilities.to_h
220
- probs_hash[1].should be_within(1e-10).of 0.4
221
- probs_hash[0].should be_within(1e-10).of 0.5
222
- probs_hash[-1].should be_within(1e-10).of 0.1
223
- probs_hash.values.inject(:+).should be_within(1e-9).of 1.0
224
- end
225
-
226
- it "should calculate aggregate probabilities" do
227
- die = GamesDice::ComplexDie.new(10, :maps => [GamesDice::MapRule.new(7, :<=, 1, 'S'),GamesDice::MapRule.new(1, :>=, -1, 'F')] )
228
- probs = die.probabilities
229
- probs.p_gt(-2).should == 1.0
230
- probs.p_gt(-1).should be_within(1e-10).of 0.9
231
- probs.p_gt(1).should == 0.0
232
-
233
- probs.p_ge(1).should be_within(1e-10).of 0.4
234
- probs.p_ge(-1).should == 1.0
235
- probs.p_ge(2).should == 0.0
236
-
237
- probs.p_lt(1).should be_within(1e-10).of 0.6
238
- probs.p_lt(-1).should == 0.0
239
- probs.p_lt(2).should == 1.0
240
-
241
- probs.p_le(-1).should be_within(1e-10).of 0.1
242
- probs.p_le(-2).should == 0.0
243
- probs.p_le(1).should == 1.0
244
- end
245
- end
246
-
247
- describe "with rerolls and maps together" do
248
- before do
249
- @die = GamesDice::ComplexDie.new( 6,
250
- :rerolls => [[6, :<=, :reroll_add]],
251
- :maps => [GamesDice::MapRule.new(9, :<=, 1, 'Success')]
252
- )
253
- end
254
-
255
- it "should calculate correct minimum and maximum results" do
256
- @die.min.should == 0
257
- @die.max.should == 1
258
- end
259
-
260
- it "should calculate an expected result" do
261
- @die.probabilities.expected.should be_within(1e-10).of 4/36.0
262
- end
263
-
264
- it "should calculate probabilities of each possible result" do
265
- probs_hash = @die.probabilities.to_h
266
- probs_hash[1].should be_within(1e-10).of 4/36.0
267
- probs_hash[0].should be_within(1e-10).of 32/36.0
268
- probs_hash.values.inject(:+).should be_within(1e-9).of 1.0
269
- end
270
-
271
- it "should calculate aggregate probabilities" do
272
- probs = @die.probabilities
273
-
274
- probs.p_gt(0).should be_within(1e-10).of 4/36.0
275
- probs.p_gt(-2).should == 1.0
276
- probs.p_gt(1).should == 0.0
277
-
278
- probs.p_ge(1).should be_within(1e-10).of 4/36.0
279
- probs.p_ge(-1).should == 1.0
280
- probs.p_ge(2).should == 0.0
281
-
282
- probs.p_lt(1).should be_within(1e-10).of 32/36.0
283
- probs.p_lt(0).should == 0.0
284
- probs.p_lt(2).should == 1.0
285
-
286
- probs.p_le(0).should be_within(1e-10).of 32/36.0
287
- probs.p_le(-1).should == 0.0
288
- probs.p_le(1).should == 1.0
289
- end
290
-
291
- it "should apply mapping to final re-rolled result" do
292
- [0,1,0,0].each do |expected|
293
- @die.roll.should == expected
294
- @die.result.should == expected
295
- end
296
- end
297
-
298
- it "should explain how it got each result" do
299
- ["5", "[6+4] 10 Success", "[6+2] 8", "5"].each do |expected|
300
- @die.roll
301
- @die.explain_result.should == expected
302
- end
303
- end
304
- end # describe "with rerolls and maps"
305
- end # describe GamesDice::ComplexDie
1
+ # frozen_string_literal: true
2
+
3
+ require 'helpers'
4
+
5
+ describe GamesDice::ComplexDie do
6
+ before do
7
+ # Set state of default PRNG
8
+ srand(4567)
9
+ end
10
+
11
+ it 'represent a basic die as an object' do
12
+ die = described_class.new(6)
13
+ expect(die.min).to be 1
14
+ expect(die.max).to be 6
15
+ expect(die.sides).to be 6
16
+ end
17
+
18
+ it "return results based on Ruby's internal rand() by default" do
19
+ die = described_class.new(10)
20
+ [5, 4, 10, 4, 7, 8, 1, 9].each do |expected|
21
+ expect(die.roll.value).to eql expected
22
+ expect(die.result.value).to eql expected
23
+ end
24
+ end
25
+
26
+ it 'use any object with a rand(Integer) method' do
27
+ prng = TestPRNG.new
28
+ die = described_class.new(20, prng: prng)
29
+ [16, 7, 3, 11, 16, 18, 20, 7].each do |expected|
30
+ expect(die.roll.value).to eql expected
31
+ expect(die.result.value).to eql expected
32
+ end
33
+ end
34
+
35
+ it 'optionally accept a rerolls param' do
36
+ described_class.new(10, rerolls: [])
37
+ described_class.new(10, rerolls: [GamesDice::RerollRule.new(6, :<=, :reroll_add)])
38
+ described_class.new(10,
39
+ rerolls: [GamesDice::RerollRule.new(6, :<=, :reroll_add),
40
+ GamesDice::RerollRule.new(1, :>=, :reroll_subtract)])
41
+ described_class.new(10, rerolls: [[6, :<=, :reroll_add]])
42
+ described_class.new(10, rerolls: [[6, :<=, :reroll_add], [1, :>=, :reroll_subtract]])
43
+
44
+ expect do
45
+ described_class.new(10, rerolls: 7)
46
+ end.to raise_error(TypeError)
47
+
48
+ expect do
49
+ described_class.new(10, rerolls: ['hello'])
50
+ end.to raise_error(TypeError)
51
+
52
+ expect do
53
+ described_class.new(10, rerolls: [GamesDice::RerollRule.new(6, :<=, :reroll_add), :reroll_add])
54
+ end.to raise_error(TypeError)
55
+
56
+ expect do
57
+ described_class.new(10, rerolls: [7])
58
+ end.to raise_error(TypeError)
59
+
60
+ expect do
61
+ described_class.new(10, rerolls: [['hello']])
62
+ end.to raise_error(ArgumentError)
63
+
64
+ expect do
65
+ described_class.new(10, rerolls: [[6, :<=, :reroll_add], :reroll_add])
66
+ end.to raise_error(TypeError)
67
+ end
68
+
69
+ it 'optionally accept a maps param' do
70
+ described_class.new(10, maps: [])
71
+ described_class.new(10, maps: [GamesDice::MapRule.new(7, :<=, 1)])
72
+ described_class.new(10, maps: [GamesDice::MapRule.new(7, :<=, 1), GamesDice::MapRule.new(1, :>, -1)])
73
+ described_class.new(10, maps: [[7, :<=, 1]])
74
+ described_class.new(10, maps: [[7, :<=, 1], [1, :>, -1]])
75
+
76
+ expect do
77
+ described_class.new(10, maps: 7)
78
+ end.to raise_error(TypeError)
79
+
80
+ expect do
81
+ described_class.new(10, maps: [7])
82
+ end.to raise_error(TypeError)
83
+
84
+ expect do
85
+ described_class.new(10, maps: [[7]])
86
+ end.to raise_error(ArgumentError)
87
+
88
+ expect do
89
+ described_class.new(10, maps: ['hello'])
90
+ end.to raise_error(TypeError)
91
+
92
+ expect do
93
+ described_class.new(10,
94
+ maps: [GamesDice::MapRule.new(7, :<=, 1),
95
+ GamesDice::RerollRule.new(6, :<=, :reroll_add)])
96
+ end.to raise_error(TypeError)
97
+ end
98
+
99
+ describe 'with rerolls' do
100
+ it 'calculate correct minimum and maximum results' do
101
+ die = described_class.new(10, rerolls: [GamesDice::RerollRule.new(10, :<=, :reroll_add, 3)])
102
+ expect(die.min).to be 1
103
+ expect(die.max).to be 40
104
+
105
+ die = described_class.new(10, rerolls: [[1, :>=, :reroll_subtract]])
106
+ expect(die.min).to be(-9)
107
+ expect(die.max).to be 10
108
+
109
+ die = described_class.new(10, rerolls: [GamesDice::RerollRule.new(10, :<=, :reroll_add)])
110
+ expect(die.min).to be 1
111
+ expect(die.max).to be 10_010
112
+ end
113
+
114
+ it 'simulate a d10 that rerolls and adds on a result of 10' do
115
+ die = described_class.new(10, rerolls: [[10, :<=, :reroll_add]])
116
+ [5, 4, 14, 7, 8, 1, 9].each do |expected|
117
+ expect(die.roll.value).to eql expected
118
+ expect(die.result.value).to eql expected
119
+ end
120
+ end
121
+
122
+ it 'explain how it got results outside range 1 to 10 on a d10' do
123
+ die = described_class.new(10, rerolls: [[10, :<=, :reroll_add], [1, :>=, :reroll_subtract]])
124
+ ['5', '4', '[10+4] 14', '7', '8', '[1-9] -8'].each do |expected|
125
+ die.roll
126
+ expect(die.explain_result).to eql expected
127
+ end
128
+ end
129
+
130
+ it 'calculate an expected result' do
131
+ die = described_class.new(10, rerolls: [[10, :<=, :reroll_add], [1, :>=, :reroll_subtract]])
132
+ expect(die.probabilities.expected).to be_within(1e-10).of 5.5
133
+
134
+ die = described_class.new(10, rerolls: [GamesDice::RerollRule.new(1, :<=, :reroll_use_best, 1)])
135
+ expect(die.probabilities.expected).to be_within(1e-10).of 7.15
136
+
137
+ die = described_class.new(10, rerolls: [GamesDice::RerollRule.new(1, :<=, :reroll_use_worst, 2)])
138
+ expect(die.probabilities.expected).to be_within(1e-10).of 3.025
139
+
140
+ die = described_class.new(6, rerolls: [GamesDice::RerollRule.new(6, :<=, :reroll_add)])
141
+ expect(die.probabilities.expected).to be_within(1e-10).of 4.2
142
+
143
+ die = described_class.new(8, rerolls: [GamesDice::RerollRule.new(1, :>=, :reroll_use_best)])
144
+ expect(die.probabilities.expected).to be_within(1e-10).of 5.0
145
+
146
+ die = described_class.new(4, rerolls: [GamesDice::RerollRule.new(1, :>=, :reroll_replace, 1)])
147
+ expect(die.probabilities.expected).to be_within(1e-10).of 2.875
148
+ end
149
+
150
+ it 'calculate probabilities of each possible result' do
151
+ die = described_class.new(6, rerolls: [GamesDice::RerollRule.new(7, :>, :reroll_add, 1)])
152
+ probs = die.probabilities.to_h
153
+ expect(probs[11]).to be_within(1e-10).of 2 / 36.0
154
+ expect(probs[8]).to be_within(1e-10).of 5 / 36.0
155
+ expect(probs.values.sum).to be_within(1e-9).of 1.0
156
+
157
+ die = described_class.new(10, rerolls: [GamesDice::RerollRule.new(10, :<=, :reroll_add)])
158
+ probs = die.probabilities.to_h
159
+ expect(probs[8]).to be_within(1e-10).of 0.1
160
+ expect(probs[10]).to be_nil
161
+ expect(probs[13]).to be_within(1e-10).of 0.01
162
+ expect(probs[27]).to be_within(1e-10).of 0.001
163
+ expect(probs.values.sum).to be_within(1e-9).of 1.0
164
+
165
+ die = described_class.new(6, rerolls: [GamesDice::RerollRule.new(1, :>=, :reroll_replace, 1)])
166
+ probs = die.probabilities.to_h
167
+ expect(probs[1]).to be_within(1e-10).of 1 / 36.0
168
+ expect(probs[2]).to be_within(1e-10).of 7 / 36.0
169
+ expect(probs.values.sum).to be_within(1e-9).of 1.0
170
+ end
171
+
172
+ it 'calculate aggregate probabilities' do
173
+ die = described_class.new(6, rerolls: [GamesDice::RerollRule.new(7, :>, :reroll_add, 1)])
174
+ probs = die.probabilities
175
+ expect(probs.p_gt(7)).to be_within(1e-10).of 15 / 36.0
176
+ expect(probs.p_gt(-10)).to be 1.0
177
+ expect(probs.p_gt(12)).to be 0.0
178
+
179
+ expect(probs.p_ge(7)).to be_within(1e-10).of 21 / 36.0
180
+ expect(probs.p_ge(2)).to be 1.0
181
+ expect(probs.p_ge(15)).to be 0.0
182
+
183
+ expect(probs.p_lt(7)).to be_within(1e-10).of 15 / 36.0
184
+ expect(probs.p_lt(-10)).to be 0.0
185
+ expect(probs.p_lt(13)).to be 1.0
186
+
187
+ expect(probs.p_le(7)).to be_within(1e-10).of 21 / 36.0
188
+ expect(probs.p_le(1)).to be 0.0
189
+ expect(probs.p_le(12)).to be 1.0
190
+ end
191
+ end
192
+
193
+ describe 'with maps' do
194
+ it 'calculate correct minimum and maximum results' do
195
+ die = described_class.new(10, maps: [GamesDice::MapRule.new(7, :<=, 1, 'S')])
196
+ expect(die.min).to be 0
197
+ expect(die.max).to be 1
198
+ end
199
+
200
+ it 'simulate a d10 that scores 1 for success on a value of 7 or more' do
201
+ die = described_class.new(10, maps: [[7, :<=, 1, 'S']])
202
+ [0, 0, 1, 0, 1, 1, 0, 1].each do |expected|
203
+ expect(die.roll.value).to eql expected
204
+ expect(die.result.value).to eql expected
205
+ end
206
+ end
207
+
208
+ it 'label the mappings applied with the provided names' do
209
+ die = described_class.new(10, maps: [[7, :<=, 1, 'S'], [1, :>=, -1, 'F']])
210
+ ['5', '4', '10 S', '4', '7 S', '8 S', '1 F', '9 S'].each do |expected|
211
+ die.roll
212
+ expect(die.explain_result).to eql expected
213
+ end
214
+ end
215
+
216
+ it 'calculate an expected result' do
217
+ die = described_class.new(10,
218
+ maps: [GamesDice::MapRule.new(7, :<=, 1, 'S'),
219
+ GamesDice::MapRule.new(1, :>=, -1, 'F')])
220
+ expect(die.probabilities.expected).to be_within(1e-10).of 0.3
221
+ end
222
+
223
+ it 'calculate probabilities of each possible result' do
224
+ die = described_class.new(10,
225
+ maps: [GamesDice::MapRule.new(7, :<=, 1, 'S'),
226
+ GamesDice::MapRule.new(1, :>=, -1, 'F')])
227
+ probs_hash = die.probabilities.to_h
228
+ expect(probs_hash[1]).to be_within(1e-10).of 0.4
229
+ expect(probs_hash[0]).to be_within(1e-10).of 0.5
230
+ expect(probs_hash[-1]).to be_within(1e-10).of 0.1
231
+ expect(probs_hash.values.sum).to be_within(1e-9).of 1.0
232
+ end
233
+
234
+ it 'calculate aggregate probabilities' do
235
+ die = described_class.new(10,
236
+ maps: [GamesDice::MapRule.new(7, :<=, 1, 'S'),
237
+ GamesDice::MapRule.new(1, :>=, -1, 'F')])
238
+ probs = die.probabilities
239
+ expect(probs.p_gt(-2)).to be 1.0
240
+ expect(probs.p_gt(-1)).to be_within(1e-10).of 0.9
241
+ expect(probs.p_gt(1)).to be 0.0
242
+
243
+ expect(probs.p_ge(1)).to be_within(1e-10).of 0.4
244
+ expect(probs.p_ge(-1)).to be 1.0
245
+ expect(probs.p_ge(2)).to be 0.0
246
+
247
+ expect(probs.p_lt(1)).to be_within(1e-10).of 0.6
248
+ expect(probs.p_lt(-1)).to be 0.0
249
+ expect(probs.p_lt(2)).to be 1.0
250
+
251
+ expect(probs.p_le(-1)).to be_within(1e-10).of 0.1
252
+ expect(probs.p_le(-2)).to be 0.0
253
+ expect(probs.p_le(1)).to be 1.0
254
+ end
255
+ end
256
+
257
+ describe 'with rerolls and maps together' do
258
+ before do
259
+ @die = described_class.new(6,
260
+ rerolls: [[6, :<=, :reroll_add]],
261
+ maps: [GamesDice::MapRule.new(9, :<=, 1, 'Success')])
262
+ end
263
+
264
+ it 'calculate correct minimum and maximum results' do
265
+ expect(@die.min).to be 0
266
+ expect(@die.max).to be 1
267
+ end
268
+
269
+ it 'calculate an expected result' do
270
+ expect(@die.probabilities.expected).to be_within(1e-10).of 4 / 36.0
271
+ end
272
+
273
+ it 'calculate probabilities of each possible result' do
274
+ probs_hash = @die.probabilities.to_h
275
+ expect(probs_hash[1]).to be_within(1e-10).of 4 / 36.0
276
+ expect(probs_hash[0]).to be_within(1e-10).of 32 / 36.0
277
+ expect(probs_hash.values.sum).to be_within(1e-9).of 1.0
278
+ end
279
+
280
+ it 'calculate aggregate probabilities' do
281
+ probs = @die.probabilities
282
+
283
+ expect(probs.p_gt(0)).to be_within(1e-10).of 4 / 36.0
284
+ expect(probs.p_gt(-2)).to be 1.0
285
+ expect(probs.p_gt(1)).to be 0.0
286
+
287
+ expect(probs.p_ge(1)).to be_within(1e-10).of 4 / 36.0
288
+ expect(probs.p_ge(-1)).to be 1.0
289
+ expect(probs.p_ge(2)).to be 0.0
290
+
291
+ expect(probs.p_lt(1)).to be_within(1e-10).of 32 / 36.0
292
+ expect(probs.p_lt(0)).to be 0.0
293
+ expect(probs.p_lt(2)).to be 1.0
294
+
295
+ expect(probs.p_le(0)).to be_within(1e-10).of 32 / 36.0
296
+ expect(probs.p_le(-1)).to be 0.0
297
+ expect(probs.p_le(1)).to be 1.0
298
+ end
299
+
300
+ it 'apply mapping to final re-rolled result' do
301
+ [0, 1, 0, 0].each do |expected|
302
+ expect(@die.roll.value).to eql expected
303
+ expect(@die.result.value).to eql expected
304
+ end
305
+ end
306
+
307
+ it 'explain how it got each result' do
308
+ ['5', '[6+4] 10 Success', '[6+2] 8', '5'].each do |expected|
309
+ @die.roll
310
+ expect(@die.explain_result).to eql expected
311
+ end
312
+ end
313
+ end
314
+ end