games_dice 0.3.9 → 0.4.0

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.
@@ -0,0 +1,68 @@
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
@@ -1,97 +1,101 @@
1
- # This class models the simplest, most-familiar kind of die.
2
- #
3
- # An object of the class represents a basic die that rolls 1..#sides, with equal weighting for each value.
4
- #
5
- # @example Create a 6-sided die, and roll it
6
- # d = GamesDice::Die.new( 6 )
7
- # d.roll # => Integer in range 1..6
8
- # d.result # => same Integer value as just returned by d.roll
9
- #
10
- # @example Create a 10-sided die, that rolls using a monkey-patch to SecureRandom
11
- # module SecureRandom
12
- # def self.rand n
13
- # random_number( n )
14
- # end
15
- # end
16
- # d = GamesDice::Die.new( 10, SecureRandom )
17
- # d.roll # => (secure) Integer in range 1..10
18
- # d.result # => same Integer value as just returned by d.roll
19
-
20
- class GamesDice::Die
21
-
22
- # Creates new instance of GamesDice::Die
23
- # @param [Integer] sides the number of sides
24
- # @param [#rand] prng random number generator, GamesDice::Die will use Ruby's built-in #rand() by default
25
- # @return [GamesDice::Die]
26
- def initialize( sides, prng=nil )
27
- @sides = Integer(sides)
28
- raise ArgumentError, "sides value #{sides} is too low, it must be 1 or greater" if @sides < 1
29
- raise ArgumentError, "prng does not support the rand() method" if prng && ! prng.respond_to?(:rand)
30
- @prng = prng
31
- @result = nil
32
- end
33
-
34
- # @return [Integer] number of sides on simulated die
35
- attr_reader :sides
36
-
37
- # @return [Integer] result of last call to #roll, nil if no call made yet
38
- attr_reader :result
39
-
40
- # @return [Object] random number generator as supplied to constructor, may be nil
41
- attr_reader :prng
42
-
43
- # @!attribute [r] min
44
- # @return [Integer] minimum possible result from a call to #roll
45
- def min
46
- 1
47
- end
48
-
49
- # @!attribute [r] max
50
- # @return [Integer] maximum possible result from a call to #roll
51
- def max
52
- @sides
53
- end
54
-
55
- # Calculates probability distribution for this die.
56
- # @return [GamesDice::Probabilities] probability distribution of the die
57
- def probabilities
58
- @probabilities ||= GamesDice::Probabilities.for_fair_die( @sides )
59
- end
60
-
61
- # Simulates rolling the die
62
- # @return [Integer] selected value between 1 and #sides inclusive
63
- def roll
64
- if @prng
65
- @result = @prng.rand(@sides) + 1
66
- else
67
- @result = rand(@sides) + 1
68
- end
69
- end
70
-
71
- # Iterates through all possible results on die.
72
- # @yieldparam [Integer] result A potential result from the die
73
- # @return [GamesDice::Die] this object
74
- def each_value
75
- (1..@sides).each { |r| yield(r) }
76
- self
77
- end
78
-
79
- # @return [Array<Integer>] All potential results from the die
80
- def all_values
81
- (1..@sides).to_a
82
- end
83
-
84
- # @!attribute [r] rerolls
85
- # Rules for when to re-roll this die.
86
- # @return [nil] always nil, available for interface equivalence with GamesDice::ComplexDie
87
- def rerolls
88
- nil
89
- end
90
-
91
- # @!attribute [r] maps
92
- # Rules for when to map return value of this die.
93
- # @return [nil] always nil, available for interface equivalence with GamesDice::ComplexDie
94
- def maps
95
- nil
96
- end
97
- end # class GamesDice::Die
1
+ # frozen_string_literal: true
2
+
3
+ module GamesDice
4
+ # This class models the simplest, most-familiar kind of die.
5
+ #
6
+ # An object of the class represents a basic die that rolls 1..#sides, with equal weighting for each value.
7
+ #
8
+ # @example Create a 6-sided die, and roll it
9
+ # d = GamesDice::Die.new( 6 )
10
+ # d.roll # => Integer in range 1..6
11
+ # d.result # => same Integer value as just returned by d.roll
12
+ #
13
+ # @example Create a 10-sided die, that rolls using a monkey-patch to SecureRandom
14
+ # module SecureRandom
15
+ # def self.rand n
16
+ # random_number( n )
17
+ # end
18
+ # end
19
+ # d = GamesDice::Die.new( 10, SecureRandom )
20
+ # d.roll # => (secure) Integer in range 1..10
21
+ # d.result # => same Integer value as just returned by d.roll
22
+ #
23
+ class Die
24
+ # Creates new instance of GamesDice::Die
25
+ # @param [Integer] sides the number of sides
26
+ # @param [#rand] prng random number generator, GamesDice::Die will use Ruby's built-in #rand() by default
27
+ # @return [GamesDice::Die]
28
+ def initialize(sides, prng = nil)
29
+ @sides = Integer(sides)
30
+ raise ArgumentError, "sides value #{sides} is too low, it must be 1 or greater" if @sides < 1
31
+ raise ArgumentError, 'prng does not support the rand() method' if prng && !prng.respond_to?(:rand)
32
+
33
+ @prng = prng
34
+ @result = nil
35
+ end
36
+
37
+ # @return [Integer] number of sides on simulated die
38
+ attr_reader :sides
39
+
40
+ # @return [Integer] result of last call to #roll, nil if no call made yet
41
+ attr_reader :result
42
+
43
+ # @return [Object] random number generator as supplied to constructor, may be nil
44
+ attr_reader :prng
45
+
46
+ # @!attribute [r] min
47
+ # @return [Integer] minimum possible result from a call to #roll
48
+ def min
49
+ 1
50
+ end
51
+
52
+ # @!attribute [r] max
53
+ # @return [Integer] maximum possible result from a call to #roll
54
+ def max
55
+ @sides
56
+ end
57
+
58
+ # Calculates probability distribution for this die.
59
+ # @return [GamesDice::Probabilities] probability distribution of the die
60
+ def probabilities
61
+ @probabilities ||= GamesDice::Probabilities.for_fair_die(@sides)
62
+ end
63
+
64
+ # Simulates rolling the die
65
+ # @return [Integer] selected value between 1 and #sides inclusive
66
+ def roll
67
+ @result = if @prng
68
+ @prng.rand(@sides) + 1
69
+ else
70
+ rand(@sides) + 1
71
+ end
72
+ end
73
+
74
+ # Iterates through all possible results on die.
75
+ # @yieldparam [Integer] result A potential result from the die
76
+ # @return [GamesDice::Die] this object
77
+ def each_value(&block)
78
+ (1..@sides).each(&block)
79
+ self
80
+ end
81
+
82
+ # @return [Array<Integer>] All potential results from the die
83
+ def all_values
84
+ (1..@sides).to_a
85
+ end
86
+
87
+ # @!attribute [r] rerolls
88
+ # Rules for when to re-roll this die.
89
+ # @return [nil] always nil, available for interface equivalence with GamesDice::ComplexDie
90
+ def rerolls
91
+ nil
92
+ end
93
+
94
+ # @!attribute [r] maps
95
+ # Rules for when to map return value of this die.
96
+ # @return [nil] always nil, available for interface equivalence with GamesDice::ComplexDie
97
+ def maps
98
+ nil
99
+ end
100
+ end
101
+ end