파이썬 기초문법/파이썬 게임 만들기

파이썬 게임 만들기 - 테트리스 🎲🎯

Family in August 2025. 3. 15. 10:04
반응형

 

파이썬으로 만드는 테트리스: 고전 게임의 재탄생 🐍🎮🧱

안녕하세요! Python Game Dev 블로그의 열다섯 번째 포스팅입니다. 지난 포스팅에서는 2048 퍼즐 게임을 만들어 보았는데요. 오늘은 약속드린 대로 '파이썬으로 만드는 테트리스(Tetris)'를 구현해보겠습니다. 누구나 한 번쯤 플레이해 본 이 고전 게임을 Pygame으로 직접 만들어 봅시다!

오늘의 게임: 파이썬으로 만드는 테트리스 🎲🎯

테트리스는 1984년 러시아의 프로그래머 알렉세이 파지트노프가 개발한 이후 전 세계적으로 사랑받는 퍼즐 게임입니다. 서로 다른 모양의 블록(테트로미노)을 회전하고 이동시켜 가로줄을 채우는 간단한 규칙이지만, 속도가 점점 빨라지면서 도전적인 게임플레이를 제공합니다. Pygame 라이브러리를 활용하여 이 고전 게임을 재현해 보겠습니다.

게임의 규칙 📜

  • 7가지 모양의 테트로미노(I, J, L, O, S, T, Z)가 무작위로 등장합니다.
  • 방향키로 블록을 좌우로 이동하거나 회전시킬 수 있습니다.
  • 블록이 바닥에 닿거나 다른 블록 위에 놓이면 고정됩니다.
  • 가로줄이 완전히 채워지면 해당 줄이 사라지고 점수를 얻습니다.
  • 블록이 화면 상단에 도달하면 게임이 종료됩니다.
  • 시간이 지날수록 블록이 떨어지는 속도가 빨라집니다.

전체 코드 💻

import pygame
import random
import time

# 색상 정의
COLORS = {
    'BLACK': (0, 0, 0),
    'WHITE': (255, 255, 255),
    'GRAY': (128, 128, 128),
    'RED': (255, 0, 0),
    'GREEN': (0, 255, 0),
    'BLUE': (0, 0, 255),
    'CYAN': (0, 255, 255),
    'MAGENTA': (255, 0, 255),
    'YELLOW': (255, 255, 0),
    'ORANGE': (255, 165, 0),
    'PURPLE': (128, 0, 128)
}

# 테트로미노 정의 (모양별 회전 상태)
TETROMINOS = {
    'I': [
        [[0, 0, 0, 0],
         [1, 1, 1, 1],
         [0, 0, 0, 0],
         [0, 0, 0, 0]],
        [[0, 1, 0, 0],
         [0, 1, 0, 0],
         [0, 1, 0, 0],
         [0, 1, 0, 0]]
    ],
    'J': [
        [[1, 0, 0],
         [1, 1, 1],
         [0, 0, 0]],
        [[0, 1, 1],
         [0, 1, 0],
         [0, 1, 0]],
        [[0, 0, 0],
         [1, 1, 1],
         [0, 0, 1]],
        [[0, 1, 0],
         [0, 1, 0],
         [1, 1, 0]]
    ],
    'L': [
        [[0, 0, 1],
         [1, 1, 1],
         [0, 0, 0]],
        [[0, 1, 0],
         [0, 1, 0],
         [0, 1, 1]],
        [[0, 0, 0],
         [1, 1, 1],
         [1, 0, 0]],
        [[1, 1, 0],
         [0, 1, 0],
         [0, 1, 0]]
    ],
    'O': [
        [[1, 1],
         [1, 1]]
    ],
    'S': [
        [[0, 1, 1],
         [1, 1, 0],
         [0, 0, 0]],
        [[0, 1, 0],
         [0, 1, 1],
         [0, 0, 1]]
    ],
    'T': [
        [[0, 1, 0],
         [1, 1, 1],
         [0, 0, 0]],
        [[0, 1, 0],
         [0, 1, 1],
         [0, 1, 0]],
        [[0, 0, 0],
         [1, 1, 1],
         [0, 1, 0]],
        [[0, 1, 0],
         [1, 1, 0],
         [0, 1, 0]]
    ],
    'Z': [
        [[1, 1, 0],
         [0, 1, 1],
         [0, 0, 0]],
        [[0, 0, 1],
         [0, 1, 1],
         [0, 1, 0]]
    ]
}

