rich_engine 0.0.0 → 0.1.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.
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RichEngine
4
+ # Plays a sequence of string frames (sprites) at a fixed frames-per-second.
5
+ #
6
+ # @example
7
+ # frames = [
8
+ # "A",
9
+ # "B"
10
+ # ]
11
+ # anim = RichEngine::Animation.new(frames: frames, fps: 8, loop: true)
12
+ #
13
+ # # Inside your Game loop:
14
+ # anim.update(elapsed_time)
15
+ # anim.draw(@canvas, x: 10, y: 2, fg: :yellow)
16
+ class Animation
17
+ # @return [Array<String>] the frame sprites, in playback order.
18
+ # @return [Integer] the index of the frame currently shown.
19
+ # @return [Integer, Float] the configured frames per second.
20
+ attr_reader :frames, :frame_index, :fps
21
+
22
+ # @return [Symbol, String, Array<Integer>, Integer] default foreground
23
+ # color used when drawing.
24
+ attr_accessor :fg
25
+
26
+ # Builds an animation from a sequence of string frames.
27
+ #
28
+ # @param frames [Array<String>] each string is a frame; can be multi-line.
29
+ # Spaces are transparent.
30
+ # @param fps [Integer, Float] how many frames per second to advance.
31
+ # @param loop [Boolean] if true, wrap to the first frame after the last;
32
+ # otherwise stop at the last frame.
33
+ # @param fg [Symbol, String, Array<Integer>, Integer] default foreground
34
+ # color when drawing.
35
+ # @example
36
+ # anim = RichEngine::Animation.new(frames: ["A", "B"], fps: 8, loop: true)
37
+ def initialize(frames:, fps: 12, loop: true, fg: :white)
38
+ @frames = frames
39
+ @fps = fps
40
+ @loop = loop
41
+ @fg = fg
42
+ @frame_index = 0
43
+ @playing = true
44
+ @stepper = RichEngine::Timer.every(seconds: frame_interval)
45
+ end
46
+
47
+ # Changes playback speed at runtime.
48
+ #
49
+ # @param value [Integer, Float] the new frames per second.
50
+ # @return [Integer, Float] the assigned value.
51
+ def fps=(value)
52
+ @fps = value
53
+ @stepper.interval = frame_interval
54
+ end
55
+
56
+ # @return [Boolean] whether the animation wraps after the last frame.
57
+ def loop? = @loop
58
+
59
+ # @return [Boolean] whether the animation is currently advancing frames.
60
+ def playing? = @playing
61
+
62
+ # Stops playback and rewinds to the first frame.
63
+ #
64
+ # @return [void]
65
+ def stop!
66
+ @playing = false
67
+ reset!
68
+ end
69
+
70
+ # Pauses playback on the current frame.
71
+ #
72
+ # @return [void]
73
+ def pause!
74
+ @playing = false
75
+ end
76
+
77
+ # Resumes playback.
78
+ #
79
+ # @return [void]
80
+ def play!
81
+ @playing = true
82
+ end
83
+
84
+ # Rewinds to the first frame and restarts the frame timer.
85
+ #
86
+ # @return [void]
87
+ def reset!
88
+ @frame_index = 0
89
+ @stepper.interval = frame_interval
90
+ end
91
+
92
+ # @return [String] the frame string currently shown.
93
+ def current_frame
94
+ @frames[@frame_index]
95
+ end
96
+
97
+ # Advances the internal timer; call once per frame with the elapsed time.
98
+ #
99
+ # @param elapsed_time [Float] seconds since the last frame.
100
+ # @return [void]
101
+ def update(elapsed_time)
102
+ return unless @playing
103
+ return if @frames.size <= 1
104
+
105
+ @stepper.update(elapsed_time)
106
+ @stepper.when_ready { advance_frame }
107
+ end
108
+
109
+ # Renders the current frame to the canvas.
110
+ #
111
+ # @param canvas [RichEngine::Canvas] the canvas to draw on.
112
+ # @param x [Integer] left column to draw at.
113
+ # @param y [Integer] top row to draw at.
114
+ # @param fg [Symbol, String, Array<Integer>, Integer, nil] foreground
115
+ # color; falls back to the animation's default when nil.
116
+ # @return [void]
117
+ def draw(canvas, x:, y:, fg: nil)
118
+ canvas.draw_sprite(current_frame, x: x, y: y, fg: fg || @fg)
119
+ end
120
+
121
+ private
122
+
123
+ def frame_interval
124
+ 1.0 / @fps.to_f
125
+ end
126
+
127
+ def advance_frame
128
+ if @loop
129
+ @frame_index = (@frame_index + 1) % @frames.size
130
+ elsif @frame_index < @frames.size - 1
131
+ @frame_index += 1
132
+ else
133
+ @playing = false
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RichEngine
4
+ class Canvas
5
+ # A sub-section of a parent Canvas. All drawing operations are translated by
6
+ # the slot's origin (x, y).
7
+ class Slot < Canvas
8
+ # @return [String, nil] the slot's background fill, or nil to inherit the
9
+ # parent's
10
+ attr_reader :bg
11
+
12
+ # @param parent [RichEngine::Canvas] the canvas this slot draws onto
13
+ # @param x [Integer] the slot's left column on the parent canvas
14
+ # @param y [Integer] the slot's top row on the parent canvas
15
+ # @param width [Integer] the slot width
16
+ # @param height [Integer] the slot height
17
+ # @param bg [String, nil] the slot's background fill, or nil to inherit
18
+ # the parent's
19
+ def initialize(parent, x, y, width, height, bg: nil)
20
+ @parent = parent
21
+ @offset_x = x
22
+ @offset_y = y
23
+ @width = width
24
+ @height = height
25
+ @bg = bg
26
+ end
27
+
28
+ # Read the cell at the slot-local (x, y), translated to parent space.
29
+ #
30
+ # @param x [Integer] the slot-local column
31
+ # @param y [Integer] the slot-local row
32
+ # @return [String, nil] the cell contents, or nil if out of the slot's
33
+ # bounds
34
+ def [](x, y)
35
+ return nil if out_of_bounds?(x, y)
36
+ @parent[@offset_x + x, @offset_y + y]
37
+ end
38
+
39
+ # Write the cell at the slot-local (x, y), translated to parent space.
40
+ # Writes outside the slot are clipped.
41
+ #
42
+ # @param x [Integer] the slot-local column
43
+ # @param y [Integer] the slot-local row
44
+ # @param value [String] the cell contents to write
45
+ # @return [void]
46
+ def []=(x, y, value)
47
+ return if out_of_bounds?(x, y)
48
+ @parent[@offset_x + x, @offset_y + y] = value
49
+ end
50
+
51
+ # Clear the slot, filling it with its background (or the parent's if the
52
+ # slot has none).
53
+ #
54
+ # @return [void]
55
+ def clear
56
+ fill_char = @bg || @parent.bg
57
+ each_coord do |x, y|
58
+ self[x, y] = fill_char
59
+ end
60
+ end
61
+
62
+ # Change the slot's background fill character and clear the slot.
63
+ #
64
+ # @param bg [String] the new background fill character
65
+ # @return [void]
66
+ def bg=(bg)
67
+ @bg = bg
68
+ clear
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,13 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "string_colors"
4
+ require_relative "canvas/slot"
4
5
 
