fifthed_sim 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|