# 테트로미노 색상 매핑
TETROMINO_COLORS = {
    'I': COLORS['CYAN'],
    'J': COLORS['BLUE'],
    'L': COLORS['ORANGE'],
    'O': COLORS['YELLOW'],
    'S': COLORS['GREEN'],
    'T': COLORS['PURPLE'],
    'Z': COLORS['RED']
}

# 게임 설정
CELL_SIZE = 30  # 각 블록 셀의 크기
GRID_WIDTH = 10  # 그리드 너비 (블록 수)
GRID_HEIGHT = 20  # 그리드 높이 (블록 수)
SCREEN_WIDTH = CELL_SIZE * (GRID_WIDTH + 8)  # 화면 너비
SCREEN_HEIGHT = CELL_SIZE * GRID_HEIGHT  # 화면 높이
FPS = 60  # 프레임 속도

class Tetris:
    def __init__(self):
        pygame.init()
        pygame.display.set_caption('파이썬 테트리스')
        
        self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
        self.clock = pygame.time.Clock()
        self.font = pygame.font.SysFont('malgungothic', 36)
        self.small_font = pygame.font.SysFont('malgungothic', 24)
        # self.font = pygame.font.Font('malgungothic', 36)
        # self.small_font = pygame.font.Font('malgungothic', 24)
        
        self.reset_game()
    
    def reset_game(self):
        # 게임 상태 초기화
        self.board = [[COLORS['BLACK'] for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
        self.current_piece = self.new_piece()
        self.next_piece = self.new_piece()
        self.score = 0
        self.level = 1
        self.lines_cleared = 0
        self.game_over = False
        self.drop_speed = 0.5  # 초기 낙하 속도 (초 단위)
        self.last_drop_time = time.time()
    
    def new_piece(self):
        # 새로운 테트로미노 생성
        shape = random.choice(list(TETROMINOS.keys()))
        return {
            'shape': shape,
            'rotation': 0,
            'x': GRID_WIDTH // 2 - len(TETROMINOS[shape][0][0]) // 2,
            'y': 0
        }
    
    def get_piece_grid(self, piece=None):
        # 현재 또는 지정된 조각의 그리드 얻기
        if piece is None:
            piece = self.current_piece
        shape = piece['shape']
        rotation = piece['rotation']
        return TETROMINOS[shape][rotation % len(TETROMINOS[shape])]
    
    def valid_position(self, piece=None, x_offset=0, y_offset=0):
        # 위치가 유효한지 확인
        if piece is None:
            piece = self.current_piece
        
        piece_grid = self.get_piece_grid(piece)
        for y, row in enumerate(piece_grid):
            for x, cell in enumerate(row):
                if cell:
                    board_x = piece['x'] + x + x_offset
                    board_y = piece['y'] + y + y_offset
                    
                    # 경계 확인
                    if (board_x < 0 or board_x >= GRID_WIDTH or 
                        board_y >= GRID_HEIGHT or 
                        (board_y >= 0 and self.board[board_y][board_x] != COLORS['BLACK'])):
                        return False
        return True
    
    def merge_piece(self):
        # 현재 조각을 게임판에 병합
        piece_grid = self.get_piece_grid()
        for y, row in enumerate(piece_grid):
            for x, cell in enumerate(row):
                if cell:
                    board_x = self.current_piece['x'] + x
                    board_y = self.current_piece['y'] + y
                    
                    # 게임판에 넣기 (범위 확인)
                    if 0 <= board_y < GRID_HEIGHT and 0 <= board_x < GRID_WIDTH:
                        self.board[board_y][board_x] = TETROMINO_COLORS[self.current_piece['shape']]
    
    def clear_lines(self):
        # 완성된 라인 제거 및 점수 업데이트
        lines_to_clear = []
        for y in range(GRID_HEIGHT):
            if all(color != COLORS['BLACK'] for color in self.board[y]):
                lines_to_clear.append(y)
        
        # 라인 제거
        for line in lines_to_clear:
            # 해당 라인 위의 모든 라인을 한 칸씩 아래로 이동
            for y in range(line, 0, -1):
                self.board[y] = self.board[y-1][:]
            # 맨 위 라인은 빈 라인으로 채우기
            self.board[0] = [COLORS['BLACK'] for _ in range(GRID_WIDTH)]
        
        # 점수 및 레벨 업데이트
        if lines_to_clear:
            lines_count = len(lines_to_clear)
            self.lines_cleared += lines_count
            
            # 점수 계산 (라인 수에 따라 다른 점수)
            line_scores = {1: 100, 2: 300, 3: 500, 4: 800}
            self.score += line_scores.get(lines_count, 0) * self.level
            
            # 10라인마다 레벨 업
            self.level = self.lines_cleared // 10 + 1
            
            # 레벨에 따라 낙하 속도 조정
            self.drop_speed = max(0.05, 0.5 - (self.level - 1) * 0.05)
    
    def move(self, dx, dy):
        # 조각 이동
        if self.valid_position(x_offset=dx, y_offset=dy):
            self.current_piece['x'] += dx
            self.current_piece['y'] += dy
            return True
        return False
    
    def rotate(self):
        # 조각 회전
        old_rotation = self.current_piece['rotation']
        self.current_piece['rotation'] = (old_rotation + 1) % len(TETROMINOS[self.current_piece['shape']])
        
        # 회전이 유효하지 않으면 원래 상태로 복원
        if not self.valid_position():
            self.current_piece['rotation'] = old_rotation
            return False
        return True
    
    def drop(self):
        # 조각 한 칸 아래로 이동
        if not self.move(0, 1):
            self.merge_piece()
            self.clear_lines()
            self.current_piece = self.next_piece
            self.next_piece = self.new_piece()
            
            # 새 조각이 유효한 위치에 있지 않으면 게임 오버
            if not self.valid_position():
                self.game_over = True
    
    def hard_drop(self):
        # 조각을 최대한 아래로 빠르게 이동
        while self.move(0, 1):
            pass
        self.drop()
    
    def draw_board(self):
        # 게임판 그리기
        for y in range(GRID_HEIGHT):
            for x in range(GRID_WIDTH):
                pygame.draw.rect(
                    self.screen,
                    self.board[y][x],
                    (x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE),
                    0
                )
                
                # 격자 선 그리기
                pygame.draw.rect(
                    self.screen,
                    COLORS['GRAY'],
                    (x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE),
                    1
                )
    
    def draw_piece(self, piece=None, offset_x=0, offset_y=0):
        # 테트로미노 그리기
        if piece is None:
            piece = self.current_piece
        
        piece_grid = self.get_piece_grid(piece)
        for y, row in enumerate(piece_grid):
            for x, cell in enumerate(row):
                if cell:
                    pygame.draw.rect(
                        self.screen,
                        TETROMINO_COLORS[piece['shape']],
                        ((piece['x'] + x) * CELL_SIZE + offset_x, 
                         (piece['y'] + y) * CELL_SIZE + offset_y,
                         CELL_SIZE, CELL_SIZE),
                        0
                    )
                    
                    # 테트로미노 격자 선
                    pygame.draw.rect(
                        self.screen,
                        COLORS['WHITE'],
                        ((piece['x'] + x) * CELL_SIZE + offset_x, 
                         (piece['y'] + y) * CELL_SIZE + offset_y,
                         CELL_SIZE, CELL_SIZE),
                        1
                    )
    
    def draw_next_piece(self):
        # 다음 조각 표시
        next_x = GRID_WIDTH * CELL_SIZE + 20
        next_y = 50
        
        # "다음 조각" 텍스트
        next_text = self.font.render("다음 조각:", True, COLORS['WHITE'])
        self.screen.blit(next_text, (next_x, next_y - 40))
        
        # 다음 조각의 미리보기 위치 조정
        next_piece_copy = self.next_piece.copy()
        next_piece_copy['x'] = 0
        next_piece_copy['y'] = 0
        
        # 중앙에 표시하기 위한 오프셋 계산
        piece_grid = self.get_piece_grid(next_piece_copy)
        width = len(piece_grid[0]) * CELL_SIZE
        height = len(piece_grid) * CELL_SIZE
        
        # 다음 조각 그리기
        for y, row in enumerate(piece_grid):
            for x, cell in enumerate(row):
                if cell:
                    pygame.draw.rect(
                        self.screen,
                        TETROMINO_COLORS[next_piece_copy['shape']],
                        (next_x + x * CELL_SIZE, next_y + y * CELL_SIZE, CELL_SIZE, CELL_SIZE),
                        0
                    )
                    pygame.draw.rect(
                        self.screen,
                        COLORS['WHITE'],
                        (next_x + x * CELL_SIZE, next_y + y * CELL_SIZE, CELL_SIZE, CELL_SIZE),
                        1
                    )
    
    def draw_info(self):
        # 게임 정보 표시 (점수, 레벨 등)
        info_x = GRID_WIDTH * CELL_SIZE + 20
        info_y = 200
        
        # 점수
        score_text = self.font.render(f"점수: {self.score}", True, COLORS['WHITE'])
        self.screen.blit(score_text, (info_x, info_y))
        
        # 레벨
        level_text = self.font.render(f"레벨: {self.level}", True, COLORS['WHITE'])
        self.screen.blit(level_text, (info_x, info_y + 40))
        
        # 지운 라인 수
        lines_text = self.font.render(f"라인: {self.lines_cleared}", True, COLORS['WHITE'])
        self.screen.blit(lines_text, (info_x, info_y + 80))
        
        # 조작 방법
        controls_y = info_y + 160
        controls = [
            "← → : 이동",
            "↑ : 회전",
            "↓ : 소프트 드롭",
            "스페이스 : 하드 드롭",
            "R : 재시작",
            "ESC : 종료"
        ]
        
        controls_title = self.small_font.render("조작 방법:", True, COLORS['WHITE'])
        self.screen.blit(controls_title, (info_x, controls_y - 30))
        
        for i, text in enumerate(controls):
            control_text = self.small_font.render(text, True, COLORS['WHITE'])
            self.screen.blit(control_text, (info_x, controls_y + i * 25))
    
    def draw_game_over(self):
        # 게임 오버 화면
        overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA)
        overlay.fill((0, 0, 0, 180))  # 반투명 오버레이
        self.screen.blit(overlay, (0, 0))
        
        # 게임 오버 텍스트
        game_over_text = self.font.render("게임 오버!", True, COLORS['RED'])
        score_text = self.font.render(f"최종 점수: {self.score}", True, COLORS['WHITE'])
        restart_text = self.font.render("R키를 눌러 재시작", True, COLORS['WHITE'])
        
        # 텍스트 위치 계산
        text_x = SCREEN_WIDTH // 2
        text_y = SCREEN_HEIGHT // 2 - 50
        
        # 텍스트 중앙 정렬로 표시
        self.screen.blit(game_over_text, (text_x - game_over_text.get_width() // 2, text_y))
        self.screen.blit(score_text, (text_x - score_text.get_width() // 2, text_y + 50))
        self.screen.blit(restart_text, (text_x - restart_text.get_width() // 2, text_y + 100))
    
    def draw(self):
        # 화면 그리기
        self.screen.fill(COLORS['BLACK'])
        
        # 게임 영역 테두리
        pygame.draw.rect(
            self.screen,
            COLORS['WHITE'],
            (0, 0, GRID_WIDTH * CELL_SIZE, GRID_HEIGHT * CELL_SIZE),
            2
        )
        
        self.draw_board()
        self.draw_piece()
        self.draw_next_piece()
        self.draw_info()
        
        if self.game_over:
            self.draw_game_over()
        
        pygame.display.flip()
    
    def handle_events(self):
        # 이벤트 처리
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False
            
            if event.type == pygame.KEYDOWN:
                if not self.game_over:
                    if event.key == pygame.K_LEFT:
                        self.move(-1, 0)
                    elif event.key == pygame.K_RIGHT:
                        self.move(1, 0)
                    elif event.key == pygame.K_DOWN:
                        self.move(0, 1)
                    elif event.key == pygame.K_UP:
                        self.rotate()
                    elif event.key == pygame.K_SPACE:
                        self.hard_drop()
                
                if event.key == pygame.K_r:
                    self.reset_game()
                elif event.key == pygame.K_ESCAPE:
                    return False
        return True
    
    def update(self):
        # 게임 업데이트
        if not self.game_over:
            # 시간 기반 낙하
            current_time = time.time()
            if current_time - self.last_drop_time > self.drop_speed:
                self.drop()
                self.last_drop_time = current_time
    
    def run(self):
        # 게임 메인 루프
        running = True
        while running:
            running = self.handle_events()
            self.update()
            self.draw()
            self.clock.tick(FPS)
        
        pygame.quit()

if __name__ == "__main__":
    game = Tetris()
    game.run()

게임 화면

 

화면 구성요소

  1. 메인 게임 영역: 10x20 그리드로 테트로미노가 내려오는 공간
  2. 다음 블록 표시: 다음에 등장할 테트로미노를 미리 보여주는 영역
  3. 게임 정보 패널: 점수, 레벨, 지운 라인 수를 표시
  4. 조작 방법: 게임 조작키 설명

코드 설명 📝

1. 게임 초기화 및 설정

def __init__(self):
    pygame.init()
    pygame.display.set_caption('파이썬 테트리스')
    
    self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
    self.clock = pygame.time.Clock()
    self.font = pygame.font.Font(None, 36)
    self.small_font = pygame.font.Font(None, 24)
    
    self.reset_game()

Pygame을 초기화하고 게임 창을 생성합니다. 화면 크기, 프레임 속도, 폰트 등을 설정하고 reset_game() 메서드를 호출해 게임 상태를 초기화합니다.

2. 테트로미노 정의 및 처리

def new_piece(self):
    # 새로운 테트로미노 생성
    shape = random.choice(list(TETROMINOS.keys()))
    return {
        'shape': shape,
        'rotation': 0,
        'x': GRID_WIDTH // 2 - len(TETROMINOS[shape][0][0]) // 2,
        'y': 0
    }

7가지 테트로미노(I, J, L, O, S, T, Z)를 정의하고 무작위로 선택하여 게임 화면 상단 중앙에 생성합니다. 각 테트로미노는 모양, 회전 상태, 좌표 정보를 갖습니다.

3. 충돌 감지 및 위치 검증

def valid_position(self, piece=None, x_offset=0, y_offset=0):
    # 위치가 유효한지 확인
    if piece is None:
        piece = self.current_piece
    
    piece_grid = self.get_piece_grid(piece)
    for y, row in enumerate(piece_grid):
        for x, cell in enumerate(row):
            if cell:
                board_x = piece['x'] + x + x_offset
                board_y = piece['y'] + y + y_offset
                
                # 경계 확인
                if (board_x < 0 or board_x >= GRID_WIDTH or 
                    board_y >= GRID_HEIGHT or 
                    (board_y >= 0 and self.board[board_y][board_x] != COLORS['BLACK'])):
                    return False
    return True

테트로미노가 유효한 위치에 있는지 확인하는 메서드입니다. 게임 영역을 벗어나거나 이미 배치된 블록과 충돌하는지 검사합니다. 이 메서드는 블록 이동, 회전, 낙하 모두에 사용됩니다.

4. 라인 제거 및 점수 계산

def clear_lines(self):
    # 완성된 라인 제거 및 점수 업데이트
    lines_to_clear = []
    for y in range(GRID_HEIGHT):
        if all(color != COLORS['BLACK'] for color in self.board[y]):
            lines_to_clear.append(y)
    
    # 라인 제거
    for line in lines_to_clear:
        # 해당 라인 위의 모든 라인을 한 칸씩 아래로 이동
        for y in range(line, 0, -1):
            self.board[y] = self.board[y-1][:]
        # 맨 위 라인은 빈 라인으로 채우기
        self.board[0] = [COLORS['BLACK'] for _ in range(GRID_WIDTH)]
    
    # 점수 및 레벨 업데이트
    if lines_to_clear:
        lines_count = len(lines_to_clear)
        self.lines_cleared += lines_count
        
        # 점수 계산 (라인 수에 따라 다른 점수)
        line_scores = {1: 100, 2: 300, 3: 500, 4: 800}
        self.score += line_scores.get(lines_count, 0) * self.level
        
        # 10라인마다 레벨 업
        self.level = self.lines_cleared // 10 + 1
        
        # 레벨에 따라 낙하 속도 조정
        self.drop_speed = max(0.05, 0.5 - (self.level - 1) * 0.05)

가로줄이 완전히 채워졌는지 확인하고, 채워진 줄을 제거한 후 위의 블록들을 아래로 이동시킵니다. 한 번에 제거된 라인 수에 따라 다양한 점수를 부여하며, 10라인마다 레벨이 상승하고 블록 낙하 속도가 빨라집니다.

5. 게임 메인 루프

def run(self):
    # 게임 메인 루프
    running = True
    while running:
        running = self.handle_events()
        self.update()
        self.draw()
        self.clock.tick(FPS)
    
    pygame.quit()

게임의 메인 루프는 이벤트 처리, 게임 상태 업데이트, 화면 그리기의 세 가지 주요 작업을 반복합니다. clock.tick(FPS)를 통해 프레임 속도를 일정하게 유지합니다.

게임 실행 방법 🚀

1. Python과 Pygame이 설치되어 있어야 합니다.

pip install pygame

2. 코드를 파일(예: 2048_game.py)에 저장합니다.

3. 터미널이나 명령 프롬프트에서 다음 명령어로 게임을 실행합니다:

python tetris.py

4. 게임 조작법:

  • ←, →: 좌우 이동
  • : 블록 회전
  • : 소프트 드롭 (한 칸 내리기)
  • 스페이스바: 하드 드롭 (바닥까지 내리기)
  • R: 게임 재시작
  • ESC: 게임 종료

게임에서 사용된 프로그래밍 개념 📚

테트리스 게임을 구현하면서 다양한 프로그래밍 개념을 활용했습니다. 이러한 개념들을 이해하면 여러분도 비슷한 게임을 만들 수 있을 것입니다.

 

1. 2차원 배열 (2D Arrays)

2. 난수 생성 (Random Number Generation)

3. 행렬 조작 (Matrix Manipulation)

4. 이벤트 처리 (Event Handling)

5. 상태 관리 (State Management)

6. 객체 지향 프로그래밍 (Object-Oriented Programming)

7. 애니메이션과 렌더링 (Animation and Rendering)

 

다음 포스팅 예고 🔮

다음 포스팅에서는 '파이썬으로 만드는 플래피 버드(Flappy Bird)'에 대해 알아보겠습니다. 모바일 게임계를 강타했던 이 단순하면서도 중독성 강한 게임을 Pygame으로 재현해볼 예정입니다. 물리 시스템을 활용한 중력 효과, 파이프 장애물 생성, 충돌 감지 등의 핵심 메커니즘을 구현하면서 게임 개발의 흥미로운 측면을 살펴보겠습니다. 또한 난이도 조절 시스템과 점수 트래킹 기능도 추가하여 완성도 높은 게임을 만들어볼 계획입니다.

여러분의 의견과 질문은 언제나 환영합니다. 행복한 코딩 되세요! 🐍🎮✨

반응형