games_dice 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -2,33 +2,33 @@
2
2
 
3
3
  [![Build Status](https://travis-ci.org/neilslater/games_dice.png?branch=master)](http://travis-ci.org/neilslater/games_dice)
4
4
 
5
- A library for simulating dice, intended for constructing a variety of dice systems as used in
6
- role-playing and board games.
5
+ A library for simulating dice. Use it to construct dice-rolling systems used in role-playing and board games.
7
6
 
8
7
  ## Description
9
8
 
10
- GamesDice is a gem to automate or simulate a variety of dice rolling systems found in board games and
11
- role-playing games.
9
+ GamesDice can emulate a variety of rules-driven dice systems that are used to generate integer results
10
+ within a game.
12
11
 
13
- GamesDice is designed for systems used to generate integer results, and that do not require the
14
- player to make decisions or apply other rules from the game. There are no game mechanics implemented
15
- in GamesDice (such as the chance to hit in a combat game).
16
-
17
- The main features of GamesDice are
12
+ The main features of GamesDice are:
18
13
 
19
14
  * Uses string dice descriptions, the basics of which are familiar to many game players e.g. '2d6 + 3'
20
- * Supports common features of dice systems automatically:
15
+ * Supports some common features of dice systems:
21
16
  * Re-rolls that replace or modify the previous roll
22
17
  * Counting number of "successes" from a set of dice
23
18
  * Keeping the best, or worst, results from a set of dice
24
19
  * Can explain how a result was achieved in terms of the individual die rolls
25
- * Can calculate probabilities and expected values (experimental feature)
20
+ * Can calculate probabilities and expected values (with some limitations)
21
+
22
+ There are no game mechanics implemented in GamesDice, such as the chance to hit in a fantasy combat
23
+ game. There is no support for player interaction within a roll, such as player choice on whether or
24
+ not to re-roll a specific die within a combined set. These things are of course possible if you use the
25
+ gem as-is, and add them as features within your project code.
26
26
 
27
27
  ## Special Note on Versions Prior to 1.0.0
28
28
 
29
- The author is using this code as an exercise in gem "best practice". As such, the gem
30
- will have a limited set of functionality prior to version 1.0.0, and there should be
31
- many small release increments before then.
29
+ As of version 0.2.0, the gem has the same feature set as planned for version
30
+ 1.0.0. Versions between 0.2.0 and 1.0.0 are being used mainly to improve
31
+ code quality, documentation and performance.
32
32
 
33
33
  ## Installation
34
34
 
@@ -91,6 +91,17 @@ Returns the value from the last call to roll. This will be nil if no roll has be
91
91
  dice.roll
92
92
  dice.result # => 12
93
93
 
94
+ #### dice.explain_result
95
+
96
+ Returns a string that attempts to show how the result from the last call to roll was composed
97
+ from individual results. This will be nil if no roll has been made yet.
98
+
99
+ dice.explain_result # => nil
100
+ dice.roll # => 12
101
+ dice.explain_result # => "3d6: 4 + 2 + 6 = 12"
102
+
103
+ The exact format is the subject of refinement in future versions of the gem.
104
+
94
105
  #### dice.max
95
106
 
96
107
  Returns the maximum possible value from a roll of the dice. Dice with the possibility of rolling
@@ -255,13 +266,13 @@ Where:
255
266
  * add - add result of reroll to running total, and ignore any subtract rules
256
267
  * subtract - subtract result of reroll from running total, and reverse sense of any further add results
257
268
  * use_best - use the new value if it is higher than the existing value
258
- * use_worst - use the new value if it is higher than the existing value
269
+ * use_worst - use the new value if it is lower than the existing value
259
270
  * LIMIT is an integer that sets the maximum number of times that the rule can be triggered, the default is 1000
260
271
 
261
272
  Examples:
262
273
 
263
274
  1d6r:1. # Same as "1d6r1"
264
- 1d10r:10,replace,1. #
275
+ 1d10r:10,replace,1. # Roll a 10-sided die, re-roll a result of 10 and take the value of the second roll
265
276
  1d20r:<=10,use_best,1. # Roll a 20-sided die, re-roll a result if 10 or lower, and use best result
266
277
 
267
278
  #### Maps
@@ -275,6 +286,24 @@ The simple form specifies a value above which the result is considered to be 1,
275
286
 
276
287
  When rolled, this will score from 0 to 3 - the number of the ten-sided dice that scored 6 or higher.
277
288
 
289
+ The full version of this modifier, allows you to specify from 1 to 3 parameters:
290
+
291
+ 3d10m:[VALUE_COMPARISON],[MAP_VALUE],[DESCRIPTION].
292
+
293
+ Where:
294
+
295
+ * VALUE_COMPARISON is one of >, >= (default), ==, <= < plus an integer to set conditions on when the map should occur
296
+ * MAP_VALUE is an integer that will be used in place of a result from a die, default value is 1
297
+ * maps are tested in order that they are declared, and first one that matches is applied
298
+ * when at least one map has been defined, all unmapped values default to 0
299
+ * DESCRIPTION is a word or character to use to denote the map in any explanation
300
+
301
+ Examples:
302
+
303
+ 9d6x.m:10. # Roll 9 six-sided "exploding" dice, and count 1 for any result of 10 or more
304
+ 9d6x.m:10,1,S. # Same as above, but with each success marked with "S" in the explanation
305
+ 5d10m:>=6,1,S.m:==1,-1,F. # Roll 5 ten-sided dice, count 1 for any result of 6 or more, or -1 for any result of 1
306
+
278
307
  #### Keepers
279
308
 
280
309
  You can specify that only a sub-set of highest or lowest dice values will contribute to the final
@@ -287,6 +316,30 @@ The simple form indicates the number of highest value dice to keep.
287
316
  When rolled, this will score from 2 to 20 - the sum of the two highest scoring ten-sided dice, out of
288
317
  five.
289
318
 
319
+ The full version of this modifier, allows you to specify from 1 to 2 parameters:
320
+
321
+ 5d10k:[KEEP_NUM],[KEEP_TYPE].
322
+
323
+ Where:
324
+
325
+ * KEEP_NUM is an integer specifying the number of dice to keep.
326
+ * KEEP_TYPE is one of
327
+ * best - keep highest values and add them together
328
+ * worst - keep lowest values and add them together
329
+
330
+ Examples:
331
+
332
+ 4d6k:3.r:1,replace,1. # Roll 4 six-sided dice, re-roll any 1s, and keep best 3.
333
+ 2d20k:1,worst. # Roll 2 twenty-sided dice, return lowest of the two results.
334
+
335
+ #### Combinations
336
+
337
+ * When there are many modifiers, they are applied in strict order:
338
+ * First by type: re-rolls, maps, keepers
339
+ * Then according to the order they were specified
340
+ * A maximum of one re-roll modifier, and one map modifier are applied to each individual die rolled
341
+ * Only one keepers modifier is applied per dice type. Specifying a second one will cause an error
342
+
290
343
  #### Aliases
291
344
 
292
345
  Some combinations of modifiers crop up in well-known games, and have been allocated single-character
@@ -296,8 +349,8 @@ This is an alias for "exploding" dice:
296
349
 
297
350
  5d10x # Same as '5d10r:10,add.'
298
351
 
299
- When rolled, this will score from 5 to theoretically any number, as results of 10 on any die mean that
300
- die rolls again and the result is added on.
352
+ When rolled, this will score from 5 to theoretically any higher number, as results of 10 on any
353
+ die mean that die rolls again and the result is added on.
301
354
 
302
355
  ## Contributing
303
356
 
@@ -306,3 +359,6 @@ die rolls again and the result is added on.
306
359
  3. Commit your changes (`git commit -am 'Add some feature'`)
307
360
  4. Push to the branch (`git push origin my-new-feature`)
308
361
  5. Create new Pull Request
362
+
363
+ I am always interested to receive information about dice rolling schemes that this library could or
364
+ should include in its repertoire.
@@ -81,6 +81,12 @@ class GamesDice::Bunch
81
81
  # after calling #roll, this is set to the final integer value from using the dice as specified
82
82
  attr_reader :result
83
83
 
84
+ # Needs refinement. Returns best available string description of the bunch.
85
+ def label
86
+ return @name if @name != ''
87
+ return @ndice.to_s + 'd' + @sides.to_s
88
+ end
89
+
84
90
  # either nil, or an array of GamesDice::RerollRule objects that are assessed on each roll of #single_die
85
91
  # Reroll types :reroll_new_die and :reroll_new_keeper do not affect the #single_die, but are instead
86
92
  # assessed in this container object
@@ -66,4 +66,35 @@ class GamesDice::Dice
66
66
  end
67
67
  end
68
68
 
69
+ def explain_result
70
+ return nil unless @result
71
+ explanations = @bunches.map { |bunch| bunch.label + ": " + bunch.explain_result }
72
+
73
+ if explanations.count == 0
74
+ return @offset.to_s
75
+ end
76
+
77
+ if explanations.count == 1
78
+ if @offset !=0
79
+ return explanations[0] + '. ' + array_to_sum( [ @bunches[0].result, @offset ] )
80
+ else
81
+ return explanations[0]
82
+ end
83
+ end
84
+
85
+ bunch_values = @bunch_multipliers.zip(@bunches).map { |m,b| m * b.result }
86
+ bunch_values << @offset if @offset != 0
87
+ explanations << array_to_sum( bunch_values )
88
+ return explanations.join('. ')
89
+ end
90
+
91
+ private
92
+
93
+ def array_to_sum array
94
+ sum_parts = [ array.first < 0 ? '-' + array.first.abs.to_s : array.first.to_s ]
95
+ sum_parts += array.drop(1).map { |n| n < 0 ? '- ' + n.abs.to_s : '+ ' + n.to_s }
96
+ sum_parts += [ '=', array.inject(:+) ]
97
+ sum_parts.join(' ')
98
+ end
99
+
69
100
  end # class Dice
@@ -3,10 +3,9 @@ require 'parslet'
3
3
  # converts string dice descriptions to data usable for the GamesDice::Dice constructor
4
4
  class GamesDice::Parser < Parslet::Parser
5
5
 
6
- # These are the Parslet rules that define the dice grammar. It's an inefficient and over-complex
7
- # use of Parslet, and could do with logical a clean-up.
8
-
6
+ # Parslet rules that define the dice string grammar.
9
7
  rule(:integer) { match('[0-9]').repeat(1) }
8
+ rule(:plus_minus_integer) { ( match('[+-]') >> integer ) | integer }
10
9
  rule(:range) { integer.as(:range_start) >> str('..') >> integer.as(:range_end) }
11
10
  rule(:dlabel) { match('[d]') }
12
11
  rule(:space) { match('\s').repeat(1) }
@@ -33,20 +32,25 @@ class GamesDice::Parser < Parslet::Parser
33
32
  rule(:stop) { str('.') }
34
33
 
35
34
  rule(:condition_only) { opint_or_int.as(:condition) }
35
+ rule(:num_only) { integer.as(:num) }
36
+
36
37
 
37
38
  rule(:condition_and_type) { opint_or_int.as(:condition) >> comma >> ctl_string.as(:type) }
38
- rule(:condition_and_num) { opint_or_int.as(:condition) >> comma >> integer.as(:num) }
39
+ rule(:condition_and_num) { opint_or_int.as(:condition) >> comma >> plus_minus_integer.as(:num) }
39
40
 
40
41
  rule(:condition_type_and_num) { opint_or_int.as(:condition) >> comma >> ctl_string.as(:type) >> comma >> integer.as(:num) }
41
- rule(:condition_num_and_output) { opint_or_int.as(:condition) >> comma >> integer.as(:num) >> comma >> ctl_string.as(:output) }
42
+ rule(:condition_num_and_output) { opint_or_int.as(:condition) >> comma >> plus_minus_integer.as(:num) >> comma >> output_string.as(:output) }
43
+ rule(:num_and_type) { integer.as(:num) >> comma >> ctl_string.as(:type) }
42
44
 
43
45
  rule(:reroll_params) { condition_type_and_num | condition_and_type | condition_only }
44
46
  rule(:map_params) { condition_num_and_output | condition_and_num | condition_only }
47
+ rule(:keeper_params) { num_and_type | num_only }
45
48
 
46
49
  rule(:full_reroll) { reroll_label >> str(':') >> reroll_params >> stop }
47
50
  rule(:full_map) { map_label >> str(':') >> map_params >> stop }
51
+ rule(:full_keepers) { keep_label >> str(':') >> keeper_params >> stop }
48
52
 
49
- rule(:complex_modifier) { full_reroll | full_map }
53
+ rule(:complex_modifier) { full_reroll | full_map | full_keepers }
50
54
 
51
55
  rule(:bunch_modifier) { complex_modifier | ( single_modifier >> stop.maybe ) | ( simple_modifier >> stop.maybe ) }
52
56
  rule(:bunch) { bunch_start >> bunch_modifier.repeat.as(:mods) }
@@ -58,13 +62,11 @@ class GamesDice::Parser < Parslet::Parser
58
62
  rule(:expressions) { dice_expression.repeat.as(:bunches) }
59
63
  root :expressions
60
64
 
61
- def parse dice_description, dice_name = nil
65
+ def parse dice_description
62
66
  dice_description = dice_description.to_s.strip
63
- dice_name ||= dice_description
64
67
  # Force first item to start '+' for simpler parse rules
65
68
  dice_description = '+' + dice_description unless dice_description =~ /\A[+-]/
66
69
  dice_expressions = super( dice_description )
67
-
68
70
  { :bunches => collect_bunches( dice_expressions ), :offset => collect_offset( dice_expressions ) }
69
71
  end
70
72
 
@@ -137,9 +139,10 @@ class GamesDice::Parser < Parslet::Parser
137
139
  def collect_reroll_rule reroll_mod, out_hash
138
140
  out_hash[:rerolls] ||= []
139
141
  if reroll_mod[:simple_value]
140
- out_hash[:rerolls] << [ reroll_mod[:simple_value].to_i, :>=, :reroll_replace, 1 ]
142
+ out_hash[:rerolls] << [ reroll_mod[:simple_value].to_i, :>=, :reroll_replace ]
141
143
  return
142
144
  end
145
+
143
146
  # Typical reroll_mod: {:reroll=>"r"@5, :condition=>{:compare_num=>"10"@7}, :type=>"add"@10}
144
147
  op = get_op_symbol( reroll_mod[:condition][:comparison] || '==' )
145
148
  v = reroll_mod[:condition][:compare_num].to_i
@@ -154,12 +157,16 @@ class GamesDice::Parser < Parslet::Parser
154
157
 
155
158
  # Called for any parsed keeper mode
156
159
  def collect_keeper_rule keeper_mod, out_hash
160
+ raise "Cannot set keepers for a bunch twice" if out_hash[:keep_mode]
157
161
  if keeper_mod[:simple_value]
158
162
  out_hash[:keep_mode] = :keep_best
159
163
  out_hash[:keep_number] = keeper_mod[:simple_value].to_i
160
164
  return
161
165
  end
162
- # TODO: Handle complex descriptions
166
+
167
+ # Typical keeper_mod: {:keep=>"k"@5, :num=>"1"@7, :type=>"worst"@9}
168
+ out_hash[:keep_number] = keeper_mod[:num].to_i
169
+ out_hash[:keep_mode] = ( 'keep_' + ( keeper_mod[:type] || 'best' ) ).to_sym
163
170
  end
164
171
 
165
172
  # Called for any parsed map mode
@@ -170,7 +177,19 @@ class GamesDice::Parser < Parslet::Parser
170
177
  return
171
178
  end
172
179
 
173
- # Typical
180
+ # Typical map_mod: {:map=>"m"@4, :condition=>{:compare_num=>"5"@6}, :num=>"2"@8, :output=>"Qwerty"@10}
181
+ op = get_op_symbol( map_mod[:condition][:comparison] || '>=' )
182
+ v = map_mod[:condition][:compare_num].to_i
183
+ out_val = 1
184
+ if map_mod[:num]
185
+ out_val = map_mod[:num].to_i
186
+ end
187
+
188
+ if map_mod[:output]
189
+ out_hash[:maps] << [ v, op, out_val, map_mod[:output].to_s ]
190
+ else
191
+ out_hash[:maps] << [ v, op, out_val ]
192
+ end
174
193
  end
175
194
 
176
195
  # The dice description language uses (r).op.x, whilst GamesDice::RerollRule uses x.op.(r), so
@@ -1,3 +1,3 @@
1
1
  module GamesDice
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.0"
3
3
  end
data/spec/parser_spec.rb CHANGED
@@ -24,9 +24,9 @@ describe GamesDice::Parser do
24
24
 
25
25
  it "should parse 'NdXrY' as 'roll N times X-sided dice, re-roll and replace a Y or less (once)'" do
26
26
  variations = {
27
- '1d6r1' => { :bunches => [{:ndice=>1, :sides=>6, :multiplier=>1, :rerolls=>[ [1,:>=,:reroll_replace,1] ]}], :offset => 0 },
28
- '2d20r7' => { :bunches => [{:ndice=>2, :sides=>20, :multiplier=>1, :rerolls=>[ [7,:>=,:reroll_replace,1] ]}], :offset => 0 },
29
- '1d8r2' => { :bunches => [{:ndice=>1, :sides=>8, :multiplier=>1, :rerolls=>[ [2,:>=,:reroll_replace,1] ]}], :offset => 0 },
27
+ '1d6r1' => { :bunches => [{:ndice=>1, :sides=>6, :multiplier=>1, :rerolls=>[ [1,:>=,:reroll_replace] ]}], :offset => 0 },
28
+ '2d20r7' => { :bunches => [{:ndice=>2, :sides=>20, :multiplier=>1, :rerolls=>[ [7,:>=,:reroll_replace] ]}], :offset => 0 },
29
+ '1d8r2' => { :bunches => [{:ndice=>1, :sides=>8, :multiplier=>1, :rerolls=>[ [2,:>=,:reroll_replace] ]}], :offset => 0 },
30
30
  }
31
31
 
32
32
  variations.each do |input,expected_output|
@@ -68,7 +68,7 @@ describe GamesDice::Parser do
68
68
 
69
69
  it "should successfully parse combinations of modifiers in any valid order" do
70
70
  variations = {
71
- '5d10r1x' => { :bunches => [{:ndice=>5, :sides=>10, :multiplier=>1, :rerolls=>[ [1,:>=,:reroll_replace,1], [10,:==,:reroll_add] ]}], :offset => 0 },
71
+ '5d10r1x' => { :bunches => [{:ndice=>5, :sides=>10, :multiplier=>1, :rerolls=>[ [1,:>=,:reroll_replace], [10,:==,:reroll_add] ]}], :offset => 0 },
72
72
  '3d6xk2' => { :bunches => [{:ndice=>3, :sides=>6, :multiplier=>1, :rerolls=>[ [6,:==,:reroll_add] ], :keep_mode=>:keep_best, :keep_number=>2 }], :offset => 0 },
73
73
  '4d6m8x' => { :bunches => [{:ndice=>4, :sides=>6, :multiplier=>1, :maps=>[ [8,:<=,1] ], :rerolls=>[ [6,:==,:reroll_add] ] }], :offset => 0 },
74
74
  }
data/spec/readme_spec.rb CHANGED
@@ -57,6 +57,25 @@ describe GamesDice::Dice do
57
57
  end
58
58
  end
59
59
 
60
+ describe "#explain_result" do
61
+ it "attempts to show how the result from the last call to roll was composed" do
62
+ expected_results = [
63
+ "3d6: 3 + 6 + 2 = 11",
64
+ "3d6: 4 + 5 + 6 = 15",
65
+ "3d6: 3 + 6 + 2 = 11",
66
+ "3d6: 5 + 6 + 1 = 12"
67
+ ]
68
+ expected_results.each do |expected|
69
+ dice.roll
70
+ dice.explain_result.should == expected
71
+ end
72
+ end
73
+
74
+ it "will be nil if no roll has been made yet" do
75
+ dice.explain_result.should be_nil
76
+ end
77
+ end
78
+
60
79
  describe "#max" do
61
80
  it "returns the maximum possible value from a roll of the dice" do
62
81
  dice.max.should == 18
@@ -210,9 +229,155 @@ describe 'String Dice Description' do
210
229
  end
211
230
 
212
231
  describe "'5d10x'" do
213
- it "returns expected results from rolling" do
232
+ it "is the same as '5d10r:10,add.'" do
233
+ srand(235241)
214
234
  d = GamesDice.create '5d10x'
215
- (1..5).map { |n| d.roll }.should == [22, 22, 31, 53, 25]
235
+ results1 = (1..50).map { d.roll }
236
+
237
+ srand(235241)
238
+ d = GamesDice.create '5d10r:10,add.'
239
+ results2 = (1..50).map { d.roll }
240
+
241
+ results1.should == results2
242
+ end
243
+ end
244
+
245
+ describe "'1d6r:1.'" do
246
+ it "should return same as '1d6r1'" do
247
+ srand(235241)
248
+ d = GamesDice.create '1d6r:1.'
249
+ results1 = (1..50).map { d.roll }
250
+
251
+ srand(235241)
252
+ d = GamesDice.create '1d6r1'
253
+ results2 = (1..50).map { d.roll }
254
+
255
+ results1.should == results2
256
+ end
257
+ end
258
+
259
+ describe "'1d10r:10,replace,1.'" do
260
+ it "should roll a 10-sided die, re-roll a result of 10 and take the value of the second roll" do
261
+ d = GamesDice.create '1d10r:10,replace,1.'
262
+ (1..27).map { d.roll }.should == [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]
263
+ end
264
+ end
265
+
266
+ describe "'1d20r:<=10,use_best,1.'" do
267
+ it "should roll a 20-sided die, re-roll a result if 10 or lower, and use best result" do
268
+ d = GamesDice.create '1d20r:<=10,use_best,1.'
269
+ (1..20).map { d.roll }.should == [ 18, 19, 20, 20, 3, 11, 7, 20, 15, 19, 6, 16, 17, 16, 15, 11, 9, 15, 20, 16 ]
270
+ end
271
+ end
272
+
273
+ describe "'5d10r:10,add.k2', '5d10xk2' and '5d10x.k2'" do
274
+ it "should all be equivalent" do
275
+ srand(135241)
276
+ d = GamesDice.create '5d10r:10,add.k2'
277
+ results1 = (1..50).map { d.roll }
278
+
279
+ srand(135241)
280
+ d = GamesDice.create '5d10xk2'
281
+ results2 = (1..50).map { d.roll }
282
+
283
+ srand(135241)
284
+ d = GamesDice.create '5d10x.k2'
285
+ results3 = (1..50).map { d.roll }
286
+
287
+ results1.should == results2
288
+ results2.should == results3
289
+ end
290
+ end
291
+
292
+ describe "'5d10r:>8,add.'" do
293
+ it "returns expected results from rolling" do
294
+ d = GamesDice.create '5d10r:>8,add.'
295
+ (1..5).map { |n| d.roll }.should == [22, 22, 31, 64, 26]
296
+ end
297
+ end
298
+
299
+ describe "'9d6x.m:10.'" do
300
+ it "returns expected results from rolling" do
301
+ d = GamesDice.create '9d6x.m:10.'
302
+ (1..5).map { |n| d.roll }.should == [1, 2, 1, 1, 1]
303
+ end
304
+ it "can be explained as number of exploding dice scoring 10+" do
305
+ d = GamesDice.create '9d6x.m:10.'
306
+ (1..5).map { |n| d.roll; d.explain_result }.should == [
307
+ "9d6: [6+3] 9, 2, 3, 4, [6+4] 10, 2, [6+3] 9, 3, 5. Successes: 1",
308
+ "9d6: [6+6+3] 15, [6+5] 11, 2, 1, 4, 2, 1, 3, 5. Successes: 2",
309
+ "9d6: 1, [6+6+1] 13, 2, 1, 1, 3, [6+1] 7, 5, 4. Successes: 1",
310
+ "9d6: [6+4] 10, 3, 4, 5, 5, 1, [6+3] 9, 3, 5. Successes: 1",
311
+ "9d6: [6+3] 9, 3, [6+5] 11, 4, 2, 2, 1, 4, 5. Successes: 1"
312
+ ]
313
+ end
314
+ end
315
+
316
+ describe "'9d6x.m:10,1,S.'" do
317
+ it "returns expected results from rolling" do
318
+ d = GamesDice.create '9d6x.m:10,1,S.'
319
+ (1..5).map { |n| d.roll }.should == [1, 2, 1, 1, 1]
320
+ end
321
+ it "includes the string 'S' next to each success" do
322
+ d = GamesDice.create '9d6x.m:10,1,S.'
323
+ (1..5).map { |n| d.roll; d.explain_result }.should == [
324
+ "9d6: [6+3] 9, 2, 3, 4, [6+4] 10 S, 2, [6+3] 9, 3, 5. Successes: 1",
325
+ "9d6: [6+6+3] 15 S, [6+5] 11 S, 2, 1, 4, 2, 1, 3, 5. Successes: 2",
326
+ "9d6: 1, [6+6+1] 13 S, 2, 1, 1, 3, [6+1] 7, 5, 4. Successes: 1",
327
+ "9d6: [6+4] 10 S, 3, 4, 5, 5, 1, [6+3] 9, 3, 5. Successes: 1",
328
+ "9d6: [6+3] 9, 3, [6+5] 11 S, 4, 2, 2, 1, 4, 5. Successes: 1"
329
+ ]
330
+ end
331
+ end
332
+
333
+ describe "'5d10m:>=6,1,S.m:==1,-1,F.'" do
334
+ it "returns expected results from rolling" do
335
+ d = GamesDice.create '5d10m:>=6,1,S.m:==1,-1,F.'
336
+ (1..10).map { |n| d.roll }.should == [2, 2, 4, 3, 2, 1, 1, 3, 3, 0]
337
+ end
338
+ it "includes the string 'S' next to each success, and 'F' next to each 'fumble'" do
339
+ d = GamesDice.create '5d10m:>=6,1,S.m:==1,-1,F.'
340
+ (1..5).map { |n| d.roll; d.explain_result }.should == [
341
+ "5d10: 2, 3, 4, 7 S, 6 S. Successes: 2",
342
+ "5d10: 7 S, 4, 2, 6 S, 3. Successes: 2",
343
+ "5d10: 7 S, 5, 6 S, 7 S, 6 S. Successes: 4",
344
+ "5d10: 6 S, 5, 10 S, 9 S, 4. Successes: 3",
345
+ "5d10: 10 S, 9 S, 8 S, 3, 1 F. Successes: 2"
346
+ ]
347
+ end
348
+ end
349
+
350
+ describe "'4d6k:3.r:1,replace,1.'" do
351
+ it "represents roll 4 six-sided dice, re-roll any 1s, and keep best 3." do
352
+ d = GamesDice.create '4d6k:3.r:1,replace,1.'
353
+ (1..10).map { |n| d.roll }.should == [12, 14, 14, 18, 11, 17, 11, 15, 14, 14]
354
+ end
355
+ it "includes re-rolls and keeper choice in explanations" do
356
+ d = GamesDice.create '4d6k:3.r:1,replace,1.'
357
+ (1..5).map { |n| d.roll; d.explain_result }.should == [
358
+ "4d6: 6, 3, 2, 3. Keep: 3 + 3 + 6 = 12",
359
+ "4d6: 4, 6, 4, 2. Keep: 4 + 4 + 6 = 14",
360
+ "4d6: 6, 3, 3, 5. Keep: 3 + 5 + 6 = 14",
361
+ "4d6: 6, 6, 3, 6. Keep: 6 + 6 + 6 = 18",
362
+ "4d6: 5, 2, [1|4] 4, 2. Keep: 2 + 4 + 5 = 11"
363
+ ]
364
+ end
365
+ end
366
+
367
+ describe "'2d20k:1,worst.'" do
368
+ it "represents roll 2 twenty-sided dice, return lowest of the two results" do
369
+ d = GamesDice.create '2d20k:1,worst.'
370
+ (1..10).map { |n| d.roll }.should == [18, 6, 2, 3, 5, 10, 15, 1, 7, 10]
371
+ end
372
+ it "includes keeper choice in explanations" do
373
+ d = GamesDice.create '2d20k:1,worst.'
374
+ (1..5).map { |n| d.roll; d.explain_result }.should == [
375
+ "2d20: 18, 19. Keep: 18",
376
+ "2d20: 20, 6. Keep: 6",
377
+ "2d20: 20, 2. Keep: 2",
378
+ "2d20: 3, 11. Keep: 3",
379
+ "2d20: 5, 7. Keep: 5"
380
+ ]
216
381
  end
217
382
  end
218
383
 
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.1.3
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-05-28 00:00:00.000000000 Z
12
+ date: 2013-05-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -112,7 +112,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
112
112
  version: '0'
113
113
  segments:
114
114
  - 0
115
- hash: -4383036419295474875
115
+ hash: 4046591063539756555
116
116
  required_rubygems_version: !ruby/object:Gem::Requirement
117
117
  none: false
118
118
  requirements:
@@ -121,7 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
121
121
  version: '0'
122
122
  segments:
123
123
  - 0
124
- hash: -4383036419295474875
124
+ hash: 4046591063539756555
125
125
  requirements: []
126
126
  rubyforge_project:
127
127
  rubygems_version: 1.8.24