glimmer-dsl-swt 4.18.2.1 → 4.18.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +175 -32
  4. data/VERSION +1 -1
  5. data/glimmer-dsl-swt.gemspec +18 -6
  6. data/lib/glimmer/dsl/swt/color_expression.rb +4 -4
  7. data/lib/glimmer/dsl/swt/data_binding_expression.rb +3 -3
  8. data/lib/glimmer/dsl/swt/display_expression.rb +3 -3
  9. data/lib/glimmer/dsl/swt/dsl.rb +1 -0
  10. data/lib/glimmer/dsl/swt/multiply_expression.rb +53 -0
  11. data/lib/glimmer/dsl/swt/property_expression.rb +4 -2
  12. data/lib/glimmer/dsl/swt/shape_expression.rb +2 -4
  13. data/lib/glimmer/dsl/swt/transform_expression.rb +55 -0
  14. data/lib/glimmer/dsl/swt/widget_expression.rb +2 -1
  15. data/lib/glimmer/rake_task/scaffold.rb +4 -3
  16. data/lib/glimmer/swt/color_proxy.rb +28 -6
  17. data/lib/glimmer/swt/custom/drawable.rb +8 -0
  18. data/lib/glimmer/swt/custom/shape.rb +66 -26
  19. data/lib/glimmer/swt/display_proxy.rb +1 -1
  20. data/lib/glimmer/swt/layout_data_proxy.rb +3 -3
  21. data/lib/glimmer/swt/shell_proxy.rb +4 -3
  22. data/lib/glimmer/swt/transform_proxy.rb +109 -0
  23. data/lib/glimmer/swt/widget_proxy.rb +34 -25
  24. data/lib/glimmer/ui/custom_shell.rb +15 -2
  25. data/lib/glimmer/ui/custom_widget.rb +44 -31
  26. data/samples/elaborate/meta_sample.rb +21 -0
  27. data/samples/elaborate/tetris.rb +106 -0
  28. data/samples/elaborate/tetris/model/block.rb +48 -0
  29. data/samples/elaborate/tetris/model/game.rb +185 -0
  30. data/samples/elaborate/tetris/model/tetromino.rb +313 -0
  31. data/samples/elaborate/tetris/view/block.rb +70 -0
  32. data/samples/elaborate/tetris/view/game_over_dialog.rb +72 -0
  33. data/samples/elaborate/tetris/view/playfield.rb +56 -0
  34. data/samples/elaborate/tetris/view/score_lane.rb +87 -0
  35. data/samples/hello/hello_canvas_transform.rb +40 -0
  36. metadata +16 -4
@@ -1,3 +1,24 @@
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
+
1
22
  require 'fileutils'
2
23
  require 'etc'
3
24
 
