games_dice 0.3.12 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,60 +1,68 @@
1
- # @!visibility private
2
- module GamesDice::ComplexDieHelpers
3
-
4
- private
5
-
6
- def recursive_probabilities probabilities={},prior_probability=1.0,depth=0,prior_result=nil,rerolls_left=nil,roll_reason=:basic,subtracting=false
7
- each_probability = prior_probability / @basic_die.sides
8
- depth += 1
9
- if depth >= 20 || each_probability < 1.0e-16
10
- @probabilities_complete = false
11
- stop_recursing = true
12
- end
13
-
14
- @basic_die.each_value do |v|
15
- recurse_probs_for_value( v, roll_reason, probabilities, each_probability, depth, prior_result, rerolls_left, subtracting, stop_recursing )
16
- end
17
- probabilities
18
- end
19
-
20
- def recurse_probs_for_value v, roll_reason, probabilities, each_probability, depth, prior_result, rerolls_left, subtracting, stop_recursing
21
- # calculate value, recurse if there is a reroll
22
- result_so_far, rerolls_remaining = calc_result_so_far(prior_result, rerolls_left, v, roll_reason )
23
-
24
- # Find which rule, if any, is being triggered
25
- rule_idx = find_matching_reroll_rule( v, result_so_far.rolls.length, rerolls_remaining )
26
-
27
- if rule_idx && ! stop_recursing
28
- recurse_probs_with_rule( probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule_idx, subtracting )
29
- else
30
- t = result_so_far.total
31
- probabilities[ t ] ||= 0.0
32
- probabilities[ t ] += each_probability
33
- end
34
- end
35
-
36
- def recurse_probs_with_rule probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule_idx, subtracting
37
- rule = @rerolls[ rule_idx ]
38
- rerolls_remaining[ rule_idx ] -= 1
39
- is_subtracting = true if subtracting || rule.type == :reroll_subtract
40
-
41
- # Apply the rule (note reversal for additions, after a subtract)
42
- if subtracting && rule.type == :reroll_add
43
- recursive_probabilities probabilities, each_probability, depth, result_so_far, rerolls_remaining, :reroll_subtract, is_subtracting
44
- else
45
- recursive_probabilities probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule.type, is_subtracting
46
- end
47
- end
48
-
49
- def calc_result_so_far prior_result, rerolls_left, v, roll_reason
50
- if prior_result
51
- result_so_far = prior_result.clone
52
- result_so_far.add_roll(v,roll_reason)
53
- rerolls_remaining = rerolls_left.clone
54
- else
55
- result_so_far = GamesDice::DieResult.new(v,roll_reason)
56
- rerolls_remaining = @rerolls.map { |rule| rule.limit }
57
- end
58
- [result_so_far, rerolls_remaining]
59
- end
60
- end
1
+ # frozen_string_literal: true
2
+
3
+ # @!visibility private
4
+ module GamesDice
5
+ # Private extension methods for GamesDice::ComplexDie
6
+ module ComplexDieHelpers
7
+ private
8
+
9
+ def recursive_probabilities(probabilities = {}, prior_probability = 1.0, depth = 0, prior_result = nil, rerolls_left = nil, roll_reason = :basic, subtracting = false)
10
+ each_probability = prior_probability / @basic_die.sides
11
+ depth += 1
12
+ if depth >= 20 || each_probability < 1.0e-16
13
+ @probabilities_complete = false
14
+ stop_recursing = true
15
+ end
16
+
17
+ @basic_die.each_value do |v|
18
+ recurse_probs_for_value(v, roll_reason, probabilities, each_probability, depth, prior_result, rerolls_left,
19
+ subtracting, stop_recursing)
20
+ end
21
+ probabilities
22
+ end
23
+
24
+ def recurse_probs_for_value(v, roll_reason, probabilities, each_probability, depth, prior_result, rerolls_left, subtracting, stop_recursing)
25
+ # calculate value, recurse if there is a reroll
26
+ result_so_far, rerolls_remaining = calc_result_so_far(prior_result, rerolls_left, v, roll_reason)
27
+
28
+ # Find which rule, if any, is being triggered
29
+ rule_idx = find_matching_reroll_rule(v, result_so_far.rolls.length, rerolls_remaining)
30
+
31
+ if rule_idx && !stop_recursing
32
+ recurse_probs_with_rule(probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule_idx,
33
+ subtracting)
34
+ else
35
+ t = result_so_far.total
36
+ probabilities[t] ||= 0.0
37
+ probabilities[t] += each_probability
38
+ end
39
+ end
40
+
41
+ def recurse_probs_with_rule(probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule_idx, subtracting)
42
+ rule = @rerolls[rule_idx]
43
+ rerolls_remaining[rule_idx] -= 1
44
+ is_subtracting = true if subtracting || rule.type == :reroll_subtract
45
+
46
+ # Apply the rule (note reversal for additions, after a subtract)
47
+ if subtracting && rule.type == :reroll_add
48
+ recursive_probabilities probabilities, each_probability, depth, result_so_far, rerolls_remaining,
49
+ :reroll_subtract, is_subtracting
50
+ else
51
+ recursive_probabilities probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule.type,
52
+ is_subtracting
53
+ end
54
+ end
55
+
56
+ def calc_result_so_far(prior_result, rerolls_left, v, roll_reason)
57
+ if prior_result
58
+ result_so_far = prior_result.clone
59
+ result_so_far.add_roll(v, roll_reason)
60
+ rerolls_remaining = rerolls_left.clone
61
+ else
62
+ result_so_far = GamesDice::DieResult.new(v, roll_reason)
63
+ rerolls_remaining = @rerolls.map(&:limit)
64
+ end
65
+ [result_so_far, rerolls_remaining]
66
+ end
67
+ end
68
+ end
@@ -1,16 +1,16 @@
1
- module GamesDice
1
+ # frozen_string_literal: true
2
2
 
