games_dice 0.0.6 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -27,7 +27,7 @@ The main features of GamesDice are
27
27
  ## Special Note on Versions Prior to 1.0.0
28
28
 
29
29
  The author is using this code as an exercise in gem "best practice". As such, the gem
30
- will have a deliberately limited set of functionality prior to version 1.0.0, and there should be
30
+ will have a limited set of functionality prior to version 1.0.0, and there should be
31
31
  many small release increments before then.
32
32
 
33
33
  ## Installation
@@ -60,7 +60,9 @@ dice rolls, explain the results or calculate probabilties as required.
60
60
 
61
61
  ### GamesDice factory methods
62
62
 
63
- #### GamesDice.create dice_description, prng
63
+ #### GamesDice.create
64
+
65
+ dice = GamesDice.create dice_description, prng
64
66
 
65
67
  Converts a string such as '3d6+6' into a GamesDice::Dice object
66
68
 
@@ -73,14 +75,70 @@ Returns a GamesDice::Dice object.
73
75
 
74
76
  ### GamesDice::Dice instance methods
75
77
 
78
+ Example results given for '3d6'. Unless noted, methods do not take any parameters.
79
+
76
80
  #### dice.roll
77
81
 
78
82
  Simulates rolling the dice as they were described in the constructor, and keeps a record of how the
79
83
  simulation result was achieved.
80
84
 
81
- Takes no parameters.
85
+ dice.roll # => 12
86
+
87
+ #### dice.result
88
+
89
+ Returns the value from the last call to roll. This will be nil if no roll has been made yet.
90
+
91
+ dice.result # => nil
92
+ dice.roll
93
+ dice.result # => 12
94
+
95
+ #### dice.max
96
+
97
+ Returns the maximum possible value from a roll of the dice. Dice with the possibility of rolling
98
+ progressively higher and higher values will return an arbitrary high value.
99
+
100
+ dice.max # => 18
101
+
102
+ #### dice.min
103
+
104
+ Returns the minimum possible value from a roll of the dice. Dice with the possibility of rolling
105
+ progressively lower and lower values will return an arbitrary low value.
106
+
107
+ dice.min # => 3
108
+
109
+ #### dice.minmax
110
+
111
+ Convenience method, returns an array [ dice.min, dice.max ]
112
+
113
+ dice.minmax # => [3,18]
114
+
115
+ #### dice.probabilities
116
+
117
+ Calculates probability distribution for the dice. Note that some distributions, involving keeping
118
+ a number best or worst results, can take significant time to calculate.
119
+
120
+ Returns a GamesDice::Probabilities object that describes the probability distribution.
121
+
122
+ probabilities = dice.probabilities
123
+
124
+ ### GamesDice::Probabilities instance methods
125
+
126
+ #### probabilities.to_h
127
+
128
+ Returns a hash representation of the probability distribution. Each keys is a possible result
129
+ from rolling the dice (an Integer), and the associated value is the probability of a roll
130
+ returning that value (a Float).
131
+
132
+ #### probabilities.max
133
+
134
+ Returns maximum value in the probability distribution. This may not be the theoretical maximum
135
+ possible on the dice, if for example the dice can roll open-ended high results.
136
+
137
+ #### probabilities.min
138
+
139
+ Returns minimum value in the probability distribution. This may not be the theoretical minimum
140
+ possible on the dice, if for example the dice can roll open-ended low results.
82
141
 
83
- Returns the integer result of the roll.
84
142
 
85
143
  ## String Dice Descriptions
86
144
 
@@ -101,6 +159,69 @@ That is the limit of combining dice and constants though, no multiplications, or
101
159
  like "(1d8)d8" - you can still use games_dice to help simulate these, but you will need to add your own
102
160
  code to do so.
103
161
 
