lib/games_dice/probabilities.rb in games_dice-0.2.4 vs lib/games_dice/probabilities.rb in games_dice-0.3.0

- old
+ new

@@ -216,39 +216,118 @@ end end GamesDice::Probabilities.new( new_probs, combined_min ) end - # Adds a distribution to itself repeatedly, to simulate a number of dice # results being summed. - # @param [GamesDice::Probabilities] pd Distribution to repeat # @param [Integer] n Number of repetitions, must be at least 1 - # @return [GamesDice::Probabilities] - def self.repeat_distribution pd, n + # @return [GamesDice::Probabilities] new distribution + def repeat_sum n n = Integer( n ) raise "Cannot combine probabilities less than once" if n < 1 revbin = n.to_s(2).reverse.each_char.to_a.map { |c| c == '1' } - pd_power = pd + pd_power = self pd_result = nil max_power = revbin.count - 1 revbin.each_with_index do |use_power, i| if use_power if pd_result - pd_result = add_distributions( pd_result, pd_power ) + pd_result = GamesDice::Probabilities.add_distributions( pd_result, pd_power ) else pd_result = pd_power end end - pd_power = add_distributions( pd_power, pd_power ) unless i == max_power + pd_power = GamesDice::Probabilities.add_distributions( pd_power, pd_power ) unless i == max_power end pd_result end + # Calculates distribution generated by summing best k results of n iterations + # of the distribution. + # @param [Integer] n Number of repetitions, must be at least 1 + # @param [Integer] k Number of best results to keep and sum + # @return [GamesDice::Probabilities] new distribution + def repeat_n_sum_k n, k, kmode = :keep_best + n = Integer( n ) + k = Integer( k ) + raise "Cannot combine probabilities less than once" if n < 1 + if k >= n + return repeat_sum( n ) + end + new_probs = Array.new( @probs.count * k, 0.0 ) + new_offset = @offset * k + d = n - k + + each do | q, p_maybe | + next unless p_maybe > 0.0 + + # keep_distributions is array of Probabilities, indexed by number of keepers > q, which is in 0...k + keep_distributions = calc_keep_distributions( k, q, kmode ) + p_table = calc_p_table( q, p_maybe, kmode ) + + (0...k).each do |n| + keepers = [2] * n + [1] * (k-n) + p_so_far = keepers.inject(1.0) { |p,idx| p * p_table[idx] } + next unless p_so_far > 0.0 + (0..d).each do |dn| + discards = [1] * (d-dn) + [0] * dn + sequence = keepers + discards + p_sequence = discards.inject( p_so_far ) { |p,idx| p * p_table[idx] } + next unless p_sequence > 0.0 + p_sequence *= GamesDice::Combinations.count_variations( sequence ) + kd = keep_distributions[n] + kd.each { |r,p_r| new_probs[r-new_offset] += p_r * p_sequence } + end + end + end + GamesDice::Probabilities.new( new_probs, new_offset ) + end + private + def calc_keep_distributions k, q, kmode + if kmode == :keep_best + keep_distributions = [ GamesDice::Probabilities.new( [1.0], q * k ) ] + if p_gt(q) > 0.0 && k > 1 + kd_probabilities = given_ge( q + 1 ) + (1...k).each do |n| + extra_o = GamesDice::Probabilities.new( [1.0], q * ( k - n ) ) + n_probs = kd_probabilities.repeat_sum( n ) + keep_distributions[n] = GamesDice::Probabilities.add_distributions( extra_o, n_probs ) + end + end + elsif kmode == :keep_worst + keep_distributions = [ GamesDice::Probabilities.new( [1.0], q * k ) ] + if p_lt(q) > 0.0 && k > 1 + kd_probabilities = given_le( q - 1 ) + (1...k).each do |n| + extra_o = GamesDice::Probabilities.new( [1.0], q * ( k - n ) ) + n_probs = kd_probabilities.repeat_sum( n ) + keep_distributions[n] = GamesDice::Probabilities.add_distributions( extra_o, n_probs ) + end + end + else + raise "Keep mode #{kmode.inspect} not recognised" + end + keep_distributions + end + + def calc_p_table q, p_maybe, kmode + if kmode == :keep_best + p_kept = p_gt(q) + p_rejected = p_lt(q) + elsif kmode == :keep_worst + p_kept = p_lt(q) + p_rejected = p_gt(q) + else + raise "Keep mode #{kmode.inspect} not recognised" + end + [ p_rejected, p_maybe, p_kept ] + end + # Convert hash to array,offset notation def self.prob_h_to_ao h rmin,rmax = h.keys.minmax o = rmin a = Array.new( 1 + rmax - rmin, 0.0 ) @@ -268,5 +347,57 @@ @probs.each_with_index { |v,i| total += (i+@offset)*v } total end end # class GamesDice::Probabilities + +# Helper module with optimised Ruby for counting variations of arrays, such as those returned by +# Array#repeated_combination +# +# @example How many ways can [3,3,6] be arranged? +# GamesDice::Combinations.count_variations( [3,3,6] ) +# => 3 +# +# @example When prob( a ) and result( a ) are same for any arrangement of Array a +# items = [1,2,3,4,5,6] +# items.repeated_combination(5).each do |a| +# this_result = result( a ) +# this_prob = prob( a ) * GamesDice::Combinations.count_variations( a ) +# # Do something useful with this knowledge! E.g. save it to probability array. +# end +# +module GamesDice::Combinations + @@variations_cache = {} + @@factorial_cache = [1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800] + + # Counts variations of an array. A unique variation is an arrangement of the array elements which + # is detectably different (using ==) from any other. So [1,1,1] has only 1 unique arrangement, + # but [1,2,3] has 6 possibilities. + # @param [Array] array List of things that can be arranged + # @return [Integer] Number of unique arrangements + def self.count_variations array + all_count = array.count + group_sizes = group_counts( array ) + cache_key = all_count.to_s + ":" + group_sizes.join(',') + @@variations_cache[cache_key] ||= variations_of( all_count, group_sizes ) + end + + private + + def self.variations_of all_count, groups + all_arrangements = factorial( all_count ) + # The reject is an optimisation to avoid calculating and multplying by factorial(1) (==1) + identical_arrangements = groups.reject {|x| x==1 }.inject(1) { |prod,g| prod * factorial(g) } + all_arrangements/identical_arrangements + end + + # Returns counts of unique items in array e.g. [8,8,8,7,6,6] returns [1,2,3] + # Sort is for caching + def self.group_counts array + array.group_by {|x| x}.values.map {|v| v.count}.sort + end + + def self.factorial n + # Can start range from 2 because we have pre-cached the result for n=1 + @@factorial_cache[n] ||= (2..n).inject(:*) + end +end