road_to_rubykaigi 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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.standard.yml +18 -0
  4. data/CHANGELOG.md +3 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +49 -0
  8. data/Rakefile +10 -0
  9. data/bin/road_to_rubykaigi +5 -0
  10. data/lib/road_to_rubykaigi/ansi.rb +22 -0
  11. data/lib/road_to_rubykaigi/fireworks.rb +63 -0
  12. data/lib/road_to_rubykaigi/game.rb +97 -0
  13. data/lib/road_to_rubykaigi/graphics/fireworks.rb +16 -0
  14. data/lib/road_to_rubykaigi/graphics/fireworks.txt +369 -0
  15. data/lib/road_to_rubykaigi/graphics/map.rb +11 -0
  16. data/lib/road_to_rubykaigi/graphics/map.txt +30 -0
  17. data/lib/road_to_rubykaigi/graphics/mask.rb +11 -0
  18. data/lib/road_to_rubykaigi/graphics/mask.txt +30 -0
  19. data/lib/road_to_rubykaigi/graphics/player.rb +81 -0
  20. data/lib/road_to_rubykaigi/manager/collision_manager.rb +133 -0
  21. data/lib/road_to_rubykaigi/manager/drawing_manager.rb +48 -0
  22. data/lib/road_to_rubykaigi/manager/game_manager.rb +55 -0
  23. data/lib/road_to_rubykaigi/manager/update_manager.rb +27 -0
  24. data/lib/road_to_rubykaigi/map.rb +120 -0
  25. data/lib/road_to_rubykaigi/opening_screen.rb +56 -0
  26. data/lib/road_to_rubykaigi/score_board.rb +17 -0
  27. data/lib/road_to_rubykaigi/sprite/attack.rb +72 -0
  28. data/lib/road_to_rubykaigi/sprite/bonus.rb +127 -0
  29. data/lib/road_to_rubykaigi/sprite/deadline.rb +57 -0
  30. data/lib/road_to_rubykaigi/sprite/effect.rb +88 -0
  31. data/lib/road_to_rubykaigi/sprite/enemy.rb +178 -0
  32. data/lib/road_to_rubykaigi/sprite/player.rb +178 -0
  33. data/lib/road_to_rubykaigi/sprite/sprite.rb +22 -0
  34. data/lib/road_to_rubykaigi/version.rb +5 -0
  35. data/lib/road_to_rubykaigi.rb +46 -0
  36. data/sig/road_to_rubykaigi.rbs +4 -0
  37. metadata +82 -0