5
6
  module RichEngine
7
+ # A 2D character grid that you draw to each frame, with colored text and
8
+ # shapes. Cells are stored row-major in a flat array.
6
9
  class Canvas
7
10
  using StringColors
8
11
 
9
- attr_reader :canvas, :bg
12
+ # @return [Array<String>] the flat, row-major array of cell contents
13
+ # @return [String] the background fill character
14
+ # @return [Integer] the canvas width in characters
15
+ # @return [Integer] the canvas height in characters
16
+ attr_reader :canvas, :bg, :width, :height
10
17
 
18
+ # @param width [Integer] the canvas width in characters
19
+ # @param height [Integer] the canvas height in characters
20
+ # @param bg [String] the background fill character
11
21
  def initialize(width, height, bg: " ")
12
22
  @width = width
13
23
  @height = height
@@ -15,10 +25,18 @@ module RichEngine
15
25
  clear
16
26
  end
17
27
 
28
+ # The canvas dimensions as a [width, height] pair.
29
+ #
30
+ # @return [Array(Integer, Integer)] the width and height
18
31
  def dimensions
19
32
  [@width, @height]
20
33
  end
21
34
 
35
+ # Yield every (x, y) coordinate in the canvas.
36
+ #
37
+ # @yieldparam x [Integer] the column
38
+ # @yieldparam y [Integer] the row
39
+ # @return [void]
22
40
  def each_coord(&block)
