glimmer-dsl-gtk 0.0.2 → 0.0.6

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 +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +1072 -27
  4. data/VERSION +1 -1
  5. data/glimmer-dsl-gtk.gemspec +0 -0
  6. data/images/breaking-blue-wave.png +0 -0
  7. data/lib/glimmer/dsl/gtk/observe_expression.rb +35 -0
  8. data/lib/glimmer/gtk/shape/arc_negative.rb +70 -0
  9. data/lib/glimmer/gtk/shape/path.rb +33 -0
  10. data/lib/glimmer/gtk/shape/square.rb +76 -0
  11. data/lib/glimmer/gtk/shape.rb +56 -10
  12. data/lib/glimmer/gtk/transformable.rb +93 -0
  13. data/lib/glimmer/gtk/widget_proxy/drawing_area_proxy.rb +16 -0
  14. data/lib/glimmer/gtk/widget_proxy.rb +2 -2
  15. data/samples/cairo/arc.rb +44 -0
  16. data/samples/cairo/arc_negative.rb +44 -0
  17. data/samples/cairo/clip.rb +34 -0
  18. data/samples/cairo/clip_image.rb +28 -0
  19. data/samples/cairo/curve_to.rb +39 -0
  20. data/samples/cairo/dashes.rb +30 -0
  21. data/samples/cairo/fill_and_stroke2.rb +36 -0
  22. data/samples/cairo/fill_style.rb +43 -0
  23. data/samples/cairo/gradient.rb +31 -0
  24. data/samples/cairo/image.rb +23 -0
  25. data/samples/cairo/image_gradient.rb +32 -0
  26. data/samples/cairo/multi_segment_caps.rb +27 -0
  27. data/samples/cairo/rounded_rectangle.rb +20 -0
  28. data/samples/cairo/set_line_cap.rb +53 -0
  29. data/samples/cairo/set_line_join.rb +43 -0
  30. data/samples/elaborate/tetris/model/block.rb +48 -0
  31. data/samples/elaborate/tetris/model/game.rb +320 -0
  32. data/samples/elaborate/tetris/model/past_game.rb +39 -0
  33. data/samples/elaborate/tetris/model/tetromino.rb +329 -0
  34. data/samples/elaborate/tetris.rb +338 -0
  35. data/samples/hello/hello_drawing_area.rb +1 -3
  36. data/samples/hello/hello_drawing_area_manual.rb +20 -21
  37. metadata +29 -4
