lib/games_dice/dice.rb in games_dice-0.3.12 vs lib/games_dice/dice.rb in games_dice-0.4.0
- old
+ new
@@ -1,143 +1,146 @@
-# This class models a combination of GamesDice::Bunch objects plus a fixed offset.
-#
-# An object of this class is a dice "recipe" that specifies the numbers and types of
-# dice that can be rolled to generate an integer value.
-#
-# @example '3d6+6' hitpoints, whatever that means in the game you are playing
-# d = GamesDice::Dice.new( [{:ndice => 3, :sides => 6}], 6, 'Hit points' )
-# d.roll # => 20
-# d.result # => 20
-# d.explain_result # => "3d6: 3 + 5 + 6 = 14. 14 + 6 = 20"
-# d.probabilities.expected # => 16.5
-#
-# @example Roll d20 twice, take best result, and add 5.
-# d = GamesDice::Dice.new( [{:ndice => 2, :sides => 20 , :keep_mode => :keep_best, :keep_number => 1}], 5 )
-# d.roll # => 21
-# d.result # => 21
-# d.explain_result # => "2d20: 4, 16. Keep: 16. 16 + 5 = 21"
-#
-class GamesDice::Dice
- # The first parameter is an array of values that are passed to GamesDice::Bunch constructors.
- # @param [Array<Hash>] bunches Array of options for creating bunches
- # @param [Integer] offset Total offset
- # @param [String] name Optional label for the dice
- # @option bunches [Integer] :ndice Number of dice in the bunch, *mandatory*
- # @option bunches [Integer] :sides Number of sides on a single die in the bunch, *mandatory*
- # @option bunches [String] :name Optional name for the bunch
- # @option bunches [Array<GamesDice::RerollRule,Array>] :rerolls Optional rules that cause the die to roll again
- # @option bunches [Array<GamesDice::MapRule,Array>] :maps Optional rules to convert a value into a final result for the die
- # @option bunches [#rand] :prng Optional alternative source of randomness to Ruby's built-in #rand, passed to GamesDice::Die's constructor
- # @option bunches [Symbol] :keep_mode Optional, either *:keep_best* or *:keep_worst*
- # @option bunches [Integer] :keep_number Optional number of dice to keep when :keep_mode is not nil
- # @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
- # @return [GamesDice::Dice]
- def initialize( bunches, offset = 0, name = '' )
- @name = name
- @offset = offset
- @bunches = bunches.map { |b| GamesDice::Bunch.new( b ) }
- @bunch_multipliers = bunches.map { |b| b[:multiplier] || 1 }
- @result = nil
- end
-
- # Name to help identify dice
- # @return [String]
- attr_reader :name
-
- # Bunches of dice that are components of the object
- # @return [Array<GamesDice::Bunch>]
- attr_reader :bunches
-
- # Multipliers for each bunch of identical dice. Typically 1 or -1 to represent groups of dice that
- # are either added or subtracted from the total.
- # @return [Array<Integer>]
- attr_reader :bunch_multipliers
-
- # Fixed offset added to sum of all bunches.
- # @return [Integer]
- attr_reader :offset
-
- # Result of most-recent roll, or nil if no roll made yet.
- # @return [Integer,nil]
- attr_reader :result
-
- # Simulates rolling dice
- # @return [Integer] Sum of all rolled dice
- def roll
- @result = @offset + bunches_weighted_sum( :roll )
- end
-
- # @!attribute [r] min
- # Minimum possible result from a call to #roll
- # @return [Integer]
- def min
- @min ||= @offset + bunches_weighted_sum( :min )
- end
-
- # @!attribute [r] max
- # Maximum possible result from a call to #roll
- # @return [Integer]
- def max
- @max ||= @offset + bunches_weighted_sum( :max )
- end
-
- # @!attribute [r] minmax
- # Convenience method, same as [dice.min, dice.max]
- # @return [Array<Integer>]
- def minmax
- [min,max]
- end
-
- # Calculates the probability distribution for the dice. When the dice include components with
- # open-ended re-roll rules, there are some arbitrary limits imposed to prevent large amounts of
- # recursion.
- # @return [GamesDice::Probabilities] Probability distribution of dice.
- def probabilities
- return @probabilities if @probabilities
- probs = @bunch_multipliers.zip(@bunches).inject( GamesDice::Probabilities.new( [1.0], @offset ) ) do |probs, mb|
- m,b = mb
- GamesDice::Probabilities.add_distributions_mult( 1, probs, m, b.probabilities )
- end
- end
-
- # @!attribute [r] explain_result
- # @return [String,nil] Explanation of result, or nil if no call to #roll yet.
- def explain_result
- return nil unless @result
- explanations = @bunches.map { |bunch| bunch.label + ": " + bunch.explain_result }
-
- if explanations.count == 0
- return @offset.to_s
- end
-
- if explanations.count == 1
- if @offset !=0
- return explanations[0] + '. ' + array_to_sum( [ @bunches[0].result, @offset ] )
- else
- return explanations[0]
- end
- end
-
- bunch_values = @bunch_multipliers.zip(@bunches).map { |m,b| m * b.result }
- bunch_values << @offset if @offset != 0
- explanations << array_to_sum( bunch_values )
- return explanations.join('. ')
- end
-
- private
-
- def array_to_sum array
- ( numbers_to_strings(array) + [ '=', array.inject(:+) ] ).join(' ')
- end
-
- def numbers_to_strings array
- [ array.first.to_s ] + array.drop(1).map { |n| n < 0 ? '- ' + n.abs.to_s : '+ ' + n.to_s }
- end
-
- def bunches_weighted_sum summed_method
- @bunch_multipliers.zip(@bunches).inject(0) do |total,mb|
- m,b = mb
- total += m * b.send( summed_method )
- end
- end
-
-end # class Dice
+# frozen_string_literal: true
+
+module GamesDice
+ # This class models a combination of GamesDice::Bunch objects plus a fixed offset.
+ #
+ # An object of this class is a dice "recipe" that specifies the numbers and types of
+ # dice that can be rolled to generate an integer value.
+ #
+ # @example '3d6+6' hitpoints, whatever that means in the game you are playing
+ # d = GamesDice::Dice.new( [{:ndice => 3, :sides => 6}], 6, 'Hit points' )
+ # d.roll # => 20
+ # d.result # => 20
+ # d.explain_result # => "3d6: 3 + 5 + 6 = 14. 14 + 6 = 20"
+ # d.probabilities.expected # => 16.5
+ #
+ # @example Roll d20 twice, take best result, and add 5.
+ # d = GamesDice::Dice.new( [{:ndice => 2, :sides => 20 , :keep_mode => :keep_best, :keep_number => 1}], 5 )
+ # d.roll # => 21
+ # d.result # => 21
+ # d.explain_result # => "2d20: 4, 16. Keep: 16. 16 + 5 = 21"
+ #
+ class Dice
+ # The first parameter is an array of values that are passed to GamesDice::Bunch constructors.
+ # @param [Array<Hash>] bunches Array of options for creating bunches
+ # @param [Integer] offset Total offset
+ # @param [String] name Optional label for the dice
+ # @option bunches [Integer] :ndice Number of dice in the bunch, *mandatory*
+ # @option bunches [Integer] :sides Number of sides on a single die in the bunch, *mandatory*
+ # @option bunches [String] :name Optional name for the bunch
+ # @option bunches [Array<GamesDice::RerollRule,Array>] :rerolls Optional rules that cause the die to roll again
+ # @option bunches [Array<GamesDice::MapRule,Array>] :maps Optional rules to convert a value into a final result for the die
+ # @option bunches [#rand] :prng Optional alternative source of randomness to Ruby's built-in #rand, passed to GamesDice::Die's constructor
+ # @option bunches [Symbol] :keep_mode Optional, either *:keep_best* or *:keep_worst*
+ # @option bunches [Integer] :keep_number Optional number of dice to keep when :keep_mode is not nil
+ # @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
+ # @return [GamesDice::Dice]
+ def initialize(bunches, offset = 0, name = '')
+ @name = name
+ @offset = offset
+ @bunches = bunches.map { |b| GamesDice::Bunch.new(b) }
+ @bunch_multipliers = bunches.map { |b| b[:multiplier] || 1 }
+ @result = nil
+ end
+
+ # Name to help identify dice
+ # @return [String]
+ attr_reader :name
+
+ # Bunches of dice that are components of the object
+ # @return [Array<GamesDice::Bunch>]
+ attr_reader :bunches
+
+ # Multipliers for each bunch of identical dice. Typically 1 or -1 to represent groups of dice that
+ # are either added or subtracted from the total.
+ # @return [Array<Integer>]
+ attr_reader :bunch_multipliers
+
+ # Fixed offset added to sum of all bunches.
+ # @return [Integer]
+ attr_reader :offset
+
+ # Result of most-recent roll, or nil if no roll made yet.
+ # @return [Integer,nil]
+ attr_reader :result
+
+ # Simulates rolling dice
+ # @return [Integer] Sum of all rolled dice
+ def roll
+ @result = @offset + bunches_weighted_sum(:roll)
+ end
+
+ # @!attribute [r] min
+ # Minimum possible result from a call to #roll
+ # @return [Integer]
+ def min
+ @min ||= @offset + bunches_weighted_sum(:min)
+ end
+
+ # @!attribute [r] max
+ # Maximum possible result from a call to #roll
+ # @return [Integer]
+ def max
+ @max ||= @offset + bunches_weighted_sum(:max)
+ end
+
+ # @!attribute [r] minmax
+ # Convenience method, same as [dice.min, dice.max]
+ # @return [Array<Integer>]
+ def minmax
+ [min, max]
+ end
+
+ # Calculates the probability distribution for the dice. When the dice include components with
+ # open-ended re-roll rules, there are some arbitrary limits imposed to prevent large amounts of
+ # recursion.
+ # @return [GamesDice::Probabilities] Probability distribution of dice.
+ def probabilities
+ return @probabilities if @probabilities
+
+ @bunch_multipliers.zip(@bunches).inject(GamesDice::Probabilities.new([1.0], @offset)) do |probs, mb|
+ m, b = mb
+ GamesDice::Probabilities.add_distributions_mult(1, probs, m, b.probabilities)
+ end
+ end
+
+ # @!attribute [r] explain_result
+ # @return [String,nil] Explanation of result, or nil if no call to #roll yet.
+ def explain_result
+ return nil unless @result
+
+ explanations = @bunches.map { |bunch| "#{bunch.label}: #{bunch.explain_result}" }
+
+ return @offset.to_s if explanations.count.zero?
+
+ return simple_explanation(explanations.first) if explanations.count == 1
+
+ bunch_values = @bunch_multipliers.zip(@bunches).map { |m, b| m * b.result }
+ bunch_values << @offset if @offset != 0
+ explanations << array_to_sum(bunch_values)
+ explanations.join('. ')
+ end
+
+ private
+
+ def simple_explanation(explanation)
+ return explanation if @offset.zero?
+
+ "#{explanation}. #{array_to_sum([@bunches[0].result, @offset])}"
+ end
+
+ def array_to_sum(array)
+ (numbers_to_strings(array) + ['=', array.inject(:+)]).join(' ')
+ end
+
+ def numbers_to_strings(array)
+ [array.first.to_s] + array.drop(1).map { |n| n.negative? ? "- #{n.abs}" : "+ #{n}" }
+ end
+
+ def bunches_weighted_sum(summed_method)
+ @bunch_multipliers.zip(@bunches).inject(0) do |total, mb|
+ m, b = mb
+ total + (m * b.send(summed_method))
+ end
+ end
+ end
+end