Class: FifthedSim::Distribution
- Inherits:
-
Object
- Object
- FifthedSim::Distribution
- Defined in:
- lib/fifthed_sim/distribution.rb
Overview
Models a probabilistic distribution.
Constant Summary collapse
- COMPARE_EPSILON =
0.00001
Instance Attribute Summary collapse
-
#max ⇒ Object
readonly
Returns the value of attribute max.
-
#min ⇒ Object
readonly
Returns the value of attribute min.
-
#total_possible ⇒ Object
readonly
Returns the value of attribute total_possible.
Class Method Summary collapse
- .for(obj) ⇒ Object
-
.for_number(num) ⇒ Object
Get a distrubtion for a number.
- .for_range(rng) ⇒ Object
Instance Method Summary collapse
- #==(other) ⇒ Object
- #average ⇒ Object
- #convolve(other) ⇒ Object
-
#convolve_divide(other) ⇒ Object
Get the distribution of a result from this distribution divided by one from another distribution.
- #convolve_greater(other) ⇒ Object
- #convolve_least(other) ⇒ Object
- #convolve_multiply(other) ⇒ Object
-
#convolve_subtract(other) ⇒ Object
TODO: Optimize this.
-
#hit_when(other, &block) ⇒ Object
Obtain a new distribution of values.
-
#initialize(map) ⇒ Distribution
constructor
We initialize class with a map of results to occurences, and a total number of possible different occurences.
- #map ⇒ Object
- #percent_exactly(num) ⇒ Object
- #percent_greater(n) ⇒ Object
- #percent_greater_equal(num) ⇒ Object
- #percent_lower(n) ⇒ Object
- #percent_lower_equal(num) ⇒ Object (also: #percentile_of)
- #percent_where(&block) ⇒ Object
- #percent_within(range) ⇒ Object
- #range ⇒ Object
-
#results_when(&block) ⇒ Object
Takes a block or callable object.
- #std_dev ⇒ Object
- #text_histogram(cols = 60) ⇒ Object
- #variance ⇒ Object
Constructor Details
#initialize(map) ⇒ Distribution
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.
34 35 36 37 38 39 40 |
# 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 |
Instance Attribute Details
#max ⇒ Object (readonly)
Returns the value of attribute max.
42 43 44 |
# File 'lib/fifthed_sim/distribution.rb', line 42 def max @max end |
#min ⇒ Object (readonly)
Returns the value of attribute min.
42 43 44 |
# File 'lib/fifthed_sim/distribution.rb', line 42 def min @min end |
#total_possible ⇒ Object (readonly)
Returns the value of attribute total_possible.
42 43 44 |
# File 'lib/fifthed_sim/distribution.rb', line 42 def total_possible @total_possible end |
Class Method Details
.for(obj) ⇒ Object
20 21 22 23 24 25 26 27 28 29 |
# 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) ⇒ Object
Get a distrubtion for a number. This will be a uniform distribution with P = 1 at this number and P = 0 elsewhere.
10 11 12 |
# File 'lib/fifthed_sim/distribution.rb', line 10 def self.for_number(num) self.new({num => 1.0}) end |
.for_range(rng) ⇒ Object
14 15 16 17 18 |
# 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 |
Instance Method Details
#==(other) ⇒ Object
246 247 248 249 250 251 252 253 254 |
# 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 ⇒ Object
55 56 57 |
# File 'lib/fifthed_sim/distribution.rb', line 55 def average map.map{|k, v| k * v}.inject(:+) end |
#convolve(other) ⇒ Object
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
# 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) ⇒ Object
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.
198 199 200 201 202 203 204 205 206 207 208 209 |
# 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) ⇒ Object
222 223 224 225 226 227 228 229 230 231 232 |
# 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) ⇒ Object
234 235 236 237 238 239 240 241 242 243 |
# 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) ⇒ Object
211 212 213 214 215 216 217 218 219 |
# 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) ⇒ Object
TODO: Optimize this
184 185 186 187 188 189 190 191 192 |
# 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) ⇒ Object
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
66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# 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 ⇒ Object
51 52 53 |
# File 'lib/fifthed_sim/distribution.rb', line 51 def map @map.dup end |
#percent_exactly(num) ⇒ Object
123 124 125 126 |
# 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) ⇒ Object
146 147 148 149 150 151 |
# 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) ⇒ Object
157 158 159 |
# File 'lib/fifthed_sim/distribution.rb', line 157 def percent_greater_equal(num) percent_greater(num - 1) end |
#percent_lower(n) ⇒ Object
139 140 141 142 143 144 |
# 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) ⇒ Object Also known as: percentile_of
153 154 155 |
# File 'lib/fifthed_sim/distribution.rb', line 153 def percent_lower_equal(num) percent_lower(num + 1) end |
#percent_where(&block) ⇒ Object
116 117 118 119 120 121 |
# 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) ⇒ Object
112 113 114 |
# File 'lib/fifthed_sim/distribution.rb', line 112 def percent_within(range) percent_where{|x| range.contains? x} end |
#range ⇒ Object
47 48 49 |
# File 'lib/fifthed_sim/distribution.rb', line 47 def range (@min..@max) end |
#results_when(&block) ⇒ Object
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
100 101 102 103 104 105 106 107 108 109 110 |
# 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 ⇒ Object
135 136 137 |
# File 'lib/fifthed_sim/distribution.rb', line 135 def std_dev Math.sqrt(variance) end |
#text_histogram(cols = 60) ⇒ Object
256 257 258 259 260 261 262 263 |
# 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 ⇒ Object
128 129 130 131 132 133 |
# File 'lib/fifthed_sim/distribution.rb', line 128 def variance avg = average @map.map do |k, v| ((k - avg)**2) * v end.inject(:+) end |