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