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        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 MyGame(arcade.Window):
123    """ Main application class. """
124
125    def __init__(self, width, height, title):
126        """ Set up the application. """
127
128        super().__init__(width, height, title)
129
130        self.background_color = arcade.color.WHITE
131
132        self.board = None
133        self.frame_count = 0
134        self.game_over = False
135        self.paused = False
136        self.board_sprite_list = None
137
138        self.stone = None
139        self.stone_x = 0
140        self.stone_y = 0
141
142    def new_stone(self):
143        """
144        Randomly grab a new stone and set the stone location to the top.
145        If we immediately collide, then game-over.
146        """
147        self.stone = random.choice(tetris_shapes)
148        self.stone_x = int(COLUMN_COUNT / 2 - len(self.stone[0]) / 2)
149        self.stone_y = 0
150
151        if check_collision(self.board, self.stone, (self.stone_x, self.stone_y)):
152            self.game_over = True
153
154    def setup(self):
155        self.board = new_board()
156
157        self.board_sprite_list = arcade.SpriteList()
158        for row in range(len(self.board)):
159            for column in range(len(self.board[0])):
160                sprite = arcade.Sprite(texture_list[0])
161                sprite.textures = texture_list
162                sprite.center_x = (MARGIN + WIDTH) * column + MARGIN + WIDTH // 2
163                sprite.center_y = SCREEN_HEIGHT - (MARGIN + HEIGHT) * row + MARGIN + HEIGHT // 2
164
165                self.board_sprite_list.append(sprite)
166
167        self.new_stone()
168        self.update_board()
169
170    def drop(self):
171        """
172        Drop the stone down one place.
173        Check for collision.
174        If collided, then
175          join matrixes
176          Check for rows we can remove
177          Update sprite list with stones
178          Create a new stone
179        """
180        if not self.game_over and not self.paused:
181            self.stone_y += 1
182            if check_collision(self.board, self.stone, (self.stone_x, self.stone_y)):
183                self.board = join_matrixes(self.board, self.stone, (self.stone_x, self.stone_y))
184                while True:
185                    for i, row in enumerate(self.board[:-1]):
186                        if 0 not in row:
187                            self.board = remove_row(self.board, i)
188                            break
189                    else:
190                        break
191                self.update_board()
192                self.new_stone()
193
194    def rotate_stone(self):
195        """ Rotate the stone, check collision. """
196        if not self.game_over and not self.paused:
197            new_stone = rotate_counterclockwise(self.stone)
198            if self.stone_x + len(new_stone[0]) >= COLUMN_COUNT:
199                self.stone_x = COLUMN_COUNT - len(new_stone[0])
200            if not check_collision(self.board, new_stone, (self.stone_x, self.stone_y)):
201                self.stone = new_stone
202
203    def on_update(self, dt):
204        """ Update, drop stone if warrented """
205        self.frame_count += 1
206        if self.frame_count % 10 == 0:
207            self.drop()
208
209    def move(self, delta_x):
210        """ Move the stone back and forth based on delta x. """
211        if not self.game_over and not self.paused:
212            new_x = self.stone_x + delta_x
213            if new_x < 0:
214                new_x = 0
215            if new_x > COLUMN_COUNT - len(self.stone[0]):
216                new_x = COLUMN_COUNT - len(self.stone[0])
217            if not check_collision(self.board, self.stone, (new_x, self.stone_y)):
218                self.stone_x = new_x
219
220    def on_key_press(self, key, modifiers):
221        """
222        Handle user key presses
223        User goes left, move -1
224        User goes right, move 1
225        Rotate stone,
226        or drop down
227        """
228        if key == arcade.key.LEFT:
229            self.move(-1)
230        elif key == arcade.key.RIGHT:
231            self.move(1)
232        elif key == arcade.key.UP:
233            self.rotate_stone()
234        elif key == arcade.key.DOWN:
235            self.drop()
236
237    # noinspection PyMethodMayBeStatic
238    def draw_grid(self, grid, offset_x, offset_y):
239        """
240        Draw the grid. Used to draw the falling stones. The board is drawn
241        by the sprite list.
242        """
243        # Draw the grid
244        for row in range(len(grid)):
245            for column in range(len(grid[0])):
246                # Figure out what color to draw the box
247                if grid[row][column]:
248                    color = colors[grid[row][column]]
249                    # Do the math to figure out where the box is
250                    x = (MARGIN + WIDTH) * (column + offset_x) + MARGIN + WIDTH // 2
251                    y = SCREEN_HEIGHT - (MARGIN + HEIGHT) * (row + offset_y) + MARGIN + HEIGHT // 2
252
253                    # Draw the box
254                    arcade.draw_rectangle_filled(x, y, WIDTH, HEIGHT, color)
255
256    def update_board(self):
257        """
258        Update the sprite list to reflect the contents of the 2d grid
259        """
260        for row in range(len(self.board)):
261            for column in range(len(self.board[0])):
262                v = self.board[row][column]
263                i = row * COLUMN_COUNT + column
264                self.board_sprite_list[i].set_texture(v)
265
266    def on_draw(self):
267        """ Render the screen. """
268
269        # This command has to happen before we start drawing
270        self.clear()
271        self.board_sprite_list.draw()
272        self.draw_grid(self.stone, self.stone_x, self.stone_y)
273
274
275def main():
276    """ Create the game window, setup, run """
277    my_game = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
278    my_game.setup()
279    arcade.run()
280
281
282if __name__ == "__main__":
283    main()