티스토리 뷰

PyGame을 사용하여 Python에서 Tetris를 작성하는 단계별 가이드

 

이 글에서는 Python의 PyGame 라이브러리를 사용하여 간단한 Tetris 게임을 만들어 보겠습니다. 적용된 알고리즘은 비교적 간단한 편이지만 초보자에게는 조금 어려울 수 있습니다. 이 글에서는 PyGame 기초에 대해서는 다루지 않고 게임 로직을 중심으로 작성하였습니다. 본문 맨 마지막에 게임의 전체 코드가 있으니 내용을 복사하여 게임을 즐겨 볼 수 있습니다.

Prerequisites

  1. Python3. 파이썬 공식 사이트(official website)에서 다운로드하여 설치합니다.
  2. PyGame. 명령어 쉘에서 pip install pygame 또는 pip3 install pygame을 실행하여 설치합니다.
  3. 파이썬에 대한 기초 지식.

PyGame 또는 Python 자체를 설치하면서 문제가 발생할 수도 있는데 여기서는 다루지 않습니다. StackOverflow나 구글 검색을 참고하세요.

 

개인적으로는 Mac에서 화면에 아무것도 표시되지 않는 문제가 발생했으며 PyGame의 특정 버전을 설치하여 pip install pygame == 2.0.0.dev4와 같은 문제가 해결되었습니다.

'Figure' 클래스

먼저 Figures 클래스로 시작합니다. 첫 번째 목표는 도형과 회전을 함께 저장하는 것입니다. 물론 행렬 회전을 사용하여 회전시키는 방법도 있지만 여기서는 간단한 방법을 사용하겠습니다.

 

테트리스 블럭을 행렬로 표현하는 방법

테트리스의 도형들을 다음과 같이 리스트로 나타낼 수 있습니다.

class Figure:
    figures = [
        [[1, 5, 9, 13], [4, 5, 6, 7]],
        [[1, 2, 5, 9], [0, 4, 5, 6], [1, 5, 9, 8], [4, 5, 6, 10]],
        [[1, 2, 6, 10], [5, 6, 7, 9], [2, 6, 10, 11], [3, 5, 6, 7]],  
        [[1, 4, 5, 6], [1, 4, 5, 9], [4, 5, 6, 9], [1, 5, 6, 9]],
        [[1, 2, 5, 6]],
    ]

위 리스트는 중복 리스트인데, 내부 리스트는 각 도형이 회전한 것을 나타냅니다. 숫자는 4x4 행렬의 위치를 나타냅니다. 예를 들어, [1,5,9,13]는 세로로 긴 일직선 모양의 도형을 나타냅니다. 위의 그림을 참조하세요.

 

여기에 누락 된 그림 “z”는 직접 추가해 보세요.

 

__init__ 함수는 다음과 같습니다.

class Figure:
    ...    
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.type = random.randint(0, len(self.figures) - 1)
        self.color = random.randint(1, len(colors) - 1)
        self.rotation = 0

도형과 색상은 random 함수를 사용하여 무작위로 선택합니다.

 

다음의 두 가지 메소드를 사용하여 도형을 빠르게 회전 시키고 현재의 이미지를 얻어 옵니다.

class Figure:
    ...
    def image(self):
        return self.figures[self.type][self.rotation]

    def rotate(self):
        self.rotation = (self.rotation + 1) % len(self.figures[self.type])

The Tetris Class

게임 클래스에서 사용하는 변수 들을 다음과 같이 초기화 합니다.

class Tetris:
    level = 2
    score = 0
    state = "start"
    field = []
    height = 0
    width = 0
    x = 100
    y = 60
    zoom = 20
    figure = None

"state" 변수는 우리가 게임을하고 있는지 아닌지를 나타내는 상태를 나타냅니다. "field" 변수는 게임판을 나타내며 도형이 없는 빈 칸은 0의 값을 갖고 있게 됩니다. 도형이 있는 경우에는 색상 값을 갖고 있게 됩니다. (단, 움직이고 있는 도형은 제외합니다.)

 

다음과 같은 간단한 방법으로 게임을 초기화합니다.

class Tetris:
    ...    
    def __init__(self, height, width):
        self.height = height
        self.width = width
        for i in range(height):
            new_line = []
            for j in range(width):
                new_line.append(0)
            self.field.append(new_line)

 

높이 x 너비의 필드를 만듭니다.

class Tetris:
    ...
    def new_figure(self):
        self.figure = Figure(3, 0)

좌표 (3,0)에 새 도형을 생성합니다.

 

다음은 떨어지고 있는 도형이 바닥에 쌓인 도형들과 충돌하는지 확인하는 메소드입니다. 도형의 충돌은 도형이 왼쪽, 오른쪽으로 움직이거나 아래로 떨어질 때, 또는 회전 할 때 발생할 수 있습니다.

