games_dice 0.2.2 → 0.2.3
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 +6 -0
- data/lib/games_dice/bunch.rb +89 -52
- data/lib/games_dice/dice.rb +60 -16
- data/lib/games_dice/die_result.rb +63 -24
- data/lib/games_dice/map_rule.rb +2 -2
- data/lib/games_dice/parser.rb +10 -1
- data/lib/games_dice/probabilities.rb +72 -23
- data/lib/games_dice/version.rb +1 -1
- data/spec/bunch_spec.rb +1 -0
- data/spec/complex_die_spec.rb +1 -0
- data/spec/die_spec.rb +1 -0
- data/spec/probability_spec.rb +1 -0
- metadata +4 -4
data/README.md
CHANGED
@@ -197,6 +197,12 @@ Returns the probability of a result less than the integer n.
|
|
197
197
|
probabilities.p_lt( 17 ) # => 0.9953703703703
|
198
198
|
probabilities.p_lt( 3 ) # => 0.0
|
199
199
|
|
200
|
+
#### probabilities.expected
|
201
|
+
|
202
|
+
Returns the mean result, weighted by probabality of each value.
|
203
|
+
|
204
|
+
probabilities.expected # => 10.5 (rounded to nearest 1e-9)
|
205
|
+
|
200
206
|
## String Dice Descriptions
|
201
207
|
|
202
208
|
The dice descriptions are a mini-language. A simple six-sided die is described like this:
|
data/lib/games_dice/bunch.rb
CHANGED
@@ -1,126 +1,159 @@
|
|
1
|
-
# models a
|
2
|
-
#
|
1
|
+
# This class models a number of identical dice, which may be either GamesDice::Die or
|
2
|
+
# GamesDice::ComplexDie objects.
|
3
|
+
#
|
4
|
+
# An object of this class represents a fixed number of indentical dice that may be rolled and their
|
5
|
+
# values summed to make a total for the bunch.
|
6
|
+
#
|
7
|
+
# @example The ubiquitous '3d6'
|
8
|
+
# d = GamesDice::Bunch.new( :ndice => 3, :sides => 6 )
|
9
|
+
# d.roll # => 14
|
10
|
+
# d.result # => 14
|
11
|
+
# d.explain_result # => "2 + 6 + 6 = 14"
|
12
|
+
# d.max # => 18
|
13
|
+
#
|
14
|
+
# @example Roll 5d10, and keep the best 2
|
15
|
+
# d = GamesDice::Bunch.new( :ndice => 5, :sides => 10 , :keep_mode => :keep_best, :keep_number => 2 )
|
16
|
+
# d.roll # => 18
|
17
|
+
# d.result # => 18
|
18
|
+
# d.explain_result # => "4, 9, 2, 9, 1. Keep: 9 + 9 = 18"
|
19
|
+
#
|
20
|
+
|
3
21
|
class GamesDice::Bunch
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
@
|
19
|
-
@ndice = Integer(attributes[:ndice])
|
22
|
+
# The constructor accepts parameters that are suitable for either GamesDice::Die or GamesDice::ComplexDie
|
23
|
+
# and decides which of those classes to instantiate.
|
24
|
+
# @param [Hash] options
|
25
|
+
# @option options [Integer] :ndice Number of dice in the bunch, *mandatory*
|
26
|
+
# @option options [Integer] :sides Number of sides on a single die in the bunch, *mandatory*
|
27
|
+
# @option options [String] :name Optional name for the bunch
|
28
|
+
# @option options [Array<GamesDice::RerollRule,Array>] :rerolls Optional rules that cause the die to roll again
|
29
|
+
# @option options [Array<GamesDice::MapRule,Array>] :maps Optional rules to convert a value into a final result for the die
|
30
|
+
# @option options [#rand] :prng Optional alternative source of randomness to Ruby's built-in #rand, passed to GamesDice::Die's constructor
|
31
|
+
# @option options [Symbol] :keep_mode Optional, either *:keep_best* or *:keep_worst*
|
32
|
+
# @option options [Integer] :keep_number Optional number of dice to keep when :keep_mode is not nil
|
33
|
+
# @return [GamesDice::Bunch]
|
34
|
+
def initialize( options )
|
35
|
+
@name = options[:name].to_s
|
36
|
+
@ndice = Integer(options[:ndice])
|
20
37
|
raise ArgumentError, ":ndice must be 1 or more, but got #{@ndice}" unless @ndice > 0
|
21
|
-
@sides = Integer(
|
38
|
+
@sides = Integer(options[:sides])
|
22
39
|
raise ArgumentError, ":sides must be 1 or more, but got #{@sides}" unless @sides > 0
|
23
40
|
|
24
|
-
|
41
|
+
attr = Hash.new
|
25
42
|
|
26
|
-
if
|
43
|
+
if options[:prng]
|
27
44
|
# We deliberately do not clone this object, it will often be intended that it is shared
|
28
|
-
prng =
|
45
|
+
prng = options[:prng]
|
29
46
|
raise ":prng does not support the rand() method" if ! prng.respond_to?(:rand)
|
30
47
|
end
|
31
48
|
|
32
49
|
needs_complex_die = false
|
33
50
|
|
34
|
-
if
|
51
|
+
if options[:rerolls]
|
35
52
|
needs_complex_die = true
|
36
|
-
|
53
|
+
attr[:rerolls] = options[:rerolls].clone
|
37
54
|
end
|
38
55
|
|
39
|
-
if
|
56
|
+
if options[:maps]
|
40
57
|
needs_complex_die = true
|
41
|
-
|
58
|
+
attr[:maps] = options[:maps].clone
|
42
59
|
end
|
43
60
|
|
44
61
|
if needs_complex_die
|
45
|
-
|
46
|
-
@single_die = GamesDice::ComplexDie.new( @sides,
|
62
|
+
attr[:prng] = prng
|
63
|
+
@single_die = GamesDice::ComplexDie.new( @sides, attr )
|
47
64
|
else
|
48
65
|
@single_die = GamesDice::Die.new( @sides, prng )
|
49
66
|
end
|
50
67
|
|
51
|
-
case
|
68
|
+
case options[:keep_mode]
|
52
69
|
when nil then
|
53
70
|
@keep_mode = nil
|
54
71
|
when :keep_best then
|
55
72
|
@keep_mode = :keep_best
|
56
|
-
@keep_number = Integer(
|
73
|
+
@keep_number = Integer(options[:keep_number] || 1)
|
57
74
|
when :keep_worst then
|
58
75
|
@keep_mode = :keep_worst
|
59
|
-
@keep_number = Integer(
|
76
|
+
@keep_number = Integer(options[:keep_number] || 1)
|
60
77
|
else
|
61
|
-
raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{
|
78
|
+
raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{options[:keep_mode].inspect}"
|
62
79
|
end
|
63
80
|
end
|
64
81
|
|
65
|
-
#
|
82
|
+
# Name to help identify bunch
|
83
|
+
# @return [String]
|
66
84
|
attr_reader :name
|
67
85
|
|
68
|
-
#
|
86
|
+
# Number of dice to roll
|
87
|
+
# @return [Integer]
|
69
88
|
attr_reader :ndice
|
70
89
|
|
71
|
-
#
|
90
|
+
# Individual die from the bunch
|
91
|
+
# @return [GamesDice::Die,GamesDice::ComplexDie]
|
72
92
|
attr_reader :single_die
|
73
93
|
|
74
|
-
#
|
94
|
+
# Can be nil, :keep_best or :keep_worst
|
95
|
+
# @return [Symbol,nil]
|
75
96
|
attr_reader :keep_mode
|
76
97
|
|
77
|
-
#
|
78
|
-
#
|
98
|
+
# Number of "best" or "worst" results to select when #keep_mode is not nil.
|
99
|
+
# @return [Integer,nil]
|
79
100
|
attr_reader :keep_number
|
80
101
|
|
81
|
-
#
|
102
|
+
# Result of most-recent roll, or nil if no roll made yet.
|
103
|
+
# @return [Integer,nil]
|
82
104
|
attr_reader :result
|
83
105
|
|
84
|
-
#
|
106
|
+
# @!attribute [r] label
|
107
|
+
# Description that will be used in explanations with more than one bunch
|
108
|
+
# @return [String]
|
85
109
|
def label
|
86
110
|
return @name if @name != ''
|
87
111
|
return @ndice.to_s + 'd' + @sides.to_s
|
88
112
|
end
|
89
113
|
|
90
|
-
#
|
91
|
-
#
|
92
|
-
#
|
114
|
+
# @!attribute [r] rerolls
|
115
|
+
# Sequence of re-roll rules, or nil if re-rolls are not required.
|
116
|
+
# @return [Array<GamesDice::RerollRule>, nil]
|
93
117
|
def rerolls
|
94
118
|
@single_die.rerolls
|
95
119
|
end
|
96
120
|
|
97
|
-
#
|
121
|
+
# @!attribute [r] maps
|
122
|
+
# Sequence of map rules, or nil if mapping is not required.
|
123
|
+
# @return [Array<GamesDice::MapRule>, nil]
|
98
124
|
def maps
|
99
|
-
@single_die.
|
125
|
+
@single_die.maps
|
100
126
|
end
|
101
127
|
|
102
|
-
#
|
128
|
+
# @!attribute [r] result_details
|
129
|
+
# After calling #roll, this is an array of GamesDice::DieResult objects. There is one from each #single_die rolled,
|
103
130
|
# allowing inspection of how the result was obtained.
|
131
|
+
# @return [Array<GamesDice::DieResult>, nil] Sequence of GamesDice::DieResult objects.
|
104
132
|
def result_details
|
105
133
|
return nil unless @raw_result_details
|
106
134
|
@raw_result_details.map { |r| r.is_a?(Fixnum) ? GamesDice::DieResult.new(r) : r }
|
107
135
|
end
|
108
136
|
|
109
|
-
#
|
137
|
+
# @!attribute [r] min
|
138
|
+
# Minimum possible result from a call to #roll
|
139
|
+
# @return [Integer]
|
110
140
|
def min
|
111
141
|
n = @keep_mode ? [@keep_number,@ndice].min : @ndice
|
112
142
|
return n * @single_die.min
|
113
143
|
end
|
114
144
|
|
115
|
-
#
|
145
|
+
# @!attribute [r] max
|
146
|
+
# Maximum possible result from a call to #roll
|
147
|
+
# @return [Integer]
|
116
148
|
def max
|
117
149
|
n = @keep_mode ? [@keep_number,@ndice].min : @ndice
|
118
150
|
return n * @single_die.max
|
119
151
|
end
|
120
152
|
|
121
|
-
#
|
122
|
-
#
|
123
|
-
#
|
153
|
+
# Calculates the probability distribution for the bunch. When the bunch is composed of dice with
|
154
|
+
# open-ended re-roll rules, there are some arbitrary limits imposed to prevent large amounts of
|
155
|
+
# recursion.
|
156
|
+
# @return [GamesDice::Probabilities] Probability distribution of bunch.
|
124
157
|
def probabilities
|
125
158
|
return @probabilities if @probabilities
|
126
159
|
@probabilities_complete = true
|
@@ -158,7 +191,8 @@ class GamesDice::Bunch
|
|
158
191
|
@probabilities = GamesDice::Probabilities.new( combined_probs )
|
159
192
|
end
|
160
193
|
|
161
|
-
#
|
194
|
+
# Simulates rolling the bunch of identical dice
|
195
|
+
# @return [Integer] Sum of all rolled dice, or sum of all keepers
|
162
196
|
def roll
|
163
197
|
@result = 0
|
164
198
|
@raw_result_details = []
|
@@ -184,6 +218,9 @@ class GamesDice::Bunch
|
|
184
218
|
@result = use_dice.inject(0) { |so_far, die_result| so_far + die_result }
|
185
219
|
end
|
186
220
|
|
221
|
+
# @!attribute [r] explain_result
|
222
|
+
# Explanation of result, or nil if no call to #roll yet.
|
223
|
+
# @return [String,nil]
|
187
224
|
def explain_result
|
188
225
|
return nil unless @result
|
189
226
|
|
data/lib/games_dice/dice.rb
CHANGED
@@ -1,12 +1,36 @@
|
|
1
|
-
# models
|
2
|
-
#
|
1
|
+
# This class models a combination of GamesDice::Bunch objects plus a fixed offset.
|
2
|
+
#
|
3
|
+
# An object of this class is a dice "recipe" that specifies the numbers and types of
|
4
|
+
# dice that can be rolled to generate an integer value.
|
5
|
+
#
|
6
|
+
# @example '3d6+6' hitpoints, whatever that means in the game you are playing
|
7
|
+
# d = GamesDice::Dice.new( [{:ndice => 3, :sides => 6}], 6, 'Hit points' )
|
8
|
+
# d.roll # => 20
|
9
|
+
# d.result # => 20
|
10
|
+
# d.explain_result # => "3d6: 3 + 5 + 6 = 14. 14 + 6 = 20"
|
11
|
+
# d.probabilities.expected # => 16.5
|
12
|
+
#
|
13
|
+
# @example Roll d20 twice, take best result, and add 5.
|
14
|
+
# d = GamesDice::Dice.new( [{:ndice => 2, :sides => 20 , :keep_mode => :keep_best, :keep_number => 1}], 5 )
|
15
|
+
# d.roll # => 21
|
16
|
+
# d.result # => 21
|
17
|
+
# d.explain_result # => "2d20: 4, 16. Keep: 16. 16 + 5 = 21"
|
18
|
+
#
|
3
19
|
class GamesDice::Dice
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
20
|
+
# The first parameter is an array of values that are passed to GamesDice::Bunch constructors.
|
21
|
+
# @param [Array<Hash>] bunches Array of options for creating bunches
|
22
|
+
# @param [Integer] offset Total offset
|
23
|
+
# @param [String] name Optional label for the dice
|
24
|
+
# @option bunches [Integer] :ndice Number of dice in the bunch, *mandatory*
|
25
|
+
# @option bunches [Integer] :sides Number of sides on a single die in the bunch, *mandatory*
|
26
|
+
# @option bunches [String] :name Optional name for the bunch
|
27
|
+
# @option bunches [Array<GamesDice::RerollRule,Array>] :rerolls Optional rules that cause the die to roll again
|
28
|
+
# @option bunches [Array<GamesDice::MapRule,Array>] :maps Optional rules to convert a value into a final result for the die
|
29
|
+
# @option bunches [#rand] :prng Optional alternative source of randomness to Ruby's built-in #rand, passed to GamesDice::Die's constructor
|
30
|
+
# @option bunches [Symbol] :keep_mode Optional, either *:keep_best* or *:keep_worst*
|
31
|
+
# @option bunches [Integer] :keep_number Optional number of dice to keep when :keep_mode is not nil
|
32
|
+
# @option bunches [Integer] :multiplier Optional, defaults to 1, and typically 1 or -1 to describe whether the Bunch total is to be added or subtracted
|
33
|
+
# @return [GamesDice::Dice]
|
10
34
|
def initialize( bunches, offset = 0, name = '' )
|
11
35
|
@name = name
|
12
36
|
@offset = offset
|
@@ -15,24 +39,29 @@ class GamesDice::Dice
|
|
15
39
|
@result = nil
|
16
40
|
end
|
17
41
|
|
18
|
-
#
|
42
|
+
# Name to help identify dice
|
43
|
+
# @return [String]
|
19
44
|
attr_reader :name
|
20
45
|
|
21
|
-
#
|
22
|
-
#
|
46
|
+
# Bunches of dice that are components of the object
|
47
|
+
# @return [Array<GamesDice::Bunch>]
|
23
48
|
attr_reader :bunches
|
24
49
|
|
25
|
-
#
|
50
|
+
# Multipliers for each bunch of identical dice. Typically 1 or -1 to represent groups of dice that
|
51
|
+
# are either added or subtracted from the total.
|
52
|
+
# @return [Array<Integer>]
|
26
53
|
attr_reader :bunch_multipliers
|
27
54
|
|
28
|
-
#
|
55
|
+
# Fixed offset added to sum of all bunches.
|
56
|
+
# @return [Integer]
|
29
57
|
attr_reader :offset
|
30
58
|
|
31
|
-
#
|
32
|
-
#
|
59
|
+
# Result of most-recent roll, or nil if no roll made yet.
|
60
|
+
# @return [Integer,nil]
|
33
61
|
attr_reader :result
|
34
62
|
|
35
|
-
#
|
63
|
+
# Simulates rolling dice
|
64
|
+
# @return [Integer] Sum of all rolled dice
|
36
65
|
def roll
|
37
66
|
@result = @offset + @bunch_multipliers.zip(@bunches).inject(0) do |total,mb|
|
38
67
|
m,b = mb
|
@@ -40,6 +69,9 @@ class GamesDice::Dice
|
|
40
69
|
end
|
41
70
|
end
|
42
71
|
|
72
|
+
# @!attribute [r] min
|
73
|
+
# Minimum possible result from a call to #roll
|
74
|
+
# @return [Integer]
|
43
75
|
def min
|
44
76
|
@min ||= @offset + @bunch_multipliers.zip(@bunches).inject(0) do |total,mb|
|
45
77
|
m,b = mb
|
@@ -47,6 +79,9 @@ class GamesDice::Dice
|
|
47
79
|
end
|
48
80
|
end
|
49
81
|
|
82
|
+
# @!attribute [r] max
|
83
|
+
# Maximum possible result from a call to #roll
|
84
|
+
# @return [Integer]
|
50
85
|
def max
|
51
86
|
@max ||= @offset + @bunch_multipliers.zip(@bunches).inject(0) do |total,mb|
|
52
87
|
m,b = mb
|
@@ -54,10 +89,17 @@ class GamesDice::Dice
|
|
54
89
|
end
|
55
90
|
end
|
56
91
|
|
92
|
+
# @!attribute [r] minmax
|
93
|
+
# Convenience method, same as [dice.min, dice.max]
|
94
|
+
# @return [Array<Integer>]
|
57
95
|
def minmax
|
58
96
|
[min,max]
|
59
97
|
end
|
60
98
|
|
99
|
+
# Calculates the probability distribution for the dice. When the dice include components with
|
100
|
+
# open-ended re-roll rules, there are some arbitrary limits imposed to prevent large amounts of
|
101
|
+
# recursion.
|
102
|
+
# @return [GamesDice::Probabilities] Probability distribution of dice.
|
61
103
|
def probabilities
|
62
104
|
return @probabilities if @probabilities
|
63
105
|
probs = @bunch_multipliers.zip(@bunches).inject( GamesDice::Probabilities.new( { @offset => 1.0 } ) ) do |probs, mb|
|
@@ -66,6 +108,8 @@ class GamesDice::Dice
|
|
66
108
|
end
|
67
109
|
end
|
68
110
|
|
111
|
+
# @!attribute [r] explain_result
|
112
|
+
# @return [String,nil] Explanation of result, or nil if no call to #roll yet.
|
69
113
|
def explain_result
|
70
114
|
return nil unless @result
|
71
115
|
explanations = @bunches.map { |bunch| bunch.label + ": " + bunch.explain_result }
|
@@ -1,16 +1,37 @@
|
|
1
|
-
#
|
1
|
+
# This class models the output of GamesDice::ComplexDie.
|
2
|
+
#
|
3
|
+
# An object of the class represents the results of a roll of a ComplexDie, including any re-rolls and
|
4
|
+
# value mapping.
|
5
|
+
#
|
6
|
+
# @example Building up a result manually
|
2
7
|
# dr = GamesDice::DieResult.new
|
3
|
-
# dr.add_roll
|
4
|
-
# dr.add_roll
|
5
|
-
# dr.value # =>
|
6
|
-
# dr.rolls # => [5,4]
|
7
|
-
# dr.roll_reasons # => [:basic
|
8
|
-
# dr
|
9
|
-
#
|
8
|
+
# dr.add_roll 5
|
9
|
+
# dr.add_roll 4, :reroll_replace
|
10
|
+
# dr.value # => 4
|
11
|
+
# dr.rolls # => [5, 4]
|
12
|
+
# dr.roll_reasons # => [:basic, :reroll_replace]
|
13
|
+
# # dr can behave as dr.value due to coercion and support for some operators
|
14
|
+
# dr + 6 # => 10
|
15
|
+
#
|
16
|
+
# @example Using a result from GamesDice::ComplexDie
|
17
|
+
# # An "exploding" six-sided die that needs a result of 8 to score "1 Success"
|
18
|
+
# d = GamesDice::ComplexDie.new( 6, :rerolls => [[6, :<=, :reroll_add]], :maps => [[8, :<=, 1, 'Success']] )
|
19
|
+
# # Generate result object by rolling the die
|
20
|
+
# dr = d.roll
|
21
|
+
# dr.rolls # => [6, 3]
|
22
|
+
# dr.roll_reasons # => [:basic, :reroll_add]
|
23
|
+
# dr.total # => 9
|
24
|
+
# dr.value # => 1
|
25
|
+
# dr.explain_value # => "[6+3] 9 Success"
|
26
|
+
#
|
27
|
+
|
10
28
|
class GamesDice::DieResult
|
11
29
|
include Comparable
|
12
30
|
|
13
|
-
#
|
31
|
+
# Creates new instance of GamesDice::DieResult. The object can be initialised "empty" or with a first result.
|
32
|
+
# @param [Integer,nil] first_roll_result Value for first roll of the die.
|
33
|
+
# @param [Symbol] first_roll_reason Reason for first roll of the die.
|
34
|
+
# @return [GamesDice::DieResult]
|
14
35
|
def initialize( first_roll_result=nil, first_roll_reason=:basic )
|
15
36
|
unless GamesDice::REROLL_TYPES.has_key?(first_roll_reason)
|
16
37
|
raise ArgumentError, "Unrecognised reason for roll #{first_roll_reason}"
|
@@ -29,27 +50,31 @@ class GamesDice::DieResult
|
|
29
50
|
@value = @total
|
30
51
|
end
|
31
52
|
|
32
|
-
|
33
|
-
|
34
|
-
# array of integers
|
53
|
+
# The individual die rolls that combined to generate this result.
|
54
|
+
# @return [Array<Integer>] Un-processed values of each die roll used for this result.
|
35
55
|
attr_reader :rolls
|
36
56
|
|
37
|
-
#
|
57
|
+
# The individual reasons for each roll of the die. See GamesDice::RerollRule for allowed values.
|
58
|
+
# @return [Array<Symbol>] Reasons for each die roll, indexes match the #rolls Array.
|
38
59
|
attr_reader :roll_reasons
|
39
60
|
|
40
|
-
#
|
41
|
-
#
|
61
|
+
# Combined result of all rolls, *before* mapping.
|
62
|
+
# @return [Integer,nil]
|
42
63
|
attr_reader :total
|
43
64
|
|
44
|
-
#
|
65
|
+
# Combined result of all rolls, *after* mapping.
|
66
|
+
# @return [Integer,nil]
|
45
67
|
attr_reader :value
|
46
68
|
|
47
|
-
#
|
69
|
+
# Whether or not #value has been mapped from #total.
|
70
|
+
# @return [Boolean]
|
48
71
|
attr_reader :mapped
|
49
72
|
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
73
|
+
# Adds value from a new roll to the object. GamesDice::DieResult tracks reasons for the roll
|
74
|
+
# and makes the correct adjustment to the total so far. Any mapped value is cleared.
|
75
|
+
# @param [Integer] roll_result Value result from rolling the die.
|
76
|
+
# @param [Symbol] roll_reason Reason for rolling the die.
|
77
|
+
# @return [Integer] Total so far
|
53
78
|
def add_roll( roll_result, roll_reason=:basic )
|
54
79
|
unless GamesDice::REROLL_TYPES.has_key?(roll_reason)
|
55
80
|
raise ArgumentError, "Unrecognised reason for roll #{roll_reason}"
|
@@ -83,14 +108,21 @@ class GamesDice::DieResult
|
|
83
108
|
@value = @total
|
84
109
|
end
|
85
110
|
|
86
|
-
#
|
111
|
+
# Sets value arbitrarily, and notes that the value has been mapped. Used by GamesDice::ComplexDie
|
112
|
+
# when there are one or more GamesDice::MapRule objects to process for a die.
|
113
|
+
# @param [Integer] to_value Replacement value.
|
114
|
+
# @param [String] description Description of what the mapped value represents e.g. "Success"
|
115
|
+
# @return [nil]
|
87
116
|
def apply_map( to_value, description = '' )
|
88
117
|
@mapped = true
|
89
118
|
@value = to_value
|
90
119
|
@map_description = description
|
120
|
+
return
|
91
121
|
end
|
92
122
|
|
93
|
-
#
|
123
|
+
# Generates a text description of how #value is determined. If #value has been mapped, includes the
|
124
|
+
# map description, but does not include the mapped value.
|
125
|
+
# @return [String] Explanation of #value.
|
94
126
|
def explain_value
|
95
127
|
text = ''
|
96
128
|
if @rolls.length < 2
|
@@ -104,39 +136,46 @@ class GamesDice::DieResult
|
|
104
136
|
return text
|
105
137
|
end
|
106
138
|
|
107
|
-
#
|
139
|
+
# @!visibility private
|
140
|
+
# This is mis-named, it doesn't explain the total at all! It is used to generate summaries of keeper dice.
|
108
141
|
def explain_total
|
109
142
|
text = @total.to_s
|
110
143
|
text += ' ' + @map_description if @mapped && @map_description && @map_description.length > 0
|
111
144
|
return text
|
112
145
|
end
|
113
146
|
|
147
|
+
# @!visibility private
|
114
148
|
# all coercions simply use #value (i.e. nil or a Fixnum)
|
115
149
|
def coerce(thing)
|
116
150
|
@value.coerce(thing)
|
117
151
|
end
|
118
152
|
|
153
|
+
# @!visibility private
|
119
154
|
# addition uses #value
|
120
155
|
def +(thing)
|
121
156
|
@value + thing
|
122
157
|
end
|
123
158
|
|
159
|
+
# @!visibility private
|
124
160
|
# subtraction uses #value
|
125
161
|
def -(thing)
|
126
162
|
@value - thing
|
127
163
|
end
|
128
164
|
|
165
|
+
# @!visibility private
|
129
166
|
# multiplication uses #value
|
130
167
|
def *(thing)
|
131
168
|
@value * thing
|
132
169
|
end
|
133
170
|
|
171
|
+
# @!visibility private
|
134
172
|
# comparison <=> uses #value
|
135
173
|
def <=>(other)
|
136
174
|
self.value <=> other
|
137
175
|
end
|
138
176
|
|
139
|
-
#
|
177
|
+
# This is a deep clone, all attributes are cloned.
|
178
|
+
# @return [GamesDice::DieResult]
|
140
179
|
def clone
|
141
180
|
cloned = GamesDice::DieResult.new()
|
142
181
|
cloned.instance_variable_set('@rolls', @rolls.clone)
|
data/lib/games_dice/map_rule.rb
CHANGED
@@ -48,11 +48,11 @@ class GamesDice::MapRule
|
|
48
48
|
# @return [Integer,Range,Object] Object that receives (#trigger_op, die_result)
|
49
49
|
attr_reader :trigger_value
|
50
50
|
|
51
|
-
#
|
51
|
+
# Value that a die will use after the value has been mapped.
|
52
52
|
# @return [Integer]
|
53
53
|
attr_reader :mapped_value
|
54
54
|
|
55
|
-
# Name for mapped value.
|
55
|
+
# Name for mapped value, used in explanations.
|
56
56
|
# @return [String]
|
57
57
|
attr_reader :mapped_name
|
58
58
|
|
data/lib/games_dice/parser.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
require 'parslet'
|
2
2
|
|
3
|
-
#
|
3
|
+
# Based on the parslet gem, this class defines the dice mini-language used by GamesDice.create
|
4
|
+
#
|
5
|
+
# An instance of this class is a parser for the language. There are no user-definable instance
|
6
|
+
# variables.
|
7
|
+
#
|
8
|
+
|
4
9
|
class GamesDice::Parser < Parslet::Parser
|
5
10
|
|
6
11
|
# Parslet rules that define the dice string grammar.
|
@@ -62,6 +67,10 @@ class GamesDice::Parser < Parslet::Parser
|
|
62
67
|
rule(:expressions) { dice_expression.repeat.as(:bunches) }
|
63
68
|
root :expressions
|
64
69
|
|
70
|
+
# Parses a string description in the dice mini-language, and returns data for feeding into
|
71
|
+
# GamesDice::Dice constructore.
|
72
|
+
# @param [String] dice_description Text to parse e.g. '1d6'
|
73
|
+
# @return [Hash] Analysis of dice_description
|
65
74
|
def parse dice_description
|
66
75
|
dice_description = dice_description.to_s.strip
|
67
76
|
# Force first item to start '+' for simpler parse rules
|
@@ -1,49 +1,84 @@
|
|
1
|
-
#
|
1
|
+
# This class models probability distributions for dice systems.
|
2
|
+
#
|
3
|
+
# An object of this class represents a single distribution, which might be the result of a complex
|
4
|
+
# combination of dice.
|
5
|
+
#
|
6
|
+
# @example Distribution for a six-sided die
|
7
|
+
# probs = GamesDice::Probabilities.for_fair_die( 6 )
|
8
|
+
# probs.min # => 1
|
9
|
+
# probs.max # => 6
|
10
|
+
# probs.expected # => 3.5
|
11
|
+
# probs.p_ge( 4 ) # => 0.5
|
12
|
+
#
|
13
|
+
# @example Adding two distributions
|
14
|
+
# pd6 = GamesDice::Probabilities.for_fair_die( 6 )
|
15
|
+
# probs = GamesDice::Probabilities.add_distributions( pd6, pd6 )
|
16
|
+
# probs.min # => 2
|
17
|
+
# probs.max # => 12
|
18
|
+
# probs.expected # => 7.0
|
19
|
+
# probs.p_ge( 10 ) # => 0.16666666666666669
|
20
|
+
#
|
2
21
|
class GamesDice::Probabilities
|
3
|
-
|
4
|
-
#
|
5
|
-
#
|
22
|
+
|
23
|
+
# Creates new instance of GamesDice::Probabilities.
|
24
|
+
# @param [Hash] prob_hash A hash representation of the distribution, each key is an integer result,
|
25
|
+
# and the matching value is probability of getting that result
|
26
|
+
# @return [GamesDice::Probabilities]
|
6
27
|
def initialize( prob_hash = { 0 => 1.0 } )
|
7
|
-
# This should *probably* be validated in future
|
28
|
+
# This should *probably* be validated in future, but that would impact performance
|
8
29
|
@ph = prob_hash
|
9
30
|
end
|
10
31
|
|
11
|
-
#
|
12
|
-
#
|
32
|
+
# @!visibility private
|
33
|
+
# the Hash representation of probabilities.
|
13
34
|
attr_reader :ph
|
14
35
|
|
15
|
-
#
|
36
|
+
# A hash representation of the distribution. Each key is an integer result,
|
37
|
+
# and the matching value is probability of getting that result. A new hash is generated on each
|
38
|
+
# call to this method.
|
39
|
+
# @return [Hash]
|
16
40
|
def to_h
|
17
41
|
@ph.clone
|
18
42
|
end
|
19
43
|
|
44
|
+
# @!attribute [r] min
|
45
|
+
# Minimum result in the distribution
|
46
|
+
# @return [Integer]
|
20
47
|
def min
|
21
48
|
(@minmax ||= @ph.keys.minmax )[0]
|
22
49
|
end
|
23
50
|
|
51
|
+
# @!attribute [r] max
|
52
|
+
# Maximum result in the distribution
|
53
|
+
# @return [Integer]
|
24
54
|
def max
|
25
55
|
(@minmax ||= @ph.keys.minmax )[1]
|
26
56
|
end
|
27
57
|
|
28
|
-
#
|
58
|
+
# @!attribute [r] expected
|
59
|
+
# Expected value of distribution.
|
60
|
+
# @return [Float]
|
29
61
|
def expected
|
30
62
|
@expected ||= @ph.inject(0.0) { |accumulate,p| accumulate + p[0] * p[1] }
|
31
63
|
end
|
32
64
|
|
33
|
-
#
|
34
|
-
#
|
65
|
+
# Probability of result equalling specific target
|
66
|
+
# @param [Integer] target
|
67
|
+
# @return [Float] in range (0.0..1.0)
|
35
68
|
def p_eql target
|
36
69
|
@ph[ Integer(target) ] || 0.0
|
37
70
|
end
|
38
71
|
|
39
|
-
#
|
40
|
-
#
|
72
|
+
# Probability of result being greater than specific target
|
73
|
+
# @param [Integer] target
|
74
|
+
# @return [Float] in range (0.0..1.0)
|
41
75
|
def p_gt target
|
42
76
|
p_ge( Integer(target) + 1 )
|
43
77
|
end
|
44
78
|
|
45
|
-
#
|
46
|
-
#
|
79
|
+
# Probability of result being equal to or greater than specific target
|
80
|
+
# @param [Integer] target
|
81
|
+
# @return [Float] in range (0.0..1.0)
|
47
82
|
def p_ge target
|
48
83
|
target = Integer(target)
|
49
84
|
return @prob_ge[target] if @prob_ge && @prob_ge[target]
|
@@ -54,7 +89,9 @@ class GamesDice::Probabilities
|
|
54
89
|
@prob_ge[target] = @ph.select {|k,v| target <= k}.inject(0.0) {|so_far,pv| so_far + pv[1] }
|
55
90
|
end
|
56
91
|
|
57
|
-
#
|
92
|
+
# Probability of result being equal to or less than specific target
|
93
|
+
# @param [Integer] target
|
94
|
+
# @return [Float] in range (0.0..1.0)
|
58
95
|
def p_le target
|
59
96
|
target = Integer(target)
|
60
97
|
return @prob_le[target] if @prob_le && @prob_le[target]
|
@@ -65,12 +102,16 @@ class GamesDice::Probabilities
|
|
65
102
|
@prob_le[target] = @ph.select {|k,v| target >= k}.inject(0.0) {|so_far,pv| so_far + pv[1] }
|
66
103
|
end
|
67
104
|
|
68
|
-
#
|
105
|
+
# Probability of result being less than specific target
|
106
|
+
# @param [Integer] target
|
107
|
+
# @return [Float] in range (0.0..1.0)
|
69
108
|
def p_lt target
|
70
109
|
p_le( Integer(target) - 1 )
|
71
110
|
end
|
72
111
|
|
73
|
-
#
|
112
|
+
# Distribution for a die with equal chance of rolling 1..N
|
113
|
+
# @param [Integer] sides Number of sides on die
|
114
|
+
# @return [GamesDice::Probabilities]
|
74
115
|
def self.for_fair_die sides
|
75
116
|
sides = Integer(sides)
|
76
117
|
raise ArgumentError, "sides must be at least 1" unless sides > 0
|
@@ -80,8 +121,11 @@ class GamesDice::Probabilities
|
|
80
121
|
GamesDice::Probabilities.new( h )
|
81
122
|
end
|
82
123
|
|
83
|
-
#
|
84
|
-
#
|
124
|
+
# Combines two distributions to create a third, that represents the distribution created when adding
|
125
|
+
# results together.
|
126
|
+
# @param [GamesDice::Probabilities] pd_a First distribution
|
127
|
+
# @param [GamesDice::Probabilities] pd_b Second distribution
|
128
|
+
# @return [GamesDice::Probabilities]
|
85
129
|
def self.add_distributions pd_a, pd_b
|
86
130
|
h = {}
|
87
131
|
pd_a.ph.each do |ka,pa|
|
@@ -94,8 +138,13 @@ class GamesDice::Probabilities
|
|
94
138
|
GamesDice::Probabilities.new( h )
|
95
139
|
end
|
96
140
|
|
97
|
-
#
|
98
|
-
#
|
141
|
+
# Combines two distributions with multipliers to create a third, that represents the distribution
|
142
|
+
# created when adding weighted results together.
|
143
|
+
# @param [Integer] m_a Weighting for first distribution
|
144
|
+
# @param [GamesDice::Probabilities] pd_a First distribution
|
145
|
+
# @param [Integer] m_b Weighting for second distribution
|
146
|
+
# @param [GamesDice::Probabilities] pd_b Second distribution
|
147
|
+
# @return [GamesDice::Probabilities]
|
99
148
|
def self.add_distributions_mult m_a, pd_a, m_b, pd_b
|
100
149
|
h = {}
|
101
150
|
pd_a.ph.each do |ka,pa|
|
@@ -108,4 +157,4 @@ class GamesDice::Probabilities
|
|
108
157
|
GamesDice::Probabilities.new( h )
|
109
158
|
end
|
110
159
|
|
111
|
-
end # class
|
160
|
+
end # class GamesDice::Probabilities
|
data/lib/games_dice/version.rb
CHANGED
data/spec/bunch_spec.rb
CHANGED
data/spec/complex_die_spec.rb
CHANGED
data/spec/die_spec.rb
CHANGED
data/spec/probability_spec.rb
CHANGED
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.2.
|
4
|
+
version: 0.2.3
|
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-06-
|
12
|
+
date: 2013-06-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
@@ -144,7 +144,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
144
144
|
version: '0'
|
145
145
|
segments:
|
146
146
|
- 0
|
147
|
-
hash:
|
147
|
+
hash: 1797445249921181101
|
148
148
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
149
149
|
none: false
|
150
150
|
requirements:
|
@@ -153,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
153
|
version: '0'
|
154
154
|
segments:
|
155
155
|
- 0
|
156
|
-
hash:
|
156
|
+
hash: 1797445249921181101
|
157
157
|
requirements: []
|
158
158
|
rubyforge_project:
|
159
159
|
rubygems_version: 1.8.24
|