Tetris

Screenshot of Tetris clone
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()