class Tetris:
    ...
    def intersects(self):
        intersection = False
        for i in range(4):
            for j in range(4):
                if i * 4 + j in self.figure.image():
                    if i + self.figure.y > self.height - 1 or \
                            j + self.figure.x > self.width - 1 or \
                            j + self.figure.x < 0 or \
                            self.field[i + self.figure.y][j + self.figure.x] > 0:
                        intersection = True
        return intersection

충돌 여부를 확인하는 방법은 간단합니다. 4x4 행렬에 있는 각 셀이 게임 경계를 벗어 났는지, busy field에 닿았는지 여부를 확인합니다. self.field[..][..]의 값이 '0'보다 크면 어떤 도형의 일부가 있다는 것을 의미합니다.

 

이 충돌 감지 메소드를 사용하여 도형을 이동하거나 회전시킬 수 있는지 확인할 수 있습니다. 만약 도형이 아래로 내려가려다가 충돌을 감지하였다면 바닥에 닿았음을 의미하는 것이므로 도형을 더 이상 움직일 수 없게 합니다. 이것을 "freeze"라고 했습니다.

class Tetris:
    ...
    def freeze(self):
        for i in range(4):
            for j in range(4):
                if i * 4 + j in self.figure.image():
                    self.field[i + self.figure.y][j + self.figure.x] = self.figure.color
        self.break_lines()
        self.new_figure()
        if self.intersects():
            game.state = "gameover"

도형을 "freeze" 한 후에는 블럭이 가득 채워진 수평 라인이 없는지 확인합니다. 도형이 가득 채워진 수평 라인은 삭제를 합니다. 수평 라인 삭제 후에는 새 도형을 생성합니다. 새 도형이 생성하자마자 충돌이 발생하면 게임을 종료합니다.

 

전체 라인을 확인하는 것은 비교적 간단하지만 라인을 파괴하는 것은 아래에서 위로 진행된다는 점은 주의해야 합니다.

(Checking the full lines is relatively simple and straightforward, but pay attention to the fact that destroying a line goes from the bottom to the top)

class Tetris:
    ...
    def break_lines(self):
        lines = 0
        for i in range(1, self.height):
            zeros = 0
            for j in range(self.width):
                if self.field[i][j] == 0:
                    zeros += 1
            if zeros == 0:
                lines += 1
                for i1 in range(i, 1, -1):
                    for j in range(self.width):
                        self.field[i1][j] = self.field[i1 - 1][j]
        self.score += lines ** 2

이제 이동 방법을 추가합니다.

class Tetris:
    ...
    def go_space(self):
        while not self.intersects():
            self.figure.y += 1
        self.figure.y -= 1
        self.freeze()

    def go_down(self):
        self.figure.y += 1
        if self.intersects():
            self.figure.y -= 1
            self.freeze()

    def go_side(self, dx):
        old_x = self.figure.x
        self.figure.x += dx
        if self.intersects():
            self.figure.x = old_x

    def rotate(self):
        old_rotation = self.figure.rotation
        self.figure.rotate()
        if self.intersects():
            self.figure.rotation = old_rotation

위 코드에서 보이는 바와 같이 go_space 메소드는 go_down 메소드와 내용이 같습니다. 다만 맨 아래 정해진 수치에 도달 할 때까지 계속 반복하여 내려갑니다.

 

모든 메소드에는 도형의 마지막 위치를 기억하고 좌표를 변경하며 다른 도형들과 충돌이 없는지 확인합니다. 충돌이 있을 경우에는 이전 상태로 돌아갑니다.

전체 코드

거의 다 끝났습니다!

게임 루프와 간단한 로직이 남아 있습니다. 전체 코드를 살펴 보겠습니다.

import pygame
import random

colors = [
    (0, 0, 0),
    (120, 37, 179),
    (100, 179, 179),
    (80, 34, 22),
    (80, 134, 22),
    (180, 34, 22),
    (180, 34, 122),
]


class Figure:
    x = 0
    y = 0

    figures = [
        [[1, 5, 9, 13], [4, 5, 6, 7]],
        [[1, 2, 5, 9], [0, 4, 5, 6], [1, 5, 9, 8], [4, 5, 6, 10]],
        [[1, 2, 6, 10], [5, 6, 7, 9], [2, 6, 10, 11], [3, 5, 6, 7]],
        [[1, 4, 5, 6], [1, 4, 5, 9], [4, 5, 6, 9], [1, 5, 6, 9]],
        [[1, 2, 5, 6]],
    ]

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.type = random.randint(0, len(self.figures) - 1)
        self.color = random.randint(1, len(colors) - 1)
        self.rotation = 0

    def image(self):
        return self.figures[self.type][self.rotation]

    def rotate(self):
        self.rotation = (self.rotation + 1) % len(self.figures[self.type])


