lib/games_dice/bunch.rb in games_dice-0.2.2 vs lib/games_dice/bunch.rb in games_dice-0.2.3
- old
+ new
@@ -1,128 +1,161 @@
-# models a set of identical dice, that can be "rolled" and combined into a simple integer result. The
-# dice are identical in number of sides, and any re-roll or mapping rules that apply to them
+# This class models a number of identical dice, which may be either GamesDice::Die or
+# GamesDice::ComplexDie objects.
+#
+# An object of this class represents a fixed number of indentical dice that may be rolled and their
+# values summed to make a total for the bunch.
+#
+# @example The ubiquitous '3d6'
+# d = GamesDice::Bunch.new( :ndice => 3, :sides => 6 )
+# d.roll # => 14
+# d.result # => 14
+# d.explain_result # => "2 + 6 + 6 = 14"
+# d.max # => 18
+#
+# @example Roll 5d10, and keep the best 2
+# d = GamesDice::Bunch.new( :ndice => 5, :sides => 10 , :keep_mode => :keep_best, :keep_number => 2 )
+# d.roll # => 18
+# d.result # => 18
+# d.explain_result # => "4, 9, 2, 9, 1. Keep: 9 + 9 = 18"
+#
+
class GamesDice::Bunch
- # attributes is a hash of symbols used to set attributes of the new Bunch object. Each
- # attribute is explained in more detail in its own section. The following hash keys and values
- # are mandatory:
- # :ndice
- # :sides
- # The following are optional, and modify the behaviour of the Bunch object
- # :name
- # :prng
- # :rerolls
- # :maps
- # :keep_mode
- # :keep_number
- # Any other keys provided to the constructor are ignored
- def initialize( attributes )
- @name = attributes[:name].to_s
- @ndice = Integer(attributes[:ndice])
+ # The constructor accepts parameters that are suitable for either GamesDice::Die or GamesDice::ComplexDie
+ # and decides which of those classes to instantiate.
+ # @param [Hash] options
+ # @option options [Integer] :ndice Number of dice in the bunch, *mandatory*
+ # @option options [Integer] :sides Number of sides on a single die in the bunch, *mandatory*
+ # @option options [String] :name Optional name for the bunch
+ # @option options [Array<GamesDice::RerollRule,Array>] :rerolls Optional rules that cause the die to roll again
+ # @option options [Array<GamesDice::MapRule,Array>] :maps Optional rules to convert a value into a final result for the die
+ # @option options [#rand] :prng Optional alternative source of randomness to Ruby's built-in #rand, passed to GamesDice::Die's constructor
+ # @option options [Symbol] :keep_mode Optional, either *:keep_best* or *:keep_worst*
+ # @option options [Integer] :keep_number Optional number of dice to keep when :keep_mode is not nil
+ # @return [GamesDice::Bunch]
+ def initialize( options )
+ @name = options[:name].to_s
+ @ndice = Integer(options[:ndice])
raise ArgumentError, ":ndice must be 1 or more, but got #{@ndice}" unless @ndice > 0
- @sides = Integer(attributes[:sides])
+ @sides = Integer(options[:sides])
raise ArgumentError, ":sides must be 1 or more, but got #{@sides}" unless @sides > 0
- options = Hash.new
+ attr = Hash.new
- if attributes[:prng]
+ if options[:prng]
# We deliberately do not clone this object, it will often be intended that it is shared
- prng = attributes[:prng]
+ prng = options[:prng]
raise ":prng does not support the rand() method" if ! prng.respond_to?(:rand)
end
needs_complex_die = false
- if attributes[:rerolls]
+ if options[:rerolls]
needs_complex_die = true
- options[:rerolls] = attributes[:rerolls].clone
+ attr[:rerolls] = options[:rerolls].clone
end
- if attributes[:maps]
+ if options[:maps]
needs_complex_die = true
- options[:maps] = attributes[:maps].clone
+ attr[:maps] = options[:maps].clone
end
if needs_complex_die
- options[:prng] = prng
- @single_die = GamesDice::ComplexDie.new( @sides, options )
+ attr[:prng] = prng
+ @single_die = GamesDice::ComplexDie.new( @sides, attr )
else
@single_die = GamesDice::Die.new( @sides, prng )
end
- case attributes[:keep_mode]
+ case options[:keep_mode]
when nil then
@keep_mode = nil
when :keep_best then
@keep_mode = :keep_best
- @keep_number = Integer(attributes[:keep_number] || 1)
+ @keep_number = Integer(options[:keep_number] || 1)
when :keep_worst then
@keep_mode = :keep_worst
- @keep_number = Integer(attributes[:keep_number] || 1)
+ @keep_number = Integer(options[:keep_number] || 1)
else
- raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{attributes[:keep_mode].inspect}"
+ raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{options[:keep_mode].inspect}"
end
end
- # the string name as provided to the constructor, it will appear in explain_result
+ # Name to help identify bunch
+ # @return [String]
attr_reader :name
- # integer number of dice to roll (initially, before re-rolls etc)
+ # Number of dice to roll
+ # @return [Integer]
attr_reader :ndice
- # individual die that will be rolled, #ndice times, an GamesDice::Die or GamesDice::ComplexDie object.
+ # Individual die from the bunch
+ # @return [GamesDice::Die,GamesDice::ComplexDie]
attr_reader :single_die
- # may be nil, :keep_best or :keep_worst
+ # Can be nil, :keep_best or :keep_worst
+ # @return [Symbol,nil]
attr_reader :keep_mode
- # number of "best" or "worst" results to select when #keep_mode is not nil. This attribute is
- # 1 by default if :keep_mode is supplied, or nil by default otherwise.
+ # Number of "best" or "worst" results to select when #keep_mode is not nil.
+ # @return [Integer,nil]
attr_reader :keep_number
- # after calling #roll, this is set to the final integer value from using the dice as specified
+ # Result of most-recent roll, or nil if no roll made yet.
+ # @return [Integer,nil]
attr_reader :result
- # Needs refinement. Returns best available string description of the bunch.
+ # @!attribute [r] label
+ # Description that will be used in explanations with more than one bunch
+ # @return [String]
def label
return @name if @name != ''
return @ndice.to_s + 'd' + @sides.to_s
end
- # either nil, or an array of GamesDice::RerollRule objects that are assessed on each roll of #single_die
- # Reroll types :reroll_new_die and :reroll_new_keeper do not affect the #single_die, but are instead
- # assessed in this container object
+ # @!attribute [r] rerolls
+ # Sequence of re-roll rules, or nil if re-rolls are not required.
+ # @return [Array<GamesDice::RerollRule>, nil]
def rerolls
@single_die.rerolls
end
- # either nil, or an array of GamesDice::MapRule objects that are assessed on each result of #single_die (after rerolls are completed)
+ # @!attribute [r] maps
+ # Sequence of map rules, or nil if mapping is not required.
+ # @return [Array<GamesDice::MapRule>, nil]
def maps
- @single_die.rerolls
+ @single_die.maps
end
- # after calling #roll, this is an array of GamesDice::DieResult objects, one from each #single_die rolled,
+ # @!attribute [r] result_details
+ # After calling #roll, this is an array of GamesDice::DieResult objects. There is one from each #single_die rolled,
# allowing inspection of how the result was obtained.
+ # @return [Array<GamesDice::DieResult>, nil] Sequence of GamesDice::DieResult objects.
def result_details
return nil unless @raw_result_details
@raw_result_details.map { |r| r.is_a?(Fixnum) ? GamesDice::DieResult.new(r) : r }
end
- # minimum possible integer value
+ # @!attribute [r] min
+ # Minimum possible result from a call to #roll
+ # @return [Integer]
def min
n = @keep_mode ? [@keep_number,@ndice].min : @ndice
return n * @single_die.min
end
- # maximum possible integer value
+ # @!attribute [r] max
+ # Maximum possible result from a call to #roll
+ # @return [Integer]
def max
n = @keep_mode ? [@keep_number,@ndice].min : @ndice
return n * @single_die.max
end
- # returns a hash of value (Integer) => probability (Float) pairs. Warning: Some dice schemes
- # cause this method to take a long time, and use a lot of memory. The worst-case offenders are
- # dice schemes with a #keep_mode of :keep_best or :keep_worst.
+ # Calculates the probability distribution for the bunch. When the bunch is composed of dice with
+ # open-ended re-roll rules, there are some arbitrary limits imposed to prevent large amounts of
+ # recursion.
+ # @return [GamesDice::Probabilities] Probability distribution of bunch.
def probabilities
return @probabilities if @probabilities
@probabilities_complete = true
# TODO: It is possible to optimise this slightly by combining already-calculated values
@@ -156,11 +189,12 @@
@probabilities_min, @probabilities_max = combined_probs.keys.minmax
@probabilities = GamesDice::Probabilities.new( combined_probs )
end
- # simulate dice roll according to spec. Returns integer final total, and also stores it in #result
+ # Simulates rolling the bunch of identical dice
+ # @return [Integer] Sum of all rolled dice, or sum of all keepers
def roll
@result = 0
@raw_result_details = []
@ndice.times do
@@ -182,9 +216,12 @@
end
@result = use_dice.inject(0) { |so_far, die_result| so_far + die_result }
end
+ # @!attribute [r] explain_result
+ # Explanation of result, or nil if no call to #roll yet.
+ # @return [String,nil]
def explain_result
return nil unless @result
explanation = ''