glimmer-dsl-opal 0.25.4 → 0.26.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,48 @@
1
+ # Copyright (c) 2007-2021 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ class Tetris
23
+ module Model
24
+ class Block
25
+ COLOR_CLEAR = :white
26
+
27
+ attr_accessor :color
28
+
29
+ # Initializes with color. Default color (gray) signifies an empty block
30
+ def initialize(color = COLOR_CLEAR)
31
+ @color = color
32
+ end
33
+
34
+ # Clears block color. `quietly` option indicates if it should not notify observers by setting value quietly via variable not attribute writer.
35
+ def clear
36
+ self.color = COLOR_CLEAR unless self.color == COLOR_CLEAR
37
+ end
38
+
39
+ def clear?
40
+ self.color == COLOR_CLEAR
41
+ end
42
+
43
+ def occupied?
44
+ !clear?
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,275 @@
1
+ # Copyright (c) 2007-2021 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require 'fileutils'
23
+ require 'json'
24
+ require 'glimmer/data_binding/observer'
25
+ require 'glimmer/config'
26
+
27
+ require_relative 'block'
28
+ require_relative 'tetromino'
29
+ require_relative 'past_game'
30
+
31
+ class Tetris
32
+ module Model
33
+ class Game
34
+ PLAYFIELD_WIDTH = 10
35
+ PLAYFIELD_HEIGHT = 20
36
+ PREVIEW_PLAYFIELD_WIDTH = 4
37
+ PREVIEW_PLAYFIELD_HEIGHT = 2
38
+ SCORE_MULTIPLIER = {1 => 40, 2 => 100, 3 => 300, 4 => 1200}
39
+
40
+ attr_reader :playfield_width, :playfield_height
41
+ attr_accessor :game_over, :paused, :preview_tetromino, :lines, :score, :level, :high_scores, :added_high_score, :show_high_scores, :up_arrow_action
42
+ alias game_over? game_over
43
+ alias paused? paused
44
+ alias added_high_score? added_high_score
45
+
46
+ def initialize(playfield_width = PLAYFIELD_WIDTH, playfield_height = PLAYFIELD_HEIGHT)
47
+ @playfield_width = playfield_width
48
+ @playfield_height = playfield_height
49
+ @high_scores = []
50
+ @show_high_scores = false
51
+ @up_arrow_action = :rotate_left
52
+ end
53
+
54
+ def game_in_progress?
55
+ !game_over? && !paused?
56
+ end
57
+
58
+ def start!
59
+ self.show_high_scores = false
60
+ self.paused = false
61
+ self.level = 1
62
+ self.score = 0
63
+ self.lines = 0
64
+ reset_playfield
65
+ reset_preview_playfield
66
+ reset_tetrominoes
67
+ preview_next_tetromino!
68
+ consider_adding_tetromino
69
+ self.game_over = false
70
+ end
71
+ alias restart! start!
72
+
73
+ def game_over!
74
+ add_high_score!
75
+ self.game_over = true
76
+ end
77
+
78
+ def clear_high_scores!
79
+ high_scores.clear
80
+ end
81
+
82
+ def add_high_score!
83
+ self.added_high_score = true
84
+ high_scores.prepend(PastGame.new("Player #{high_scores.count + 1}", score, lines, level))
85
+ end
86
+
87
+ def tetris_dir
88
+ @tetris_dir ||= File.join(File.expand_path('~'), '.glimmer-tetris')
89
+ end
90
+
91
+ def tetris_high_score_file
92
+ File.join(tetris_dir, "high_scores.txt")
93
+ end
94
+
95
+ def down!(instant: false)
96
+ return unless game_in_progress?
97
+ current_tetromino.down!(instant: instant)
98
+ game_over! if current_tetromino.row <= 0 && current_tetromino.stopped?
99
+ end
100
+
101
+ def right!
102
+ return unless game_in_progress?
103
+ current_tetromino.right!
104
+ end
105
+
106
+ def left!
107
+ return unless game_in_progress?
108
+ current_tetromino.left!
109
+ end
110
+
111
+ def rotate!(direction)
112
+ return unless game_in_progress?
113
+ current_tetromino.rotate!(direction)
114
+ end
115
+
116
+ def current_tetromino
117
+ tetrominoes.last
118
+ end
119
+
120
+ def tetrominoes
121
+ @tetrominoes ||= reset_tetrominoes
122
+ end
123
+
124
+ # Returns blocks in the playfield
125
+ def playfield
126
+ @playfield ||= @original_playfield = @playfield_height.times.map {
127
+ @playfield_width.times.map {
128
+ Block.new
129
+ }
130
+ }
131
+ end
132
+
133
+ # Executes a hypothetical scenario without truly changing playfield permanently
134
+ def hypothetical(&block)
135
+ @playfield = hypothetical_playfield
136
+ block.call
137
+ @playfield = @original_playfield
138
+ end
139
+
140
+ # Returns whether currently executing a hypothetical scenario
141
+ def hypothetical?
142
+ @playfield != @original_playfield
143
+ end
144
+
145
+ def hypothetical_playfield
146
+ @playfield_height.times.map { |row|
147
+ @playfield_width.times.map { |column|
148
+ playfield[row][column].clone
149
+ }
150
+ }
151
+ end
152
+
153
+ def preview_playfield
154
+ @preview_playfield ||= PREVIEW_PLAYFIELD_HEIGHT.times.map {|row|
155
+ PREVIEW_PLAYFIELD_WIDTH.times.map {|column|
156
+ Block.new
157
+ }
158
+ }
159
+ end
160
+
161
+ def preview_next_tetromino!
162
+ self.preview_tetromino = Tetromino.new(self)
163
+ end
164
+
165
+ def calculate_score!(eliminated_lines)
166
+ new_score = SCORE_MULTIPLIER[eliminated_lines] * (level + 1)
167
+ self.score += new_score
168
+ end
169
+
170
+ def level_up!
171
+ self.level += 1 if lines >= self.level*10
172
+ end
173
+
174
+ def delay
175
+ [1.1 - (level.to_i * 0.1), 0.001].max
176
+ end
177
+
178
+ def instant_down_on_up=(value)
179
+ self.up_arrow_action = :instant_down if value
180
+ end
181
+
182
+ def instant_down_on_up
183
+ self.up_arrow_action == :instant_down
184
+ end
185
+
186
+ def rotate_right_on_up=(value)
187
+ self.up_arrow_action = :rotate_right if value
188
+ end
189
+
190
+ def rotate_right_on_up
191
+ self.up_arrow_action == :rotate_right
192
+ end
193
+
194
+ def rotate_left_on_up=(value)
195
+ self.up_arrow_action = :rotate_left if value
196
+ end
197
+
198
+ def rotate_left_on_up
199
+ self.up_arrow_action == :rotate_left
200
+ end
201
+
202
+ def reset_tetrominoes
203
+ @tetrominoes = []
204
+ end
205
+
206
+ def reset_playfield
207
+ playfield.each do |row|
208
+ row.each do |block|
209
+ block.clear
210
+ end
211
+ end
212
+ end
213
+
214
+ def reset_preview_playfield
215
+ preview_playfield.each do |row|
216
+ row.each do |block|
217
+ block.clear
218
+ end
219
+ end
220
+ end
221
+
222
+ def consider_adding_tetromino
223
+ if tetrominoes.empty? || current_tetromino.stopped?
224
+ preview_tetromino.launch!
225
+ preview_next_tetromino!
226
+ end
227
+ end
228
+
229
+ def consider_eliminating_lines
230
+ eliminated_lines = 0
231
+ playfield.each_with_index do |row, playfield_row|
232
+ if row.all? {|block| !block.clear?}
233
+ eliminated_lines += 1
234
+ shift_blocks_down_above_row(playfield_row)
235
+ end
236
+ end
237
+ if eliminated_lines > 0
238
+ self.lines += eliminated_lines
239
+ level_up!
240
+ calculate_score!(eliminated_lines)
241
+ end
242
+ end
243
+
244
+ def playfield_remaining_heights(tetromino = nil)
245
+ @playfield_width.times.map do |playfield_column|
246
+ bottom_most_block = tetromino.bottom_most_block_for_column(playfield_column)
247
+ (playfield.each_with_index.detect do |row, playfield_row|
248
+ !row[playfield_column].clear? &&
249
+ (
250
+ tetromino.nil? ||
251
+ bottom_most_block.nil? ||
252
+ (playfield_row > tetromino.row + bottom_most_block[:row_index])
253
+ )
254
+ end || [nil, @playfield_height])[1]
255
+ end.to_a
256
+ end
257
+
258
+ private
259
+
260
+ def shift_blocks_down_above_row(row)
261
+ row.downto(0) do |playfield_row|
262
+ playfield[playfield_row].each_with_index do |block, playfield_column|
263
+ previous_row = playfield[playfield_row - 1]
264
+ previous_block = previous_row[playfield_column]
265
+ block.color = previous_block.color unless block.color == previous_block.color
266
+ end
267
+ end
268
+ playfield[0].each(&:clear)
269
+ end
270
+
271
+ end
272
+
273
+ end
274
+
275
+ end
@@ -0,0 +1,39 @@
1
+ # Copyright (c) 2007-2021 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ class Tetris
23
+ module Model
24
+ class PastGame
25
+ attr_accessor :name, :score, :lines, :level
26
+
27
+ def initialize(name, score, lines, level)
28
+ @name = name
29
+ @score = score.to_i
30
+ @lines = lines.to_i
31
+ @level = level.to_i
32
+ end
33
+
34
+ def to_a
35
+ [@name, @score, @lines, @level]
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,329 @@
1
+ # Copyright (c) 2007-2021 Andy Maleh
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ require_relative 'block'
23
+
24
+ require 'matrix'
25
+
26
+ class Tetris
27
+ module Model
28
+ class Tetromino
29
+ ORIENTATIONS = [:north, :east, :south, :west]
30
+
31
+ LETTER_COLORS = {
32
+ I: :cyan,
33
+ J: :blue,
34
+ L: :dark_yellow,
35
+ O: :yellow,
36
+ S: :green,
37
+ T: :magenta,
38
+ Z: :red,
39
+ }
40
+
41
+ attr_reader :game, :letter, :preview
42
+ alias preview? preview
43
+ attr_accessor :orientation, :blocks, :row, :column
44
+
45
+ def initialize(game)
46
+ @game = game
47
+ @letter = LETTER_COLORS.keys.sample
48
+ @orientation = :north
49
+ @blocks = default_blocks
50
+ @preview = true
51
+ new_row = 0
52
+ new_column = ((Model::Game::PREVIEW_PLAYFIELD_WIDTH - width)/2).to_i
53
+ update_playfield(new_row, new_column)
54
+ end
55
+
56
+ def playfield
57
+ @preview ? game.preview_playfield : game.playfield
58
+ end
59
+
60
+ def launch!
61
+ remove_from_playfield
62
+ @preview = false
63
+ new_row = 1 - height
64
+ new_column = ((game.playfield_width - width)/2).to_i
65
+ update_playfield(new_row, new_column)
66
+ game.tetrominoes << self
67
+ end
68
+
69
+ def update_playfield(new_row = nil, new_column = nil)
70
+ remove_from_playfield
71
+ if !new_row.nil? && !new_column.nil?
72
+ @row = new_row
73
+ @column = new_column
74
+ add_to_playfield
75
+ end
76
+ end
77
+
78
+ def add_to_playfield
79
+ update_playfield_block do |playfield_row, playfield_column, row_index, column_index|
80
+ playfield[playfield_row][playfield_column].color = blocks[row_index][column_index].color if playfield_row >= 0 && playfield[playfield_row][playfield_column]&.clear? && !blocks[row_index][column_index].clear? && playfield[playfield_row][playfield_column].color != blocks[row_index][column_index].color
81
+ end
82
+ end
83
+
84
+ def remove_from_playfield
85
+ return if @row.nil? || @column.nil?
86
+ update_playfield_block do |playfield_row, playfield_column, row_index, column_index|
87
+ playfield[playfield_row][playfield_column].clear if playfield_row >= 0 && !blocks[row_index][column_index].clear? && playfield[playfield_row][playfield_column]&.color == color
88
+ end
89
+ end
90
+
91
+ def stopped?
92
+ return true if @stopped || @preview
93
+ playfield_remaining_heights = game.playfield_remaining_heights(self)
94
+ result = bottom_most_blocks.any? do |bottom_most_block|
95
+ playfield_column = @column + bottom_most_block[:column_index]
96
+ playfield_remaining_heights[playfield_column] &&
97
+ @row + bottom_most_block[:row_index] >= playfield_remaining_heights[playfield_column] - 1
98
+ end
99
+ if result && !game.hypothetical?
100
+ @stopped = result
101
+ game.consider_eliminating_lines
102
+ @game.consider_adding_tetromino
103
+ end
104
+ result
105
+ end
106
+
107
+ # Returns bottom-most blocks of a tetromino, which could be from multiple rows depending on shape (e.g. T)
108
+ def bottom_most_blocks
109
+ width.times.map do |column_index|
110
+ row_blocks_with_row_index = @blocks.each_with_index.to_a.reverse.detect do |row_blocks, row_index|
111
+ !row_blocks[column_index].clear?
112
+ end
113
+ bottom_most_block = row_blocks_with_row_index[0][column_index]
114
+ bottom_most_block_row = row_blocks_with_row_index[1]
115
+ {
116
+ block: bottom_most_block,
117
+ row_index: bottom_most_block_row,
118
+ column_index: column_index
119
+ }
120
+ end
121
+ end
122
+
123
+ def bottom_most_block_for_column(column)
124
+ bottom_most_blocks.detect {|bottom_most_block| (@column + bottom_most_block[:column_index]) == column}
125
+ end
126
+
127
+ def right_blocked?
128
+ (@column == game.playfield_width - width) ||
129
+ right_most_blocks.any? { |right_most_block|
130
+ (@row + right_most_block[:row_index]) >= 0 &&
131
+ playfield[@row + right_most_block[:row_index]][@column + right_most_block[:column_index] + 1].occupied?
132
+ }
133
+ end
134
+
135
+ # Returns right-most blocks of a tetromino, which could be from multiple columns depending on shape (e.g. T)
136
+ def right_most_blocks
137
+ @blocks.each_with_index.map do |row_blocks, row_index|
138
+ column_block_with_column_index = row_blocks.each_with_index.to_a.reverse.detect do |column_block, column_index|
139
+ !column_block.clear?
140
+ end
141
+ if column_block_with_column_index
142
+ right_most_block = column_block_with_column_index[0]
143
+ {
144
+ block: right_most_block,
145
+ row_index: row_index,
146
+ column_index: column_block_with_column_index[1]
147
+ }
148
+ end
149
+ end.compact
150
+ end
151
+
152
+ def left_blocked?
153
+ (@column == 0) ||
154
+ left_most_blocks.any? { |left_most_block|
155
+ (@row + left_most_block[:row_index]) >= 0 &&
156
+ playfield[@row + left_most_block[:row_index]][@column + left_most_block[:column_index] - 1].occupied?
157
+ }
158
+ end
159
+
160
+ # Returns right-most blocks of a tetromino, which could be from multiple columns depending on shape (e.g. T)
161
+ def left_most_blocks
162
+ @blocks.each_with_index.map do |row_blocks, row_index|
163
+ column_block_with_column_index = row_blocks.each_with_index.to_a.detect do |column_block, column_index|
164
+ !column_block.clear?
165
+ end
166
+ if column_block_with_column_index
167
+ left_most_block = column_block_with_column_index[0]
168
+ {
169
+ block: left_most_block,
170
+ row_index: row_index,
171
+ column_index: column_block_with_column_index[1]
172
+ }
173
+ end
174
+ end.compact
175
+ end
176
+
177
+ def width
178
+ @blocks[0].size
179
+ end
180
+
181
+ def height
182
+ @blocks.size
183
+ end
184
+
185
+ def down!(instant: false)
186
+ launch! if preview?
187
+ unless stopped?
188
+ block_count = 1
189
+ if instant
190
+ remaining_height, bottom_touching_block = remaining_height_and_bottom_touching_block
191
+ block_count = remaining_height - @row
192
+ end
193
+ new_row = @row + block_count
194
+ update_playfield(new_row, @column)
195
+ end
196
+ end
197
+
198
+ def left!
199
+ unless left_blocked?
200
+ new_column = @column - 1
201
+ update_playfield(@row, new_column)
202
+ end
203
+ end
204
+
205
+ def right!
206
+ unless right_blocked?
207
+ new_column = @column + 1
208
+ update_playfield(@row, new_column)
209
+ end
210
+ end
211
+
212
+ # Rotate in specified direcation, which can be :right (clockwise) or :left (counterclockwise)
213
+ def rotate!(direction)
214
+ return if stopped?
215
+ can_rotate = nil
216
+ new_blocks = nil
217
+ game.hypothetical do
218
+ hypothetical_rotated_tetromino = hypothetical_tetromino
219
+ new_blocks = hypothetical_rotated_tetromino.rotate_blocks(direction)
220
+ can_rotate = !hypothetical_rotated_tetromino.stopped? && !hypothetical_rotated_tetromino.right_blocked? && !hypothetical_rotated_tetromino.left_blocked?
221
+ end
222
+ if can_rotate
223
+ remove_from_playfield
224
+ self.orientation = ORIENTATIONS[ORIENTATIONS.rotate(direction == :right ? -1 : 1).index(@orientation)]
225
+ self.blocks = new_blocks
226
+ update_playfield(@row, @column)
227
+ end
228
+ rescue => e
229
+ puts e.full_message
230
+ end
231
+
232
+ def rotate_blocks(direction)
233
+ new_blocks = Matrix[*@blocks].transpose.to_a
234
+ if direction == :right
235
+ new_blocks = new_blocks.map(&:reverse)
236
+ else
237
+ new_blocks = new_blocks.reverse
238
+ end
239
+ Matrix[*new_blocks].to_a
240
+ end
241
+
242
+ def hypothetical_tetromino
243
+ clone.tap do |hypo_clone|
244
+ remove_from_playfield
245
+ hypo_clone.blocks = @blocks.map do |row_blocks|
246
+ row_blocks.map do |column_block|
247
+ column_block.clone
248
+ end
249
+ end
250
+ end
251
+ end
252
+
253
+ def remaining_height_and_bottom_touching_block
254
+ playfield_remaining_heights = game.playfield_remaining_heights(self)
255
+ bottom_most_blocks.map do |bottom_most_block|
256
+ playfield_column = @column + bottom_most_block[:column_index]
257
+ [playfield_remaining_heights[playfield_column] - (bottom_most_block[:row_index] + 1), bottom_most_block]
258
+ end.min_by(&:first)
259
+ end
260
+
261
+ def default_blocks
262
+ case @letter
263
+ when :I
264
+ [
265
+ [block, block, block, block]
266
+ ]
267
+ when :J
268
+ [
269
+ [block, block, block],
270
+ [empty, empty, block],
271
+ ]
272
+ when :L
273
+ [
274
+ [block, block, block],
275
+ [block, empty, empty],
276
+ ]
277
+ when :O
278
+ [
279
+ [block, block],
280
+ [block, block],
281
+ ]
282
+ when :S
283
+ [
284
+ [empty, block, block],
285
+ [block, block, empty],
286
+ ]
287
+ when :T
288
+ [
289
+ [block, block, block],
290
+ [empty, block, empty],
291
+ ]
292
+ when :Z
293
+ [
294
+ [block, block, empty],
295
+ [empty, block, block],
296
+ ]
297
+ end
298
+ end
299
+
300
+ def color
301
+ LETTER_COLORS[@letter]
302
+ end
303
+
304
+ def include_block?(block)
305
+ @blocks.flatten.include?(block)
306
+ end
307
+
308
+ private
309
+
310
+ def block
311
+ Block.new(color)
312
+ end
313
+
314
+ def empty
315
+ Block.new
316
+ end
317
+
318
+ def update_playfield_block(&updater)
319
+ @row.upto(@row + height - 1) do |playfield_row|
320
+ @column.upto(@column + width - 1) do |playfield_column|
321
+ row_index = playfield_row - @row
322
+ column_index = playfield_column - @column
323
+ updater.call(playfield_row, playfield_column, row_index, column_index)
324
+ end
325
+ end
326
+ end
327
+ end
328
+ end
329
+ end