23
41
  (0...@width).each do |x|
24
42
  (0...@height).each do |y|
@@ -27,27 +45,55 @@ module RichEngine
27
45
  end
28
46
  end
29
47
 
48
+ # Enumerate the canvas one row at a time.
49
+ #
50
+ # @return [Enumerator] an enumerator of rows, each an array of cells
30
51
  def rows
31
52
  @canvas.each_slice(@width)
32
53
  end
33
54
 
34
- def draw_sprite(sprite, x: 0, y: 0, fg: :white)
55
+ # Draw a multi-line string as a sprite; spaces are treated as transparent
56
+ # and left untouched.
57
+ #
58
+ # @param sprite [String] the multi-line sprite to draw
59
+ # @param x [Integer] the left column to start drawing at
60
+ # @param y [Integer] the top row to start drawing at
61
+ # @param fg [Symbol, String, Array<Integer>, Integer] the foreground color
62
+ # @param bg [Symbol, String, Array<Integer>, Integer] the background color
63
+ # @return [void]
64
+ def draw_sprite(sprite, x: 0, y: 0, fg: :white, bg: :transparent)
35
65
  sprite.split("\n").each.with_index do |line, i|
36
66
  line.each_char.with_index do |char, j|
37
67
  next if char == " "
38
68
 
39
- self[x + j, y + i] = char.fg(fg)
69
+ self[x + j, y + i] = char.fg(fg).bg(bg)
40
70
  end
41
71
  end
42
72
  end
43
73
 
74
+ # Write colored text at a position. Pass :center for x or y to center the
75
+ # text along that axis. A single color applies to every character; an array
76
+ # of colors cycles per character.
77
+ #
78
+ # @param str [String] the text to write
79
+ # @param x [Integer, Symbol] the left column, or :center to horizontally
80
+ # center the text
81
+ # @param y [Integer, Symbol] the row, or :center to vertically center the
82
+ # text
83
+ # @param fg [Symbol, String, Array<Integer>, Integer, Array] the foreground
84
+ # color, or an array of color specs to cycle per character
85
+ # @param bg [Symbol, String, Array<Integer>, Integer, Array] the background
86
+ # color, or an array of color specs to cycle per character
87
+ # @return [void]
88
+ # @example Cycle foreground colors per character
89
+ # canvas.write_string("Hello", x: :center, y: 1, fg: [:red, :green, :blue])
44
90
  def write_string(str, x: 0, y: 0, fg: :white, bg: :transparent)
45
91
  if x == :center
46
92
  x = (@width - str.length) / 2
47
93
  end
48
94
 
49
95
  if y == :center
50
- y = (@height - str.length) / 2
96
+ y = (@height - 1) / 2
51
97
  end
52
98
 
53
99
  fg = Array(fg).cycle
@@ -58,6 +104,15 @@ module RichEngine
58
104
  end
59
105
  end
60
106
 
107
+ # Draw a filled rectangle. Coordinates and sizes are rounded to integers.
108
+ #
109
+ # @param x [Integer] the left column
110
+ # @param y [Integer] the top row
111
+ # @param width [Integer] the rectangle width
112
+ # @param height [Integer] the rectangle height
113
+ # @param char [String] the character to fill with
114
+ # @param color [Symbol, String, Array<Integer>, Integer] the fill color
115
+ # @return [void]
61
116
  def draw_rect(x:, y:, width:, height:, char: "█", color: :white)
62
117
  x = x.round
63
118
  y = y.round
@@ -71,6 +126,15 @@ module RichEngine
71
126
  end
72
127
  end
73
128
 