@@ -0,0 +1,320 @@
1
+ # Copyright (c) 2021-2022 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, :beeping, :added_high_score, :show_high_scores, :up_arrow_action
42
+ alias game_over? game_over
43
+ alias paused? paused
44
+ alias beeping? beeping
45
+ alias added_high_score? added_high_score
46
+
47
+ def initialize(playfield_width = PLAYFIELD_WIDTH, playfield_height = PLAYFIELD_HEIGHT)
48
+ @playfield_width = playfield_width
49
+ @playfield_height = playfield_height
50
+ @high_scores = []
51
+ @show_high_scores = false
52
+ @beeping = true
53
+ @up_arrow_action = :rotate_left
54
+ load_high_scores!
55
+ end
56
+
57
+ def configure_beeper(&beeper)
58
+ @beeper = beeper
59
+ end
60
+
61
+ def game_in_progress?
62
+ !game_over? && !paused?
63
+ end
64
+
65
+ def start!
66
+ self.show_high_scores = false
67
+ self.paused = false
68
+ self.level = 1
69
+ self.score = 0
70
+ self.lines = 0
71
+ reset_playfield
72
+ reset_preview_playfield
73
+ reset_tetrominoes
74
+ preview_next_tetromino!
75
+ consider_adding_tetromino
76
+ self.game_over = false
77
+ end
78
+ alias restart! start!
79
+
80
+ def game_over!
81
+ add_high_score!
82
+ beep
83
+ self.game_over = true
84
+ end
85
+
86
+ def clear_high_scores!
87
+ high_scores.clear
88
+ save_high_scores!
89
+ end
90
+
91
+ def add_high_score!
92
+ self.added_high_score = true
93
+ high_scores.prepend(PastGame.new("Player #{high_scores.count + 1}", score, lines, level))
94
+ save_high_scores!
95
+ end
96
+
97
+ def save_high_scores!
98
+ high_score_file_content = @high_scores.map {|past_game| past_game.to_a.join("\t") }.join("\n")
99
+ FileUtils.mkdir_p(tetris_dir)
100
+ File.write(tetris_high_score_file, high_score_file_content)
101
+ rescue => e
102
+ # Fail safely by keeping high scores in memory if unable to access disk
103
+ Glimmer::Config.logger.error {"Failed to save high scores in: #{tetris_high_score_file}\n#{e.full_message}"}
104
+ end
105
+
106
+ def load_high_scores!
107
+ if File.exist?(tetris_high_score_file)
108
+ self.high_scores = File.read(tetris_high_score_file).split("\n").map {|line| PastGame.new(*line.split("\t")) }
109
+ end
110
+ rescue => e
111
+ # Fail safely by keeping high scores in memory if unable to access disk
112
+ Glimmer::Config.logger.error {"Failed to load high scores from: #{tetris_high_score_file}\n#{e.full_message}"}
113
+ end
114
+
115
+ def tetris_dir
116
+ @tetris_dir ||= File.join(Dir.home, '.glimmer-tetris')
117
+ end
118
+
119
+ def tetris_high_score_file
120
+ File.join(tetris_dir, "high_scores.txt")
121
+ end
122
+
123
+ def down!(instant: false)
124
+ return unless game_in_progress?
125
+ current_tetromino.down!(instant: instant)
126
+ game_over! if current_tetromino.row <= 0 && current_tetromino.stopped?
127
+ end
128
+
129
+ def right!
130
+ return unless game_in_progress?
131
+ current_tetromino.right!
132
+ end
133
+
134
+ def left!
135
+ return unless game_in_progress?
136
+ current_tetromino.left!
137
+ end
138
+
139
+ def rotate!(direction)
140
+ return unless game_in_progress?
141
+ current_tetromino.rotate!(direction)
142
+ end
143
+
144
+ def current_tetromino
145
+ tetrominoes.last
146
+ end
147
+
148
+ def tetrominoes
149
+ @tetrominoes ||= reset_tetrominoes
150
+ end
151
+
152
+ # Returns blocks in the playfield
153
+ def playfield
154
+ @playfield ||= @original_playfield = @playfield_height.times.map do
155
+ @playfield_width.times.map do
156
+ Block.new
157
+ end
158
+ end
159
+ end
160
+
161
+ # Executes a hypothetical scenario without truly changing playfield permanently
162
+ def hypothetical(&block)
163
+ @playfield = hypothetical_playfield
164
+ block.call
165
+ @playfield = @original_playfield
166
+ end
167
+
168
+ # Returns whether currently executing a hypothetical scenario
169
+ def hypothetical?
170
+ @playfield != @original_playfield
171
+ end
172
+
173
+ def hypothetical_playfield
174
+ @playfield_height.times.map { |row|
175
+ @playfield_width.times.map { |column|
176
+ playfield[row][column].clone
177
+ }
178
+ }
179
+ end
180
+
181
+ def preview_playfield
182
+ @preview_playfield ||= PREVIEW_PLAYFIELD_HEIGHT.times.map {|row|
183
+ PREVIEW_PLAYFIELD_WIDTH.times.map {|column|
184
+ Block.new
185
+ }
186
+ }
187
+ end
188
+
189
+ def preview_next_tetromino!
190
+ self.preview_tetromino = Tetromino.new(self)
191
+ end
192
+
193
+ def calculate_score!(eliminated_lines)
194
+ new_score = SCORE_MULTIPLIER[eliminated_lines] * (level + 1)
195
+ self.score += new_score
196
+ end
197
+
198
+ def level_up!
199
+ self.level += 1 if lines >= self.level*10
200
+ end
201
+
202
+ def delay
203
+ [1.1 - (level.to_i * 0.1), 0.001].max
204
+ end
205
+
206
+ def beep
207
+ @beeper&.call if beeping
208
+ end
209
+
210
+ def instant_down_on_up=(value)
211
+ self.up_arrow_action = :instant_down if value
212
+ end
213
+
214
+ def instant_down_on_up
215
+ self.up_arrow_action == :instant_down
216
+ end
217
+
218
+ def instant_down_on_up!
219
+ self.up_arrow_action = :instant_down
220
+ end
221
+
222
+ def rotate_right_on_up=(value)
223
+ self.up_arrow_action = :rotate_right if value
224
+ end
225
+
226
+ def rotate_right_on_up
227
+ self.up_arrow_action == :rotate_right
228
+ end
229
+
230
+ def rotate_right_on_up!
231
+ self.up_arrow_action = :rotate_right
232
+ end
233
+
234
+ def rotate_left_on_up=(value)
235
+ self.up_arrow_action = :rotate_left if value
236
+ end
237
+
238
+ def rotate_left_on_up
239
+ self.up_arrow_action == :rotate_left
240
+ end
241
+
242
+ def rotate_left_on_up!
243
+ self.up_arrow_action = :rotate_left
244
+ end
245
+
246
+ def reset_tetrominoes
247
+ @tetrominoes = []
248
+ end
249
+
250
+ def reset_playfield
251
+ playfield.each do |row|
252
+ row.each do |block|
253
+ block.clear
254
+ end
255
+ end
256
+ end
257
+
258
+ def reset_preview_playfield
259
+ preview_playfield.each do |row|
260
+ row.each do |block|
261
+ block.clear
262
+ end
263
+ end
264
+ end
265
+
266
+ def consider_adding_tetromino
267
+ if tetrominoes.empty? || current_tetromino.stopped?
268
+ preview_tetromino.launch!
269
+ preview_next_tetromino!
270
+ end
271
+ end
272
+
273
+ def consider_eliminating_lines
274
+ eliminated_lines = 0
275
+ playfield.each_with_index do |row, playfield_row|
276
+ if row.all? {|block| !block.clear?}
277
+ eliminated_lines += 1
278
+ shift_blocks_down_above_row(playfield_row)
279
+ end
280
+ end
281
+ if eliminated_lines > 0
282
+ beep
283
+ self.lines += eliminated_lines
284
+ level_up!
285
+ calculate_score!(eliminated_lines)
286
+ end
287
+ end
288
+
289
+ def playfield_remaining_heights(tetromino = nil)
290
+ @playfield_width.times.map do |playfield_column|
291
+ bottom_most_block = tetromino.bottom_most_block_for_column(playfield_column)
292
+ (playfield.each_with_index.detect do |row, playfield_row|
293
+ !row[playfield_column].clear? &&
294
+ (
295
+ tetromino.nil? ||
296
+ bottom_most_block.nil? ||
297
+ (playfield_row > tetromino.row + bottom_most_block[:row_index])
298
+ )
299
+ end || [nil, @playfield_height])[1]
300
+ end.to_a
301
+ end
302
+
303
+ private
304
+
305
+ def shift_blocks_down_above_row(row)
306
+ row.downto(0) do |playfield_row|
307
+ playfield[playfield_row].each_with_index do |block, playfield_column|
308
+ previous_row = playfield[playfield_row - 1]
309
+ previous_block = previous_row[playfield_column]
310
+ block.color = previous_block.color unless block.color == previous_block.color
311
+ end
312
+ end
313
+ playfield[0].each(&:clear)
314
+ end
315
+
316
+ end
317
+
318
+ end
319
+
320
+ end
@@ -0,0 +1,39 @@
1
+ # Copyright (c) 2021-2022 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) 2021-2022 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: [0, 255, 255],
33
+ J: [0, 0, 255],
34
+ L: [128, 128, 0],
35
+ O: [255, 255, 0],
36
+ S: [0, 255, 0],
37
+ T: [255, 0, 255],
38
+ Z: [255, 0, 0],
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
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
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