Make Tetris in Pygames

Table of Content

  1. Setting Play Area
  2. Creating Tetrominos (Blocks)
  3. Player Inputs (Moving Sideways)
  4. Player Inputs (Moving down)
  5. Randomizing next Tetromino
  6. Stoping Figure on bottom
  7. Player Inputs (Rotation)
  8. Removing Full lines (Basically finished with gameplay)
  9. Modifying game window
  10. Adding color to Tetrominos
  11. Title and Next Block
  12. Score and Feel
  13. Game Over Handler (New high score)
  14. Optionals
  15. CONGRATS YOU MADE TETRIS

Image of Final product

Soly Image Caption

Download stuff from our github

Click here to download stuff
This includes:

Double check that python and pygames is installed on your machine

Check Python
For Windows: py --version
For Mac: python3 --version

Check for pygames Run in terminal: pip show pygame

Empty 2D array for Tetromino (No need copy this one)

# Tetromino Objects and Variables
tetrominos_pos = [[(),(),(),()],
              [(),(),(),()],
              [(),(),(),()],
              [(),(),(),()],
              [(),(),(),()],
              [(),(),(),()],
              [(),(),(),()]]

Filled 2D array of Tetromino

# Tetromino Objects and Variables
tetrominos_pos = [[(-1,-1),(-2,-1),(0,-1),(1,-1)], # I
              [(0,-1),(-1,-1),(-1,0),(0,0)], # O
              [(0,0),(-1,-1),(0,-1),(1,0)], # Z
              [(0,0),(-1,0),(0,-1),(1,-1)], # S
              [(0,0),(-1,0),(1,0),(1,-1)], # L
              [(0,0),(-1,-1),(-1,0),(1,0)], # J
              [(0,0),(-1,0),(1,0),(0,-1)]] # T
