개발/Python

[Python] 파이썬으로 고전 게임 '테트리스' 만들기 – 직접 만들어보며 배운 시행착오의 기록

일요일좋아하는사람 2025. 4. 21. 20:37
728x90
반응형

파이썬

들어가며

예전부터 한 번쯤은 만들어보고 싶었던 게임이 있다면 단연코 테트리스였다. 간단한 구조지만 의외로 중독성이 있고, 적당한 난이도 조절과 함께 구현할 수 있다면 파이썬의 GUI 및 로직 처리 능력을 익히기에 최적의 게임이라고 생각했다. 처음엔 tkinter로 UI를 만들려 했는데, 도형이 부드럽게 움직이지 않아서 포기했고, 다음엔 pygame이라는 라이브러리를 시도했다. 처음엔 설치부터 안됐고, 경로 문제로 이미지가 안 불러와지는 등 수많은 시행착오를 겪었다. 결국 필요한 모듈을 제대로 설치하고 도형들을 직접 그리면서 움직임을 제어하는 방식으로 접근했더니, 제법 훌륭한 결과물을 만들 수 있었다.


1. 필요한 라이브러리 설치

테트리스는 그래픽 기반 게임이기 때문에 pygame이라는 게임 개발 라이브러리를 사용했다. 다음 명령어로 설치할 수 있다.

pip install pygame

2. 게임 화면 및 설정 초기화

import pygame
import random

pygame.init()

# 화면 크기와 색상 정의
display_width = 300
display_height = 600
block_size = 30
cols = display_width // block_size
rows = display_height // block_size
screen = pygame.display.set_mode((display_width, display_height))
pygame.display.set_caption("파이썬 테트리스")

이 코드는 게임 창을 생성하고 테트리스 블럭 하나가 차지할 공간을 정의한다. block_size는 각 테트로미노 조각이 차지하는 정사각형의 픽셀 크기다. cols와 rows는 화면을 블럭 기준으로 나눈 그리드의 너비와 높이다. 이렇게 하면 각 블럭이 일정한 규격으로 움직이고, 충돌 감지 시 그리드에 맞게 처리가 가능하다. pygame.display.set_mode로 실제 창이 뜨고, 제목도 설정해준다.


3. 테트로미노 도형 정의하기

shapes = [
    [[1, 1, 1, 1]],  # I
    [[1, 1], [1, 1]],  # O
    [[0, 1, 0], [1, 1, 1]],  # T
    [[1, 0, 0], [1, 1, 1]],  # J
    [[0, 0, 1], [1, 1, 1]],  # L
    [[0, 1, 1], [1, 1, 0]],  # S
    [[1, 1, 0], [0, 1, 1]],  # Z
]

이 배열은 테트리스에서 사용할 기본 도형들을 2차원 리스트 형태로 정의한 것이다. 1은 블록이 있는 부분을 의미하고 0은 공백이다. 도형마다 회전이 가능해야 하므로, 이 리스트는 이후에 .rotate() 같은 함수에 넘겨 회전 처리를 할 수 있다. 리스트 형태이기 때문에 계산하기 쉽고, 화면 그리기나 충돌 감지에도 유리하다. 여기서는 기본적인 테트로미노 7가지를 정의했다.


4. 도형 클래스 만들기

class Piece:
    def __init__(self, x, y, shape):
        self.x = x
        self.y = y
        self.shape = shape
        self.color = (random.randint(50,255), random.randint(50,255), random.randint(50,255))
        self.rotation = 0

이 Piece 클래스는 각 도형을 생성할 때 사용된다. x와 y는 현재 위치, shape는 위에서 정의한 도형 중 하나를 받아서 해당 조각을 구성한다. color는 도형마다 랜덤하게 색을 부여해서 시각적으로 식별이 되게 해준다. 회전을 위해 rotation이라는 변수를 두었고, 이후 게임 루프에서 방향키 입력을 통해 값이 변하게 된다. 이 구조를 통해 테트리스의 핵심인 도형 이동과 회전을 제어한다.