162
+ ### Die Modifiers
163
+
164
+ After the number of sides, you may add one or more modifiers, that affect all of the dice in that
165
+ "NdX" group. A die modifier can be a single character, e.g.
166
+
167
+ 1d10x
168
+
169
+ A die modifier can also be a single letter plus an integer value, e.g.
170
+
171
+ 1d6r1
172
+
173
+ More complex die modifiers are possible, with parameters supplied in square brackets, and multiple
174
+ modifiers should combine as expected e.g.
175
+
176
+ 5d10r[10,add]k2
177
+
178
+ #### Rerolls
179
+
180
+ You can specify that dice rolling certain values should be re-rolled, and how that re-roll should be
181
+ interpretted.
182
+
183
+ The simple form specifies a low value that will automatically trigger a one-time replacement:
184
+
185
+ 1d6r1
186
+
187
+ When rolled, this die will score from 1 to 6. If it rolls a 1, it will roll again automatically
188
+ and use that result instead.
189
+
190
+ #### Maps
191
+
192
+ You can specify that the value shown on each die is converted to some other set of values. If
193
+ you add at least one map modifier, all unmapped values will map to 0 by default.
194
+
195
+ The simple form specifies a value above which the result is considered to be 1, as in "one success":
196
+
197
+ 3d10m6
198
+
199
+ When rolled, this will score from 0 to 3 - the number of the ten-sided dice that scored 6 or higher.
200
+
201
+ #### Keepers
202
+
203
+ You can specify that only a sub-set of highest or lowest dice values will contribute to the final
204
+ total.
205
+
206
+ The simple form indicates the number of highest value dice to keep.
207
+
208
+ 5d10k2
209
+
210
+ When rolled, this will score from 2 to 20 - the sum of the two highest scoring ten-sided dice, out of
211
+ five.
212
+
213
+ #### Aliases
214
+
215
+ Some combinations of modifiers crop up in well-known games, and have been allocated single-character
216
+ short codes.
217
+
218
+ This is an alias for "exploding" dice:
219
+
220
+ 5d10x
221
+
222
+ When rolled, this will score from 5 to theoretically any number, as results of 10 on any die mean that
223
+ die rolls again and the result is added on.
224
+
104
225
  ## Contributing
105
226
 
106
227
  1. Fork it
@@ -19,10 +19,8 @@ class GamesDice::ComplexDie
19
19
  def initialize(sides, options_hash = {})
20
20
  @basic_die = GamesDice::Die.new(sides, options_hash[:prng])
21
21
 
22
- @rerolls = options_hash[:rerolls]
23
- validate_rerolls
24
- @maps = options_hash[:maps]
25
- validate_maps
22
+ @rerolls = construct_rerolls( options_hash[:rerolls] )
23
+ @maps = construct_maps( options_hash[:maps] )
26
24
 
27
25
  @total = nil
28
26
  @result = nil
@@ -149,6 +147,32 @@ class GamesDice::ComplexDie
149
147
 
150
148
  private
151
149
 
150
+ def construct_rerolls rerolls_input
151
+ return nil unless rerolls_input
152
+ raise TypeError, "rerolls should be an Array, instead got #{rerolls_input.inspect}" unless rerolls_input.is_a?(Array)
153
+ rerolls_input.map do |reroll_item|
154
+ case reroll_item
155
+ when Array then GamesDice::RerollRule.new( reroll_item[0], reroll_item[1], reroll_item[2], reroll_item[3] )
156
+ when GamesDice::RerollRule then reroll_item
157
+ else
158
+ raise TypeError, "items in rerolls should be GamesDice::RerollRule or Array, instead got #{reroll_item.inspect}"
159
+ end
160
+ end
161
+ end
162
+
163
+ def construct_maps maps_input
164
+ return nil unless maps_input
165
+ raise TypeError, "maps should be an Array, instead got #{maps_input.inspect}" unless maps_input.is_a?(Array)
166
+ maps_input.map do |map_item|
167
+ case map_item
168
+ when Array then GamesDice::MapRule.new( map_item[0], map_item[1], map_item[2], map_item[3] )
169
+ when GamesDice::MapRule then map_item
170
+ else
171
+ raise TypeError, "items in maps should be GamesDice::MapRule or Array, instead got #{map_item.inspect}"
172
+ end
173
+ end
174
+ end
175
+
152
176
  def calc_maps x
153
177
  y, n = 0, ''
154
178
  @maps.find do |rule|
@@ -162,22 +186,6 @@ class GamesDice::ComplexDie
162
186
  [y, n]
163
187
  end
164
188
 
165
- def validate_rerolls
166
- return unless @rerolls
167
- raise TypeError, "rerolls should be an Array, instead got #{@rerolls.inspect}" unless @rerolls.is_a?(Array)
168
- @rerolls.each do |rule|
169
- raise TypeError, "items in rerolls should be GamesDice::RerollRule, instead got #{rule.inspect}" unless rule.is_a?(GamesDice::RerollRule)
170
- end
171
- end
172
-
173
- def validate_maps
174
- return unless @maps
175
- raise TypeError, "maps should be an Array, instead got #{@maps.inspect}" unless @maps.is_a?(Array)
176
- @maps.each do |rule|
177
- raise TypeError, "items in maps should be GamesDice::MapRule, instead got #{rule.inspect}" unless rule.is_a?(GamesDice::MapRule)
178
- end
179
- end
180
-
181
189
  def minmax_mappings possible_values
