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 +125 -4
- data/lib/games_dice/complex_die.rb +28 -20
- data/lib/games_dice/dice.rb +26 -0
- data/lib/games_dice/parser.rb +73 -9
- data/lib/games_dice/probabilities.rb +14 -0
- data/lib/games_dice/reroll_rule.rb +1 -1
- data/lib/games_dice/version.rb +1 -1
- data/spec/complex_die_spec.rb +34 -9
- data/spec/parser_spec.rb +57 -1
- data/spec/probability_spec.rb +1 -1
- data/spec/readme_spec.rb +96 -0
- metadata +6 -4
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
|
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
|
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
|
-
|
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
|
-
|
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
|
data/lib/games_dice/dice.rb
CHANGED
@@ -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
|
data/lib/games_dice/parser.rb
CHANGED
@@ -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 >>
|
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
|
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
|
|
data/lib/games_dice/version.rb
CHANGED
data/spec/complex_die_spec.rb
CHANGED
@@ -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 =>
|
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 => [
|
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 => [
|
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 => [
|
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 => [
|
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 => [
|
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 => [
|
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 => [
|
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
|
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
|
data/spec/probability_spec.rb
CHANGED
data/spec/readme_spec.rb
ADDED
@@ -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.
|
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-
|
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:
|
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:
|
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
|