5. 보드 초기화 및 충돌 판정

def create_board():
    return [[(0, 0, 0) for _ in range(cols)] for _ in range(rows)]

def valid_space(piece, board):
    shape = piece.shape[piece.rotation % len(piece.shape)]
    for i, row in enumerate(shape):
        for j, cell in enumerate(row):
            if cell:
                x = piece.x + j
                y = piece.y + i
                if x < 0 or x >= cols or y >= rows or board[y][x] != (0, 0, 0):
                    return False
    return True

create_board()는 전체 보드를 2차원 RGB값 리스트로 초기화하는 함수다. (0,0,0)은 공백, 즉 블럭이 없는 상태를 의미한다. valid_space() 함수는 도형이 움직일 수 있는지 확인하는 역할을 한다. 만약 현재 위치에서 벗어나거나 다른 도형과 겹치면 False를 반환한다. 이 두 함수는 테트리스 로직에서 도형이 벽이나 다른 도형과 충돌했는지를 판별하는 데 필수적이다.


6. 게임 루프 및 블록 그리기

def draw_window(screen, board, current_piece):
    screen.fill((0, 0, 0))
    for y in range(rows):
        for x in range(cols):
            color = board[y][x]
            pygame.draw.rect(screen, color, (x * block_size, y * block_size, block_size, block_size), 0)

    shape = current_piece.shape[current_piece.rotation % len(current_piece.shape)]
    for i, row in enumerate(shape):
        for j, cell in enumerate(row):
            if cell:
                pygame.draw.rect(screen, current_piece.color, ((current_piece.x + j) * block_size, (current_piece.y + i) * block_size, block_size, block_size), 0)

    pygame.display.update()

이 함수는 게임의 화면을 실제로 그려주는 역할을 한다. 기존 보드에 있는 정적인 블록과, 현재 움직이고 있는 블록을 동시에 그려야 하므로 이중 반복문을 통해 각각의 색상과 위치를 계산한다. pygame.draw.rect를 통해 각 블럭을 사각형으로 표현하고, pygame.display.update()를 통해 실제 화면에 그린다. 이 함수는 매 프레임마다 호출되어 실시간으로 화면을 갱신하는 데 중요한 역할을 한다.


7. 메인 게임 루프 실행하기

board = create_board()
clock = pygame.time.Clock()
current_piece = Piece(3, 0, random.choice(shapes))
running = True

while running:
    clock.tick(5)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    current_piece.y += 1
    if not valid_space(current_piece, board):
        current_piece.y -= 1
        shape = current_piece.shape[current_piece.rotation % len(current_piece.shape)]
        for i, row in enumerate(shape):
            for j, cell in enumerate(row):
                if cell:
                    board[current_piece.y + i][current_piece.x + j] = current_piece.color
        current_piece = Piece(3, 0, random.choice(shapes))

    draw_window(screen, board, current_piece)

pygame.quit()

이 메인 루프는 게임의 심장이다. clock.tick(5)는 1초에 5프레임씩 진행되도록 속도를 조절한다. 키 입력이나 종료 이벤트를 처리하고, 일정 시간마다 도형이 아래로 떨어지도록 y 값을 증가시킨다. valid_space로 공간 유효성을 체크하고, 도형이 멈췄다면 보드에 해당 도형의 색상을 기록한 뒤 새 도형을 생성한다. draw_window()는 현재 상태를 매번 갱신해서 플레이어가 실시간으로 확인할 수 있도록 도와준다.

 


결과 코드 전체 보기

전체 코드는 위에 작성한 각 코드 블럭을 순서대로 붙이면 실행됩니다. pygame이 설치되어 있고, Python 3.8 이상이라면 바로 실행해볼 수 있습니다. 아래는 모든 기능이 통합된 최종 코드입니다.