182
190
  possible_values.map { |x| m, n = calc_maps( x ); m }.minmax
183
191
  end
@@ -40,4 +40,30 @@ class GamesDice::Dice
40
40
  end
41
41
  end
42
42
 
43
+ def min
44
+ @min ||= @offset + @bunch_multipliers.zip(@bunches).inject(0) do |total,mb|
45
+ m,b = mb
46
+ total += m * b.min
47
+ end
48
+ end
49
+
50
+ def max
51
+ @max ||= @offset + @bunch_multipliers.zip(@bunches).inject(0) do |total,mb|
52
+ m,b = mb
53
+ total += m * b.max
54
+ end
55
+ end
56
+
57
+ def minmax
58
+ [min,max]
59
+ end
60
+
61
+ def probabilities
62
+ return @probabilities if @probabilities
63
+ probs = @bunch_multipliers.zip(@bunches).inject( GamesDice::Probabilities.new( { @offset => 1.0 } ) ) do |probs, mb|
64
+ m,b = mb
65
+ GamesDice::Probabilities.add_distributions_mult( 1, probs, m, b.probabilities )
66
+ end
67
+ end
68
+
43
69
  end # class Dice
@@ -4,27 +4,33 @@ require 'parslet'
4
4
  class GamesDice::Parser < Parslet::Parser
5
5
 
6
6
  # Descriptive language examples (capital letters stand in for integers)
7
- # NdX - a roll of N dice, with X sides each, values summed to total
8
- # NdX + C - a roll of N dice, with X sides each, values summed to total plus a constant C
9
- # NdX - C - a roll of N dice, with X sides each, values summed to total minus a constant C
10
- # NdX + MdY + C - any number of +C, -C, +NdX, -NdX etc sub-expressions can be combined
11
- # NdXkZ - a roll of N dice, sides X, keep best Z results and sum them
12
7
  # NdXk[Z,worst] - a roll of N dice, sides X, keep worst Z results and sum them
13
- # NdXrZ - a roll of N dice, sides X, re-roll and replace Zs
14
8
  # NdXr[Z,add] - a roll of N dice, sides X, re-roll and add on a result of Z
15
9
  # NdXr[Y..Z,add] - a roll of N dice, sides X, re-roll and add on a result of Y..Z
16
- # NdXx - exploding dice, synonym for NdXr[X,add]
17
- # NdXmZ - mapped dice, values below Z score 0, values above Z score 1
18
10
  # NdXm[>=Z,A] - mapped dice, values greater than or equal to Z score A (unmapped values score 0 by default)
19
11
 
20
12
  # These are the Parslet rules that define the dice grammar
21
13
  rule(:integer) { match('[0-9]').repeat(1) }
14
+ rule(:range) { integer.as(:range_start) >> str('..') >> integer.as(:range_end) }
22
15
  rule(:dlabel) { match('[d]') }
23
16
  rule(:bunch_start) { integer.as(:ndice) >> dlabel >> integer.as(:sides) }
17
+
18
+ rule(:reroll_label) { match(['r']).as(:reroll) }
19
+ rule(:keep_label) { match(['k']).as(:keep) }
20
+ rule(:map_label) { match(['m']).as(:map) }
21
+ rule(:alias_label) { match(['x']).as(:alias) }
22
+
23
+ rule(:single_modifier) { alias_label }
24
+ rule(:modifier_label) { reroll_label | keep_label | map_label }
25
+ rule(:simple_modifier) { modifier_label >> integer.as(:simple_value) }
26
+ rule(:complex_modifier) { modifier_label >> str('[') >> str(']') } # TODO: param extraction
27
+
28
+ rule(:bunch_modifier) { single_modifier | simple_modifier }
29
+ rule(:bunch) { bunch_start >> bunch_modifier.repeat.as(:mods) }
24
30
  rule(:space) { match('\s').repeat(1) }
25
31
  rule(:space?) { space.maybe }
26
32
  rule(:operator) { match('[+-]').as(:op) >> space? }
