[PYTHON] Make a Tetris-style game!

Introduction

This time, I made a Tetris-like game using tkinter of Python. Since the program has become quite long, I will omit the explanation significantly, but I will explain only the points.

Types of tetrimino

There were 4 types in the program of the site that I referred to, but I tried to make it 7 types with a little ingenuity. Tetromino includes: t.PNG  Tミノ    z.PNG  Zミノ      i.PNG  Iミノ        j.PNG  Lミノ l.PNG  Jミノ     o.PNG  Oミノ      s.PNG  Sミノ

Game screen

テトリス.PNG  20200707_212249.gif

The game screen has 20 squares vertically and 10 squares horizontally. If you press the Start button on the right side, tetrimino will come down and the game will start. Like the original Tetris, if one horizontal row is aligned, one row disappears, and if Tetrimino is piled up to the top, the game is over. gameover.PNG←ゲームオーバー時の表示

However, it should be noted that unlike the original Tetris, Tetrimino cannot be rotated. Therefore, the difficulty of surviving for a long time has increased considerably. By the way, I also tried test play, but I had a hard time.

Completed example of the program

# -*- coding:utf-8 -*-
import tkinter as tk
import random      

#constant
BLOCK_SIZE = 25  #Vertical and horizontal size of the block px
FIELD_WIDTH = 10  #Field width
FIELD_HEIGHT = 20  #Field height

MOVE_LEFT = 0  #A constant that indicates moving the block to the left
MOVE_RIGHT = 1  #A constant that indicates moving the block to the right
MOVE_DOWN = 2  #A constant that indicates moving the block down

#A class of squares that make up a block
class TetrisSquare():
    def __init__(self, x=0, y=0, color="gray"):
        'Create one square'
        self.x = x
        self.y = y
        self.color = color

    def set_cord(self, x, y):
        'Set the coordinates of the square'
        self.x = x
        self.y = y

    def get_cord(self):
        'Get the coordinates of the square'
        return int(self.x), int(self.y)

    def set_color(self, color):
        'Set the color of the square'
        self.color = color

    def get_color(self):
        'Get the color of the square'
        return self.color

    def get_moved_cord(self, direction):
        'Get the coordinates of the square after moving'

        #Get the coordinates of the square before moving
        x, y = self.get_cord()

        #Calculate the coordinates after moving in consideration of the moving direction
        if direction == MOVE_LEFT:
            return x - 1, y
        elif direction == MOVE_RIGHT:
            return x + 1, y
        elif direction == MOVE_DOWN:
            return x, y + 1
        else:
            return x, y

#Canvas class for drawing Tetris screen
class TetrisCanvas(tk.Canvas):
    def __init__(self, master, field):
        'Create a canvas to draw tetris'

        canvas_width = field.get_width() * BLOCK_SIZE
        canvas_height = field.get_height() * BLOCK_SIZE

        # tk.Canvas class init
        super().__init__(master, width=canvas_width, height=canvas_height, bg="white")

        #Place the canvas on the screen
        self.place(x=25, y=25)

        #Create a Tetris screen by drawing 10x20 squares
        for y in range(field.get_height()):
            for x in range(field.get_width()):
                square = field.get_square(x, y)
                x1 = x * BLOCK_SIZE
                x2 = (x + 1) * BLOCK_SIZE
                y1 = y * BLOCK_SIZE
                y2 = (y + 1) * BLOCK_SIZE
                self.create_rectangle(
                    x1, y1, x2, y2,
                    outline="white", width=1,
                    fill=square.get_color()
                )

        #Set the previously drawn field
        self.before_field = field

    def update(self, field, block):
        'Tetris screen updated'

        #Create a drawing field (field + block)
        new_field = TetrisField()
        for y in range(field.get_height()):
            for x in range(field.get_width()):
                square = field.get_square(x, y)
                color = square.get_color()

                new_square = new_field.get_square(x, y)
                new_square.set_color(color)

        #Combine block square information with fields
        if block is not None:
            block_squares = block.get_squares()
            for block_square in block_squares:
                #Get the coordinates and color of the block square
                x, y = block_square.get_cord()
                color = block_square.get_color()

                #Update the color of the square on the field of the acquired coordinates
                new_field_square = new_field.get_square(x, y)
                new_field_square.set_color(color)

        #Draw on canvas using drawing fields
        for y in range(field.get_height()):
            for x in range(field.get_width()):

                # (x,y)Get the color of the coordinate field
                new_square = new_field.get_square(x, y)
                new_color = new_square.get_color()

                # (x,y)Do not draw if the coordinates have not changed since the last drawing
                before_square = self.before_field.get_square(x, y)
                before_color = before_square.get_color()
                if(new_color == before_color):
                    continue

                x1 = x * BLOCK_SIZE
                x2 = (x + 1) * BLOCK_SIZE
                y1 = y * BLOCK_SIZE
                y2 = (y + 1) * BLOCK_SIZE
                #Draw a rectangle with the color of each position in the field
                self.create_rectangle(
                    x1, y1, x2, y2,
                    outline="white", width=1, fill=new_color
                )

        #Update the information of the field drawn last time
        self.before_field = new_field