# 전체 실행 가능한 테트리스 코드
import pygame
import random

pygame.init()
display_width, display_height = 300, 600
block_size = 30
cols, rows = display_width // block_size, display_height // block_size
screen = pygame.display.set_mode((display_width, display_height))
pygame.display.set_caption("파이썬 테트리스")

shapes = [
    [[1, 1, 1, 1]],
    [[1, 1], [1, 1]],
    [[0, 1, 0], [1, 1, 1]],
    [[1, 0, 0], [1, 1, 1]],
    [[0, 0, 1], [1, 1, 1]],
    [[0, 1, 1], [1, 1, 0]],
    [[1, 1, 0], [0, 1, 1]],
]

class Piece:
    def __init__(self, x, y, shape):
        self.x, self.y = x, y
        self.shape = shape
        self.color = (random.randint(50,255), random.randint(50,255), random.randint(50,255))
        self.rotation = 0

def create_board():
    return [[(0,0,0) for _ in range(cols)] for _ in range(rows)]

def valid_space(piece, board):
    shape = piece.shape[piece.rotation % len(piece.shape)]
    for i, row in enumerate(shape):
        for j, cell in enumerate(row):
            if cell:
                x, y = piece.x + j, piece.y + i
                if x < 0 or x >= cols or y >= rows or (y >= 0 and board[y][x] != (0,0,0)):
                    return False
    return True

def clear_rows(board):
    full_rows = [i for i in range(rows) if all(cell != (0,0,0) for cell in board[i])]
    for i in full_rows:
        del board[i]
        board.insert(0, [(0,0,0)] * cols)
    return len(full_rows)

def draw_text(text, size, x, y):
    font = pygame.font.SysFont("comicsans", size)
    label = font.render(text, True, (255,255,255))
    screen.blit(label, (x, y))

def draw_window(screen, board, current_piece, score):
    screen.fill((0,0,0))
    for y in range(rows):
        for x in range(cols):
            pygame.draw.rect(screen, board[y][x], (x*block_size, y*block_size, block_size, block_size), 0)
    shape = current_piece.shape[current_piece.rotation % len(current_piece.shape)]
    for i, row in enumerate(shape):
        for j, cell in enumerate(row):
            if cell:
                pygame.draw.rect(screen, current_piece.color, ((current_piece.x + j) * block_size, (current_piece.y + i) * block_size, block_size, block_size), 0)
    draw_text(f"SCORE: {score}", 24, 10, 10)
    pygame.display.update()

board = create_board()
clock = pygame.time.Clock()
current_piece = Piece(3, 0, random.choice(shapes))
score = 0
fall_speed = 5
frame_count = 0
running = True

while running:
    clock.tick(30)
    frame_count += 1
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_LEFT:
                current_piece.x -= 1
                if not valid_space(current_piece, board):
                    current_piece.x += 1
            elif event.key == pygame.K_RIGHT:
                current_piece.x += 1
                if not valid_space(current_piece, board):
                    current_piece.x -= 1
            elif event.key == pygame.K_DOWN:
                current_piece.y += 1
                if not valid_space(current_piece, board):
                    current_piece.y -= 1
            elif event.key == pygame.K_UP:
                current_piece.rotation += 1
                if not valid_space(current_piece, board):
                    current_piece.rotation -= 1

    if frame_count >= fall_speed:
        current_piece.y += 1
        if not valid_space(current_piece, board):
            current_piece.y -= 1
            shape = current_piece.shape[current_piece.rotation % len(current_piece.shape)]
            for i, row in enumerate(shape):
                for j, cell in enumerate(row):
                    if cell:
                        board[current_piece.y + i][current_piece.x + j] = current_piece.color
            score += clear_rows(board) * 100
            current_piece = Piece(3, 0, random.choice(shapes))
        frame_count = 0

    draw_window(screen, board, current_piece, score)

pygame.quit()

 


 

728x90
반응형