27
- rule(:add_bunch) { operator >> bunch_start >> space? }
33
+ rule(:add_bunch) { operator >> bunch >> space? }
28
34
  rule(:add_constant) { operator >> integer.as(:constant) >> space? }
29
35
  rule(:dice_expression) { add_bunch | add_constant }
30
36
  rule(:expressions) { dice_expression.repeat.as(:bunches) }
@@ -59,6 +65,25 @@ class GamesDice::Parser < Parslet::Parser
59
65
  when '-' then -1
60
66
  end
61
67
  end
68
+
69
+ # Modifiers
70
+ if in_hash[:mods]
71
+ in_hash[:mods].each do |mod|
72
+ case
73
+ when mod[:alias]
74
+ collect_alias_modifier mod, out_hash
75
+ when mod[:keep]
76
+ collect_keeper_rule mod, out_hash
77
+ when mod[:map]
78
+ out_hash[:maps] ||= []
79
+ collect_map_rule mod, out_hash
80
+ when mod[:reroll]
81
+ out_hash[:rerolls] ||= []
82
+ collect_reroll_rule mod, out_hash
83
+ end
84
+ end
85
+ end
86
+
62
87
  out_hash
63
88
  end
64
89
  end
@@ -76,4 +101,43 @@ class GamesDice::Parser < Parslet::Parser
76
101
  end
77
102
  end
78
103
 
104
+ # Called when we have a single letter convenient alias for common dice adjustments
105
+ def collect_alias_modifier alias_mod, out_hash
106
+ alias_name = alias_mod[:alias].to_s
107
+ case alias_name
108
+ when 'x' # Exploding re-roll
109
+ out_hash[:rerolls] ||= []
110
+ out_hash[:rerolls] << [ out_hash[:sides], :==, :reroll_add ]
111
+ end
112
+ end
113
+
114
+ # Called for any parsed reroll rule
115
+ def collect_reroll_rule reroll_mod, out_hash
116
+ out_hash[:rerolls] ||= []
117
+ if reroll_mod[:simple_value]
118
+ out_hash[:rerolls] << [ reroll_mod[:simple_value].to_i, :>=, :reroll_replace, 1 ]
119
+ end
120
+ # TODO: Handle complex descriptions
121
+ end
122
+
123
+ # Called for any parsed keeper mode
124
+ def collect_keeper_rule keeper_mod, out_hash
125
+ if keeper_mod[:simple_value]
126
+ out_hash[:keep_mode] = :keep_best
127
+ out_hash[:keep_number] = keeper_mod[:simple_value].to_i
128
+ return
129
+ end
130
+ # TODO: Handle complex descriptions
131
+ end
132
+
133
+ # Called for any parsed map mode
134
+ def collect_map_rule map_mod, out_hash
135
+ out_hash[:maps] ||= []
136
+ if map_mod[:simple_value]
137
+ out_hash[:maps] << [ map_mod[:simple_value].to_i, :<=, 1 ]
138
+ return
139
+ end
140
+ # TODO: Handle complex descriptions
141
+ end
142
+
79
143
  end # class Parser
@@ -94,4 +94,18 @@ class GamesDice::Probabilities
94
94
  GamesDice::Probabilities.new( h )
95
95
  end
96
96
 
97
+ # adding two probability distributions calculates a new distribution, representing what would
98
+ # happen if you created a random number using the sum of numbers from both distributions
99
+ def self.add_distributions_mult m_a, pd_a, m_b, pd_b
100
+ h = {}
101
+ pd_a.ph.each do |ka,pa|
102
+ pd_b.ph.each do |kb,pb|
103
+ kc = m_a * ka + m_b * kb
104
+ pc = pa * pb
105
+ h[kc] = h[kc] ? h[kc] + pc : pc
106
+ end
107
+ end
108
+ GamesDice::Probabilities.new( h )
109
+ end
110
+
97
111
  end # class Dice
@@ -31,7 +31,7 @@ class GamesDice::RerollRule
31
31
  # :reroll_add - add result of reroll to running total, and ignore :reroll_subtract for this die
32
32
  # :reroll_subtract - subtract result of reroll from running total, and reverse sense of any further :reroll_add results
33
33
  # :reroll_replace - use the new value in place of existing value for the die
34
- # :reroll_use_best - use the new value if it is higher than the erxisting value
34
+ # :reroll_use_best - use the new value if it is higher than the existing value
35
35
  # :reroll_use_worst - use the new value if it is higher than the existing value