#Field class that manages information on stacked blocks
class TetrisField():
    def __init__(self):
        self.width = FIELD_WIDTH
        self.height = FIELD_HEIGHT

        #Initialize field
        self.squares = []
        for y in range(self.height):
            for x in range(self.width):
                #Manage fields as a list of square instances
                self.squares.append(TetrisSquare(x, y, "gray"))

    def get_width(self):
        'Get the number of squares in the field (horizontal)'

        return self.width

    def get_height(self):
        'Get the number of squares in the field (vertical)'

        return self.height

    def get_squares(self):
        'Get a list of the squares that make up the field'

        return self.squares

    def get_square(self, x, y):
        'Get the square with the specified coordinates'

        return self.squares[y * self.width + x]

    def judge_game_over(self, block):
        'Determine if the game is over'

        #Create a set of coordinates that are already filled on the field
        no_empty_cord = set(square.get_cord() for square
                            in self.get_squares() if square.get_color() != "gray")

        #Creating a set of coordinates with blocks
        block_cord = set(square.get_cord() for square
                         in block.get_squares())

        #With a set of block coordinates
        #Create an intersection of sets of coordinates that are already filled in the field
        collision_set = no_empty_cord & block_cord

        #If the intersection is empty, the game is not over
        if len(collision_set) == 0:
            ret = False
        else:
            ret = True

        return ret

    def judge_can_move(self, block, direction):
        'Determine if the block can be moved in the specified direction'

        #Create a set of coordinates that are already filled on the field
        no_empty_cord = set(square.get_cord() for square
                            in self.get_squares() if square.get_color() != "gray")

        #Creating a set of coordinates with the moved block
        move_block_cord = set(square.get_moved_cord(direction) for square
                              in block.get_squares())

        #Determine if it is out of the field
        for x, y in move_block_cord:

            #Cannot move if it sticks out
            if x < 0 or x >= self.width or \
                    y < 0 or y >= self.height:
                return False

        #With the set of coordinates of the block after moving
        #Create an intersection of sets of coordinates that are already filled in the field
        collision_set = no_empty_cord & move_block_cord

        #Movable if the intersection is empty
        if len(collision_set) == 0:
            ret = True
        else:
            ret = False

        return ret

    def fix_block(self, block):
        'Fix block and add to field'

        for square in block.get_squares():
            #Get the coordinates and colors of the squares contained in the block
            x, y = square.get_cord()
            color = square.get_color()

            #Reflect the coordinates and color in the field
            field_square = self.get_square(x, y)
            field_square.set_color(color)

    def delete_line(self):
        'Delete a row'

        #Check if all lines can be deleted
        for y in range(self.height):
            for x in range(self.width):
                #It cannot be erased if there is even one empty in the line
                square = self.get_square(x, y)
                if(square.get_color() == "gray"):
                    #To the next line
                    break
            else:
                #If not broken, the line is full
                #Delete this line and move the line above this line down one line
                for down_y in range(y, 0, -1):
                    for x in range(self.width):
                        src_square = self.get_square(x, down_y - 1)
                        dst_square = self.get_square(x, down_y)
                        dst_square.set_color(src_square.get_color())
                #The top line is always empty
                for x in range(self.width):
                    square = self.get_square(x, 0)
                    square.set_color("gray")