129
+ # Draw a filled circle centered on (x, y). The center is rounded to
130
+ # integers.
131
+ #
132
+ # @param x [Integer] the center column
133
+ # @param y [Integer] the center row
134
+ # @param radius [Integer] the circle radius
135
+ # @param char [String] the character to fill with
136
+ # @param color [Symbol, String, Array<Integer>, Integer] the fill color
137
+ # @return [void]
74
138
  def draw_circle(x:, y:, radius:, char: "█", color: :white)
75
139
  x = x.round
76
140
  y = y.round
@@ -84,6 +148,11 @@ module RichEngine
84
148
  end
85
149
  end
86
150
 
151
+ # Whether the given coordinate falls outside the canvas.
152
+ #
153
+ # @param x [Integer] the column
154
+ # @param y [Integer] the row
155
+ # @return [Boolean] true if (x, y) is outside the canvas bounds
87
156
  def out_of_bounds?(x, y)
88
157
  return true if x < 0
89
158
  return true if x >= @width
@@ -93,6 +162,11 @@ module RichEngine
93
162
  false
94
163
  end
95
164
 
165
+ # Read the cell at (x, y). Coordinates are rounded to integers.
166
+ #
167
+ # @param x [Integer] the column
168
+ # @param y [Integer] the row
169
+ # @return [String, nil] the cell contents
96
170
  def [](x, y)
97
171
  x = x.round
98
172
  y = y.round
@@ -100,6 +174,13 @@ module RichEngine
100
174
  @canvas[at(x, y)]
101
175
  end
102
176
 
177
+ # Write a cell at (x, y), ignoring out-of-bounds writes. Coordinates are
178
+ # rounded to integers.
179
+ #
180
+ # @param x [Integer] the column
181
+ # @param y [Integer] the row
182
+ # @param value [String] the cell contents to write
183
+ # @return [void]
103
184
  def []=(x, y, value)
104
185
  x = x.round
105
186
  y = y.round
@@ -108,15 +189,39 @@ module RichEngine
108
189
  @canvas[at(x, y)] = value
109
190
  end
110
191
 
192
+ # Clear the entire canvas, resetting every cell to the background fill.
193
+ #
194
+ # @return [void]
111
195
  def clear
112
196
  @canvas = create_blank_canvas
113
197
  end
114
198
 
199
+ # Change the background fill character and clear the canvas.
200
+ #
201
+ # @param bg [String] the new background fill character
202
+ # @return [void]
115
203
  def bg=(bg)
116
204
  @bg = bg
117
205
  clear
118
206
  end
119
207
 
208
+ # Define a logical sub-region of this canvas that translates local
209
+ # coordinates into the parent canvas space and clips drawing to the region.
210
+ #
211
+ # @param x [Integer] the region's left column on the parent canvas
212
+ # @param y [Integer] the region's top row on the parent canvas
213
+ # @param width [Integer] the region width
214
+ # @param height [Integer] the region height
215
+ # @param bg [String, nil] the slot's background fill, or nil to inherit the
216
+ # parent's
217
+ # @return [RichEngine::Canvas::Slot] the sub-region canvas
218
+ # @example
219
+ # log = canvas.slot(x: 80, y: 0, width: 20, height: 10)
220
+ # log.write_string("Hello", x: 1, y: 1) # => writes at (81, 1)
221
+ def slot(x:, y:, width:, height:, bg: nil)
222
+ Slot.new(self, x, y, width, height, bg: bg)
223
+ end
224
+
120
225
  private
121
226
 
122
227
  def at(x, y)
@@ -1,7 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RichEngine
4
+ # Random helpers for probability checks.
4
5
  module Chance
6
+ # Returns true with the given probability.
7
+ #
8
+ # @param value [Integer, Float] a probability as a fraction (0.2) or as a
9
+ # percentage (20); values greater than 1 are treated as percentages.
10
+ # @param rand_gen [#call] a generator returning a float in [0, 1).
11
+ # @return [Boolean] whether the chance succeeded.
12
+ # @example
13
+ # RichEngine::Chance.of(0.2) # 20% chance
14
+ # RichEngine::Chance.of(20) # also 20% (percent form)
5
15
  def self.of(value, rand_gen: method(:rand))
6
16
  percent = if value > 1
7
17
  value / 100.0
@@ -12,6 +22,13 @@ module RichEngine
12
22
  rand_gen.call < percent
