glimmer-dsl-swt 4.18.2.2 → 4.18.3.1

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 (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