<commit_before>module GamesDice

  # 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 rolling rules that apply to them
  class 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])
      raise ArgumentError, ":ndice must be 1 or more, but got #{@ndice}" unless @ndice > 0
      @sides = Integer(attributes[:sides])
      raise ArgumentError, ":sides must be 1 or more, but got #{@sides}" unless @sides > 0

      options = Hash.new

      if attributes[:prng]
        # We deliberately do not clone this object, it will often be intended that it is shared
        prng = attributes[:prng]
        raise ":prng does not support the rand() method" if ! prng.respond_to?(:rand)
      end

      needs_complex_die = false

      if attributes[:rerolls]
        needs_complex_die = true
        options[:rerolls] = attributes[:rerolls].clone
      end

      if attributes[:maps]
        needs_complex_die = true
        options[:maps] = attributes[:maps].clone
      end

      if needs_complex_die
        options[:prng] = prng
        @single_die = GamesDice::ComplexDie.new( @sides, options )
      else
        @single_die = GamesDice::Die.new( @sides, prng )
      end

      case attributes[:keep_mode]
      when nil then
        @keep_mode = nil
      when :keep_best then
        @keep_mode = :keep_best
        @keep_number = Integer(attributes[:keep_number] || 1)
      when :keep_worst then
        @keep_mode = :keep_worst
        @keep_number = Integer(attributes[:keep_number] || 1)
      else
        raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{attributes[:keep_mode].inspect}"
      end
    end

    # the string name as provided to the constructor, it will appear in explain_result
    attr_reader :name

    # integer number of dice to roll (initially, before re-rolls etc)
    attr_reader :ndice

    # individual die that will be rolled, #ndice times, an GamesDice::Die or GamesDice::ComplexDie object.
    attr_reader :single_die

    # may be nil, :keep_best or :keep_worst
    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.
    attr_reader :keep_number

    # after calling #roll, this is set to the final integer value from using the dice as specified
    attr_reader :result

    # 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
    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)
    def maps
      @single_die.rerolls
    end

    # after calling #roll, this is an array of GamesDice::DieResult objects, one from each #single_die rolled,
    # allowing inspection of how the result was obtained.
    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
    def min
      n = @keep_mode ? [@keep_number,@ndice].min : @ndice
      return n * @single_die.min
    end

    # maximum possible integer value
    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. For efficiency with re-rolls, the calculation may cut
    # short based on depth of recursion or closeness to total 1.0 probability. Therefore low probabilities
    # (less than one in a billion) in open-ended re-rolls are not always represented in the hash.
    def probabilities
      return @probabilities if @probabilities
      @probabilities_complete = true

      # TODO: It is possible to optimise this slightly by combining already-calculated values
      # Adding dice is same as multiplying probability sets for that number of dice
      # Combine(probabililities_3_dice, probabililities_single_die) == Combine(probabililities_2_dice, probabililities_2_dice)
      # It is possible to minimise the total number of multiplications, gaining about 30% efficiency, with careful choices
      single_roll_probs = @single_die.probabilities<commit_msg>Add speed warning to probabilities documentation<commit_after>module GamesDice

  # 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 rolling rules that apply to them
  class 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])
      raise ArgumentError, ":ndice must be 1 or more, but got #{@ndice}" unless @ndice > 0
      @sides = Integer(attributes[:sides])
      raise ArgumentError, ":sides must be 1 or more, but got #{@sides}" unless @sides > 0

      options = Hash.new

      if attributes[:prng]
        # We deliberately do not clone this object, it will often be intended that it is shared
        prng = attributes[:prng]
        raise ":prng does not support the rand() method" if ! prng.respond_to?(:rand)
      end

      needs_complex_die = false

      if attributes[:rerolls]
        needs_complex_die = true
        options[:rerolls] = attributes[:rerolls].clone
      end

      if attributes[:maps]
        needs_complex_die = true
        options[:maps] = attributes[:maps].clone
      end

      if needs_complex_die
        options[:prng] = prng
        @single_die = GamesDice::ComplexDie.new( @sides, options )
      else
        @single_die = GamesDice::Die.new( @sides, prng )
      end

      case attributes[:keep_mode]
      when nil then
        @keep_mode = nil
      when :keep_best then
        @keep_mode = :keep_best
        @keep_number = Integer(attributes[:keep_number] || 1)
      when :keep_worst then
        @keep_mode = :keep_worst
        @keep_number = Integer(attributes[:keep_number] || 1)
      else
        raise ArgumentError, ":keep_mode can be nil, :keep_best or :keep_worst. Got #{attributes[:keep_mode].inspect}"
      end
    end

    # the string name as provided to the constructor, it will appear in explain_result
    attr_reader :name

    # integer number of dice to roll (initially, before re-rolls etc)
    attr_reader :ndice

    # individual die that will be rolled, #ndice times, an GamesDice::Die or GamesDice::ComplexDie object.
    attr_reader :single_die

    # may be nil, :keep_best or :keep_worst
    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.
    attr_reader :keep_number

    # after calling #roll, this is set to the final integer value from using the dice as specified
    attr_reader :result

    # 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
    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)
    def maps
      @single_die.rerolls
    end

    # after calling #roll, this is an array of GamesDice::DieResult objects, one from each #single_die rolled,
    # allowing inspection of how the result was obtained.
    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
    def min
      n = @keep_mode ? [@keep_number,@ndice].min : @ndice
      return n * @single_die.min
    end

    # maximum possible integer value
    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.
    def probabilities
      return @probabilities if @probabilities
      @probabilities_complete = true

      # TODO: It is possible to optimise this slightly by combining already-calculated values
      # Adding dice is same as multiplying probability sets for that number of dice
      # Combine(probabililities_3_dice, probabililities_single_die) == Combine(probabililities_2_dice, probabililities_2_dice)
      # It is possible to minimise the total number of multiplications, gaining about 30% efficiency, with careful choices
      single_roll_probs = @single_die.probabilities
{'fraction_non_alphanumeric': 0.07054516671613734, 'fraction_numerical': 0.0025724745226080935, 'mean_word_length': 3.006341656757828, 'pattern_counts': {'":': 8, '<': 3, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1271649', 'n_tokens_mistral': 2938, 'n_tokens_neox': 2843, 'n_words': 1126}