#Tetris block class
class TetrisBlock():
    def __init__(self):
        'Create a block of tetris'

        #List of squares that make up the block
        self.squares = []

        #Randomly determine the shape of the block
        block_type = random.randint(1, 7)

        #Determine the coordinates and colors of the four squares according to the shape of the block
        if block_type == 1:
            color = "aqua"
            cords = [
                [FIELD_WIDTH / 2, 0],
                [FIELD_WIDTH / 2, 1],
                [FIELD_WIDTH / 2, 2],
                [FIELD_WIDTH / 2, 3],
            ]
        elif block_type == 2:
            color = "yellow"
            cords = [
                [FIELD_WIDTH / 2, 0],
                [FIELD_WIDTH / 2, 1],
                [FIELD_WIDTH / 2 - 1, 0],
                [FIELD_WIDTH / 2 - 1, 1],
            ]
        elif block_type == 3:
            color = "orange"
            cords = [
                [FIELD_WIDTH / 2 - 1, 0],
                [FIELD_WIDTH / 2, 0],
                [FIELD_WIDTH / 2, 1],
                [FIELD_WIDTH / 2, 2],
            ]
        elif block_type == 4:
            color = "blue"
            cords = [
                [FIELD_WIDTH / 2, 0],
                [FIELD_WIDTH / 2 - 1, 0],
                [FIELD_WIDTH / 2 - 1, 1],
                [FIELD_WIDTH / 2 - 1, 2],
            ] 

        elif block_type == 5:
            color = "red"
            cords = [
                [FIELD_WIDTH / 2, 0],
                [FIELD_WIDTH / 2, 1],
                [FIELD_WIDTH / 2 - 1, 1],
                [FIELD_WIDTH / 2 - 1, 2],
            ]     

        elif block_type == 6:
            color = "green"
            cords = [
                [FIELD_WIDTH / 2 - 1, 0],
                [FIELD_WIDTH / 2 - 1, 1],
                [FIELD_WIDTH / 2, 2],
                [FIELD_WIDTH / 2, 1],
            ]      

        elif block_type == 7:
            color = "purple"
            cords = [
                [FIELD_WIDTH / 2, 1],
                [FIELD_WIDTH / 2 - 1, 0],
                [FIELD_WIDTH / 2 - 1, 1],
                [FIELD_WIDTH / 2 - 1, 2],
            ]     

        #Create a square with the determined color and coordinates and add it to the list
        for cord in cords:
            self.squares.append(TetrisSquare(cord[0], cord[1], color))

    def get_squares(self):
        'Get the squares that make up the block'

        # return [square for square in self.squares]
        return self.squares

    def move(self, direction):
        'Move blocks'

        #Move the squares that make up the block
        for square in self.squares:
            x, y = square.get_moved_cord(direction)
            square.set_cord(x, y)

#Class that controls the Tetris game
class TetrisGame():

    def __init__(self, master):
        'Tetris instantiation'

        #Initialize block management list
        self.field = TetrisField()

        #Set the fall block
        self.block = None

        #Set tetris screen
        self.canvas = TetrisCanvas(master, self.field)

        #Tetris screen update
        self.canvas.update(self.field, self.block)

    def start(self, func):
        'Start tetris'

        #Set the function to call at the end
        self.end_func = func

        #Initialize block management list
        self.field = TetrisField()

        #New fall block added
        self.new_block()

    def new_block(self):
        'Add new block'

        #Create a falling block instance
        self.block = TetrisBlock()

        if self.field.judge_game_over(self.block):
           self.end_func()
           print("Game Over!")

            #Tetris screen updated
        self.canvas.update(self.field, self.block)
        
    def move_block(self, direction):
        'Move blocks'

        #Move only if you can move
        if self.field.judge_can_move(self.block, direction):

            #Move blocks
            self.block.move(direction)

            #Update screen
            self.canvas.update(self.field, self.block)

        else:
            #If the block cannot move downwards
            if direction == MOVE_DOWN:
                #Fix the block
                self.field.fix_block(self.block)
                self.field.delete_line()
                self.new_block()

