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