3
+ module GamesDice
3
4
  # Reasons for making a reroll, and text explanation symbols for them
4
5
  REROLL_TYPES = {
5
- :basic => ',',
6
- :reroll_add => '+',
7
- :reroll_subtract => '-',
8
- :reroll_replace => '|',
9
- :reroll_use_best => '/',
10
- :reroll_use_worst => '\\',
6
+ basic: ',',
7
+ reroll_add: '+',
8
+ reroll_subtract: '-',
9
+ reroll_replace: '|',
10
+ reroll_use_best: '/',
11
+ reroll_use_worst: '\\'
11
12
  # These are not yet implemented:
12
13
  # :reroll_new_die => '*',
13
14
  # :reroll_new_keeper => '*',
14
- }
15
-
16
- end
15
+ }.freeze
16
+ end
@@ -1,143 +1,146 @@
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
- #
19
- class GamesDice::Dice
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]
34
- def initialize( bunches, offset = 0, name = '' )
35
- @name = name
36
- @offset = offset
37
- @bunches = bunches.map { |b| GamesDice::Bunch.new( b ) }
38
- @bunch_multipliers = bunches.map { |b| b[:multiplier] || 1 }
39
- @result = nil
40
- end
41
-
42
- # Name to help identify dice
43
- # @return [String]
44
- attr_reader :name
45
-
46
- # Bunches of dice that are components of the object
47
- # @return [Array<GamesDice::Bunch>]
48
- attr_reader :bunches
49
-
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>]
53
- attr_reader :bunch_multipliers
54
-
55
- # Fixed offset added to sum of all bunches.
56
- # @return [Integer]
57
- attr_reader :offset
58
-
59
- # Result of most-recent roll, or nil if no roll made yet.
60
- # @return [Integer,nil]
61
- attr_reader :result
62
-
63
- # Simulates rolling dice
64
- # @return [Integer] Sum of all rolled dice
65
- def roll
66
- @result = @offset + bunches_weighted_sum( :roll )
67
- end
68
-
69
- # @!attribute [r] min
70
- # Minimum possible result from a call to #roll
71
- # @return [Integer]
72
- def min
73
- @min ||= @offset + bunches_weighted_sum( :min )
74
- end
75
-
76
- # @!attribute [r] max
77
- # Maximum possible result from a call to #roll
78
- # @return [Integer]
79
- def max
80
- @max ||= @offset + bunches_weighted_sum( :max )
81
- end
82
-
83
- # @!attribute [r] minmax
84
- # Convenience method, same as [dice.min, dice.max]
85
- # @return [Array<Integer>]
86
- def minmax
87
- [min,max]
88
- end
89
-
90
- # Calculates the probability distribution for the dice. When the dice include components with
91
- # open-ended re-roll rules, there are some arbitrary limits imposed to prevent large amounts of
92
- # recursion.
93
- # @return [GamesDice::Probabilities] Probability distribution of dice.
94
- def probabilities
95
- return @probabilities if @probabilities
96
- probs = @bunch_multipliers.zip(@bunches).inject( GamesDice::Probabilities.new( [1.0], @offset ) ) do |probs, mb|
97
- m,b = mb
98
- GamesDice::Probabilities.add_distributions_mult( 1, probs, m, b.probabilities )
99
- end
100
- end
101
-
102
- # @!attribute [r] explain_result
103
- # @return [String,nil] Explanation of result, or nil if no call to #roll yet.
104
- def explain_result
105
- return nil unless @result
106
- explanations = @bunches.map { |bunch| bunch.label + ": " + bunch.explain_result }
107
-
108
- if explanations.count == 0
109
- return @offset.to_s
110
- end
111
-
112
- if explanations.count == 1
113
- if @offset !=0
114
- return explanations[0] + '. ' + array_to_sum( [ @bunches[0].result, @offset ] )
115
- else
116
- return explanations[0]
117
- end
118
- end
119
-
120
- bunch_values = @bunch_multipliers.zip(@bunches).map { |m,b| m * b.result }
121
- bunch_values << @offset if @offset != 0
122
- explanations << array_to_sum( bunch_values )
123
- return explanations.join('. ')
124
- end
125
-
126
- private
127
-
128
- def array_to_sum array
129
- ( numbers_to_strings(array) + [ '=', array.inject(:+) ] ).join(' ')
130
- end
131
-
132
- def numbers_to_strings array
133
- [ array.first.to_s ] + array.drop(1).map { |n| n < 0 ? '- ' + n.abs.to_s : '+ ' + n.to_s }
134
- end
135
-
136
- def bunches_weighted_sum summed_method
137
- @bunch_multipliers.zip(@bunches).inject(0) do |total,mb|
138
- m,b = mb
139
- total += m * b.send( summed_method )
140
- end
141
- end
142
-
143
- end # class Dice
1
+ # frozen_string_literal: true
2
+
3
+ module GamesDice
4
+ # This class models a combination of GamesDice::Bunch objects plus a fixed offset.
5
+ #
6
+ # An object of this class is a dice "recipe" that specifies the numbers and types of
7
+ # dice that can be rolled to generate an integer value.
8
+ #
9
+ # @example '3d6+6' hitpoints, whatever that means in the game you are playing
10
+ # d = GamesDice::Dice.new( [{:ndice => 3, :sides => 6}], 6, 'Hit points' )
11
+ # d.roll # => 20
12
+ # d.result # => 20
13
+ # d.explain_result # => "3d6: 3 + 5 + 6 = 14. 14 + 6 = 20"
14
+ # d.probabilities.expected # => 16.5
15
+ #
16
+ # @example Roll d20 twice, take best result, and add 5.
17
+ # d = GamesDice::Dice.new( [{:ndice => 2, :sides => 20 , :keep_mode => :keep_best, :keep_number => 1}], 5 )
18
+ # d.roll # => 21
19
+ # d.result # => 21
20
+ # d.explain_result # => "2d20: 4, 16. Keep: 16. 16 + 5 = 21"
21
+ #
22
+ class Dice
23
+ # The first parameter is an array of values that are passed to GamesDice::Bunch constructors.
24
+ # @param [Array<Hash>] bunches Array of options for creating bunches
25
+ # @param [Integer] offset Total offset
26
+ # @param [String] name Optional label for the dice
27
+ # @option bunches [Integer] :ndice Number of dice in the bunch, *mandatory*
28
+ # @option bunches [Integer] :sides Number of sides on a single die in the bunch, *mandatory*
29
+ # @option bunches [String] :name Optional name for the bunch
30
+ # @option bunches [Array<GamesDice::RerollRule,Array>] :rerolls Optional rules that cause the die to roll again
31
+ # @option bunches [Array<GamesDice::MapRule,Array>] :maps Optional rules to convert a value into a final result for the die
32
+ # @option bunches [#rand] :prng Optional alternative source of randomness to Ruby's built-in #rand, passed to GamesDice::Die's constructor
33
+ # @option bunches [Symbol] :keep_mode Optional, either *:keep_best* or *:keep_worst*
34
+ # @option bunches [Integer] :keep_number Optional number of dice to keep when :keep_mode is not nil
35
+ # @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
36
+ # @return [GamesDice::Dice]
37
+ def initialize(bunches, offset = 0, name = '')
38
+ @name = name
39
+ @offset = offset
40
+ @bunches = bunches.map { |b| GamesDice::Bunch.new(b) }
41
+ @bunch_multipliers = bunches.map { |b| b[:multiplier] || 1 }
42
+ @result = nil
43
+ end
44
+
45
+ # Name to help identify dice
46
+ # @return [String]
47
+ attr_reader :name
48
+
49
+ # Bunches of dice that are components of the object
50
+ # @return [Array<GamesDice::Bunch>]
51
+ attr_reader :bunches
52
+
53
+ # Multipliers for each bunch of identical dice. Typically 1 or -1 to represent groups of dice that
54
+ # are either added or subtracted from the total.
55
+ # @return [Array<Integer>]
56
+ attr_reader :bunch_multipliers
57
+
58
+ # Fixed offset added to sum of all bunches.
59
+ # @return [Integer]
60
+ attr_reader :offset
61
+
62
+ # Result of most-recent roll, or nil if no roll made yet.
63
+ # @return [Integer,nil]
64
+ attr_reader :result
65
+
66
+ # Simulates rolling dice
67
+ # @return [Integer] Sum of all rolled dice
68
+ def roll
69
+ @result = @offset + bunches_weighted_sum(:roll)
70
+ end
71
+
72
+ # @!attribute [r] min
73
+ # Minimum possible result from a call to #roll
74
+ # @return [Integer]
75
+ def min
76
+ @min ||= @offset + bunches_weighted_sum(:min)
77
+ end
78
+
79
+ # @!attribute [r] max
80
+ # Maximum possible result from a call to #roll
81
+ # @return [Integer]
82
+ def max
83
+ @max ||= @offset + bunches_weighted_sum(:max)
84
+ end
85
+
86
+ # @!attribute [r] minmax
87
+ # Convenience method, same as [dice.min, dice.max]
88
+ # @return [Array<Integer>]
89
+ def minmax
90
+ [min, max]
91
+ end
92
+
93
+ # Calculates the probability distribution for the dice. When the dice include components with
94
+ # open-ended re-roll rules, there are some arbitrary limits imposed to prevent large amounts of
95
+ # recursion.
96
+ # @return [GamesDice::Probabilities] Probability distribution of dice.
97
+ def probabilities
98
+ return @probabilities if @probabilities
99
+
100
+ @bunch_multipliers.zip(@bunches).inject(GamesDice::Probabilities.new([1.0], @offset)) do |probs, mb|
101
+ m, b = mb
102
+ GamesDice::Probabilities.add_distributions_mult(1, probs, m, b.probabilities)
103
+ end
104
+ end
105
+
106
+ # @!attribute [r] explain_result
107
+ # @return [String,nil] Explanation of result, or nil if no call to #roll yet.
108
+ def explain_result
109
+ return nil unless @result
110
+
111
+ explanations = @bunches.map { |bunch| "#{bunch.label}: #{bunch.explain_result}" }
112
+
113
+ return @offset.to_s if explanations.count.zero?
114
+
115
+ return simple_explanation(explanations.first) if explanations.count == 1
116
+
117
+ bunch_values = @bunch_multipliers.zip(@bunches).map { |m, b| m * b.result }
118
+ bunch_values << @offset if @offset != 0
119
+ explanations << array_to_sum(bunch_values)
120
+ explanations.join('. ')
121
+ end
122
+
123
+ private
124
+
125
+ def simple_explanation(explanation)
126
+ return explanation if @offset.zero?
127
+
128
+ "#{explanation}. #{array_to_sum([@bunches[0].result, @offset])}"
129
+ end
130
+
131
+ def array_to_sum(array)
132
+ (numbers_to_strings(array) + ['=', array.inject(:+)]).join(' ')
133
+ end
134
+
135
+ def numbers_to_strings(array)
136
+ [array.first.to_s] + array.drop(1).map { |n| n.negative? ? "- #{n.abs}" : "+ #{n}" }
137
+ end
138
+
139
+ def bunches_weighted_sum(summed_method)
140
+ @bunch_multipliers.zip(@bunches).inject(0) do |total, mb|
141
+ m, b = mb
142
+ total + (m * b.send(summed_method))
143
+ end
144
+ end
145
+ end
146
+ end