#A class that accepts events and controls Tetris in response to those events
class EventHandller():
    def __init__(self, master, game):
        self.master = master

        #Game to control
        self.game = game

        #A timer that issues events on a regular basis
        self.timer = None

        #Install a game start button
        button = tk.Button(master, text='START', command=self.start_event)
        button.place(x=25 + BLOCK_SIZE * FIELD_WIDTH + 25, y=30)

    def start_event(self):
        'Processing when the game start button is pressed'

        #Tetris start
        self.game.start(self.end_event)
        self.running = True

        #Timer set
        self.timer_start()

        #Start accepting key operation input
        self.master.bind("<Left>", self.left_key_event)
        self.master.bind("<Right>", self.right_key_event)
        self.master.bind("<Down>", self.down_key_event)

    def end_event(self):
        'Processing at the end of the game'
        self.running = False

        #Stop accepting events
        self.timer_end()
        self.master.unbind("<Left>")
        self.master.unbind("<Right>")
        self.master.unbind("<Down>")

    def timer_end(self):
        'End timer'

        if self.timer is not None:
            self.master.after_cancel(self.timer)
            self.timer = None

    def timer_start(self):
        'Start timer'

        if self.timer is not None:
            #Cancel the timer once
            self.master.after_cancel(self.timer)

        #Timer starts only when Tetris is running
        if self.running:
            #Start timer
            self.timer = self.master.after(1000, self.timer_event)

    def left_key_event(self, event):
        'Processing when accepting left key input'

        #Move the block to the left
        self.game.move_block(MOVE_LEFT)

    def right_key_event(self, event):
        'Processing when accepting right key input'

        #Move the block to the right
        self.game.move_block(MOVE_RIGHT)

    def down_key_event(self, event):
        'Processing when accepting lower key input'

        #Move the block down
        self.game.move_block(MOVE_DOWN)

        #Restart the fall timer
        self.timer_start()

    def timer_event(self):
        'Processing when the timer expires'

        #Performs the same processing as when accepting down key input
        self.down_key_event(None)


class Application(tk.Tk):
    def __init__(self):
        super().__init__()

        #App window settings
        self.geometry("400x600")
        self.title("Tetris")

        #Tetris generation
        game = TetrisGame(self)

        #Event handler generation
        EventHandller(self, game)


def main():
    'main function'

    #GUI application generation
    app = Application()
    app.mainloop()


if __name__ == "__main__":
    main()

Finally

It's a pity that we couldn't reproduce the rotation of Tetrimino, which is the real thrill of Tetris, but I think it's a game that anyone can enjoy. I tried to make a game using Python for the first time, and the sense of accomplishment when it was completed was wonderful. Next, we will revenge to make a rotatable Tetris!

Reference site

https://daeudaeu.com/programming/python/tkinter/tetris/

Recommended Posts

Make a Tetris-style game!
Make a squash game
Let's make a rock-paper-scissors game
Let's make a shiritori game with Python
[Python] Make a game with Pyxel-Use an editor-
I want to make a game with Python
Make a cocos2d game in a pixel double window
Make a distance matrix
[Python] Make a simple maze game with Pyxel
Make a rock-paper-scissors game in one line (python)
I'll make a password!
Make a Nyan button
I tried to make a ○ ✕ game using TensorFlow
Make a Base64 decoder
Let's make a simple game with Python 3 and iPhone
Make a Blueqat backend ~ Part 1
Make a Blueqat backend ~ Part 2
[Django] Make a pull-down menu
Make a LINE BOT (chat)
Make a fortune with Python
Make Responder a daemon (service)
Make a fire with kdeplot
Make a math drill print
How to make a multiplayer online action game on Slack
How to make a simple Flappy Bird game with pygame
[Python] Make a simple maze game with Pyxel-Make enemies appear-
Let's make a number guessing game in your own language!
Let's make a remote rumba [Hardware]
How to make a Japanese-English translation
Make a Santa classifier from a Santa image
Make a Tweet box for Pepper
Let's make a GUI with python.
Make a sound with Jupyter notebook
Let's make a spot sale service 2
Make a face recognizer using TensorFlow
Let's make a breakout with wxPython
Let's make a spot sale service 1
How to make a crawler --Advanced
How to make a recursive function
Make C compilation a little easier
python / Make a dict from a list.
[Python] Make the function a lambda function
Make a recommender system with python
How to make a deadman's switch
[Blender] How to make a Blender plugin
Make Flask a Cloud Native application
Make a filter with a django template
Zura made like a life game
Let's make a graph with python! !!
Let's make a supercomputer with xCAT
How to make a crawler --Basic
Make a model iterator with PySide
Make a nice graph with plotly
Make a curtain generator in Blender
Let's make a spot sale service 3
Make a video player with PySimpleGUI + OpenCV
[Python] How to make a class iterable
Try to make a kernel of Jupyter
I tried playing a ○ ✕ game using TensorFlow
Make a relation diagram of Python module
Make a rare gacha simulator with Flask