games_dice 0.0.6 → 0.1.1

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
@@ -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