# Tetromino Objects and Variables
tetrominos_pos = [[...]]
# NEW CODE HERE
tetrominos = [[pygame.Rect(x + WIDTH // 2, y +1, 1, 1) for x, y in block_pos] for block_pos in tetrominos_pos]
tetromino_rect = pygame.Rect(0,0, TILE -2, TILE -2)
tetromino = deepcopy(tetrominos[0])

# End Zone 
feild = [[0 for i in range(WIDTH)] for j in range(HEIGHT)]         
#NEW CODE END

Drawing the Tetromoino (Inside game loop at bottom)

# Rendering / Drawing sprites
# NEW CODE HERE
for i in range(4):
        tetromino_rect.x = tetromino[i].x * TILE
        tetromino_rect.y = tetromino[i].y * TILE
        pygame.draw.rect(screen, pygame.Color("white"), tetromino_rect)
# NEW CODE END

Tetris Shapes

UNCOMMENT COMMENTS FROM check_borders() AND REMOVE pass

def check_borders():
    if tetromino[i].x < 0 or tetromino[i].x > WIDTH-1:
        return False
    elif tetromino[i].y > HEIGHT -1 or feild[tetromino[i].y][tetromino[i].x]:          
        return False
    return True

Add inputs

    # Controls
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        # NEW CODE HERE 
        if event.type == pygame.KEYDOWN:
            # Sideways controls 
            if event.key == pygame.K_LEFT:
                dir_x = -1
            elif event.key == pygame.K_RIGHT:
                dir_x = 1
    # X movement
    tetromino_old = deepcopy(tetromino)
    for i in range(4):
        tetromino[i].x += dir_x
        if not check_borders():
            tetromino = deepcopy(tetromino_old)
            break

Create new Variables

# Animation Variables 
anim_count, anim_speed, anim_limit = 0, 60, 2000        # NEW CODE HERE

Make the Tetrominos fall

    # Y movement
    anim_count += anim_speed
    if anim_count > anim_limit:
        anim_count = 0
        tetromino_old = deepcopy(tetromino)
        for i in range(4):
            tetromino[i].y += 1

Increase fall speed when Down arrow is pressed

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        if event.type == pygame.KEYDOWN:
            # Controls 
            if event.key == pygame.K_LEFT:
                dir_x = -1
            elif event.key == pygame.K_RIGHT:
                dir_x = 1
            # NEW CODE HERE 
            elif event.key == pygame.K_DOWN:
                anim_limit = 100
        # IN THE OUTER IF STATEMENT        
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_DOWN:
                anim_limit = 2000

Stoping Tetrominos that has hit the bottom

    # y movement
    anim_count += anim_speed
    if anim_count > anim_limit:
        anim_count = 0
        tetromino_old = deepcopy(tetromino)
        for i in range(4):
            tetromino[i].y += 1

            # NEW CODE HERE
            if not check_borders():
                for i in range(4):
                    feild[tetromino_old[i].y][tetromino_old[i].x] = pygame.Color('white')
                tetromino = deepcopy(choice(tetrominos))
                break

Modify Tetromino Objects and Variables to randomly choose First Tetromino

# Tetromino Objects and Variables
tetrominos_pos = [[...]]

tetrominos = [[pygame.Rect(x + WIDTH // 2, y +1, 1, 1) for x, y in block_pos] for block_pos in tetrominos_pos]
tetromino_rect = pygame.Rect(0,0, TILE -2, TILE -2)

# OLD CODE tetromino = deepcopy(tetrominos[0]) REPLACE WITH
tetromino = deepcopy(choice(tetrominos)) # NEW LINE OF CODE

Drawing the feild / End zone

    # Rendering / Drawing sprites
    for i in range(4):
        tetromino_rect.x = tetromino[i].x * TILE
        tetromino_rect.y = tetromino[i].y * TILE
        pygame.draw.rect(screen, pygame.Color("white"), tetromino_rect)

    # NEW CODE HERE 
    for y, row in enumerate(feild):
        for x, col in enumerate(row):
            if col:
                tetromino_rect.x, tetromino_rect.y = x * TILE, y * TILE
                pygame.draw.rect(screen, col, tetromino_rect)

New Input (Up arrow) Inside of Controls

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT:
                dir_x = -1
            elif event.key == pygame.K_RIGHT:
                dir_x = 1
            elif event.key == pygame.K_DOWN:
                anim_limit = 100

            # NEW CODE HERE
            elif event.key == pygame.K_UP:
                rotate = True
            # NEW CODE END

        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_DOWN:
                anim_limit = 2000

Rotation logic

    # Rotating
    center = tetromino[0]
    tetromino_old = deepcopy(tetromino)
    if rotate:
        for i in range(4):
            x = tetromino[i].y - center.y
            y = tetromino[i].x - center.x
            tetromino[i].x = center.x - x
            tetromino[i].y = center.y + y
            if not check_borders():
                tetromino = deepcopy(tetromino_old)
                break

Removing lines

    # Line Check / Removal
    line = HEIGHT - 1
    for row in range(HEIGHT - 1, -1, -1):
        count = 0
        for i in range(WIDTH):
            if feild[row][i]:
                count += 1
            feild[line][i] = feild[row][i]
        if count < WIDTH:
            line -= 1
        else: 
            anim_speed += 3
            lines += 1

Create new Variables

WIDTH, HEIGHT = 10, 18;
TILE = 45;
GAME_RES = (WIDTH * TILE, HEIGHT * TILE)
RES = 750, 940      # NEW LINE HERE

Change screen to surface and a new sc to display

screen = pygame.Surface(GAME_RES)       
sc = pygame.display.set_mode(RES)

Adding our background (Inside Game loop)

while True:
    dir_x = 0
    rotate = False
    # OLD CODE screen.fill(pygame.Color("black")) REPLACE WITH

    # NEW CODE HERE
    sc.blit(bg, (0,0))
    sc.blit(screen, (20,20))
    screen.blit(game_bg, (0,0))
    # NEW CODE END

    # Controls

Adding a new element to our positions

tetrominos_pos = [[(-1,-1),(-2,-1),(0,-1),(1,-1), (0,173,238)], # I
              [(0,-1),(-1,-1),(-1,0),(0,0), (255,241,0)], # O
              [(0,0),(-1,-1),(0,-1),(1,0), (236,27,36)], # Z
              [(0,0),(-1,0),(0,-1),(1,-1), (139,197,63)], # S
              [(0,0),(-1,0),(1,0),(1,-1), (246, 146, 30)], # L
              [(0,0),(-1,-1),(-1,0),(1,0), (27, 116, 187)], # J
              [(0,0),(-1,0),(1,0),(0,-1), (101,45,144)]] # T

Change tetrominos To ignore last element

# Change block_pos to block_pos[:-1], and add + [block_pos[-1]]
tetrominos = [
    [pygame.Rect(x + WIDTH // 2, y + 1, 1, 1) for x, y in block_pos[:-1]] + [block_pos[-1]]
    for block_pos in tetrominos_pos
]

Replace pygame.Color("white") with tetromino[4]

Inside Y movement

            if not check_borders():
                for i in range(4):
                    feild[tetromino_old[i].y][tetromino_old[i].x] = tetromino[4]
                tetromino = deepcopy(choice(tetrominos))
                break

Inside Rendering / Drawing sprites, when rendering feild / Endzone

    # Rendering / Drawing sprites
    for i in range(4):
        tetromino_rect.x = tetromino[i].x * TILE
        tetromino_rect.y = tetromino[i].y * TILE
        pygame.draw.rect(screen, tetromino[4], tetromino_rect)

Making it so Tetromino "O" no longer rotates

    # Rotating
    center = tetromino[0]
    tetromino_old = deepcopy(tetromino)
    if rotate and tetromino[4] != (255, 241, 0) :       # MODIFY CODE HERE
        for i in range(4):
            x = tetromino[i].y - center.y
            y = tetromino[i].x - center.x
            tetromino[i].x = center.x - x
            tetromino[i].y = center.y + y
            if not check_borders():
                tetromino = deepcopy(tetromino_old)
                break

Create new Font variables and text variable

# Fonts and texts
title_font = pygame.font.Font('HANGTHEDJ.ttf', 65)
font = pygame.font.Font('HANGTHEDJ.ttf', 45)

title_tetris = title_font.render('TETRIS', True, pygame.Color('darkorange'))

Show next block, Modify Tetromino Initialization inside Tetromino Objects and Variables

# Tetromino Objects and Variables
tetrominos_pos = [[...]]

tetrominos = [[pygame.Rect(x + WIDTH // 2, y +1, 1, 1) for x, y in block_pos] for block_pos in tetrominos_pos]
tetromino_rect = pygame.Rect(0,0, TILE -2, TILE -2)

# OLD CODE tetromino = deepcopy(choice(tetrominos)) REPLACE WITH
tetromino, next_tetromino = deepcopy(choice(tetrominos)), deepcopy(choice(tetrominos))      # NEW LINE OF CODE

Inside Y movement, change spawning tetromino

    for i in range(4):
        tetromino[i].y += 1
        if not check_borders():
            for i in range(4):
                feild[tetromino_old[i].y][tetromino_old[i].x] = tetromino[4]
            tetromino = next_tetromino      # MODIFIED CODE
            next_tetromino = deepcopy(choice(tetrominos))       # NEW CODE HERE
            break

Rendering the next figure and title

    # Rendering / Drawing sprites 
    # Rendering Tetromino
    ...
    # Rendering feild / Endzone
    ...

    # Rendering the title
    sc.blit(title_tetris, (475, 20))        

    # Rendering the next tetromino
    for i in range(4):
        tetromino_rect.x = next_tetromino[i].x * TILE + 375
        tetromino_rect.y = next_tetromino[i].y * TILE + 185
        pygame.draw.rect(sc, next_tetromino[4], tetromino_rect)

Modifying Line Check / Removal and computing score

    # Line Check / Removal
    line, lines = HEIGHT - 1, 0         # MODIFIED CODE HERE
    for row in range(HEIGHT - 1, -1, -1):
        count = 0
        for i in range(WIDTH):
            if feild[row][i]:
                count += 1
            feild[line][i] = feild[row][i]
        if count < WIDTH:
            line -= 1
        else: 
            anim_speed += 3
            lines += 1

    score += scores[lines]              # NEW LINE HERE

Adding slight delay when clearing a line (At top of Game loop)

while True:
    dir_x = 0
    rotate = False

    sc.blit(bg, (0,0))
    sc.blit(screen, (20,20))
    screen.blit(game_bg, (0,0))

    # NEW CODE HERE
    if lines > 0:
        pygame.time.wait(200)


    # Controls

Add new Score text variable

# Fonts and texts
title_font = pygame.font.Font('HANGTHEDJ.ttf', 65)
font = pygame.font.Font('HANGTHEDJ.ttf', 45)

title_tetris = title_font.render('TETRIS', True, pygame.Color('darkorange'))

# NEW CODE HERE
title_score = font.render('score:', True, pygame.Color('green'))

Displaying score (Inside Rendering / Drawing sprites)

    # Rendering the title
    sc.blit(title_tetris, (475, 20))     

    # NEW CODE HERE 
    sc.blit(title_score, (535, 780))
    sc.blit(font.render(str(score), True, pygame.Color('white')), (550, 840))

Creating Records (Inside Fonts and texts)

# Fonts and texts
title_font = pygame.font.Font('HANGTHEDJ.ttf', 65)
font = pygame.font.Font('HANGTHEDJ.ttf', 45)
title_tetris = title_font.render('TETRIS', True, pygame.Color('darkorange'))
title_score = font.render('score:', True, pygame.Color('green'))

# NEW CODE HERE
title_HIGH = font.render('High', True, pygame.Color('purple'))
title_SCORE = font.render('Score:', True, pygame.Color('purple'))    

Displaying High Scores (Inside Rendering / Drawing sprites)

    # Rendering the title
    sc.blit(title_tetris, (475, 20))     

    # Rendering current score
    sc.blit(title_score, (535, 780))
    sc.blit(font.render(str(score), True, pygame.Color('white')), (550, 840))

    # NEW CODE HERE
    sc.blit(title_HIGH, (525, 600))        
    sc.blit(title_SCORE, (525, 650))       

    record = str(max(int(record) if record.isdigit() else 0, score))
    sc.blit(font.render(str(record), True, pygame.Color('gold')), (550, 710))

Creating new Game over text

# Fonts and texts
title_font = pygame.font.Font('HANGTHEDJ.ttf', 65)
font = pygame.font.Font('HANGTHEDJ.ttf', 45)

gameOverFont = pygame.font.Font('HANGTHEDJ.ttf', 100)           # NEW CODE HERE

title_tetris = title_font.render('TETRIS', True, pygame.Color('darkorange'))
title_score = font.render('score:', True, pygame.Color('green'))
title_HIGH = font.render('High', True, pygame.Color('purple'))
title_SCORE = font.render('Score:', True, pygame.Color('purple')) 

GameOverTxt = gameOverFont.render('GAME OVER', True, pygame.Color('firebrick1'))    # NEW CODE HERE

Handinling Game Over Logic

    # Game over
    for i in range(WIDTH):
        if feild[0][i]:
            set_record(record,score)
            death_flag = True
            sc.blit(GameOverTxt, (WIDTH * TILE/8, HEIGHT * TILE/2))

Below Controls and Above X movement add this line

    #Controls

    if death_flag: continue     # NEW LINE HERE

    # X movement

In the coming workshops we will be doing Gotdot, Pixel art, and Unity.

Soly Image Caption

Additionally for those who don't know this seal is Solly. This is an image of Solly from 2 years ago, happy and not coding. however Solly now is sad and secluded s̶l̶a̶v̶i̶n̶g̶ ̶a̶w̶a̶y̶ working for a Triple A game company. Here at the Game Dev Club we miss Solly's smile and want to feed him down in ̶t̶h̶e̶ ̶b̶a̶s̶e̶m̶e̶n̶t̶ office, however the filthy esports people are taking away our funding to help feed Solly.

So please do not forget to sign in to help Solly.

Fun Fact, Valorant Club Alone at USF got around $30,000 in funding last year. Did you know one McDonalds meal cost about $7.01, you can feed Solly 4,279 times with that much money.