fifthed_sim 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +0 -1
- data/HelpWanted.md +23 -0
- data/README.md +144 -5
- data/bin/console +2 -8
- data/bin/playground +42 -0
- data/exe/diceroll +9 -0
- data/fifthed_sim.gemspec +4 -0
- data/lib/fifthed_sim/actor.rb +80 -0
- data/lib/fifthed_sim/attack.rb +86 -0
- data/lib/fifthed_sim/calculated_fixnum.rb +57 -0
- data/lib/fifthed_sim/compiler/parser.rb +46 -0
- data/lib/fifthed_sim/compiler/transform.rb +28 -0
- data/lib/fifthed_sim/compiler.rb +51 -0
- data/lib/fifthed_sim/damage.rb +54 -0
- data/lib/fifthed_sim/damage_types.rb +39 -0
- data/lib/fifthed_sim/dice_expression.rb +134 -0
- data/lib/fifthed_sim/distribution.rb +219 -20
- data/lib/fifthed_sim/nodes/addition_node.rb +75 -0
- data/lib/fifthed_sim/nodes/block_node.rb +46 -0
- data/lib/fifthed_sim/nodes/division_node.rb +26 -0
- data/lib/fifthed_sim/nodes/greater_node.rb +41 -0
- data/lib/fifthed_sim/nodes/less_node.rb +46 -0
- data/lib/fifthed_sim/nodes/multi_node.rb +135 -0
- data/lib/fifthed_sim/nodes/multiplication_node.rb +25 -0
- data/lib/fifthed_sim/nodes/number_node.rb +38 -0
- data/lib/fifthed_sim/nodes/roll_node.rb +88 -0
- data/lib/fifthed_sim/nodes/subtraction_node.rb +24 -0
- data/lib/fifthed_sim/roll_repl.rb +117 -0
- data/lib/fifthed_sim/spell.rb +74 -0
- data/lib/fifthed_sim/stat.rb +49 -0
- data/lib/fifthed_sim/stat_block.rb +57 -0
- data/lib/fifthed_sim/version.rb +3 -1
- data/lib/fifthed_sim.rb +28 -4
- metadata +74 -8
- data/lib/fifthed_sim/dice_calculation.rb +0 -88
- data/lib/fifthed_sim/dice_result.rb +0 -108
- data/lib/fifthed_sim/die_roll.rb +0 -66
- data/lib/fifthed_sim/helpers/average_comparison.rb +0 -14
@@ -0,0 +1,54 @@
|
|
1
|
+
require_relative './damage_types'
|
2
|
+
require_relative './calculated_fixnum'
|
3
|
+
module FifthedSim
|
4
|
+
class Damage
|
5
|
+
using CalculatedFixnum
|
6
|
+
|
7
|
+
class DefinitionProxy
|
8
|
+
def initialize(&block)
|
9
|
+
@attrs = {}
|
10
|
+
instance_eval(&block)
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :attrs
|
14
|
+
|
15
|
+
DAMAGE_TYPES.each do |type|
|
16
|
+
self.send(:define_method, type) do |arg|
|
17
|
+
@attrs[type] = DiceExpression(arg)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.define(&block)
|
23
|
+
h = DefinitionProxy.new(&block).attrs
|
24
|
+
self.new(h)
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(hash)
|
28
|
+
@hash = hash
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Obtain a dice roll of how much damage we're doing to a particular enemy
|
33
|
+
def to(enemy)
|
34
|
+
mapped = @hash.map do |k, v|
|
35
|
+
if enemy.immune_to?(k)
|
36
|
+
0.to_dice_expression
|
37
|
+
elsif enemy.resistant_to?(k)
|
38
|
+
(v / 2)
|
39
|
+
else
|
40
|
+
v
|
41
|
+
end
|
42
|
+
end
|
43
|
+
if mapped.empty?
|
44
|
+
0.to_dice_expression
|
45
|
+
else
|
46
|
+
mapped.inject{|memo, x| memo + x}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def raw
|
51
|
+
@hash.values.inject{|s, k| s + k}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module FifthedSim
|
2
|
+
class DamageTypes
|
3
|
+
def self.convert(t)
|
4
|
+
ret = case t
|
5
|
+
when String
|
6
|
+
t.to_sym
|
7
|
+
when Symbol
|
8
|
+
t
|
9
|
+
else
|
10
|
+
raise ArgumentError, "Cannot convert to damage type"
|
11
|
+
end
|
12
|
+
unless self.valid_damage_type?(ret)
|
13
|
+
raise InvalidDamageType, "#{ret} is not a type of damage"
|
14
|
+
end
|
15
|
+
ret
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.valid_damage_type?(sym)
|
19
|
+
DAMAGE_TYPES.include? sym
|
20
|
+
end
|
21
|
+
|
22
|
+
class InvalidDamageType < StandardError
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
DAMAGE_TYPES = %i(slashing
|
27
|
+
bludgeoning
|
28
|
+
piercing
|
29
|
+
fire
|
30
|
+
cold
|
31
|
+
poison
|
32
|
+
acid
|
33
|
+
psychic
|
34
|
+
necrotic
|
35
|
+
radiant
|
36
|
+
lightning
|
37
|
+
thunder
|
38
|
+
force)
|
39
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'rainbow'
|
2
|
+
|
3
|
+
module FifthedSim
|
4
|
+
##
|
5
|
+
# This is an abstract dice expression class
|
6
|
+
class DiceExpression
|
7
|
+
|
8
|
+
def to_i
|
9
|
+
value
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_f
|
13
|
+
value.to_f
|
14
|
+
end
|
15
|
+
|
16
|
+
def average
|
17
|
+
distribution.average
|
18
|
+
end
|
19
|
+
|
20
|
+
def +(other)
|
21
|
+
AdditionNode.new(self, other.to_dice_expression)
|
22
|
+
end
|
23
|
+
|
24
|
+
def -(other)
|
25
|
+
SubtractionNode.new(self, other.to_dice_expression)
|
26
|
+
end
|
27
|
+
|
28
|
+
def /(other)
|
29
|
+
DivisionNode.new(self, other.to_dice_expression)
|
30
|
+
end
|
31
|
+
|
32
|
+
def *(other)
|
33
|
+
MultiplicationNode.new(self, other.to_dice_expression)
|
34
|
+
end
|
35
|
+
|
36
|
+
def or_greater(other)
|
37
|
+
GreaterNode.new(self, other.to_dice_expression)
|
38
|
+
end
|
39
|
+
|
40
|
+
def or_least(other)
|
41
|
+
LessNode.new(self, other.to_dice_expression)
|
42
|
+
end
|
43
|
+
|
44
|
+
def percentile
|
45
|
+
distribution.percent_lower_equal(value)
|
46
|
+
end
|
47
|
+
|
48
|
+
def max
|
49
|
+
distribution.max
|
50
|
+
end
|
51
|
+
|
52
|
+
def min
|
53
|
+
distribution.min
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Takes a block, which should take a single argument
|
58
|
+
# This block should return another DiceExpression type, based on the result of this DiceExpression.
|
59
|
+
def test_then(&block)
|
60
|
+
BlockNode.new(self, &block)
|
61
|
+
end
|
62
|
+
|
63
|
+
{"above_" => :>, "below_" => :<, "" => :==}.each do |k,v|
|
64
|
+
define_method "#{k}average?" do
|
65
|
+
value.public_send(v, average)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
##
|
70
|
+
# Get this difference of the average value and the current value.
|
71
|
+
# For example, if the average is 10 and we have a value of 20, it will return 10.
|
72
|
+
# Meanwhile, if the average is 10 and we have a value of 2, it will return -8.
|
73
|
+
def difference_from_average
|
74
|
+
value - average
|
75
|
+
end
|
76
|
+
|
77
|
+
def range
|
78
|
+
(min..max)
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_dice_expression
|
82
|
+
self.dup
|
83
|
+
end
|
84
|
+
|
85
|
+
protected
|
86
|
+
|
87
|
+
def self.define_binary_op_equations(op)
|
88
|
+
self.send(:define_method, :value_equation) do |terminal: false|
|
89
|
+
lhs = instance_variable_get(:@lhs).value_equation(terminal: terminal)
|
90
|
+
rhs = instance_variable_get(:@rhs).value_equation(terminal: terminal)
|
91
|
+
"(#{lhs} #{op} #{rhs}"
|
92
|
+
end
|
93
|
+
|
94
|
+
self.send(:define_method, :expression_equation) do
|
95
|
+
lhs = instance_variable_get(:@lhs)
|
96
|
+
rhs = instance_variable_get(:@rhs)
|
97
|
+
"(#{lhs.expression_equation} #{op} #{rhs.expression_equation})"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# Allow .to_dice_expression on Fixnum
|
105
|
+
class Fixnum
|
106
|
+
def to_dice_expression
|
107
|
+
FifthedSim::NumberNode.new(self)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
class String
|
112
|
+
def to_dice_expression
|
113
|
+
FifthedSim::Compiler.compile(self)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
##
|
118
|
+
# C-style conversion, yay
|
119
|
+
def DiceExpression(arg)
|
120
|
+
return arg.to_dice_expression if arg.respond_to? :to_dice_expression
|
121
|
+
throw ArgumentError, "Cannot convert #{arg.class} to DiceExpression"
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
require_relative './nodes/multi_node'
|
126
|
+
require_relative './nodes/addition_node'
|
127
|
+
require_relative './nodes/roll_node'
|
128
|
+
require_relative './nodes/block_node'
|
129
|
+
require_relative './nodes/division_node'
|
130
|
+
require_relative './nodes/greater_node'
|
131
|
+
require_relative './nodes/less_node'
|
132
|
+
require_relative './nodes/number_node'
|
133
|
+
require_relative './nodes/multiplication_node'
|
134
|
+
require_relative './nodes/subtraction_node'
|
@@ -1,72 +1,271 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
1
3
|
module FifthedSim
|
4
|
+
##
|
5
|
+
# Models a probabilistic distribution.
|
2
6
|
class Distribution
|
7
|
+
##
|
8
|
+
# Get a distrubtion for a number.
|
9
|
+
# This will be a uniform distribution with P = 1 at this number and P = 0 elsewhere.
|
3
10
|
def self.for_number(num)
|
4
|
-
self.new({num => 1}
|
11
|
+
self.new({num => 1.0})
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.for_range(rng)
|
15
|
+
size = rng.size.to_f
|
16
|
+
e = 1.0 / size
|
17
|
+
self.new(Hash[rng.map{|x| [x, e]}])
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.for(obj)
|
21
|
+
case obj
|
22
|
+
when Fixnum
|
23
|
+
self.for_number(obj)
|
24
|
+
when Range
|
25
|
+
self.for_range(obj)
|
26
|
+
else
|
27
|
+
raise ArgumentError, "can't amke a distribution for that"
|
28
|
+
end
|
5
29
|
end
|
6
30
|
|
7
|
-
|
31
|
+
##
|
32
|
+
# We initialize class with a map of results to occurences, and a total number of possible different occurences.
|
33
|
+
# Generally, you will not ever initialize this yourself.
|
34
|
+
def initialize(map)
|
8
35
|
keys = map.keys
|
9
36
|
@max = keys.max
|
10
37
|
@min = keys.min
|
11
|
-
@map = map
|
12
|
-
@
|
38
|
+
@map = map.dup
|
39
|
+
@map.default = 0
|
13
40
|
end
|
14
41
|
|
15
42
|
attr_reader :total_possible,
|
16
43
|
:min,
|
17
44
|
:max
|
18
45
|
|
46
|
+
|
19
47
|
def range
|
20
|
-
(@max
|
48
|
+
(@min..@max)
|
21
49
|
end
|
22
50
|
|
23
51
|
def map
|
24
52
|
@map.dup
|
25
53
|
end
|
26
54
|
|
27
|
-
def
|
28
|
-
|
55
|
+
def average
|
56
|
+
map.map{|k, v| k * v}.inject(:+)
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# Obtain a new distribution of values.
|
61
|
+
# When block.call(value) for this distribution is true, we will allow
|
62
|
+
# values from the second distribution.
|
63
|
+
# Otherwise, the value will be zero.
|
64
|
+
#
|
65
|
+
# This is mostly used in hit calculation - AKA, if we're higher than an AC, then we hit, otherwise we do zero damage
|
66
|
+
def hit_when(other, &block)
|
67
|
+
hit_prob = map.map do |k, v|
|
68
|
+
if block.call(k)
|
69
|
+
v
|
70
|
+
else
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
end.compact.inject(:+)
|
74
|
+
miss_prob = 1 - hit_prob
|
75
|
+
omap = other.map
|
76
|
+
h = Hash[omap.map{|k, v| [k, v * hit_prob]}]
|
77
|
+
h[0] = (h[0] || 0) + miss_prob
|
78
|
+
Distribution.new(h)
|
79
|
+
end
|
80
|
+
|
81
|
+
##
|
82
|
+
# Takes a block or callable object.
|
83
|
+
# This function will call the callable with all possible outcomes of this distribution.
|
84
|
+
# The callable should return another distribution, representing the possible values when this possibility happens.
|
85
|
+
# This will then return a value of those possibilities.
|
86
|
+
#
|
87
|
+
# An example is probably helpful here.
|
88
|
+
# 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.
|
89
|
+
# If we want a distribution of possible outcomes of this attack, we can do:
|
90
|
+
#
|
91
|
+
# 1.d(20).distribution.results_when do |x|
|
92
|
+
# if x < 16
|
93
|
+
# Distribution.for_number(0)
|
94
|
+
# elseif x < 20
|
95
|
+
# 1.d(4).distribution
|
96
|
+
# else
|
97
|
+
# 2.d(4).distribution
|
98
|
+
# end
|
99
|
+
# end
|
100
|
+
def results_when(&block)
|
101
|
+
h = Hash.new{|h, k| h[k] = 0}
|
102
|
+
range.each do |v|
|
103
|
+
prob = @map[v]
|
104
|
+
o_dist = block.call(v)
|
105
|
+
o_dist.map.each do |k, v|
|
106
|
+
h[k] += (v * prob)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
Distribution.new(h)
|
110
|
+
end
|
111
|
+
|
112
|
+
def percent_within(range)
|
113
|
+
percent_where{|x| range.contains? x}
|
114
|
+
end
|
115
|
+
|
116
|
+
def percent_where(&block)
|
117
|
+
@map.to_a
|
118
|
+
.keep_if{|(k, v)| block.call(k)}
|
119
|
+
.map{|(k, v)| v}
|
120
|
+
.inject(:+)
|
29
121
|
end
|
30
122
|
|
31
123
|
def percent_exactly(num)
|
32
124
|
return 0 if num < @min || num > @max
|
33
|
-
@map[num]
|
125
|
+
@map[num] || 0
|
126
|
+
end
|
127
|
+
|
128
|
+
def variance
|
129
|
+
avg = average
|
130
|
+
@map.map do |k, v|
|
131
|
+
((k - avg)**2) * v
|
132
|
+
end.inject(:+)
|
133
|
+
end
|
134
|
+
|
135
|
+
def std_dev
|
136
|
+
Math.sqrt(variance)
|
137
|
+
end
|
138
|
+
|
139
|
+
def percent_lower(n)
|
140
|
+
num = n - 1
|
141
|
+
return 0.0 if num < @min
|
142
|
+
return 1.0 if num > @max
|
143
|
+
@min.upto(num).map(&map_proc).inject(:+)
|
144
|
+
end
|
145
|
+
|
146
|
+
def percent_greater(n)
|
147
|
+
num = n + 1
|
148
|
+
return 0.0 if num > @max
|
149
|
+
return 1.0 if num < @min
|
150
|
+
num.upto(@max).map(&map_proc).inject(:+)
|
34
151
|
end
|
35
152
|
|
36
|
-
def
|
37
|
-
|
38
|
-
return 1 if num >= @max
|
39
|
-
|
40
|
-
n = @min.upto(num).map(&map_proc).inject(:+)
|
41
|
-
n / @total_possible.to_f
|
153
|
+
def percent_lower_equal(num)
|
154
|
+
percent_lower(num + 1)
|
42
155
|
end
|
43
156
|
|
157
|
+
def percent_greater_equal(num)
|
158
|
+
percent_greater(num - 1)
|
159
|
+
end
|
160
|
+
|
161
|
+
alias_method :percentile_of,
|
162
|
+
:percent_lower_equal
|
163
|
+
|
164
|
+
|
165
|
+
|
44
166
|
def convolve(other)
|
45
167
|
h = {}
|
46
168
|
abs_min = [@min, other.min].min
|
47
169
|
abs_max = [@max, other.max].max
|
48
170
|
min_possible = @min + other.min
|
49
171
|
max_possible = @max + other.max
|
50
|
-
tp = @total_possible * other.total_possible
|
51
172
|
# TODO: there has to be a less stupid way to do this right?
|
52
173
|
v = min_possible.upto(max_possible).map do |val|
|
53
174
|
sum = abs_min.upto(abs_max).map do |m|
|
54
175
|
percent_exactly(m) * other.percent_exactly(val - m)
|
55
176
|
end.inject(:+)
|
56
|
-
[val,
|
177
|
+
[val, sum]
|
178
|
+
end
|
179
|
+
self.class.new(Hash[v])
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# TODO: Optimize this
|
184
|
+
def convolve_subtract(other)
|
185
|
+
h = Hash.new{|h, k| h[k] = 0}
|
186
|
+
range.each do |v1|
|
187
|
+
other.range.each do |v2|
|
188
|
+
h[v1 - v2] += percent_exactly(v1) * other.percent_exactly(v2)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
self.class.new(h)
|
192
|
+
end
|
193
|
+
|
194
|
+
##
|
195
|
+
# Get the distribution of a result from this distribution divided by
|
196
|
+
# one from another distribution.
|
197
|
+
# If the other distribution may contain zero this will break horribly.
|
198
|
+
def convolve_divide(other)
|
199
|
+
throw ArgumentError, "Divisor may be zero" if other.min < 1
|
200
|
+
h = Hash.new{|h, k| h[k] = 0}
|
201
|
+
# We can do this faster using a sieve, but be lazy for now
|
202
|
+
# TODO: Be less lazy
|
203
|
+
range.each do |v1|
|
204
|
+
other.range.each do |v2|
|
205
|
+
h[v1 / v2] += percent_exactly(v1) * other.percent_exactly(v2)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
self.class.new(h)
|
209
|
+
end
|
210
|
+
|
211
|
+
def convolve_multiply(other)
|
212
|
+
h = Hash.new{|h, k| h[k] = 0}
|
213
|
+
range.each do |v1|
|
214
|
+
other.range.each do |v2|
|
215
|
+
h[v1 * v2] += percent_exactly(v1) * other.percent_exactly(v2)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
self.class.new(h)
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
def convolve_greater(other)
|
223
|
+
h = Hash.new{|h, k| h[k] = 0}
|
224
|
+
# for each value
|
225
|
+
range.each do |s|
|
226
|
+
(s..other.max).each do |e|
|
227
|
+
h[e] += (other.percent_exactly(e) * percent_exactly(s))
|
228
|
+
end
|
229
|
+
h[s] += (other.percent_lower(s) * percent_exactly(s))
|
57
230
|
end
|
58
|
-
self.class.new(
|
231
|
+
self.class.new(h)
|
59
232
|
end
|
60
233
|
|
234
|
+
def convolve_least(other)
|
235
|
+
h = Hash.new{|h, k| h[k] = 0}
|
236
|
+
range.each do |s|
|
237
|
+
(other.min..s).each do |e|
|
238
|
+
h[e] += (other.percent_exactly(e) * percent_exactly(s))
|
239
|
+
end
|
240
|
+
h[s] += (other.percent_greater(s + 1) * percent_exactly(s))
|
241
|
+
end
|
242
|
+
self.class.new(h)
|
243
|
+
end
|
244
|
+
|
245
|
+
COMPARE_EPSILON = 0.00001
|
61
246
|
def ==(other)
|
62
|
-
|
63
|
-
|
247
|
+
omap = other.map
|
248
|
+
max_possible = (@max / other.min)
|
249
|
+
same_keys = (Set.new(@map.keys) == Set.new(omap.keys))
|
250
|
+
same_vals = @map.keys.each do |k|
|
251
|
+
(@map[k] - other.map[k]).abs <= COMPARE_EPSILON
|
252
|
+
end
|
253
|
+
same_keys && same_vals
|
254
|
+
end
|
255
|
+
|
256
|
+
def text_histogram(cols = 60)
|
257
|
+
max_width = @max.to_s.length
|
258
|
+
justwidth = max_width + 1
|
259
|
+
linewidth = (cols - justwidth)
|
260
|
+
range.map do |v|
|
261
|
+
"#{v}:".rjust(justwidth) + ("*" * (percent_exactly(v) * linewidth))
|
262
|
+
end.join("\n")
|
64
263
|
end
|
65
264
|
|
66
265
|
private
|
67
266
|
def map_proc
|
68
267
|
return Proc.new do |arg|
|
69
|
-
|
268
|
+
@map[arg]
|
70
269
|
end
|
71
270
|
end
|
72
271
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require_relative '../dice_expression'
|
2
|
+
require_relative '../calculated_fixnum'
|
3
|
+
|
4
|
+
module FifthedSim
|
5
|
+
class AdditionNode < DiceExpression
|
6
|
+
using CalculatedFixnum
|
7
|
+
|
8
|
+
def initialize(*values)
|
9
|
+
values.each(&method(:check_type))
|
10
|
+
@components = values.flatten
|
11
|
+
end
|
12
|
+
|
13
|
+
def +(other)
|
14
|
+
check_type(other)
|
15
|
+
self.class.new(*@components, other)
|
16
|
+
end
|
17
|
+
|
18
|
+
def value
|
19
|
+
# Symbol::& uses send, so refinements would break
|
20
|
+
# How sad
|
21
|
+
@components.map{|x| x.value}.inject(:+)
|
22
|
+
end
|
23
|
+
|
24
|
+
def dice
|
25
|
+
self.class.new(*@components.find_all{|x| x.is_a?(RollNode)})
|
26
|
+
end
|
27
|
+
|
28
|
+
def average
|
29
|
+
@components.map{|x| x.average}.inject(:+)
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_critfail?
|
33
|
+
@components.any?{|x| x.has_critfail?}
|
34
|
+
end
|
35
|
+
|
36
|
+
def has_crit?
|
37
|
+
@components.any?{|x| x.has_crit?}
|
38
|
+
end
|
39
|
+
|
40
|
+
def distribution
|
41
|
+
# TODO: Maybe figure out how to minimize convolution expense?
|
42
|
+
@components.map{|x| x.distribution}.inject do |memo, p|
|
43
|
+
memo.convolve(p)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def reroll
|
48
|
+
self.class.new(*@components.map{|x| x.reroll})
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def value_equation(terminal: false)
|
53
|
+
arglist = @components.map do |c|
|
54
|
+
c.value_equation(terminal: terminal)
|
55
|
+
end.inject{|m, x| m + "+ #{x}"}
|
56
|
+
"(#{arglist})"
|
57
|
+
end
|
58
|
+
|
59
|
+
def expression_equation
|
60
|
+
"(" + @components.map do |c|
|
61
|
+
c.expression_equation
|
62
|
+
end.inject{|m, x| m + "+ #{x}"} + ")"
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
ALLOWED_TYPES = [DiceExpression,
|
67
|
+
Fixnum]
|
68
|
+
|
69
|
+
def check_type(obj)
|
70
|
+
unless ALLOWED_TYPES.any?{|x| obj.kind_of?(x)}
|
71
|
+
raise TypeError, "#{obj.inspect} is not a DiceExpression"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative '../dice_expression'
|
2
|
+
require_relative '../calculated_fixnum'
|
3
|
+
require_relative '../distribution'
|
4
|
+
|
5
|
+
module FifthedSim
|
6
|
+
class BlockNode < DiceExpression
|
7
|
+
using CalculatedFixnum
|
8
|
+
|
9
|
+
def initialize(arg, &block)
|
10
|
+
@arg = arg
|
11
|
+
@block = block
|
12
|
+
@current_expression = block.call(arg.value)
|
13
|
+
@current_value = @current_expression.value
|
14
|
+
end
|
15
|
+
|
16
|
+
def value
|
17
|
+
@current_value
|
18
|
+
end
|
19
|
+
|
20
|
+
def reroll
|
21
|
+
self.class.new(@arg.reroll, &@block)
|
22
|
+
end
|
23
|
+
|
24
|
+
def distribution
|
25
|
+
@arg.distribution.results_when(&distribution_block)
|
26
|
+
end
|
27
|
+
|
28
|
+
def value_equation(terminal: false)
|
29
|
+
arg = @arg.value_equation(terminal: terminal)
|
30
|
+
ce = @current_expression.value_equation(terminal: terminal)
|
31
|
+
"blockNode(#{arg} => #{ce})"
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def distribution_block
|
37
|
+
h = Hash[@arg.range.map do |k|
|
38
|
+
[k, @block.call(k).distribution]
|
39
|
+
end]
|
40
|
+
require 'pry'
|
41
|
+
proc do |x|
|
42
|
+
h.fetch(x)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require_relative '../dice_expression'
|
2
|
+
require_relative '../calculated_fixnum'
|
3
|
+
module FifthedSim
|
4
|
+
class DivisionNode < DiceExpression
|
5
|
+
using CalculatedFixnum
|
6
|
+
|
7
|
+
def initialize(num, div)
|
8
|
+
@lhs = num
|
9
|
+
@rhs = div
|
10
|
+
end
|
11
|
+
|
12
|
+
def value
|
13
|
+
@lhs.value / @rhs.value
|
14
|
+
end
|
15
|
+
|
16
|
+
def reroll
|
17
|
+
self.class.new(@lhs.reroll, @rhs.reroll)
|
18
|
+
end
|
19
|
+
|
20
|
+
def distribution
|
21
|
+
@lhs.distribution.convolve_divide(@rhs.distribution)
|
22
|
+
end
|
23
|
+
|
24
|
+
define_binary_op_equations "/"
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require_relative '../dice_expression'
|
2
|
+
require_relative '../calculated_fixnum'
|
3
|
+
|
4
|
+
module FifthedSim
|
5
|
+
class GreaterNode < DiceExpression
|
6
|
+
using CalculatedFixnum
|
7
|
+
def initialize(lhs, rhs)
|
8
|
+
@lhs, @rhs = lhs, rhs
|
9
|
+
end
|
10
|
+
|
11
|
+
def value
|
12
|
+
[@lhs.value, @rhs.value].max
|
13
|
+
end
|
14
|
+
|
15
|
+
def distribution
|
16
|
+
@lhs.distribution.convolve_greater(@rhs.distribution)
|
17
|
+
end
|
18
|
+
|
19
|
+
def reroll
|
20
|
+
self.class.new(@lhs.reroll, @rhs.reroll)
|
21
|
+
end
|
22
|
+
|
23
|
+
def min
|
24
|
+
[@lhs.min, @rhs.min].max
|
25
|
+
end
|
26
|
+
|
27
|
+
def max
|
28
|
+
[@lhs.max, @rhs.max].max
|
29
|
+
end
|
30
|
+
|
31
|
+
def value_equation(terminal: false)
|
32
|
+
lhs = @lhs.value_equation(terminal: terminal)
|
33
|
+
rhs = @rhs.value_equation(terminal: terminal)
|
34
|
+
"max(#{lhs}, #{rhs}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def expression_equation
|
38
|
+
"max(#{lhs.expression_equation}, #{rhs.expression_equation})"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|