36
36
  attr_reader :type
37
37
 
@@ -1,3 +1,3 @@
1
1
  module GamesDice
2
- VERSION = "0.0.6"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -45,6 +45,8 @@ describe GamesDice::ComplexDie do
45
45
  GamesDice::ComplexDie.new( 10, :rerolls => [] )
46
46
  GamesDice::ComplexDie.new( 10, :rerolls => [GamesDice::RerollRule.new(6, :<=, :reroll_add)] )
47
47
  GamesDice::ComplexDie.new( 10, :rerolls => [GamesDice::RerollRule.new(6, :<=, :reroll_add),GamesDice::RerollRule.new(1, :>=, :reroll_subtract)] )
48
+ GamesDice::ComplexDie.new( 10, :rerolls => [[6, :<=, :reroll_add]] )
49
+ GamesDice::ComplexDie.new( 10, :rerolls => [[6, :<=, :reroll_add],[1, :>=, :reroll_subtract]] )
48
50
 
49
51
  lambda do
50
52
  GamesDice::ComplexDie.new( 10, :rerolls => 7 )
@@ -57,23 +59,46 @@ describe GamesDice::ComplexDie do
57
59
  lambda do
58
60
  GamesDice::ComplexDie.new( 10, :rerolls => [GamesDice::RerollRule.new(6, :<=, :reroll_add), :reroll_add] )
59
61
  end.should raise_error( TypeError )
62
+
63
+ lambda do
64
+ GamesDice::ComplexDie.new( 10, :rerolls => [7] )
65
+ end.should raise_error( TypeError )
66
+
67
+ lambda do
68
+ GamesDice::ComplexDie.new( 10, :rerolls => [['hello']] )
69
+ end.should raise_error( TypeError )
70
+
71
+ lambda do
72
+ GamesDice::ComplexDie.new( 10, :rerolls => [ [6, :<=, :reroll_add ], :reroll_add] )
73
+ end.should raise_error( TypeError )
74
+
60
75
  end
61
76
 
62
77
  it "should optionally accept a maps param" do
63
78
  GamesDice::ComplexDie.new( 10, :maps => [] )
64
79
  GamesDice::ComplexDie.new( 10, :maps => [GamesDice::MapRule.new(7, :<=, 1)] )
65
80
  GamesDice::ComplexDie.new( 10, :maps => [GamesDice::MapRule.new(7, :<=, 1), GamesDice::MapRule.new(1, :>, -1) ] )
81
+ GamesDice::ComplexDie.new( 10, :maps => [ [7, :<=, 1] ] )
82
+ GamesDice::ComplexDie.new( 10, :maps => [ [7, :<=, 1], [1, :>, -1] ] )
66
83
 
67
84
  lambda do
68
85
  GamesDice::ComplexDie.new( 10, :maps => 7 )
69
86
  end.should raise_error( TypeError )
70
87
 
88
+ lambda do
89
+ GamesDice::ComplexDie.new( 10, :maps => [7] )
90
+ end.should raise_error( TypeError )
91
+
92
+ lambda do
93
+ GamesDice::ComplexDie.new( 10, :maps => [ [7] ] )
94
+ end.should raise_error( TypeError )
95
+
71
96
  lambda do
72
97
  GamesDice::ComplexDie.new( 10, :maps => ['hello'] )
73
98
  end.should raise_error( TypeError )
74
99
 
75
100
  lambda do
76
- GamesDice::ComplexDie.new( 10, :maps => [GamesDice::MapRule.new(7, :<=, 1),GamesDice::RerollRule.new(6, :<=, :reroll_add)] )
101
+ GamesDice::ComplexDie.new( 10, :maps => [GamesDice::MapRule.new(7, :<=, 1),GamesDice::RerollRule.new(6, :<=, :reroll_add)] )
77
102
  end.should raise_error( TypeError )
78
103
  end
79
104
 
@@ -83,7 +108,7 @@ describe GamesDice::ComplexDie do
83
108
  die.min.should == 1
84
109
  die.max.should == 40
85
110
 
86
- die = GamesDice::ComplexDie.new( 10, :rerolls => [GamesDice::RerollRule.new(1, :>=, :reroll_subtract)] )
111
+ die = GamesDice::ComplexDie.new( 10, :rerolls => [[1, :>=, :reroll_subtract]] )
87
112
  die.min.should == -9
88
113
  die.max.should == 10
89
114
 
