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