games_dice 0.1.3 → 0.2.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.
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