<commit_before># frozen_string_literal: true

module GamesDice
  # @!visibility private
  # Private extension methods for GamesDice::ComplexDie
  module ComplexDieHelpers
    private

    def recursive_probabilities(probabilities = {}, prior_probability = 1.0, depth = 0, prior_result = nil, rerolls_left = nil, roll_reason = :basic, subtracting = false)
      each_probability = prior_probability / @basic_die.sides
      depth += 1
      if depth >= 20 || each_probability < 1.0e-16
        @probabilities_complete = false
        stop_recursing = true
      end

      @basic_die.each_value do |v|
        recurse_probs_for_value(v, roll_reason, probabilities, each_probability, depth, prior_result, rerolls_left,
                                subtracting, stop_recursing)
      end
      probabilities
    end

    def recurse_probs_for_value(v, roll_reason, probabilities, each_probability, depth, prior_result, rerolls_left, subtracting, stop_recursing)
      # calculate value, recurse if there is a reroll
      result_so_far, rerolls_remaining = calc_result_so_far(prior_result, rerolls_left, v, roll_reason)

      # Find which rule, if any, is being triggered
      rule_idx = find_matching_reroll_rule(v, result_so_far.rolls.length, rerolls_remaining)

      if rule_idx && !stop_recursing
        recurse_probs_with_rule(probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule_idx,
                                subtracting)
      else
        t = result_so_far.total
        probabilities[t] ||= 0.0
        probabilities[t] += each_probability
      end
    end

    def recurse_probs_with_rule(probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule_idx, subtracting)
      rule = @rerolls[rule_idx]
      rerolls_remaining[rule_idx] -= 1
      is_subtracting = true if subtracting || rule.type == :reroll_subtract

      # Apply the rule (note reversal for additions, after a subtract)
      if subtracting && rule.type == :reroll_add
        recursive_probabilities probabilities, each_probability, depth, result_so_far, rerolls_remaining,
                                :reroll_subtract, is_subtracting
      else
        recursive_probabilities probabilities, each_probability, depth, result_so_far, rerolls_remaining, rule.type,
                                is_subtracting
      end
    end

    def calc_result_so_far(prior_result, rerolls_left, v, roll_reason)
      if prior_result
        result_so_far = prior_result.clone
        result_so_far.add_roll(v, roll_reason)
        rerolls_remaining = rerolls_left.clone
      else
        result_so_far = GamesDice::DieResult.new(v, roll_reason)
        rerolls_remaining = @rerolls.map(&:limit)
      end
      [result_so_far, rerolls_remaining]
    end
  end
end
<commit_msg>Combine recursion stack variables into a single struct<commit_after># frozen_string_literal: true

module GamesDice
  # @!visibility private
  # Private extension methods for GamesDice::ComplexDie
  module ComplexDieHelpers
    private

    RecurseStack = Struct.new(:depth, :roll_reason, :subtracting, :probabilities, :prior_probability, :prior_result,
                              :rerolls_left) do
      def initialize
        self.depth = 0
        self.roll_reason = :basic
        self.subtracting = false
        self.probabilities = {}
        self.prior_probability = 1.0
      end
    end

    def recursive_probabilities(stack = RecurseStack.new)
      stack.prior_probability = stack.prior_probability / @basic_die.sides
      stack.depth += 1

      @basic_die.each_value do |die_val|
        recurse_probs_for_value(die_val, stack)
      end
      stack.probabilities
    end

    def recurse_probs_for_value(die_val, stack)
      result_so_far, rerolls_remaining = calc_result_so_far(die_val, stack)
      rule_idx = find_matching_reroll_rule(die_val, result_so_far.rolls.length, rerolls_remaining)

      if conintue_recursing?(stack, rule_idx)
        continue_recursion(stack, result_so_far, rerolls_remaining, rule_idx)
      else
        end_recursion_store_probs(stack, result_so_far)
      end
    end

    def conintue_recursing?(stack, rule_idx)
      if stack.depth >= 20 || stack.prior_probability < 1.0e-16
        @probabilities_complete = false
        return false
      end

      !rule_idx.nil?
    end

    def continue_recursion(stack, result_so_far, rerolls_remaining, rule_idx)
      rule = @rerolls[rule_idx]
      rerolls_remaining[rule_idx] -= 1
      recurse_probs_with_rule(stack, result_so_far, rerolls_remaining, rule)
    end

    def end_recursion_store_probs(stack, result_so_far)
      t = result_so_far.total
      stack.probabilities[t] ||= 0.0
      stack.probabilities[t] += stack.prior_probability
    end

    def recurse_probs_with_rule(stack, result_so_far, rerolls_remaining, rule)
      next_stack = stack.clone
      next_stack.prior_result = result_so_far
      next_stack.rerolls_left = rerolls_remaining
      next_stack.subtracting = true if stack.subtracting || rule.type == :reroll_subtract

      # Apply the rule (note reversal for additions, after a subtract)
      next_stack.roll_reason = if stack.subtracting && rule.type == :reroll_add
                                 :reroll_subtract
                               else
                                 rule.type
                               end

      recursive_probabilities next_stack
    end

    def calc_result_so_far(die_val, stack)
      if stack.prior_result
        result_so_far = stack.prior_result.clone
        rerolls_remaining = stack.rerolls_left.clone
        result_so_far.add_roll(die_val, stack.roll_reason)
      else
        rerolls_remaining = @rerolls.map(&:limit)
        result_so_far = GamesDice::DieResult.new(die_val, stack.roll_reason)
      end
      [result_so_far, rerolls_remaining]
    end
  end
end

{'fraction_non_alphanumeric': 0.06836004091374019, 'fraction_numerical': 0.004432321854756222, 'mean_word_length': 2.8573307034845494, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '914871', 'n_tokens_mistral': 1940, 'n_tokens_neox': 1831, 'n_words': 374}