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