1"""
2Tetris
3
4Tetris clone, with some ideas from silvasur's code:
5https://gist.github.com/silvasur/565419/d9de6a84e7da000797ac681976442073045c74a4
6
7If Python and Arcade are installed, this example can be run from the command line with:
8python -m arcade.examples.tetris
9"""
10from __future__ import annotations
11
12# flake8: noqa: E241
13import arcade
14import random
15import PIL
16
17# Set how many rows and columns we will have
18ROW_COUNT = 24
19COLUMN_COUNT = 10
20
21# This sets the WIDTH and HEIGHT of each grid location
22WIDTH = 30
23HEIGHT = 30
24
25# This sets the margin between each cell
26# and on the edges of the screen.
27MARGIN = 5
28
29# Do the math to figure out our screen dimensions
30SCREEN_WIDTH = (WIDTH + MARGIN) * COLUMN_COUNT + MARGIN
31SCREEN_HEIGHT = (HEIGHT + MARGIN) * ROW_COUNT + MARGIN
32SCREEN_TITLE = "Tetris"
33
34colors = [
35 (0, 0, 0, 255),
36 (255, 0, 0, 255),
37 (0, 150, 0, 255),
38 (0, 0, 255, 255),
39 (255, 120, 0, 255),
40 (255, 255, 0, 255),
41 (180, 0, 255, 255),
42 (0, 220, 220, 255)
43]
44
45# Define the shapes of the single parts
46tetris_shapes = [
47 [[1, 1, 1],
48 [0, 1, 0]],
49
50 [[0, 2, 2],
51 [2, 2, 0]],
52
53 [[3, 3, 0],
54 [0, 3, 3]],
55
56 [[4, 0, 0],
57 [4, 4, 4]],
58
59 [[0, 0, 5],
60 [5, 5, 5]],
61
62 [[6, 6, 6, 6]],
63
64 [[7, 7],
65 [7, 7]]
66]
67
68
69def create_textures():
70 """ Create a list of images for sprites based on the global colors. """
71 new_textures = []
72 for color in colors:
73 image = PIL.Image.new('RGBA', (WIDTH, HEIGHT), color)
74 new_textures.append(arcade.Texture(image))
75 return new_textures
76
77
78texture_list = create_textures()
79
80
81def rotate_counterclockwise(shape):
82 """ Rotates a matrix clockwise """
83 return [[shape[y][x] for y in range(len(shape))]
84 for x in range(len(shape[0]) - 1, -1, -1)]
85
86
87def check_collision(board, shape, offset):
88 """
89 See if the matrix stored in the shape will intersect anything
90 on the board based on the offset. Offset is an (x, y) coordinate.
91 """
92 off_x, off_y = offset
93 for cy, row in enumerate(shape):
94 for cx, cell in enumerate(row):
95 if cell and board[cy + off_y][cx + off_x]:
96 return True
97 return False
98
99
100def remove_row(board, row):
101 """ Remove a row from the board, add a blank row on top. """
102 del board[row]
103 return [[0 for _ in range(COLUMN_COUNT)]] + board
104
105
106def join_matrixes(matrix_1, matrix_2, matrix_2_offset):
107 """ Copy matrix 2 onto matrix 1 based on the passed in x, y offset coordinate """
108 offset_x, offset_y = matrix_2_offset
109 for cy, row in enumerate(matrix_2):
110 for cx, val in enumerate(row):
111 matrix_1[cy + offset_y - 1][cx + offset_x] += val
112 return matrix_1
113
114
115def new_board():
116 """ Create a grid of 0's. Add 1's to the bottom for easier collision detection. """
117 # Create the main board of 0's
118 board = [[0 for _x in range(COLUMN_COUNT)] for _y in range(ROW_COUNT)]
119 # Add a bottom border of 1's
120 board += [[1 for _x in range(COLUMN_COUNT)]]
121 return board
122
123
124class MyGame(arcade.Window):
125 """ Main application class. """
126
127 def __init__(self, width, height, title):
128 """ Set up the application. """
129
130 super().__init__(width, height, title)
131
132 self.background_color = arcade.color.WHITE
133
134 self.board = None
135 self.frame_count = 0
136 self.game_over = False
137 self.paused = False
138 self.board_sprite_list = None
139
140 self.stone = None
141 self.stone_x = 0
142 self.stone_y = 0
143
144 def new_stone(self):
145 """
146 Randomly grab a new stone and set the stone location to the top.
147 If we immediately collide, then game-over.
148 """
149 self.stone = random.choice(tetris_shapes)
150 self.stone_x = int(COLUMN_COUNT / 2 - len(self.stone[0]) / 2)
151 self.stone_y = 0
152
153 if check_collision(self.board, self.stone, (self.stone_x, self.stone_y)):
154 self.game_over = True
155
156 def setup(self):
157 self.board = new_board()
158
159 self.board_sprite_list = arcade.SpriteList()
160 for row in range(len(self.board)):
161 for column in range(len(self.board[0])):
162 sprite = arcade.Sprite(texture_list[0])
163 sprite.textures = texture_list
164 sprite.center_x = (MARGIN + WIDTH) * column + MARGIN + WIDTH // 2
165 sprite.center_y = SCREEN_HEIGHT - (MARGIN + HEIGHT) * row + MARGIN + HEIGHT // 2
166
167 self.board_sprite_list.append(sprite)
168
169 self.new_stone()
170 self.update_board()
171
172 def drop(self):
173 """
174 Drop the stone down one place.
175 Check for collision.
176 If collided, then
177 join matrixes
178 Check for rows we can remove
179 Update sprite list with stones
180 Create a new stone
181 """
182 if not self.game_over and not self.paused:
183 self.stone_y += 1
184 if check_collision(self.board, self.stone, (self.stone_x, self.stone_y)):
185 self.board = join_matrixes(self.board, self.stone, (self.stone_x, self.stone_y))
186 while True:
187 for i, row in enumerate(self.board[:-1]):
188 if 0 not in row:
189 self.board = remove_row(self.board, i)
190 break
191 else:
192 break
193 self.update_board()
194 self.new_stone()
195
196 def rotate_stone(self):
197 """ Rotate the stone, check collision. """
198 if not self.game_over and not self.paused:
199 new_stone = rotate_counterclockwise(self.stone)
200 if self.stone_x + len(new_stone[0]) >= COLUMN_COUNT:
201 self.stone_x = COLUMN_COUNT - len(new_stone[0])
202 if not check_collision(self.board, new_stone, (self.stone_x, self.stone_y)):
203 self.stone = new_stone
204
205 def on_update(self, dt):
206 """ Update, drop stone if warrented """
207 self.frame_count += 1
208 if self.frame_count % 10 == 0:
209 self.drop()
210
211 def move(self, delta_x):
212 """ Move the stone back and forth based on delta x. """
213 if not self.game_over and not self.paused:
214 new_x = self.stone_x + delta_x
215 if new_x < 0:
216 new_x = 0
217 if new_x > COLUMN_COUNT - len(self.stone[0]):
218 new_x = COLUMN_COUNT - len(self.stone[0])
219 if not check_collision(self.board, self.stone, (new_x, self.stone_y)):
220 self.stone_x = new_x
221
222 def on_key_press(self, key, modifiers):
223 """
224 Handle user key presses
225 User goes left, move -1
226 User goes right, move 1
227 Rotate stone,
228 or drop down
229 """
230 if key == arcade.key.LEFT:
231 self.move(-1)
232 elif key == arcade.key.RIGHT:
233 self.move(1)
234 elif key == arcade.key.UP:
235 self.rotate_stone()
236 elif key == arcade.key.DOWN:
237 self.drop()
238
239 # noinspection PyMethodMayBeStatic
240 def draw_grid(self, grid, offset_x, offset_y):
241 """
242 Draw the grid. Used to draw the falling stones. The board is drawn
243 by the sprite list.
244 """
245 # Draw the grid
246 for row in range(len(grid)):
247 for column in range(len(grid[0])):
248 # Figure out what color to draw the box
249 if grid[row][column]:
250 color = colors[grid[row][column]]
251 # Do the math to figure out where the box is
252 x = (MARGIN + WIDTH) * (column + offset_x) + MARGIN + WIDTH // 2
253 y = SCREEN_HEIGHT - (MARGIN + HEIGHT) * (row + offset_y) + MARGIN + HEIGHT // 2
254
255 # Draw the box
256 arcade.draw_rectangle_filled(x, y, WIDTH, HEIGHT, color)
257
258 def update_board(self):
259 """
260 Update the sprite list to reflect the contents of the 2d grid
261 """
262 for row in range(len(self.board)):
263 for column in range(len(self.board[0])):
264 v = self.board[row][column]
265 i = row * COLUMN_COUNT + column
266 self.board_sprite_list[i].set_texture(v)
267
268 def on_draw(self):
269 """ Render the screen. """
270
271 # This command has to happen before we start drawing
272 self.clear()
273 self.board_sprite_list.draw()
274 self.draw_grid(self.stone, self.stone_x, self.stone_y)
275
276
277def main():
278 """ Create the game window, setup, run """
279 my_game = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
280 my_game.setup()
281 arcade.run()
282
283
284if __name__ == "__main__":
285 main()