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

파이썬으로 게임 만들기 - 2048 게임 🧠🎲

Family in August 2025. 3. 15. 09:44
반응형

파이썬으로 만드는 퍼즐 게임: 2048 🐍🎮🧩

안녕하세요! Python Game Dev 블로그의 열네 번째 포스팅입니다. 지난 포스팅에서는 메모리 매칭 게임을 만들어 보았는데요. 오늘은 약속드린 대로 '파이썬으로 만드는 퍼즐 게임'을 구현해보겠습니다. 특히 인기 있는 2048 게임을 Pygame을 활용해 만들어 볼 예정입니다!

오늘의 게임: 파이썬으로 만드는 2048 게임 🧠🎲

2048은 간단한 규칙을 가진 퍼즐 게임이지만 전략적 사고가 필요하며 중독성이 강합니다. 2와 2가 만나면 4가 되고, 4와 4가 만나면 8이 되는 방식으로 숫자를 합쳐 궁극적으로 2048 타일을 만드는 것이 목표입니다. Pygame 라이브러리를 활용하여 시각적으로 멋진 게임을 구현해보겠습니다.

게임의 규칙 📜

  • 4x4 그리드에서 플레이합니다.
  • 화살표 키를 사용하여 모든 타일을 상하좌우로 밀 수 있습니다.
  • 이동 후 같은 값을 가진 두 타일이 충돌하면 합쳐져 두 배 값의 타일이 됩니다.
  • 매 이동 후 그리드의 빈 셀에 무작위로 2 또는 4가 나타납니다.
  • 2048 타일을 만들면 승리합니다!
  • 더 이상 이동할 수 없으면(그리드가 가득 차고 합칠 수 있는 타일이 없으면) 게임이 종료됩니다.

전체 코드 💻

import pygame
import random
import sys

# 게임 설정
WIDTH, HEIGHT = 400, 500
GRID_SIZE = 4
TILE_SIZE = WIDTH // GRID_SIZE
PADDING = 10
TITLE_HEIGHT = 100

# 색상 정의
BACKGROUND_COLOR = (187, 173, 160)
EMPTY_TILE_COLOR = (205, 193, 180)
TILE_COLORS = {
    0: (205, 193, 180),
    2: (238, 228, 218),
    4: (237, 224, 200),
    8: (242, 177, 121),
    16: (245, 149, 99),
    32: (246, 124, 95),
    64: (246, 94, 59),
    128: (237, 207, 114),
    256: (237, 204, 97),
    512: (237, 200, 80),
    1024: (237, 197, 63),
    2048: (237, 194, 46)
}

TEXT_COLORS = {
    2: (119, 110, 101),
    4: (119, 110, 101),
    8: (249, 246, 242),
    16: (249, 246, 242),
    32: (249, 246, 242),
    64: (249, 246, 242),
    128: (249, 246, 242),
    256: (249, 246, 242),
    512: (249, 246, 242),
    1024: (249, 246, 242),
    2048: (249, 246, 242)
}