class Tetris:
    level = 2
    score = 0
    state = "start"
    field = []
    height = 0
    width = 0
    x = 100
    y = 60
    zoom = 20
    figure = None

    def __init__(self, height, width):
        self.height = height
        self.width = width
        for i in range(height):
            new_line = []
            for j in range(width):
                new_line.append(0)
            self.field.append(new_line)

    def new_figure(self):
        self.figure = Figure(3, 0)

    def intersects(self):
        intersection = False
        for i in range(4):
            for j in range(4):
                if i * 4 + j in self.figure.image():
                    if i + self.figure.y > self.height - 1 or \
                            j + self.figure.x > self.width - 1 or \
                            j + self.figure.x < 0 or \
                            self.field[i + self.figure.y][j + self.figure.x] > 0:
                        intersection = True
        return intersection

    def break_lines(self):
        lines = 0
        for i in range(1, self.height):
            zeros = 0
            for j in range(self.width):
                if self.field[i][j] == 0:
                    zeros += 1
            if zeros == 0:
                lines += 1
                for i1 in range(i, 1, -1):
                    for j in range(self.width):
                        self.field[i1][j] = self.field[i1 - 1][j]
        self.score += lines ** 2

    def go_space(self):
        while not self.intersects():
            self.figure.y += 1
        self.figure.y -= 1
        self.freeze()

    def go_down(self):
        self.figure.y += 1
        if self.intersects():
            self.figure.y -= 1
            self.freeze()

    def freeze(self):
        for i in range(4):
            for j in range(4):
                if i * 4 + j in self.figure.image():
                    self.field[i + self.figure.y][j + self.figure.x] = self.figure.color
        self.break_lines()
        self.new_figure()
        if self.intersects():
            game.state = "gameover"

    def go_side(self, dx):
        old_x = self.figure.x
        self.figure.x += dx
        if self.intersects():
            self.figure.x = old_x

    def rotate(self):
        old_rotation = self.figure.rotation
        self.figure.rotate()
        if self.intersects():
            self.figure.rotation = old_rotation


# Initialize the game engine
pygame.init()

# Define some colors
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GRAY = (128, 128, 128)

size = (400, 500)
screen = pygame.display.set_mode(size)

pygame.display.set_caption("Tetris")

# Loop until the user clicks the close button.
done = False
clock = pygame.time.Clock()
fps = 25
game = Tetris(20, 10)
counter = 0

pressing_down = False

while not done:
    if game.figure is None:
        game.new_figure()
    counter += 1
    if counter > 100000:
        counter = 0

    if counter % (fps // game.level // 2) == 0 or pressing_down:
        if game.state == "start":
            game.go_down()

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP:
                game.rotate()
            if event.key == pygame.K_DOWN:
                pressing_down = True
            if event.key == pygame.K_LEFT:
                game.go_side(-1)
            if event.key == pygame.K_RIGHT:
                game.go_side(1)
            if event.key == pygame.K_SPACE:
                game.go_space()
        if event.type == pygame.KEYUP:
            if event.key == pygame.K_DOWN:
                pressing_down = False

    screen.fill(WHITE)

    for i in range(game.height):
        for j in range(game.width):
            pygame.draw.rect(screen, GRAY, [game.x + game.zoom * j, game.y + game.zoom * i, game.zoom, game.zoom], 1)
            if game.field[i][j] > 0:
                pygame.draw.rect(screen, colors[game.field[i][j]],
                                 [game.x + game.zoom * j + 1, game.y + game.zoom * i + 1, game.zoom - 2, game.zoom - 1])

    if game.figure is not None:
        for i in range(4):
            for j in range(4):
                p = i * 4 + j
                if p in game.figure.image():
                    pygame.draw.rect(screen, colors[game.figure.color],
                                     [game.x + game.zoom * (j + game.figure.x) + 1,
                                      game.y + game.zoom * (i + game.figure.y) + 1,
                                      game.zoom - 2, game.zoom - 2])

    font = pygame.font.SysFont('Calibri', 25, True, False)
    font1 = pygame.font.SysFont('Calibri', 65, True, False)
    text = font.render("Score: " + str(game.score), True, BLACK)
    text_game_over = font1.render("Game Over :( ", True, (255, 0, 0))

    screen.blit(text, [0, 0])
    if game.state == "gameover":
        screen.blit(text_game_over, [10, 200])

    pygame.display.flip()
    clock.tick(fps)

pygame.quit()

파이썬 파일을 생성하고 위 코드를 복사하여 붙여 넣은 후 실행하면 간단한 테스리스 게임을 즐길 수 있습니다.

 

원문: https://levelup.gitconnected.com/writing-tetris-in-python-2a16bddb5318

 

Writing Tetris in Python

Step by step guide to writing Tetris in Python with PyGame

levelup.gitconnected.com

 

'해피 코딩' 카테고리의 다른 글

파이썬 알고리즘 문제: Anagram  (0) 2021.01.15
Yocto project 소개  (1) 2019.12.27
Docker registry 처음 사용하기  (0) 2019.12.02
Docker 처음 사용하기  (0) 2019.12.02
크롤링2  (0) 2019.08.17
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함