13
23
  end
14
24
 
25
+ # Returns true with a one-in-+value+ probability.
26
+ #
27
+ # @param value [Integer, Float] the denominator of the odds.
28
+ # @param rand_gen [#call] a generator returning a float in [0, 1).
29
+ # @return [Boolean] whether the chance succeeded.
30
+ # @example
31
+ # RichEngine::Chance.of_one_in(10) # 1 in 10 chance
15
32
  def self.of_one_in(value, rand_gen: method(:rand))
16
33
  of(1 / value.to_f, rand_gen: rand_gen)
17
34
  end
@@ -1,26 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RichEngine
4
+ # Tracks a fixed delay and reports when it has elapsed.
5
+ #
6
+ # @example
7
+ # shoot_cd = RichEngine::Cooldown.new(0.25) # seconds
8
+ # shoot_cd.update(dt)
9
+ # if shoot_cd.ready?
10
+ # shoot!
11
+ # shoot_cd.reset!
12
+ # end
4
13
  class Cooldown
14
+ # @param target_time [Integer, Float] the cooldown duration in seconds.
5
15
  def initialize(target_time)
6
- @timer = 0
7
16
  @target_time = target_time
17
+ @timer = target_time
8
18
  end
9
19
 
20
+ # Counts down by the elapsed time.
21
+ #
22
+ # @param dt [Float] seconds since the last frame.
23
+ # @return [Float] the remaining time.
10
24
  def update(dt)
11
- @timer += dt
25
+ @timer -= dt
12
26
  end
13
27
 
28
+ # @return [Float] the remaining time in seconds (negative once finished).
14
29
  def get
15
30
  @timer
16
31
  end
17
32
 
33
+ # Restarts the cooldown at its full duration.
34
+ #
35
+ # @return [Integer, Float] the reset remaining time.
18
36
  def reset!
19
- @timer = 0
37
+ @timer = @target_time
20
38
  end
21
39
 
40
+ # @return [Boolean] whether the cooldown has elapsed.
22
41
  def finished?
23
- @timer >= @target_time
42
+ @timer <= 0
24
43
  end
25
44
  alias_method :ready?, :finished?
26
45
  end
@@ -1,11 +1,45 @@
1
1
  module RichEngine
2
2
  class Enum
3
+ # Adds enum support to a class. Include it, then declare enums with
4
+ # {ClassMethods#enum} to get a class-level enum accessor and an
5
+ # instance-level reader that resolves the instance variable into an
6
+ # {Enum::Value}.
7
+ #
8
+ # @example
9
+ # class Player
10
+ # include RichEngine::Enum::Mixin
11
+ # enum :state, {idle: 0, running: 1, paused: 2}
12
+ #
13
+ # def initialize
14
+ # @state = :idle
15
+ # end
16
+ # end
17
+ #
18
+ # Player.states #=> RichEngine::Enum
19
+ # Player.new.state.idle? #=> true
3
20
  module Mixin
21
+ # Extends the including class with {ClassMethods}.
22
+ #
23
+ # @param base [Class] the class including this module
24
+ # @return [void]
4
25
  def self.included(base)
5
26
  base.extend ClassMethods
6
27
  end
7
28
 
29
+ # Class-level methods made available by including {Mixin}.
8
30
  module ClassMethods
31
+ # Declares an enum on the class. Defines a class method returning the
32
+ # {Enum} (named after +enum_name+) and an instance method (named
33
+ # +name+) that reads the +@name+ instance variable and returns the
34
+ # matching {Enum::Value}.
35
+ #
36
+ # @param name [Symbol] the name of the enum and instance reader; the
37
+ # value is read from the +@name+ instance variable
38
+ # @param enum_options [Array, Hash] the enum options, as accepted by
39
+ # {Enum#initialize}
40
+ # @param enum_name [String] the name of the class-level enum accessor;
41
+ # defaults to the pluralized +name+
42
+ # @return [void]
9
43
  def enum(name, enum_options, enum_name: "#{name}s")
10
44
  define_singleton_method(enum_name) do
11
45
  Enum.new(name, enum_options)
@@ -1,10 +1,28 @@
1
1
  module RichEngine