@@ -93,7 +118,7 @@ describe GamesDice::ComplexDie do
93
118
  end
94
119
 
95
120
  it "should simulate a d10 that rerolls and adds on a result of 10" do
96
- die = GamesDice::ComplexDie.new(10, :rerolls => [GamesDice::RerollRule.new(10, :<=, :reroll_add)] )
121
+ die = GamesDice::ComplexDie.new(10, :rerolls => [[10, :<=, :reroll_add]] )
97
122
  [5,4,14,7,8,1,9].each do |expected|
98
123
  die.roll.should == expected
99
124
  die.result.should == expected
@@ -101,7 +126,7 @@ describe GamesDice::ComplexDie do
101
126
  end
102
127
 
103
128
  it "should explain how it got results outside range 1 to 10 on a d10" do
104
- die = GamesDice::ComplexDie.new(10, :rerolls => [GamesDice::RerollRule.new(10, :<=, :reroll_add),GamesDice::RerollRule.new(1, :>=, :reroll_subtract)] )
129
+ die = GamesDice::ComplexDie.new(10, :rerolls => [[10, :<=, :reroll_add],[1, :>=, :reroll_subtract]] )
105
130
  ["5","4","[10+4] 14","7","8","[1-9] -8"].each do |expected|
106
131
  die.roll
107
132
  die.explain_result.should == expected
@@ -109,7 +134,7 @@ describe GamesDice::ComplexDie do
109
134
  end
110
135
 
111
136
  it "should calculate an expected result" do
112
- die = GamesDice::ComplexDie.new(10, :rerolls => [GamesDice::RerollRule.new(10, :<=, :reroll_add),GamesDice::RerollRule.new(1, :>=, :reroll_subtract)] )
137
+ die = GamesDice::ComplexDie.new(10, :rerolls => [[10, :<=, :reroll_add],[1, :>=, :reroll_subtract]] )
113
138
  die.probabilities.expected.should be_within(1e-10).of 5.5
114
139
 
115
140
  die = GamesDice::ComplexDie.new(10, :rerolls => [GamesDice::RerollRule.new(1, :<=, :reroll_use_best, 1)] )
@@ -179,7 +204,7 @@ describe GamesDice::ComplexDie do
179
204
  end
180
205
 
181
206
  it "should simulate a d10 that scores 1 for success on a value of 7 or more" do
182
- die = GamesDice::ComplexDie.new( 10, :maps => [GamesDice::MapRule.new(7, :<=, 1, 'S')] )
207
+ die = GamesDice::ComplexDie.new( 10, :maps => [ [ 7, :<=, 1, 'S' ] ] )
183
208
  [0,0,1,0,1,1,0,1].each do |expected|
184
209
  die.roll.should == expected
185
210
  die.result.should == expected
@@ -187,7 +212,7 @@ describe GamesDice::ComplexDie do
187
212
  end
188
213
 
189
214
  it "should label the mappings applied with the provided names" do
190
- die = GamesDice::ComplexDie.new( 10, :maps => [GamesDice::MapRule.new(7, :<=, 1, 'S'),GamesDice::MapRule.new(1, :>=, -1, 'F')] )
215
+ die = GamesDice::ComplexDie.new( 10, :maps => [ [7, :<=, 1, 'S'], [1, :>=, -1, 'F'] ] )
191
216
  ["5", "4", "10 S", "4", "7 S", "8 S", "1 F", "9 S"].each do |expected|
192
217
  die.roll
193
218
  die.explain_result.should == expected
@@ -229,10 +254,10 @@ describe GamesDice::ComplexDie do
229
254
  end
230
255
  end
231
256
 
232
- describe "with rerolls and maps" do
257
+ describe "with rerolls and maps together" do
233
258
  before do
234
259
  @die = GamesDice::ComplexDie.new( 6,
235
- :rerolls => [GamesDice::RerollRule.new(6, :<=, :reroll_add)],
260
+ :rerolls => [[6, :<=, :reroll_add]],
236
261
  :maps => [GamesDice::MapRule.new(9, :<=, 1, 'Success')]
237
262
  )
238
263
  end
data/spec/parser_spec.rb CHANGED
@@ -5,7 +5,7 @@ describe GamesDice::Parser do
5
5
  describe "#parse" do
6
6
  let(:parser) { GamesDice::Parser.new }
7
7
 