@@ -0,0 +1,133 @@
1
+ module RoadToRubykaigi
2
+ module Manager
3
+ class CollisionManager
4
+ def process
5
+ event = [player_meet_enemy] # player must hit enemy before land
6
+ player_fall
7
+ player_land
8
+
9
+ event += [
10
+ player_meet_deadline,
11
+ player_meet_bonus,
12
+ attack_hit_bonus,
13
+ attack_hit_enemy,
14
+ ]
15
+ if event.include?(:game_over)
16
+ :game_over
17
+ elsif event.include?(:bonus)
18
+ :bonus
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def initialize(background, foreground)
25
+ @map = background
26
+ @player, @deadline, @bonuses, @enemies, @attacks, @effects = foreground.layers
27
+ end
28
+
29
+ def player_fall
30
+ bounding_box = @player.bounding_box
31
+ foot_y = bounding_box[:y] + bounding_box[:height]
32
+ center_x = bounding_box[:x] + bounding_box[:width] / 2.0
33
+ if @map.passable_at?(center_x, foot_y + 1)
34
+ @player.fall
35
+ end
36
+ end
37
+
38
+ def player_land
39
+ bounding_box = @player.bounding_box
40
+ foot_y = bounding_box[:y] + bounding_box[:height]
41
+ foot_y = foot_y.clamp(bounding_box[:height], RoadToRubykaigi::Sprite::Player::BASE_Y)
42
+ (bounding_box[:x]...(bounding_box[:x] + bounding_box[:width])).each do |col|
43
+ unless @map.passable_at?(col, foot_y)
44
+ break @player.land(foot_y)
45
+ end
46
+ end
47
+ end
48
+
49
+ # @returns [:game_over, Nil]
50
+ def player_meet_deadline
51
+ find_collision_item(@player, @deadline) && :game_over
52
+ end
53
+
54
+ def player_meet_bonus
55
+ if (collided_item = find_collision_item(@player, @bonuses))
56
+ @effects.heart(
57
+ @player.x + @player.width - 1,
58
+ @player.y,
59
+ )
60
+ @bonuses.delete(collided_item)
61
+ :bonus
62
+ end
63
+ end
64
+
65
+ # @returns [:bonus, false]
66
+ def attack_hit_bonus
67
+ collided = !@attacks.dup.select do |attack|
68
+ if (collided_item = find_collision_item(attack, @bonuses))
69
+ @effects.heart(
70
+ @player.x + @player.width - 1,
71
+ @player.y,
72
+ )
73
+ @bonuses.delete(collided_item)
74
+ @attacks.delete(attack)
75
+ end
76
+ end.empty?
77
+ collided && :bonus
78
+ end
79
+
80
+ # @returns [:bonus, Nil]
81
+ def attack_hit_enemy
82
+ collided = !@attacks.dup.select do |attack|
83
+ if (collided_item = find_collision_item(attack, @enemies))
84
+ @effects.note(
85
+ @player.x + @player.width - 1,
86
+ @player.y,
87
+ )
88
+ @enemies.delete(collided_item)
89
+ @attacks.delete(attack)
90
+ end
91
+ end.empty?
92
+ collided && :bonus
93
+ end
94
+
95
+ # @returns [:bonus, Nil]
96
+ def player_meet_enemy
97
+ if (collided_item = find_collision_item(@player, @enemies))
98
+ if @player.stompable?
99
+ @effects.note(
100
+ @player.x + @player.width - 1,
101
+ @player.y,
102
+ )
103
+ @enemies.delete(collided_item)
104
+ @player.vy = @player.class::JUMP_INITIAL_VELOCITY
105
+ :bonus
106
+ else
107
+ @effects.lightning(
108
+ @player.x + @player.width - 1,
109
+ @player.y,
110
+ )
111
+ @enemies.delete(collided_item)
112
+ @player.stun
113
+ end
114
+ end
115
+ end
116
+
117
+ def find_collision_item(entity, others)
118
+ others.find do |other|
119
+ collided?(entity.bounding_box, other.bounding_box)
120
+ end
121
+ end
122
+
123
+ def collided?(box1, box2)
124
+ !(
125
+ box1[:x] + box1[:width] <= box2[:x] ||
126
+ box1[:x] >= box2[:x] + box2[:width] ||
127
+ box1[:y] + box1[:height] <= box2[:y] ||
128
+ box1[:y] >= box2[:y] + box2[:height]
129
+ )
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,48 @@
1
+ module RoadToRubykaigi
2
+ module Manager
3
+ class DrawingManager
4
+ MAP_X_START = 1
5
+ MAP_Y_START = 2
6
+
7
+ def draw(offset_x:)
8
+ buffer = Array.new(@viewport_height) { Array.new(@viewport_width) { "" } }
9
+ @layers.each do |layer|
10
+ merge_buffer(buffer, layer, offset_x: offset_x)
11
+ end
12
+
13
+ ANSI.home
14
+ ANSI.background_color
15
+ ANSI.default_text_color
16
+ print @score_board.render
17
+ @viewport_height.times do |row|
18
+ @viewport_width.times do |col|
19
+ unless buffer[row][col] == @preview_buffer[row][col]
20
+ print "\e[#{row+MAP_Y_START};#{col+MAP_X_START}H" + ANSI::BACKGROUND_COLOR + ANSI::DEFAULT_TEXT_COLOR + buffer[row][col]
21
+ end
22
+ end
23
+ end
24
+ @preview_buffer = buffer.map(&:dup)
25
+ end
26
+
27
+ private
28
+
29
+ def initialize(score_board, background, foreground, fireworks)
30
+ @viewport_width = Map::VIEWPORT_WIDTH
31
+ @viewport_height = Map::VIEWPORT_HEIGHT
32
+ @preview_buffer = Array.new(@viewport_height) { Array.new(@viewport_width) { "" } }
33
+ @score_board = score_board
34
+ @background = background
35
+ @foreground = foreground
36
+ @layers = [background, *foreground.layers, fireworks]
37
+ end
38
+
39
+ def merge_buffer(buffer, layer, offset_x:)
40
+ layer.build_buffer(offset_x: offset_x).each_with_index do |row, i|
41
+ row.each_with_index do |tile, j|
42
+ buffer[i][j] = tile unless tile == ""
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,55 @@
1
+ module RoadToRubykaigi
2
+ module Manager
3
+ class GameManager
4
+ GOAL_X = 650
5
+ STATE = {
6
+ playing: 0,
7
+ pause: 1,
8
+ game_over: 2,
9
+ ending: 3,
10
+ finished: 4,
11
+ }
12
+ attr_reader :fireworks
13
+
14
+ def update
15
+ @deadline.activate(player_x: @player.x)
16
+ @enemies.activate if player_moved?
17
+ if @player.x >= GOAL_X && playing?
18
+ game_clear
19
+ end
20
+ end
21
+
22
+ def finish
23
+ @state = STATE[:finished]
24
+ end
25
+
26
+ def finished?
27
+ @state == STATE[:finished]
28
+ end
29
+
30
+ private
31
+
32
+ def initialize(player, deadline, enemies)
33
+ @player = player
34
+ @deadline = deadline
35
+ @enemies = enemies
36
+ @fireworks = RoadToRubykaigi::Fireworks.new(self)
37
+ @state = STATE[:playing]
38
+ end
39
+
40
+ def player_moved?
41
+ @player_initial_x ||= @player.x
42
+ @player_initial_x != @player.x
43
+ end
44
+
45
+ def playing?
46
+ @state == STATE[:playing]
47
+ end
48
+
49
+ def game_clear
50
+ @state = STATE[:ending]
51
+ @fireworks.shoot
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,27 @@
1
+ module RoadToRubykaigi
2
+ module Manager
3
+ class UpdateManager
4
+ def update(offset_x:)
5
+ enemies = @entities[3]
6
+ enemies.each do |enemy|
7
+ enemy.activate_with_offset(offset_x)
8
+ end
9
+ @entities.each(&:update)
10
+ enforce_boundary(offset_x: offset_x)
11
+ end
12
+
13
+ private
14
+
15
+ def initialize(map, foreground, fireworks)
16
+ @map = map
17
+ @entities = foreground.layers + [fireworks]
18
+ end
19
+
20
+ def enforce_boundary(offset_x:)
21
+ @entities.each do |entity|
22
+ entity.respond_to?(:enforce_boundary) && entity.enforce_boundary(@map, offset_x:)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,120 @@
1
+ module RoadToRubykaigi
2
+ class Map
3
+ VIEWPORT_WIDTH = 100
4
+ VIEWPORT_HEIGHT = 30
5
+ attr_reader :width, :height
6
+
7
+ def build_buffer(offset_x:)
8
+ (0...VIEWPORT_HEIGHT).map do |row|
9
+ @tiles[row][offset_x, VIEWPORT_WIDTH].map(&:character)
10
+ end
11
+ end
12
+
13
+ def clamp_position(x:, y:, width:, height:, dx:, dy:)
14
+ clamped_x = x.clamp(2, @width - width)
15
+ clamped_y = y.clamp(2, @height - height)
16
+ return [clamped_x, clamped_y] if box_passable?(clamped_x, clamped_y, width, height)
17
+
18
+ attempt_count = 10
19
+ delta_x = nil
20
+ delta_y = nil
21
+ unless dx == 0
22
+ (1..attempt_count).each do |i|
23
+ attempt_x = clamped_x + i * dx
24
+ if box_passable?(attempt_x, clamped_y, width, height)
25
+ break delta_x = attempt_x - clamped_x
26
+ end
27
+ end
28
+ end
29
+ unless dy == 0
30
+ (1..attempt_count).each do |i|
31
+ attempt_y = [clamped_y + i * dy, RoadToRubykaigi::Sprite::Player::BASE_Y].min
32
+ if box_passable?(clamped_x, attempt_y, width, height)
33
+ break delta_y = attempt_y - clamped_y
34
+ end
35
+ end
36
+ end
37
+
38
+ case
39
+ when delta_x && delta_y
40
+ if delta_x.abs <= delta_y.abs
41
+ [clamped_x + delta_x, clamped_y]
42
+ else
43
+ [clamped_x, clamped_y + delta_y]
44
+ end
45
+ when delta_x && !delta_y
46
+ [clamped_x + delta_x, clamped_y]
47
+ when !delta_x && delta_y
48
+ [clamped_x, clamped_y + delta_y]
49
+ else
50
+ coordinates = (1..attempt_count).select do |i|
51
+ attempt_x = clamped_x + i * dx
52
+ attempt_y = [clamped_y + i * dy, RoadToRubykaigi::Sprite::Player::BASE_Y].min
53
+ if box_passable?(attempt_x, attempt_y, width, height)
54
+ break [attempt_x, attempt_y]
55
+ end
56
+ end
57
+ coordinates.empty? ? [clamped_x + dx, clamped_y + dy] : coordinates
58
+ end
59
+ end
60
+
61
+ def passable_at?(col, row)
62
+ @tiles[row-1][col-1].passable?
63
+ end
64
+
65
+ private
66
+
67
+ def initialize
68
+ map_data = RoadToRubykaigi::Graphics::Map.data
69
+ mask_data = RoadToRubykaigi::Graphics::Mask.data
70
+ @tiles = map_data.each_with_index.map do |line, row|
71
+ line.chars.each_with_index.map do |ch, col|
72
+ Tile.new(ch, mask: mask_data[row][col])
73
+ end
74
+ end
75
+ @height = @tiles.size
76
+ @width = @tiles.first.size
77
+ end
78
+
79
+ def box_passable?(x, y, width, height)
80
+ (y...(y + height)).all? do |row|
81
+ (x...(x + width)).all? do |col|
82
+ passable_at?(col, row)
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ class Layer
89
+ attr_reader :layers
90
+
91
+ def build_buffer(offset_x:)
92
+ @layers.map { |layer| layer.build_buffer(offset_x: offset_x) }
93
+ end
94
+
95
+ private
96
+
97
+ def initialize(player:, deadline:, bonuses:, enemies:, attacks:, effects:)
98
+ @layers = [player, deadline, bonuses, enemies, attacks, effects]
99
+ end
100
+ end
101
+
102
+ class Tile
103
+ MASK_CHAR = "#"
104
+
105
+ def character
106
+ @symbol
107
+ end
108
+
109
+ def passable?
110
+ @mask != MASK_CHAR
111
+ end
112
+
113
+ private
114
+
115
+ def initialize(symbol, mask:)
116
+ @symbol = symbol
117
+ @mask = mask
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,56 @@
1
+ module RoadToRubykaigi
2
+ class OpeningScreen
3
+ WIDTH = 10
4
+ OFFSET = 30
5
+ DELAY = 0.75
6
+ LOGO =<<~LOGO
7
+ ╔═══════╗
8
+ ║ ║
9
+ ║ ║ ║
10
+ ║ ║ ║
11
+ ╠═════╦═╝ ╔═══════╗ ╔═══════║ ╔══════╣
12
+ ║ ╚═╗ ║ ║ ║ ║ ╔╝ ║ ══╬══ ╔═══╗
13
+ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║
14
+ ║ ║ ╚═══════╝ ╚═══════║ ╚═══════╝ ║ ╚═══╝
15
+
16
+ ╔═══════╗ ║ ║
17
+ ║ ║ ║ ║
18
+ ║ ║ ║ ║ ║ ║ ║ ╔═══════║
19
+ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║
20
+ ╠═════╦═╝ ║ ║ ╠══════╗ ║ ║ ╠═════╦═╝ ╔═══════║ ║ ║
21
+ ║ ╚═╗ ║ ║ ║ ╚╗ ╚═══════╣ ║ ╚═╗ ║ ║ ║ ╚═══════╣ ║
22
+ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║
23
+ ║ ║ ╚═══════║ ╚═══════╝ ════════╝ ║ ║ ╚═══════║ ║ ════════╝ ║
24
+ LOGO
25
+ PLAYER =<<~PLAYER
26
+ ╭──────╮
27
+ │。・◡・│_◢◤
28
+ ╰ᜊ───ᜊ─╯
29
+ PLAYER
30
+
31
+ def display
32
+ x = 0
33
+ direction = 1
34
+
35
+ loop do
36
+ ANSI.clear
37
+ puts "\e[6;1H" + LOGO
38
+ puts [
39
+ PLAYER.lines.map.with_index do |line, i|
40
+ "\e[#{i+1};#{x+OFFSET}H" + line
41
+ end.join,
42
+ "\e[4;1H" + "Press Space to start...",
43
+ ]
44
+ if $stdin.raw { $stdin.read_nonblock(1, exception: false) == " " }
45
+ break true
46
+ end
47
+
48
+ x += direction
49
+ if x >= WIDTH || x <= 0
50
+ direction = -direction
51
+ end
52
+ sleep DELAY
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,17 @@
1
+ module RoadToRubykaigi
2
+ class ScoreBoard
3
+ def increment
4
+ @score += 1
5
+ end
6
+
7
+ def render
8
+ "Score: #{@score}".ljust(10).rjust(Map::VIEWPORT_WIDTH)
9
+ end
10
+
11
+ private
12
+
13
+ def initialize
14
+ @score = 0
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,72 @@
1
+ require "forwardable"
2
+
3
+ module RoadToRubykaigi
4
+ module Sprite
5
+ class Attacks
6
+ extend Forwardable
7
+ def_delegators :@attacks, :each, :delete, :select
8
+
9
+ def add(x, y)
10
+ @attacks << Attack.new(x, y)
11
+ end
12
+
13
+ def update
14
+ @attacks.each(&:move)
15
+ end
16
+
17
+ def enforce_boundary(map, offset_x:)
18
+ @attacks.reject! do |attack|
19
+ attack.reach_border?(map, offset_x: offset_x)
20
+ end
21
+ end
22
+
23
+ def build_buffer(offset_x:)
24
+ buffer = Array.new(Map::VIEWPORT_HEIGHT) { Array.new(Map::VIEWPORT_WIDTH) { "" } }
25
+ @attacks.each do |attack|
26
+ bounding_box = attack.bounding_box
27
+ relative_x = bounding_box[:x] - offset_x - 1
28
+ relative_y = bounding_box[:y] - 1
29
+ next if relative_x < 1
30
+ attack.characters.each_with_index do |chara, j|
31
+ buffer[relative_y][relative_x+j] = chara
32
+ end
33
+ end
34
+ buffer
35
+ end
36
+
37
+ private
38
+
39
+ def initialize
40
+ @attacks = []
41
+ end
42
+ end
43
+
44
+ class Attack < Sprite
45
+ SYMBOL = ".˖"
46
+
47
+ def move
48
+ @x += 1
49
+ end
50
+
51
+ def characters
52
+ super { SYMBOL.chars }
53
+ end
54
+
55
+ def reach_border?(map, offset_x:)
56
+ (@x - offset_x + SYMBOL.size - 1) > Map::VIEWPORT_WIDTH ||
57
+ (@x + SYMBOL.size) > map.width
58
+ end
59
+
60
+ def bounding_box
61
+ { x: @x, y: @y, width: SYMBOL.size, height: 1 }
62
+ end
63
+
64
+ private
65
+
66
+ def initialize(x, y)
67
+ @x = x
68
+ @y = y
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,127 @@
1
+ require "forwardable"
2
+
3
+ module RoadToRubykaigi
4
+ module Sprite
5
+ class Bonuses
6
+ extend Forwardable
7
+ def_delegators :@bonuses, :to_a, :find, :delete
8
+ BONUSES_DATA = {
9
+ Basic: [
10
+ { x: 39, y: 22, character: :ruby },
11
+ { x: 46, y: 22, character: :ruby },
12
+ { x: 53, y: 22, character: :ruby },
13
+ { x: 107, y: 23, character: :coffee },
14
+ { x: 110, y: 23, character: :book },
15
+ { x: 142, y: 16, character: :ruby },
16
+ { x: 146, y: 16, character: :ruby },
17
+ { x: 205, y: 19, character: :money },
18
+ { x: 212, y: 19, character: :money },
19
+ { x: 205, y: 19, character: :money },
20
+ { x: 212, y: 19, character: :money },
21
+ { x: 223, y: 17, character: :money },
22
+ { x: 231, y: 17, character: :money },
23
+ { x: 243, y: 13, character: :money },
24
+ { x: 250, y: 13, character: :money },
25
+ { x: 260, y: 10, character: :sushi },
26
+ { x: 265, y: 10, character: :meat },
27
+ { x: 270, y: 10, character: :fish },
28
+ { x: 260, y: 10, character: :sushi },
29
+ { x: 265, y: 10, character: :meat },
30
+ { x: 270, y: 10, character: :fish },
31
+ { x: 275, y: 10, character: :sushi },
32
+ { x: 280, y: 10, character: :meat },
33
+ { x: 285, y: 10, character: :fish },
34
+ { x: 290, y: 10, character: :sushi },
35
+ { x: 295, y: 10, character: :meat },
36
+ { x: 300, y: 10, character: :fish },
37
+ { x: 358, y: 15, character: :money },
38
+ { x: 363, y: 13, character: :money },
39
+ { x: 368, y: 15, character: :money },
40
+ { x: 373, y: 13, character: :money },
41
+ { x: 378, y: 15, character: :money },
42
+ { x: 383, y: 13, character: :money },
43
+ { x: 388, y: 15, character: :money },
44
+ ],
45
+ Alcohol: [
46
+ { x: 217, y: 28, character: :beer },
47
+ { x: 220, y: 28, character: :beer },
48
+ { x: 223, y: 28, character: :beer },
49
+ ],
50
+ Laptop: [
51
+ { x: 298, y: 23, character: :laptop },
52
+ ]
53
+ }
54
+
55
+ def build_buffer(offset_x:)
56
+ buffer = Array.new(Map::VIEWPORT_HEIGHT) { Array.new(Map::VIEWPORT_WIDTH) { "" } }
57
+ @bonuses.each do |bonus|
58
+ bounding_box = bonus.bounding_box
59
+ relative_x = bounding_box[:x] - offset_x - 1
60
+ relative_y = bounding_box[:y] - 1
61
+ next if relative_x < 1
62
+ bonus.characters.each_with_index do |character, j|
63
+ next if relative_x + j >= Map::VIEWPORT_WIDTH - 1
64
+ buffer[relative_y][relative_x+j] = character
65
+ end
66
+ end
67
+ buffer
68
+ end
69
+
70
+ def update
71
+ end
72
+
73
+ private
74
+
75
+ def initialize
76
+ @bonuses = BONUSES_DATA.map do |key, bonuses|
77
+ bonuses.map do |bonus|
78
+ Bonus.new(
79
+ bonus[:x],
80
+ bonus[:y],
81
+ bonus[:character],
82
+ )
83
+ end
84
+ end.flatten
85
+ end
86
+ end
87
+
88
+ class Bonus < Sprite
89
+ CHARACTER = {
90
+ ruby: "💎",
91
+ money: "💰",
92
+ coffee: "☕️",
93
+ book: "📚",
94
+ sushi: "🍣",
95
+ meat: "🍖",
96
+ fish: "🐟",
97
+ beer: "🍺",
98
+ sake: "🍶",
99
+ laptop: "💻",
100
+ }
101
+
102
+ def bounding_box
103
+ { x: @x, y: @y, width: width, height: height }
104
+ end
105
+
106
+ def characters
107
+ super { [CHARACTER[@character]] }
108
+ end
109
+
110
+ def width
111
+ 2
112
+ end
113
+
114
+ def height
115
+ 1
116
+ end
117
+
118
+ private
119
+
120
+ def initialize(x, y, character)
121
+ @x = x
122
+ @y = y
123
+ @character = character
124
+ end
125
+ end
126
+ end
127
+ end