2
2
  class Enum
3
+ # A single selected option of an {Enum}. Values are comparable (by their
4
+ # underlying option value) and expose a query method per option, e.g.
5
+ # +#idle?+. Only values from the same enum can be compared or are equal.
6
+ #
7
+ # @example
8
+ # state = RichEngine::Enum.new(:state, {idle: 0, running: 1})
9
+ # value = state.running
10
+ # value.running? #=> true
11
+ # value.value #=> 1
3
12
  class Value
4
13
  include Comparable
5
14
 
15
+ # @return [Enum] the enum this value belongs to
16
+ # @return [Symbol] the selected option
6
17
  attr_reader :enum, :selected
7
18
 
19
+ # Builds a value for a selected option and defines a query method per
20
+ # option (e.g. +#idle?+).
21
+ #
22
+ # @param enum [Enum] the enum this value belongs to
23
+ # @param selected [Symbol] the selected option; must be a valid option of
24
+ # +enum+
25
+ # @raise [ArgumentError] if +selected+ is not an option of +enum+
8
26
  def initialize(enum:, selected:)
9
27
  @enum = enum
10
28
  @selected = selected
@@ -15,16 +33,29 @@ module RichEngine
15
33
  freeze
16
34
  end
17
35
 
36
+ # The underlying value of the selected option.
37
+ #
38
+ # @return [Object] the value mapped to the selected option
18
39
  def value
19
40
  @enum[@selected]
20
41
  end
21
42
 
43
+ # Compares two values from the same enum by their underlying values.
44
+ #
45
+ # @param other [Value] another value from the same enum
46
+ # @return [Integer] -1, 0, or 1
47
+ # @raise [ArgumentError] if +other+ belongs to a different enum
22
48
  def <=>(other)
23
49
  raise ArgumentError, "Can't compare values from different enums" if enum != other.enum
24
50
 
25
51
  value <=> other.value
26
52
  end
27
53
 
54
+ # Two values are equal when they come from the same enum and have the
55
+ # same selected option.
56
+ #
57
+ # @param other [Object] the object to compare against
58
+ # @return [Boolean] whether the values are equal
28
59
  def ==(other)
29
60
  return @enum == other.enum && selected == other.selected if other.is_a? self.class
30
61
 
@@ -1,10 +1,29 @@
1
- require_relative "./enum/value"
2
- require_relative "./enum/mixin"
1
+ require_relative "enum/value"
2
+ require_relative "enum/mixin"
3
3
 
4
4
  module RichEngine
5
+ # An ergonomic, comparable enum with query methods. Options may be given as an
6
+ # array (auto-indexed from 0) or as a hash mapping each option to its value. A
7
+ # reader method is defined for every option that returns an {Enum::Value}.
8
+ #
9
+ # @example Array of options (auto-indexed)
10
+ # colors = RichEngine::Enum.new(:colors, [:red, :green, :blue])
11
+ # colors.options #=> {red: 0, green: 1, blue: 2}
12
+ # colors.red.value #=> 0
13
+ #
14
+ # @example Hash of options with explicit values
15
+ # state = RichEngine::Enum.new(:state, {idle: 0, running: 1, paused: 2})
16
+ # state.running > state.idle #=> true
5
17
  class Enum
18
+ # @return [Symbol] the name of the enum
19
+ # @return [Hash] the options as a hash mapping each option to its value
6
20
  attr_reader :name, :options
7
21
 
22
+ # Builds an enum and defines a reader method for each option.
23
+ #
24
+ # @param name [Symbol] the name of the enum
25
+ # @param options [Array, Hash] option names; an array is auto-indexed from
26
+ # 0, a hash maps each option name to its value
8
27
  def initialize(name, options)
9
28
  @name = name
10
29
  @options = if options.respond_to? :each_pair
@@ -22,6 +41,11 @@ module RichEngine
22
41
  freeze
23
42
  end
24
43
 
44
+ # Looks up the value of an option.
45
+ #
46
+ # @param option [Symbol] the option name
47
+ # @return [Object] the value mapped to that option
48
+ # @raise [KeyError] if the option is unknown
25
49
  def [](option)
26
50
  @options.fetch(option)
27
51
  end