8
- it "should convert descriptive strings to data usable in the GamesDice::Dice constructor" do
8
+ it "should parse simple dice sums" do
9
9
  variations = {
10
10
  '1d6' => { :bunches => [{:ndice=>1, :sides=>6, :multiplier=>1}], :offset => 0 },
11
11
  '2d8-1d4' => { :bunches => [{:ndice=>2, :sides=>8, :multiplier=>1},{:ndice=>1, :sides=>4, :multiplier=>-1}], :offset => 0 },
@@ -22,5 +22,61 @@ describe GamesDice::Parser do
22
22
  end
23
23
  end
24
24
 
25
+ it "should parse 'NdXrY' as 'roll N times X-sided dice, re-roll and replace a Y or less (once)'" do
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 },
30
+ }
31
+
32
+ variations.each do |input,expected_output|
33
+ parser.parse( input ).should == expected_output
34
+ end
35
+ end
36
+
37
+ it "should parse 'NdXmZ' as 'roll N times X-sided dice, a value of Z or more equals 1 (success)'" do
38
+ variations = {
39
+ '5d6m6' => { :bunches => [{:ndice=>5, :sides=>6, :multiplier=>1, :maps=>[ [6,:<=,1] ]}], :offset => 0 },
40
+ '2d10m7' => { :bunches => [{:ndice=>2, :sides=>10, :multiplier=>1, :maps=>[ [7,:<=,1] ]}], :offset => 0 },
41
+ }
42
+
43
+ variations.each do |input,expected_output|
44
+ parser.parse( input ).should == expected_output
45
+ end
46
+ end
47
+
48
+ it "should parse 'NdXkC' as 'roll N times X-sided dice, add together the best C'" do
49
+ variations = {
50
+ '5d10k3' => { :bunches => [{:ndice=>5, :sides=>10, :multiplier=>1, :keep_mode=>:keep_best, :keep_number=>3}], :offset => 0 },
51
+ }
52
+
53
+ variations.each do |input,expected_output|
54
+ parser.parse( input ).should == expected_output
55
+ end
56
+ end
57
+
58
+ it "should parse 'NdXx' as 'roll N times X-sided *exploding* dice'" do
59
+ variations = {
60
+ '5d10x' => { :bunches => [{:ndice=>5, :sides=>10, :multiplier=>1, :rerolls=>[ [10,:==,:reroll_add] ]}], :offset => 0 },
61
+ '3d6x' => { :bunches => [{:ndice=>3, :sides=>6, :multiplier=>1, :rerolls=>[ [6,:==,:reroll_add] ]}], :offset => 0 },
62
+ }
63
+
64
+ variations.each do |input,expected_output|
65
+ parser.parse( input ).should == expected_output
66
+ end
67
+ end
68
+
69
+ it "should successfully parse combinations of modifiers in any valid order" do
70
+ variations = {
71
+ '5d10r1x' => { :bunches => [{:ndice=>5, :sides=>10, :multiplier=>1, :rerolls=>[ [1,:>=,:reroll_replace,1], [10,:==,:reroll_add] ]}], :offset => 0 },
72
+ '3d6xk2' => { :bunches => [{:ndice=>3, :sides=>6, :multiplier=>1, :rerolls=>[ [6,:==,:reroll_add] ], :keep_mode=>:keep_best, :keep_number=>2 }], :offset => 0 },
73
+ '4d6m8x' => { :bunches => [{:ndice=>4, :sides=>6, :multiplier=>1, :maps=>[ [8,:<=,1] ], :rerolls=>[ [6,:==,:reroll_add] ] }], :offset => 0 },
74
+ }
75
+
76
+ variations.each do |input,expected_output|
77
+ parser.parse( input ).should == expected_output
78
+ end
79
+ end
80
+
25
81
  end # describe "#parse"
26
82
  end
@@ -35,7 +35,7 @@ RSpec::Matchers.define :be_valid_distribution do
35
35
  end
36
36
 
37
37
  description do |given|
38
- "a hash describing a complete discrete probability distribution of integers"
38
+ "a hash describing a complete probability distribution of integer results"
39
39
  end
40
40
  end
41
41
 
