Tetris

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