class FifthedSim::Distribution

Models a probabilistic distribution.

Constants

COMPARE_EPSILON

Attributes

max[R]
min[R]
total_possible[R]

Public Class Methods

for(obj) click to toggle source
# 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
for_number(num) click to toggle source

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
for_range(rng) click to toggle source
# 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
new(map) click to toggle source

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

==(other) click to toggle source
# 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
average() click to toggle source
# File lib/fifthed_sim/distribution.rb, line 55
def average
  map.map{|k, v| k * v}.inject(:+)
end
convolve(other) click to toggle source
# 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
convolve_divide(other) click to toggle source

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
convolve_greater(other) click to toggle source
# 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
convolve_least(other) click to toggle source
# 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
convolve_multiply(other) click to toggle source
# 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
convolve_subtract(other) click to toggle source

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
hit_when(other, &block) click to toggle source

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
map() click to toggle source
# File lib/fifthed_sim/distribution.rb, line 51
def map
  @map.dup
end
percent_exactly(num) click to toggle source
# File lib/fifthed_sim/distribution.rb, line 123
def percent_exactly(num)
  return 0 if num < @min || num > @max
  @map[num] || 0
end
percent_greater(n) click to toggle source
# 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
percent_greater_equal(num) click to toggle source
# File lib/fifthed_sim/distribution.rb, line 157
def percent_greater_equal(num)
  percent_greater(num - 1)
end
percent_lower(n) click to toggle source
# 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
percent_lower_equal(num) click to toggle source
# File lib/fifthed_sim/distribution.rb, line 153
def percent_lower_equal(num)
  percent_lower(num + 1)
end
Also aliased as: percentile_of
percent_where(&block) click to toggle source
# 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
percent_within(range) click to toggle source
# File lib/fifthed_sim/distribution.rb, line 112
def percent_within(range)
  percent_where{|x| range.contains? x}
end
percentile_of(num)
Alias for: percent_lower_equal
range() click to toggle source
# File lib/fifthed_sim/distribution.rb, line 47
def range
  (@min..@max)
end
results_when(&block) click to toggle source

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
std_dev() click to toggle source
# File lib/fifthed_sim/distribution.rb, line 135
def std_dev
  Math.sqrt(variance)
end
text_histogram(cols = 60) click to toggle source
# 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
variance() click to toggle source
# 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

map_proc() click to toggle source
# File lib/fifthed_sim/distribution.rb, line 266
def map_proc
  return Proc.new do |arg|
    @map[arg]
  end
end