@@ -0,0 +1,96 @@
1
+ require 'games_dice'
2
+ # This spec demonstrates that documentation from the README.md works as intended
3
+
4
+ # Test helper class, a stub of a PRNG
5
+ class TestPRNG
6
+ def initialize
7
+ @numbers = [0.123,0.234,0.345,0.999,0.876,0.765,0.543,0.111,0.333,0.777]
8
+ end
9
+ def rand(n)
10
+ Integer( n * @numbers.pop )
11
+ end
12
+ end
13
+
14
+ describe GamesDice do
15
+
16
+ describe '#create' do
17
+ it "converts a string such as '3d6+6' into a GamesDice::Dice object" do
18
+ d = GamesDice.create '3d6+6'
19
+ d.is_a?( GamesDice::Dice ).should be_true
20
+ end
21
+
22
+ it "takes a parameter 'dice_description', which is a string such as '3d6' or '2d4-1'" do
23
+ d = GamesDice.create '3d6'
24
+ d.is_a?( GamesDice::Dice ).should be_true
25
+ d = GamesDice.create '2d4-1'
26
+ d.is_a?( GamesDice::Dice ).should be_true
27
+ end
28
+
29
+ it "takes an optional parameter 'prng', which if provided it should be an object that has a method 'rand( integer )'" do
30
+ prng = TestPRNG.new
31
+ d = GamesDice.create '3d6', prng
32
+ d.is_a?( GamesDice::Dice ).should be_true
33
+ end
34
+ end # describe '#create'
35
+
36
+ end # describe GamesDice
37
+
38
+ describe GamesDice::Dice do
39
+
40
+ before :each do
41
+ srand(67809)
42
+ end
43
+
44
+ let(:dice) { GamesDice.create '3d6'}
45
+
46
+ describe "#roll" do
47
+ it "simulates rolling the dice as they were described in the constructor" do
48
+ expected_results = [11,15,11,12,12,9]
49
+ expected_results.each do |expected|
50
+ dice.roll.should == expected
51
+ end
52
+ end
53
+ end
54
+
55
+ describe "#result" do
56
+ it "returns the value from the last call to roll" do
57
+ expected_results = [11,15,11,12,12,9]
58
+ expected_results.each do |expected|
59
+ dice.roll
60
+ dice.result.should == expected
61
+ end
62
+ end
63
+
64
+ it "will be nil if no roll has been made yet" do
65
+ dice.result.should be_nil
66
+ end
67
+ end
68
+
69
+ describe "#max" do
70
+ it "returns the maximum possible value from a roll of the dice" do
71
+ dice.max.should == 18
72
+ end
73
+ end
74
+
75
+ describe "#min" do
76
+ it "returns the minimum possible value from a roll of the dice" do
77
+ dice.min.should == 3
78
+ end
79
+ end
80
+
81
+ describe "#minmax" do
82
+ it "returns an array [ dice.min, dice.max ]" do
83
+ dice.minmax.should == [3,18]
84
+ end
85
+ end
86
+
87
+ describe "#probabilities" do
88
+ it "calculates probability distribution for the dice" do
89
+ pd = dice.probabilities
90
+ pd.is_a?( GamesDice::Probabilities ).should be_true
91
+ pd.p_eql( 3).should be_within(1e-10).of 1.0/216
92
+ pd.p_eql( 11 ).should be_within(1e-10).of 27.0/216
93
+ end
94
+ end
95
+
96
+ end # describe GamesDice::Dice
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.0.6
4
+ version: 0.1.1
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-13 00:00:00.000000000 Z
12
+ date: 2013-05-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -94,6 +94,7 @@ files:
94
94
  - spec/map_rule_spec.rb
95
95
  - spec/parser_spec.rb
96
96
  - spec/probability_spec.rb
97
+ - spec/readme_spec.rb
97
98
  - spec/reroll_rule_spec.rb
98
99
  - travis.yml
99
100
  homepage: https://github.com/neilslater/games_dice
@@ -110,7 +111,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
110
111
  version: '0'
111
112
  segments:
112
113
  - 0
113
- hash: 384639309274746311
114
+ hash: 514639472622102988
114
115
  required_rubygems_version: !ruby/object:Gem::Requirement
115
116
  none: false
116
117
  requirements:
@@ -119,7 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
120
  version: '0'
120
121
  segments:
121
122
  - 0
122
- hash: 384639309274746311
123
+ hash: 514639472622102988
123
124
  requirements: []
124
125
  rubyforge_project:
125
126
  rubygems_version: 1.8.24
@@ -135,4 +136,5 @@ test_files:
135
136
  - spec/map_rule_spec.rb
136
137
  - spec/parser_spec.rb
137
138
  - spec/probability_spec.rb
139
+ - spec/readme_spec.rb
138
140
  - spec/reroll_rule_spec.rb