glimmer-dsl-swt 4.18.2.2 → 4.18.3.1

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