class Game2048:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption("2048 Game")
        self.font = pygame.font.SysFont('Arial', 36)
        self.small_font = pygame.font.SysFont('Arial', 24)
        self.clock = pygame.time.Clock()
        self.score = 0
        self.best_score = 0
        self.reset_game()

    def reset_game(self):
        self.grid = [[0 for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
        self.score = 0
        self.game_over = False
        self.game_won = False
        self.continue_after_win = False
        # 게임 시작 시 두 개의 타일 추가
        self.add_new_tile()
        self.add_new_tile()

    def add_new_tile(self):
        empty_cells = [(i, j) for i in range(GRID_SIZE) for j in range(GRID_SIZE) if self.grid[i][j] == 0]
        if empty_cells:
            i, j = random.choice(empty_cells)
            self.grid[i][j] = 2 if random.random() < 0.9 else 4

    def draw_tile(self, i, j, value):
        x = j * TILE_SIZE + PADDING
        y = i * TILE_SIZE + PADDING + TITLE_HEIGHT
        
        pygame.draw.rect(self.screen, TILE_COLORS.get(value, (0, 0, 0)), 
                        (x, y, TILE_SIZE - 2*PADDING, TILE_SIZE - 2*PADDING), 
                        border_radius=8)
        
        if value != 0:
            text_color = TEXT_COLORS.get(value, (255, 255, 255))
            text = self.font.render(str(value), True, text_color)
            text_rect = text.get_rect(center=(x + (TILE_SIZE - 2*PADDING) // 2, 
                                             y + (TILE_SIZE - 2*PADDING) // 2))
            self.screen.blit(text, text_rect)

    def draw_grid(self):
        self.screen.fill(BACKGROUND_COLOR)
        # 상단 타이틀 영역
        pygame.draw.rect(self.screen, (250, 248, 239), (0, 0, WIDTH, TITLE_HEIGHT))
        
        # 2048 타이틀
        title_text = self.font.render("2048", True, (119, 110, 101))
        self.screen.blit(title_text, (20, 30))
        
        # 점수 표시
        score_text = self.small_font.render(f"Score: {self.score}", True, (119, 110, 101))
        best_text = self.small_font.render(f"Best: {self.best_score}", True, (119, 110, 101))
        self.screen.blit(score_text, (WIDTH - 150, 20))
        self.screen.blit(best_text, (WIDTH - 150, 50))
        
        # 그리드 배경
        pygame.draw.rect(self.screen, BACKGROUND_COLOR, 
                         (0, TITLE_HEIGHT, WIDTH, HEIGHT - TITLE_HEIGHT), 
                         border_radius=6)
        
        # 각 타일 그리기
        for i in range(GRID_SIZE):
            for j in range(GRID_SIZE):
                self.draw_tile(i, j, self.grid[i][j])
                
        # 게임 오버 메시지
        if self.game_over:
            s = pygame.Surface((WIDTH, HEIGHT - TITLE_HEIGHT), pygame.SRCALPHA)
            s.fill((255, 255, 255, 128))
            self.screen.blit(s, (0, TITLE_HEIGHT))
            game_over_text = self.font.render("Game Over!", True, (119, 110, 101))
            restart_text = self.small_font.render("Press R to Restart", True, (119, 110, 101))
            self.screen.blit(game_over_text, (WIDTH//2 - game_over_text.get_width()//2, 
                                            HEIGHT//2 - game_over_text.get_height()//2))
            self.screen.blit(restart_text, (WIDTH//2 - restart_text.get_width()//2, 
                                          HEIGHT//2 + 40))
        
        # 승리 메시지
        if self.game_won and not self.continue_after_win:
            s = pygame.Surface((WIDTH, HEIGHT - TITLE_HEIGHT), pygame.SRCALPHA)
            s.fill((255, 255, 255, 128))
            self.screen.blit(s, (0, TITLE_HEIGHT))
            win_text = self.font.render("You Win!", True, (119, 110, 101))
            continue_text = self.small_font.render("Press C to Continue", True, (119, 110, 101))
            restart_text = self.small_font.render("Press R to Restart", True, (119, 110, 101))
            self.screen.blit(win_text, (WIDTH//2 - win_text.get_width()//2, 
                                      HEIGHT//2 - win_text.get_height()//2 - 30))
            self.screen.blit(continue_text, (WIDTH//2 - continue_text.get_width()//2, 
                                           HEIGHT//2 + 20))
            self.screen.blit(restart_text, (WIDTH//2 - restart_text.get_width()//2, 
                                          HEIGHT//2 + 50))

    def slide_row(self, row):
        # 0을 제거하고 값을 왼쪽으로 밀기
        new_row = [value for value in row if value != 0]
        # 값이 같은 인접한 타일 합치기
        i = 0
        while i < len(new_row) - 1:
            if new_row[i] == new_row[i + 1]:
                new_row[i] *= 2
                self.score += new_row[i]  # 점수 추가
                new_row.pop(i + 1)
                if new_row[i] == 2048 and not self.game_won:
                    self.game_won = True
            i += 1
        # 빈 공간 채우기
        while len(new_row) < GRID_SIZE:
            new_row.append(0)
        return new_row

    def move_left(self):
        moved = False
        for i in range(GRID_SIZE):
            original_row = self.grid[i].copy()
            self.grid[i] = self.slide_row(self.grid[i])
            if original_row != self.grid[i]:
                moved = True
        return moved

    def move_right(self):
        moved = False
        for i in range(GRID_SIZE):
            original_row = self.grid[i].copy()
            self.grid[i] = self.slide_row(self.grid[i][::-1])[::-1]
            if original_row != self.grid[i]:
                moved = True
        return moved

    def move_up(self):
        moved = False
        for j in range(GRID_SIZE):
            original_col = [self.grid[i][j] for i in range(GRID_SIZE)]
            new_col = self.slide_row([self.grid[i][j] for i in range(GRID_SIZE)])
            for i in range(GRID_SIZE):
                if self.grid[i][j] != new_col[i]:
                    moved = True
                self.grid[i][j] = new_col[i]
        return moved

    def move_down(self):
        moved = False
        for j in range(GRID_SIZE):
            original_col = [self.grid[i][j] for i in range(GRID_SIZE)]
            new_col = self.slide_row([self.grid[i][j] for i in range(GRID_SIZE)][::-1])[::-1]
            for i in range(GRID_SIZE):
                if self.grid[i][j] != new_col[i]:
                    moved = True
                self.grid[i][j] = new_col[i]
        return moved

    def is_game_over(self):
        # 빈 셀이 있는지 확인
        for i in range(GRID_SIZE):
            for j in range(GRID_SIZE):
                if self.grid[i][j] == 0:
                    return False
        
        # 인접한 셀에 같은 값이 있는지 확인
        for i in range(GRID_SIZE):
            for j in range(GRID_SIZE - 1):
                if self.grid[i][j] == self.grid[i][j + 1]:
                    return False
                
        for j in range(GRID_SIZE):
            for i in range(GRID_SIZE - 1):
                if self.grid[i][j] == self.grid[i + 1][j]:
                    return False
        
        return True  # 게임 오버

    def run(self):
        running = True
        while running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    running = False
                
                if event.type == pygame.KEYDOWN:
                    if not self.game_over and (not self.game_won or self.continue_after_win):
                        moved = False
                        if event.key == pygame.K_LEFT:
                            moved = self.move_left()
                        elif event.key == pygame.K_RIGHT:
                            moved = self.move_right()
                        elif event.key == pygame.K_UP:
                            moved = self.move_up()
                        elif event.key == pygame.K_DOWN:
                            moved = self.move_down()
                        
                        if moved:
                            self.add_new_tile()
                            if self.score > self.best_score:
                                self.best_score = self.score
                            if self.is_game_over():
                                self.game_over = True
                    
                    if event.key == pygame.K_r:
                        self.reset_game()
                    
                    if self.game_won and not self.continue_after_win and event.key == pygame.K_c:
                        self.continue_after_win = True
            
            self.draw_grid()
            pygame.display.flip()
            self.clock.tick(60)

        pygame.quit()
        sys.exit()

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

 

 

게임 화면

 

게임 화면 📸

게임이 실행되면 다음과 같은 화면을 볼 수 있습니다:

  1. 상단에는 게임 제목(2048)과 현재 점수 및 최고 점수가 표시됩니다.
  2. 4x4 그리드에는 숫자 타일이 표시되며, 각 숫자에 따라 다른 색상으로 구분됩니다.
  3. 타일을 합쳐 숫자가 커질수록 색상이 변화합니다.
  4. 2048 타일을 만들면 승리 메시지가 나타납니다.
  5. 그리드가 가득 차고 더 이상 이동할 수 없으면 게임 오버 메시지가 표시됩니다.

코드 설명 📝

게임 초기화 및 설정

def __init__(self):
    pygame.init()
    self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("2048 Game")
    self.font = pygame.font.SysFont('Arial', 36)
    self.small_font = pygame.font.SysFont('Arial', 24)
    self.clock = pygame.time.Clock()
    self.score = 0
    self.best_score = 0
    self.reset_game()

이 부분에서는 Pygame을 초기화하고 화면, 폰트, 시계 등을 설정합니다. 또한 점수와 최고 점수를 저장할 변수를 선언하고 게임을 초기화합니다.

그리드 관리

def reset_game(self):
    self.grid = [[0 for _ in range(GRID_SIZE)] for _ in range(GRID_SIZE)]
    self.score = 0
    self.game_over = False
    self.game_won = False
    self.continue_after_win = False
    # 게임 시작 시 두 개의 타일 추가
    self.add_new_tile()
    self.add_new_tile()

reset_game 메서드는 게임 상태를 초기화합니다. 빈 그리드를 생성하고 점수를 0으로 설정한 다음, 두 개의 초기 타일을 추가합니다.

타일 이동 로직

def slide_row(self, row):
    # 0을 제거하고 값을 왼쪽으로 밀기
    new_row = [value for value in row if value != 0]
    # 값이 같은 인접한 타일 합치기
    i = 0
    while i < len(new_row) - 1:
        if new_row[i] == new_row[i + 1]:
            new_row[i] *= 2
            self.score += new_row[i]  # 점수 추가
            new_row.pop(i + 1)
            if new_row[i] == 2048 and not self.game_won:
                self.game_won = True
        i += 1
    # 빈 공간 채우기
    while len(new_row) < GRID_SIZE:
        new_row.append(0)
    return new_row

이 메서드는 한 행이나 열을 이동시키는 핵심 로직입니다:

  1. 먼저 0(빈 셀)을 제거하고 모든 값을 한쪽으로 밉니다.
  2. 인접한 같은 값의 타일을 합칩니다.
  3. 이 과정에서 점수가 업데이트되고, 2048 타일이 생성되면 승리 조건을 설정합니다.
  4. 마지막으로 빈 공간을 0으로 채웁니다.

게임 상태 확인

def is_game_over(self):
    # 빈 셀이 있는지 확인
    for i in range(GRID_SIZE):
        for j in range(GRID_SIZE):
            if self.grid[i][j] == 0:
                return False
    
    # 인접한 셀에 같은 값이 있는지 확인
    for i in range(GRID_SIZE):
        for j in range(GRID_SIZE - 1):
            if self.grid[i][j] == self.grid[i][j + 1]:
                return False
            
    for j in range(GRID_SIZE):
        for i in range(GRID_SIZE - 1):
            if self.grid[i][j] == self.grid[i + 1][j]:
                return False
    
    return True  # 게임 오버

이 메서드는 게임이 종료되었는지 확인합니다:

  1. 빈 셀이 있는지 확인합니다. 빈 셀이 있으면 게임을 계속할 수 있습니다.
  2. 인접한 셀에 같은 값이 있는지 확인합니다. 같은 값이 있으면 합칠 수 있으므로 게임을 계속할 수 있습니다.
  3. 위 두 조건이 모두 거짓이면 더 이상 움직일 수 없으므로 게임 오버입니다.

게임 실행 방법 🚀

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

pip install pygame

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

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

python 2048_game.py

4. 게임 조작법:

  • 화살표 키로 타일을 상하좌우로 이동합니다.
  • 게임 오버나 승리 후에는 'R' 키를 눌러 재시작할 수 있습니다.
  • 2048을 달성한 후에는 'C' 키를 눌러 계속 게임을 진행할 수 있습니다.

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

  1. 2차원 배열(리스트): 게임 그리드를 표현하기 위해 2차원 리스트를 사용했습니다.
  2. 조건문과 반복문: 게임 상태를 확인하고 타일을 이동시키기 위해 다양한 조건문과 반복문을 활용했습니다.
  3. 이벤트 처리: Pygame의 이벤트 시스템을 사용하여 키보드 입력을 처리했습니다.
  4. 그래픽 UI: Pygame을 사용하여 시각적 요소(타일, 텍스트, 색상 등)를 구현했습니다.
  5. 알고리즘: 타일 이동 및 병합 로직, 게임 오버 감지 등 여러 알고리즘을 구현했습니다.

알고리즘 설명 🧮

타일 이동 알고리즘

2048 게임의 핵심은 타일 이동 알고리즘입니다. 이 코드에서는 다음과 같은 단계로 구현되었습니다:

  1. 행/열 추출: 이동할 방향에 따라 행 또는 열을 추출합니다.
  2. 공백 제거: 0(빈 셀)을 제거하여 모든 타일을 한쪽으로 모읍니다.
  3. 타일 병합: 인접한 동일한 값의 타일을 병합하고 점수를 업데이트합니다.
  4. 공백 채우기: 남은 공간을 0으로 채웁니다.
  5. 그리드 업데이트: 변경된 행/열을 그리드에 다시 넣습니다.

이 알고리즘의 시간 복잡도는 O(n²)입니다. 여기서 n은 그리드의 크기(이 경우 4)입니다.

게임 오버 감지 알고리즘

게임 오버 감지는 두 가지 조건을 확인합니다:

  1. 그리드에 빈 셀이 없음
  2. 인접한 셀 중 동일한 값을 가진 셀이 없음

두 조건이 모두 충족되면 더 이상 움직일 수 없으므로 게임이 종료됩니다.

다음 포스팅 예고 🔮

다음 포스팅에서는 '파이썬으로 만드는 테트리스(Tetris)'에 대해 알아보겠습니다. 고전 게임의 대표주자인 테트리스를 구현하면서 블록 회전, 충돌 감지, 라인 제거 등의 핵심 기능을 만들어볼 예정입니다. 더 복잡한 게임 로직과 함께 점수 시스템, 레벨 시스템도 추가해 볼 계획입니다.

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

반응형