@@ -0,0 +1,106 @@
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
+ # Tetris App View Custom Shell (represents `tetris` keyword)
23
+
24
+ require_relative 'tetris/model/game'
25
+
26
+ require_relative 'tetris/view/playfield'
27
+ require_relative 'tetris/view/score_lane'
28
+ require_relative 'tetris/view/game_over_dialog'
29
+
30
+ class Tetris
31
+ include Glimmer::UI::CustomShell
32
+
33
+ BLOCK_SIZE = 25
34
+ PLAYFIELD_WIDTH = 10
35
+ PLAYFIELD_HEIGHT = 20
36
+ PREVIEW_PLAYFIELD_WIDTH = 4
37
+ PREVIEW_PLAYFIELD_HEIGHT = 2
38
+
39
+ before_body {
40
+ display {
41
+ on_swt_keydown { |key_event|
42
+ case key_event.keyCode
43
+ when swt(:arrow_down)
44
+ Model::Game.current_tetromino.down
45
+ when swt(:arrow_left)
46
+ Model::Game.current_tetromino.left
47
+ when swt(:arrow_right)
48
+ Model::Game.current_tetromino.right
49
+ when swt(:shift)
50
+ if key_event.keyLocation == swt(:right) # right shift key
51
+ Model::Game.current_tetromino.rotate(:right)
52
+ elsif key_event.keyLocation == swt(:left) # left shift key
53
+ Model::Game.current_tetromino.rotate(:left)
54
+ end
55
+ when 'd'.bytes.first, swt(:arrow_up)
56
+ Model::Game.current_tetromino.rotate(:right)
57
+ when 'a'.bytes.first
58
+ Model::Game.current_tetromino.rotate(:left)
59
+ end
60
+ }
61
+ }
62
+
63
+ Model::Game.configure_beeper do
64
+ display.beep
65
+ end
66
+ }
67
+
68
+ after_body {
69
+ Model::Game.start
70
+
71
+ Thread.new {
72
+ loop {
73
+ sleep(Model::Game.delay)
74
+ sync_exec {
75
+ unless Model::Game.game_over?
76
+ Model::Game.current_tetromino.down
77
+ game_over_dialog(parent_shell: body_root).open if Model::Game.current_tetromino.row <= 0 && Model::Game.current_tetromino.stopped?
78
+ end
79
+ }
80
+ }
81
+ }
82
+ }
83
+
84
+ body {
85
+ shell(:no_resize) {
86
+ grid_layout {
87
+ num_columns 2
88
+ make_columns_equal_width false
89
+ margin_width 0
90
+ margin_height 0
91
+ horizontal_spacing 0
92
+ }
93
+
94
+ text 'Glimmer Tetris'
95
+ background :gray
96
+
97
+ playfield(game_playfield: Model::Game.playfield, playfield_width: PLAYFIELD_WIDTH, playfield_height: PLAYFIELD_HEIGHT, block_size: BLOCK_SIZE)
98
+
99
+ score_lane(block_size: BLOCK_SIZE) {
100
+ layout_data(:fill, :fill, false, true)
101
+ }
102
+ }
103
+ }
104
+ end
105
+
106
+ Tetris.launch
@@ -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,185 @@
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
+ require_relative 'tetromino'
24
+
25
+ class Tetris
26
+ module Model
27
+ class Game
28
+ SCORE_MULTIPLIER = {1 => 40, 2 => 100, 3 => 300, 4 => 1200}
29
+
30
+ class << self
31
+ attr_accessor :game_over, :preview_tetromino, :lines, :score, :level
32
+ alias game_over? game_over
33
+
34
+ def consider_adding_tetromino
35
+ if tetrominoes.empty? || Game.current_tetromino.stopped?
36
+ preview_tetromino.launch!
37
+ preview_next_tetromino!
38
+ end
39
+ end
40
+
41
+ def current_tetromino
42
+ tetrominoes.last
43
+ end
44
+
45
+ def tetrominoes
46
+ @tetrominoes ||= reset_tetrominoes
47
+ end
48
+
49
+ # Returns blocks in the playfield
50
+ def playfield
51
+ @playfield ||= @original_playfield = PLAYFIELD_HEIGHT.times.map {
52
+ PLAYFIELD_WIDTH.times.map {
53
+ Block.new
54
+ }
55
+ }
56
+ end
57
+
58
+ def hypothetical(&block)
59
+ @playfield = hypothetical_playfield
60
+ block.call
61
+ @playfield = @original_playfield
62
+ end
63
+
64
+ def hypothetical?
65
+ @playfield != @original_playfield
66
+ end
67
+
68
+ def hypothetical_playfield
69
+ PLAYFIELD_HEIGHT.times.map { |row|
70
+ PLAYFIELD_WIDTH.times.map { |column|
71
+ playfield[row][column].clone
72
+ }
73
+ }
74
+ end
75
+
76
+ def preview_playfield
77
+ @preview_playfield ||= PREVIEW_PLAYFIELD_HEIGHT.times.map {|row|
78
+ PREVIEW_PLAYFIELD_WIDTH.times.map {|column|
79
+ Block.new
80
+ }
81
+ }
82
+ end
83
+
84
+ def preview_next_tetromino!
85
+ self.preview_tetromino = Tetromino.new
86
+ end
87
+
88
+ def calculate_score!(eliminated_lines)
89
+ new_score = SCORE_MULTIPLIER[eliminated_lines] * (level + 1)
90
+ self.score += new_score
91
+ end
92
+
93
+ def level_up!
94
+ self.level += 1 if lines >= self.level*10
95
+ end
96
+
97
+ def delay
98
+ [1.1 - (level.to_i * 0.1), 0.001].max
99
+ end
100
+
101
+ def consider_eliminating_lines
102
+ eliminated_lines = 0
103
+ playfield.each_with_index do |row, playfield_row|
104
+ if row.all? {|block| !block.clear?}
105
+ eliminated_lines += 1
106
+ shift_blocks_down_above_row(playfield_row)
107
+ end
108
+ end
109
+ if eliminated_lines > 0
110
+ beep
111
+ self.lines += eliminated_lines
112
+ level_up!
113
+ calculate_score!(eliminated_lines)
114
+ end
115
+ end
116
+
117
+ def beep
118
+ @beeper&.call
119
+ end
120
+
121
+ def configure_beeper(&beeper)
122
+ @beeper = beeper
123
+ end
124
+
125
+ def shift_blocks_down_above_row(row)
126
+ row.downto(0) do |playfield_row|
127
+ playfield[playfield_row].each_with_index do |block, playfield_column|
128
+ previous_row = playfield[playfield_row - 1]
129
+ previous_block = previous_row[playfield_column]
130
+ block.color = previous_block.color unless block.color == previous_block.color
131
+ end
132
+ end
133
+ playfield[0].each(&:clear)
134
+ end
135
+
136
+ def start
137
+ self.level = 1
138
+ self.score = 0
139
+ self.lines = @previoius_lines = 0
140
+ reset_playfield
141
+ reset_preview_playfield
142
+ reset_tetrominoes
143
+ preview_next_tetromino!
144
+ consider_adding_tetromino
145
+ self.game_over = false
146
+ end
147
+ alias restart start
148
+
149
+ def reset_tetrominoes
150
+ @tetrominoes = []
151
+ end
152
+
153
+ def reset_playfield
154
+ playfield.each do |row|
155
+ row.each do |block|
156
+ block.clear
157
+ end
158
+ end
159
+ end
160
+
161
+ def reset_preview_playfield
162
+ preview_playfield.each do |row|
163
+ row.each do |block|
164
+ block.clear
165
+ end
166
+ end
167
+ end
168
+
169
+ def playfield_remaining_heights(tetromino = nil)
170
+ PLAYFIELD_WIDTH.times.map do |playfield_column|
171
+ (playfield.each_with_index.detect do |row, playfield_row|
172
+ !row[playfield_column].clear? &&
173
+ (
174
+ tetromino.nil? ||
175
+ (bottom_most_block = tetromino.bottom_most_block_for_column(playfield_column)).nil? ||
176
+ (playfield_row > tetromino.row + bottom_most_block[:row])
177
+ )
178
+ end || [nil, PLAYFIELD_HEIGHT])[1]
179
+ end.to_a
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+
@@ -0,0 +1,313 @@
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 :letter, :preview
42
+ alias preview? preview
43
+ attr_accessor :orientation, :blocks, :row, :column
44
+
45
+ def initialize
46
+ @letter = LETTER_COLORS.keys.sample
47
+ @orientation = :north
48
+ @blocks = default_blocks
49
+ @preview = true
50
+ new_row = 0
51
+ new_column = (PREVIEW_PLAYFIELD_WIDTH - width)/2
52
+ update_playfield(new_row, new_column)
53
+ end
54
+
55
+ def playfield
56
+ @preview ? Game.preview_playfield : Game.playfield
57
+ end
58
+
59
+ def launch!
60
+ remove_from_playfield
61
+ @preview = false
62
+ new_row = 1 - height
63
+ new_column = (PLAYFIELD_WIDTH - width)/2
64
+ update_playfield(new_row, new_column)
65
+ Game.tetrominoes << self
66
+ end
67
+
68
+ def update_playfield(new_row = nil, new_column = nil)
69
+ remove_from_playfield
70
+ if !new_row.nil? && !new_column.nil?
71
+ @row = new_row
72
+ @column = new_column
73
+ add_to_playfield
74
+ end
75
+ end
76
+
77
+ def add_to_playfield
78
+ update_playfield_block do |playfield_row, playfield_column, row_index, column_index|
79
+ 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
80
+ end
81
+ end
82
+
83
+ def remove_from_playfield
84
+ return if @row.nil? || @column.nil?
85
+ update_playfield_block do |playfield_row, playfield_column, row_index, column_index|
86
+ playfield[playfield_row][playfield_column].clear if playfield_row >= 0 && !blocks[row_index][column_index].clear? && playfield[playfield_row][playfield_column]&.color == color
87
+ end
88
+ end
89
+
90
+ def stopped?
91
+ return true if @stopped || @preview
92
+ playfield_remaining_heights = Game.playfield_remaining_heights(self)
93
+ result = bottom_most_blocks.any? do |bottom_most_block|
94
+ playfield_column = @column + bottom_most_block[:column_index]
95
+ playfield_remaining_heights[playfield_column] &&
96
+ @row + bottom_most_block[:row] >= playfield_remaining_heights[playfield_column] - 1
97
+ end
98
+ if result && !Game.hypothetical?
99
+ @stopped = result
100
+ Game.consider_eliminating_lines
101
+ Model::Game.consider_adding_tetromino
102
+ end
103
+ result
104
+ end
105
+
106
+ # Returns bottom-most blocks of a tetromino, which could be from multiple rows depending on shape (e.g. T)
107
+ def bottom_most_blocks
108
+ width.times.map do |column_index|
109
+ row_blocks_with_row_index = @blocks.each_with_index.to_a.reverse.detect do |row_blocks, row_index|
110
+ !row_blocks[column_index].clear?
111
+ end
112
+ bottom_most_block = row_blocks_with_row_index[0][column_index]
113
+ bottom_most_block_row = row_blocks_with_row_index[1]
114
+ {
115
+ block: bottom_most_block,
116
+ row: bottom_most_block_row,
117
+ column_index: column_index
118
+ }
119
+ end
120
+ end
121
+
122
+ def bottom_most_block_for_column(column)
123
+ bottom_most_blocks.detect {|bottom_most_block| (@column + bottom_most_block[:column_index]) == column}
124
+ end
125
+
126
+ def right_blocked?
127
+ (@column == PLAYFIELD_WIDTH - width) ||
128
+ right_most_blocks.any? { |right_most_block|
129
+ playfield[@row + right_most_block[:row_index]][@column + right_most_block[:column_index] + 1].occupied?
130
+ }
131
+ end
132
+
133
+ # Returns right-most blocks of a tetromino, which could be from multiple columns depending on shape (e.g. T)
134
+ def right_most_blocks
135
+ @blocks.each_with_index.map do |row_blocks, row_index|
136
+ column_block_with_column_index = row_blocks.each_with_index.to_a.reverse.detect do |column_block, column_index|
137
+ !column_block.clear?
138
+ end
139
+ if column_block_with_column_index
140
+ right_most_block = column_block_with_column_index[0]
141
+ {
142
+ block: right_most_block,
143
+ row_index: row_index,
144
+ column_index: column_block_with_column_index[1]
145
+ }
146
+ end
147
+ end.compact
148
+ end
149
+
150
+ def left_blocked?
151
+ (@column == 0) ||
152
+ left_most_blocks.any? { |left_most_block|
153
+ playfield[@row + left_most_block[:row_index]][@column + left_most_block[:column_index] - 1].occupied?
154
+ }
155
+ end
156
+
157
+ # Returns right-most blocks of a tetromino, which could be from multiple columns depending on shape (e.g. T)
158
+ def left_most_blocks
159
+ @blocks.each_with_index.map do |row_blocks, row_index|
160
+ column_block_with_column_index = row_blocks.each_with_index.to_a.detect do |column_block, column_index|
161
+ !column_block.clear?
162
+ end
163
+ if column_block_with_column_index
164
+ left_most_block = column_block_with_column_index[0]
165
+ {
166
+ block: left_most_block,
167
+ row_index: row_index,
168
+ column_index: column_block_with_column_index[1]
169
+ }
170
+ end
171
+ end.compact
172
+ end
173
+
174
+ def width
175
+ @blocks[0].size
176
+ end
177
+
178
+ def height
179
+ @blocks.size
180
+ end
181
+
182
+ def down
183
+ launch! if preview?
184
+ unless stopped?
185
+ new_row = @row + 1
186
+ update_playfield(new_row, @column)
187
+ end
188
+ end
189
+
190
+ def left
191
+ unless left_blocked?
192
+ new_column = @column - 1
193
+ update_playfield(@row, new_column)
194
+ end
195
+ end
196
+
197
+ def right
198
+ unless right_blocked?
199
+ new_column = @column + 1
200
+ update_playfield(@row, new_column)
201
+ end
202
+ end
203
+
204
+ # Rotate in specified direcation, which can be :right (clockwise) or :left (counterclockwise)
205
+ def rotate(direction)
206
+ return if stopped?
207
+ can_rotate = nil
208
+ new_blocks = nil
209
+ Game.hypothetical do
210
+ hypothetical_rotated_tetromino = hypothetical_tetromino
211
+ new_blocks = hypothetical_rotated_tetromino.rotate_blocks(direction)
212
+ can_rotate = !hypothetical_rotated_tetromino.stopped? && !hypothetical_rotated_tetromino.right_blocked? && !hypothetical_rotated_tetromino.left_blocked?
213
+ end
214
+ if can_rotate
215
+ remove_from_playfield
216
+ self.orientation = ORIENTATIONS[ORIENTATIONS.rotate(direction == :right ? -1 : 1).index(@orientation)]
217
+ self.blocks = new_blocks
218
+ update_playfield(@row, @column)
219
+ end
220
+ rescue => e
221
+ puts e.full_message
222
+ end
223
+
224
+ def rotate_blocks(direction)
225
+ new_blocks = Matrix[*@blocks].transpose.to_a
226
+ if direction == :right
227
+ new_blocks = new_blocks.map(&:reverse)
228
+ else
229
+ new_blocks = new_blocks.reverse
230
+ end
231
+ Matrix[*new_blocks].to_a
232
+ end
233
+
234
+ def hypothetical_tetromino
235
+ clone.tap do |hypo_clone|
236
+ remove_from_playfield
237
+ hypo_clone.blocks = @blocks.map do |row_blocks|
238
+ row_blocks.map do |column_block|
239
+ column_block.clone
240
+ end
241
+ end
242
+ end
243
+ end
244
+
245
+ def default_blocks
246
+ case @letter
247
+ when :I
248
+ [
249
+ [block, block, block, block]
250
+ ]
251
+ when :J
252
+ [
253
+ [block, block, block],
254
+ [empty, empty, block],
255
+ ]
256
+ when :L
257
+ [
258
+ [block, block, block],
259
+ [block, empty, empty],
260
+ ]
261
+ when :O
262
+ [
263
+ [block, block],
264
+ [block, block],
265
+ ]
266
+ when :S
267
+ [
268
+ [empty, block, block],
269
+ [block, block, empty],
270
+ ]
271
+ when :T
272
+ [
273
+ [block, block, block],
274
+ [empty, block, empty],
275
+ ]
276
+ when :Z
277
+ [
278
+ [block, block, empty],
279
+ [empty, block, block],
280
+ ]
281
+ end
282
+ end
283
+
284
+ def color
285
+ LETTER_COLORS[@letter]
286
+ end
287
+
288
+ def include_block?(block)
289
+ @blocks.flatten.include?(block)
290
+ end
291
+
292
+ private
293
+
294
+ def block
295
+ Block.new(color)
296
+ end
297
+
298
+ def empty
299
+ Block.new
300
+ end
301
+
302
+ def update_playfield_block(&updater)
303
+ @row.upto(@row + height - 1) do |playfield_row|
304
+ @column.upto(@column + width - 1) do |playfield_column|
305
+ row_index = playfield_row - @row
306
+ column_index = playfield_column - @column
307
+ updater.call(playfield_row, playfield_column, row_index, column_index)
308
+ end
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end