class FifthedSim::Distribution
Models a probabilistic distribution.
Constants
- COMPARE_EPSILON
Attributes
Public Class Methods
# File lib/fifthed_sim/distribution.rb, line 20 def self.for(obj) case obj when Fixnum self.for_number(obj) when Range self.for_range(obj) else raise ArgumentError, "can't amke a distribution for that" end end
Get a distrubtion for a number. This will be a uniform distribution with P = 1 at this number and P = 0 elsewhere.
# File lib/fifthed_sim/distribution.rb, line 10 def self.for_number(num) self.new({num => 1.0}) end
# File lib/fifthed_sim/distribution.rb, line 14 def self.for_range(rng) size = rng.size.to_f e = 1.0 / size self.new(Hash[rng.map{|x| [x, e]}]) end
We initialize class with a map of results to occurences, and a total number of possible different occurences. Generally, you will not ever initialize this yourself.
# File lib/fifthed_sim/distribution.rb, line 34 def initialize(map) keys = map.keys @max = keys.max @min = keys.min @map = map.dup @map.default = 0 end
Public Instance Methods
# File lib/fifthed_sim/distribution.rb, line 246 def ==(other) omap = other.map max_possible = (@max / other.min) same_keys = (Set.new(@map.keys) == Set.new(omap.keys)) same_vals = @map.keys.each do |k| (@map[k] - other.map[k]).abs <= COMPARE_EPSILON end same_keys && same_vals end
# File lib/fifthed_sim/distribution.rb, line 55 def average map.map{|k, v| k * v}.inject(:+) end
# File lib/fifthed_sim/distribution.rb, line 166 def convolve(other) h = {} abs_min = [@min, other.min].min abs_max = [@max, other.max].max min_possible = @min + other.min max_possible = @max + other.max # TODO: there has to be a less stupid way to do this right? v = min_possible.upto(max_possible).map do |val| sum = abs_min.upto(abs_max).map do |m| percent_exactly(m) * other.percent_exactly(val - m) end.inject(:+) [val, sum] end self.class.new(Hash[v]) end
Get the distribution of a result from this distribution divided by one from another distribution. If the other distribution may contain zero this will break horribly.
# File lib/fifthed_sim/distribution.rb, line 198 def convolve_divide(other) throw ArgumentError, "Divisor may be zero" if other.min < 1 h = Hash.new{|h, k| h[k] = 0} # We can do this faster using a sieve, but be lazy for now # TODO: Be less lazy range.each do |v1| other.range.each do |v2| h[v1 / v2] += percent_exactly(v1) * other.percent_exactly(v2) end end self.class.new(h) end
# File lib/fifthed_sim/distribution.rb, line 222 def convolve_greater(other) h = Hash.new{|h, k| h[k] = 0} # for each value range.each do |s| (s..other.max).each do |e| h[e] += (other.percent_exactly(e) * percent_exactly(s)) end h[s] += (other.percent_lower(s) * percent_exactly(s)) end self.class.new(h) end
# File lib/fifthed_sim/distribution.rb, line 234 def convolve_least(other) h = Hash.new{|h, k| h[k] = 0} range.each do |s| (other.min..s).each do |e| h[e] += (other.percent_exactly(e) * percent_exactly(s)) end h[s] += (other.percent_greater(s + 1) * percent_exactly(s)) end self.class.new(h) end
# File lib/fifthed_sim/distribution.rb, line 211 def convolve_multiply(other) h = Hash.new{|h, k| h[k] = 0} range.each do |v1| other.range.each do |v2| h[v1 * v2] += percent_exactly(v1) * other.percent_exactly(v2) end end self.class.new(h) end
TODO: Optimize this
# File lib/fifthed_sim/distribution.rb, line 184 def convolve_subtract(other) h = Hash.new{|h, k| h[k] = 0} range.each do |v1| other.range.each do |v2| h[v1 - v2] += percent_exactly(v1) * other.percent_exactly(v2) end end self.class.new(h) end
Obtain a new distribution of values. When block.call(value) for this distribution is true, we will allow values from the second distribution. Otherwise, the value will be zero.
This is mostly used in hit calculation - AKA, if we're higher than an AC, then we hit, otherwise we do zero damage
# File lib/fifthed_sim/distribution.rb, line 66 def hit_when(other, &block) hit_prob = map.map do |k, v| if block.call(k) v else nil end end.compact.inject(:+) miss_prob = 1 - hit_prob omap = other.map h = Hash[omap.map{|k, v| [k, v * hit_prob]}] h[0] = (h[0] || 0) + miss_prob Distribution.new(h) end
# File lib/fifthed_sim/distribution.rb, line 51 def map @map.dup end
# File lib/fifthed_sim/distribution.rb, line 123 def percent_exactly(num) return 0 if num < @min || num > @max @map[num] || 0 end
# File lib/fifthed_sim/distribution.rb, line 146 def percent_greater(n) num = n + 1 return 0.0 if num > @max return 1.0 if num < @min num.upto(@max).map(&map_proc).inject(:+) end
# File lib/fifthed_sim/distribution.rb, line 157 def percent_greater_equal(num) percent_greater(num - 1) end
# File lib/fifthed_sim/distribution.rb, line 139 def percent_lower(n) num = n - 1 return 0.0 if num < @min return 1.0 if num > @max @min.upto(num).map(&map_proc).inject(:+) end
# File lib/fifthed_sim/distribution.rb, line 153 def percent_lower_equal(num) percent_lower(num + 1) end
# File lib/fifthed_sim/distribution.rb, line 116 def percent_where(&block) @map.to_a .keep_if{|(k, v)| block.call(k)} .map{|(k, v)| v} .inject(:+) end
# File lib/fifthed_sim/distribution.rb, line 112 def percent_within(range) percent_where{|x| range.contains? x} end
# File lib/fifthed_sim/distribution.rb, line 47 def range (@min..@max) end
Takes a block or callable object. This function will call the callable with all possible outcomes of this distribution. The callable should return another distribution, representing the possible values when this possibility happens. This will then return a value of those possibilities.
An example is probably helpful here. Let's consider the case where a monster with +0 to hit is attacking a creature with AC 16 for 1d4 damage, and crits on a 20. If we want a distribution of possible outcomes of this attack, we can do:
1.d(20).distribution.results_when do |x| if x < 16 Distribution.for_number(0) elseif x < 20 1.d(4).distribution else 2.d(4).distribution end end
# File lib/fifthed_sim/distribution.rb, line 100 def results_when(&block) h = Hash.new{|h, k| h[k] = 0} range.each do |v| prob = @map[v] o_dist = block.call(v) o_dist.map.each do |k, v| h[k] += (v * prob) end end Distribution.new(h) end
# File lib/fifthed_sim/distribution.rb, line 135 def std_dev Math.sqrt(variance) end
# File lib/fifthed_sim/distribution.rb, line 256 def text_histogram(cols = 60) max_width = @max.to_s.length justwidth = max_width + 1 linewidth = (cols - justwidth) range.map do |v| "#{v}:".rjust(justwidth) + ("*" * (percent_exactly(v) * linewidth)) end.join("\n") end
# File lib/fifthed_sim/distribution.rb, line 128 def variance avg = average @map.map do |k, v| ((k - avg)**2) * v end.inject(:+) end
Private Instance Methods
# File lib/fifthed_sim/distribution.rb, line 266 def map_proc return Proc.new do |arg| @map[arg] end end