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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -1
  3. data/HelpWanted.md +23 -0
  4. data/README.md +144 -5
  5. data/bin/console +2 -8
  6. data/bin/playground +42 -0
  7. data/exe/diceroll +9 -0
  8. data/fifthed_sim.gemspec +4 -0
  9. data/lib/fifthed_sim/actor.rb +80 -0
  10. data/lib/fifthed_sim/attack.rb +86 -0
  11. data/lib/fifthed_sim/calculated_fixnum.rb +57 -0
  12. data/lib/fifthed_sim/compiler/parser.rb +46 -0
  13. data/lib/fifthed_sim/compiler/transform.rb +28 -0
  14. data/lib/fifthed_sim/compiler.rb +51 -0
  15. data/lib/fifthed_sim/damage.rb +54 -0
  16. data/lib/fifthed_sim/damage_types.rb +39 -0
  17. data/lib/fifthed_sim/dice_expression.rb +134 -0
  18. data/lib/fifthed_sim/distribution.rb +219 -20
  19. data/lib/fifthed_sim/nodes/addition_node.rb +75 -0
  20. data/lib/fifthed_sim/nodes/block_node.rb +46 -0
  21. data/lib/fifthed_sim/nodes/division_node.rb +26 -0
  22. data/lib/fifthed_sim/nodes/greater_node.rb +41 -0
  23. data/lib/fifthed_sim/nodes/less_node.rb +46 -0
  24. data/lib/fifthed_sim/nodes/multi_node.rb +135 -0
  25. data/lib/fifthed_sim/nodes/multiplication_node.rb +25 -0
  26. data/lib/fifthed_sim/nodes/number_node.rb +38 -0
  27. data/lib/fifthed_sim/nodes/roll_node.rb +88 -0
  28. data/lib/fifthed_sim/nodes/subtraction_node.rb +24 -0
  29. data/lib/fifthed_sim/roll_repl.rb +117 -0
  30. data/lib/fifthed_sim/spell.rb +74 -0
  31. data/lib/fifthed_sim/stat.rb +49 -0
  32. data/lib/fifthed_sim/stat_block.rb +57 -0
  33. data/lib/fifthed_sim/version.rb +3 -1
  34. data/lib/fifthed_sim.rb +28 -4
  35. metadata +74 -8
  36. data/lib/fifthed_sim/dice_calculation.rb +0 -88
  37. data/lib/fifthed_sim/dice_result.rb +0 -108
  38. data/lib/fifthed_sim/die_roll.rb +0 -66
  39. 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}, 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
- def initialize(map, total_possible)
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
- @total_possible = total_possible
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..@min)
48
+ (@min..@max)
21
49
  end
22
50
 
23
51
  def map
24
52
  @map.dup
25
53
  end
26
54
 
27
- def probability_map
28
- Hash[@map.map{|k, v| [k, v / @total_possible.to_f]}]
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] / @total_possible.to_f
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 percent_least(num)
37
- return 0 if num < @min
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, (sum * tp).to_i]
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(Hash[v], tp)
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
- @map == other.map &&
63
- @total_possible == other.total_possible
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
- res = @map[arg]
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