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