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

파이썬 게임 만들기 - 블랙잭 🃏♠️

Family in August 2025. 3. 11. 21:09
반응형

파이썬으로 시작하는 게임 개발의 세계 🐍🎮

안녕하세요! Python Game Dev 블로그의 열두 번째 포스팅입니다. 지난 포스팅에서는 전략 시뮬레이션 게임을, 그 전에는 플랫포머 게임을 만들어 보았는데요. 오늘은 약속드린 대로 '카드 게임(Card Game)'을 파이썬으로 구현해보겠습니다.

오늘의 게임: 파이썬으로 만드는 카드 게임 - 블랙잭 🃏♠️

카드 게임은 전 세계적으로 사랑받는 게임 장르 중 하나입니다. 오늘은 클래식한 카드 게임인 '블랙잭'을 파이썬으로 구현해보겠습니다. 텍스트 기반 버전과 Pygame을 활용한 그래픽 버전, 두 가지로 접근하여 여러분의 실력과 취향에 맞게 선택할 수 있도록 준비했습니다.

게임의 규칙 📜

블랙잭(21점)의 기본 규칙은 다음과 같습니다:

  1. 플레이어와 딜러는 각각 카드를 받습니다.
  2. 카드의 숫자 합이 21에 가까우면서 21을 넘지 않는 사람이 이깁니다.
  3. 숫자 카드(2-10)는 카드에 표시된 숫자대로 점수가 계산됩니다.
  4. 페이스 카드(J, Q, K)는 모두 10점입니다.
  5. 에이스(A)는 1점 또는 11점으로 계산할 수 있으며, 유리한 쪽으로 적용됩니다.
  6. 처음에 딜러는 카드 한 장을 공개합니다.
  7. 플레이어는 카드를 더 받거나(히트) 멈출(스탠드) 수 있습니다.
  8. 플레이어가 21점을 초과하면 즉시 패배합니다(버스트).
  9. 플레이어가 스탠드하면 딜러는 17점 이상이 될 때까지 카드를 계속 뽑아야 합니다.

전체 코드 - 텍스트 기반 💻

import random
import time

class Card:
    def __init__(self, suit, value):
        self.suit = suit
        self.value = value
    
    def __str__(self):
        return f"{self.value} of {self.suit}"
    
    def get_numeric_value(self):
        if self.value in ['J', 'Q', 'K']:
            return 10
        elif self.value == 'A':
            return 11
        else:
            return int(self.value)

class Deck:
    def __init__(self):
        self.cards = []
        self.build()
    
    def build(self):
        suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades']
        values = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
        self.cards = [Card(suit, value) for suit in suits for value in values]
    
    def shuffle(self):
        random.shuffle(self.cards)
    
    def deal(self):
        if len(self.cards) > 0:
            return self.cards.pop()
        else:
            return None

class Hand:
    def __init__(self):
        self.cards = []
        self.value = 0
    
    def add_card(self, card):
        self.cards.append(card)
    
    def calculate_value(self):
        self.value = 0
        has_ace = False
        
        for card in self.cards:
            self.value += card.get_numeric_value()
            if card.value == 'A':
                has_ace = True
        
        # 에이스를 11로 계산했을 때 합이 21을 초과하면 에이스를 1로 계산
        if has_ace and self.value > 21:
            self.value -= 10
        
        return self.value
    
    def display(self, show_all_dealer_cards=True):
        for i, card in enumerate(self.cards):
            if i == 0 and not show_all_dealer_cards:
                print("Hidden Card")
            else:
                print(card)
        
        if show_all_dealer_cards:
            print(f"Total Value: {self.calculate_value()}")
        print()

class Blackjack:
    def __init__(self):
        self.deck = Deck()
        self.deck.shuffle()
        self.player_hand = Hand()
        self.dealer_hand = Hand()
        self.money = 1000
        self.bet = 0
    
    def deal_initial_cards(self):
        for _ in range(2):
            self.player_hand.add_card(self.deck.deal())
            self.dealer_hand.add_card(self.deck.deal())
    
    def place_bet(self):
        while True:
            try:
                print(f"현재 보유금: ${self.money}")
                self.bet = int(input("얼마를 베팅하시겠습니까? $"))
                if 0 < self.bet <= self.money:
                    break
                else:
                    print("유효한 베팅 금액을 입력해주세요.")
            except ValueError:
                print("숫자를 입력해주세요.")
    
    def player_turn(self):
        while True:
            print("\n--- 플레이어의 카드 ---")
            self.player_hand.display()
            
            # 블랙잭 체크
            if self.player_hand.calculate_value() == 21:
                print("블랙잭! 플레이어 승리!")
                return True
            
            # 버스트 체크
            elif self.player_hand.calculate_value() > 21:
                print("버스트! 플레이어 패배!")
                return False
            
            # 히트 또는 스탠드 선택
            choice = input("히트(h) 또는 스탠드(s)? ").lower()
            
            if choice == 'h':
                self.player_hand.add_card(self.deck.deal())
            elif choice == 's':
                break
            else:
                print("'h' 또는 's'를 입력해주세요.")
        
        return None
    
    def dealer_turn(self):
        print("\n--- 딜러의 카드 ---")
        self.dealer_hand.display()
        
        # 딜러는 17 이상이 될 때까지 카드를 계속 뽑음
        while self.dealer_hand.calculate_value() < 17:
            print("딜러가 카드를 뽑습니다...")
            time.sleep(1)
            self.dealer_hand.add_card(self.deck.deal())
            self.dealer_hand.display()
            
            # 버스트 체크
            if self.dealer_hand.calculate_value() > 21:
                print("딜러 버스트! 플레이어 승리!")
                return True
        
        return None
    
    def determine_winner(self):
        player_value = self.player_hand.calculate_value()
        dealer_value = self.dealer_hand.calculate_value()
        
        print(f"\n플레이어: {player_value}")
        print(f"딜러: {dealer_value}")
        
        if player_value > dealer_value:
            print("플레이어 승리!")
            return True
        elif dealer_value > player_value:
            print("딜러 승리!")
            return False
        else:
            print("무승부!")
            return None
    
    def update_money(self, result):
        if result is True:  # 플레이어 승리
            self.money += self.bet
        elif result is False:  # 딜러 승리
            self.money -= self.bet
        # 무승부는 돈 변동 없음
    
    def display_game_state(self):
        print("\n" + "="*50)
        print("\n--- 딜러의 카드 ---")
        self.dealer_hand.display(show_all_dealer_cards=False)
        
        print("\n--- 플레이어의 카드 ---")
        self.player_hand.display()
        print("="*50 + "\n")
    
    def play(self):
        print("블랙잭 게임에 오신 것을 환영합니다!")
        
        while self.money > 0:
            # 게임 초기화
            self.player_hand = Hand()
            self.dealer_hand = Hand()
            
            # 베팅
            self.place_bet()
            
            # 초기 카드 배분
            self.deal_initial_cards()
            
            # 게임 상태 표시
            self.display_game_state()
            
            # 플레이어 턴
            result = self.player_turn()
            
            # 플레이어가 버스트하지 않았다면 딜러 턴
            if result is None:
                result = self.dealer_turn()
            
            # 누구도 버스트하지 않았다면 승자 결정
            if result is None:
                result = self.determine_winner()
            
            # 금액 업데이트
            self.update_money(result)
            print(f"현재 보유금: ${self.money}")
            
            # 게임 계속 여부
            if self.money <= 0:
                print("모든 돈을 잃었습니다! 게임 오버!")
                break
            
            play_again = input("\n다시 플레이하시겠습니까? (y/n) ").lower()
            if play_again != 'y':
                print("게임을 종료합니다. 이용해주셔서 감사합니다!")
                break

if __name__ == "__main__":
    game = Blackjack()
    game.play()

게임 화면

 

전체 코드 - 그래픽 기반 💻

import pygame
import random
import os

# 게임 초기화
pygame.init()

# 화면 설정
WIDTH, HEIGHT = 1000, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("파이썬 블랙잭")

# 색상 정의
GREEN = (34, 139, 34)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)

# 폰트 설정
font = pygame.font.SysFont('malgungothic', 32)
small_font = pygame.font.SysFont('malgungothic', 24)

# 카드 클래스
class Card:
    def __init__(self, suit, value):
        self.suit = suit
        self.value = value
        
        # 카드 이미지 파일명 형식: {value}_of_{suit}.png
        # 이미지 파일이 있는 경우 로드, 없으면 텍스트로 표시
        self.image_file = f"{value.lower()}_of_{suit.lower()}.png"
        self.image = self.load_image()
    
    def load_image(self):
        try:
            # 이미지 파일이 있는 경로 (이미지 폴더 경로로 수정 필요)
            card_image_path = os.path.join("card_images", self.image_file)
            image = pygame.image.load(card_image_path)
            return pygame.transform.scale(image, (80, 120))
        except:
            # 이미지 파일이 없으면 빈 카드 생성
            image = pygame.Surface((80, 120))
            image.fill(WHITE)
            pygame.draw.rect(image, BLACK, (0, 0, 80, 120), 2)
            
            # 카드 값과 모양 텍스트 렌더링
            text = small_font.render(f"{self.value}", True, BLACK)
            image.blit(text, (10, 10))
            
            # 모양에 따라 색상 변경
            if self.suit in ["Hearts", "Diamonds"]:
                color = RED
            else:
                color = BLACK
                
            suit_symbol = {"Hearts": "♥", "Diamonds": "♦", "Clubs": "♣", "Spades": "♠"}
            suit_text = font.render(suit_symbol.get(self.suit, "?"), True, color)
            image.blit(suit_text, (30, 50))
            
            return image
    
    def __str__(self):
        return f"{self.value} of {self.suit}"
    
    def get_numeric_value(self):
        if self.value in ['J', 'Q', 'K']:
            return 10
        elif self.value == 'A':
            return 11
        else:
            return int(self.value)

# 덱 클래스
class Deck:
    def __init__(self):
        self.cards = []
        self.build()
    
    def build(self):
        suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades']
        values = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A']
        self.cards = [Card(suit, value) for suit in suits for value in values]
    
    def shuffle(self):
        random.shuffle(self.cards)
    
    def deal(self):
        if len(self.cards) > 0:
            return self.cards.pop()
        else:
            return None

# 핸드(손패) 클래스
class Hand:
    def __init__(self):
        self.cards = []
        self.value = 0
    
    def add_card(self, card):
        self.cards.append(card)
    
    def calculate_value(self):
        self.value = 0
        has_ace = False
        
        for card in self.cards:
            self.value += card.get_numeric_value()
            if card.value == 'A':
                has_ace = True
        
        # 에이스를 11로 계산했을 때 합이 21을 초과하면 에이스를 1로 계산
        if has_ace and self.value > 21:
            self.value -= 10
        
        return self.value

# 블랙잭 게임 클래스
class BlackjackGame:
    def __init__(self):
        self.deck = Deck()
        self.deck.shuffle()
        self.player_hand = Hand()
        self.dealer_hand = Hand()
        self.game_over = False
        self.player_wins = False
        self.message = ""
        self.money = 1000
        self.bet = 100
        self.show_dealer_cards = False
        self.game_started = False
    
    def deal_initial_cards(self):
        self.player_hand = Hand()
        self.dealer_hand = Hand()
        self.game_over = False
        self.player_wins = False
        self.message = ""
        self.show_dealer_cards = False
        
        # 각자 2장의 카드 배분
        for _ in range(2):
            self.player_hand.add_card(self.deck.deal())
            self.dealer_hand.add_card(self.deck.deal())
        
        self.game_started = True
        
        # 플레이어 블랙잭 체크
        if self.player_hand.calculate_value() == 21:
            self.message = "블랙잭! 플레이어 승리!"
            self.player_wins = True
            self.game_over = True
            self.show_dealer_cards = True
            self.money += self.bet
    
    def hit(self):
        if not self.game_over and self.game_started:
            # 플레이어가 카드를 한 장 더 받음
            self.player_hand.add_card(self.deck.deal())
            
            # 버스트 체크
            if self.player_hand.calculate_value() > 21:
                self.message = "버스트! 플레이어 패배!"
                self.player_wins = False
                self.game_over = True
                self.show_dealer_cards = True
                self.money -= self.bet
    
    def stand(self):
        if not self.game_over and self.game_started:
            self.show_dealer_cards = True
            
            # 딜러 턴: 17 이상이 될 때까지 카드 뽑기
            while self.dealer_hand.calculate_value() < 17:
                self.dealer_hand.add_card(self.deck.deal())
            
            # 딜러 버스트 체크
            if self.dealer_hand.calculate_value() > 21:
                self.message = "딜러 버스트! 플레이어 승리!"
                self.player_wins = True
                self.game_over = True
                self.money += self.bet
            else:
                # 점수 비교
                player_value = self.player_hand.calculate_value()
                dealer_value = self.dealer_hand.calculate_value()
                
                if player_value > dealer_value:
                    self.message = "플레이어 승리!"
                    self.player_wins = True
                    self.money += self.bet
                elif dealer_value > player_value:
                    self.message = "딜러 승리!"
                    self.player_wins = False
                    self.money -= self.bet
                else:
                    self.message = "무승부!"
                
                self.game_over = True
    
    def change_bet(self, amount):
        if not self.game_started:
            new_bet = self.bet + amount
            if 10 <= new_bet <= self.money:
                self.bet = new_bet
    
    def draw(self, screen):
        # 배경 그리기
        screen.fill(GREEN)
        
        # 제목 표시
        title = font.render("Python 블랙잭", True, WHITE)
        #screen.blit(title, (WIDTH // 2 - title.get_width() // 2, 20))
        screen.blit(title, (20, HEIGHT // 2 - title.get_height() // 2))
        
        # 보유금과 베팅액 표시
        money_text = small_font.render(f"보유금: ${self.money}", True, WHITE)
        bet_text = small_font.render(f"베팅액: ${self.bet}", True, WHITE)
        screen.blit(money_text, (50, 20))
        screen.blit(bet_text, (50, 50))
        
        # 베팅 금액 조절 버튼
        if not self.game_started:
            pygame.draw.rect(screen, WHITE, (200, 45, 30, 30))
            pygame.draw.rect(screen, WHITE, (240, 45, 30, 30))
            minus = font.render("-", True, BLACK)
            plus = font.render("+", True, BLACK)
            screen.blit(minus, (210, 45))
            screen.blit(plus, (250, 45))
        
        # 게임 버튼 그리기
        if not self.game_started:
            pygame.draw.rect(screen, WHITE, (WIDTH // 2 - 100, 500, 200, 50))
            deal_text = font.render("게임 시작", True, BLACK)
            screen.blit(deal_text, (WIDTH // 2 - deal_text.get_width() // 2, 510))
        else:
            if not self.game_over:
                # 히트 버튼
                pygame.draw.rect(screen, WHITE, (WIDTH // 2 - 150, 500, 100, 50))
                hit_text = font.render("히트", True, BLACK)
                screen.blit(hit_text, (WIDTH // 2 - 150 + 50 - hit_text.get_width() // 2, 510))
                
                # 스탠드 버튼
                pygame.draw.rect(screen, WHITE, (WIDTH // 2 + 50, 500, 100, 50))
                stand_text = font.render("스탠드", True, BLACK)
                screen.blit(stand_text, (WIDTH // 2 + 50 + 50 - stand_text.get_width() // 2, 510))
            else:
                # 다시 시작 버튼
                pygame.draw.rect(screen, WHITE, (WIDTH // 2 - 100, 500, 200, 50))
                deal_text = font.render("다시 시작", True, BLACK)
                screen.blit(deal_text, (WIDTH // 2 - deal_text.get_width() // 2, 510))
        
        # 카드 그리기 - 딜러
        dealer_text = font.render("딜러", True, WHITE)
        screen.blit(dealer_text, (WIDTH // 2 - dealer_text.get_width() // 2, 100))
        
        # 딜러의 카드 표시
        for i, card in enumerate(self.dealer_hand.cards):
            if i == 0 and not self.show_dealer_cards:
                # 첫 번째 카드는 뒷면으로 표시
                card_back = pygame.Surface((80, 120))
                card_back.fill(RED)
                pygame.draw.rect(card_back, BLACK, (0, 0, 80, 120), 2)
                screen.blit(card_back, (WIDTH // 2 - 90 + i * 90, 150))
            else:
                screen.blit(card.image, (WIDTH // 2 - 90 + i * 90, 150))
        
        # 딜러의 점수 표시
        if self.show_dealer_cards:
            dealer_value = self.dealer_hand.calculate_value()
            dealer_value_text = font.render(f"점수: {dealer_value}", True, WHITE)
            #screen.blit(dealer_value_text, (WIDTH // 2 - dealer_value_text.get_width() // 2, 280))
            screen.blit(dealer_value_text, (20, 150))
        
        # 카드 그리기 - 플레이어
        player_text = font.render("플레이어", True, WHITE)
        screen.blit(player_text, (WIDTH // 2 - player_text.get_width() // 2, 320))
        
        # 플레이어의 카드 표시
        for i, card in enumerate(self.player_hand.cards):
            screen.blit(card.image, (WIDTH // 2 - 90 + i * 90, 370))
        
        # 플레이어의 점수 표시
        player_value = self.player_hand.calculate_value()
        player_value_text = font.render(f"점수: {player_value}", True, WHITE)
        #screen.blit(player_value_text, (WIDTH // 2 - player_value_text.get_width() // 2, 450))
        screen.blit(player_value_text, (20, 450))
        
        # 게임 결과 메시지 표시
        if self.message:
            msg_text = font.render(self.message, True, WHITE)
            screen.blit(msg_text, (WIDTH // 2 - msg_text.get_width() // 2, 30))

# 메인 게임 루프
def main():
    game = BlackjackGame()
    clock = pygame.time.Clock()
    running = True
    
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            
            if event.type == pygame.MOUSEBUTTONDOWN:
                mouse_pos = pygame.mouse.get_pos()
                
                # 게임 시작 전 베팅 금액 조절
                if not game.game_started:
                    # 베팅 감소 버튼
                    if 200 <= mouse_pos[0] <= 230 and 45 <= mouse_pos[1] <= 75:
                        game.change_bet(-10)
                    # 베팅 증가 버튼
                    elif 240 <= mouse_pos[0] <= 270 and 45 <= mouse_pos[1] <= 75:
                        game.change_bet(10)
                
                # 게임 시작/다시 시작 버튼
                if not game.game_started or game.game_over:
                    if WIDTH // 2 - 100 <= mouse_pos[0] <= WIDTH // 2 + 100 and 500 <= mouse_pos[1] <= 550:
                        if game.money > 0:
                            game.deal_initial_cards()
                
                # 게임 진행 중 - 히트와 스탠드 버튼
                if game.game_started and not game.game_over:
                    # 히트 버튼
                    if WIDTH // 2 - 150 <= mouse_pos[0] <= WIDTH // 2 - 50 and 500 <= mouse_pos[1] <= 550:
                        game.hit()
                    # 스탠드 버튼
                    elif WIDTH // 2 + 50 <= mouse_pos[0] <= WIDTH // 2 + 150 and 500 <= mouse_pos[1] <= 550:
                        game.stand()
        
        game.draw(screen)
        pygame.display.flip()
        clock.tick(60)
    
    pygame.quit()

if __name__ == "__main__":
    main()

 

게임 화면

 

게임 실행 방법 🚀

텍스트 기반 버전

  1. blackjack.py 파일을 만들고 첫 번째 코드를 복사합니다.
  2. 터미널에서 다음 명령어를 실행합니다: python blackjack.py
  3. 콘솔 창에서 지시에 따라 게임을 진행하세요.

그래픽 버전

  1. Pygame 패키지가 필요합니다. 설치되어 있지 않다면 다음 명령어로 설치하세요: pip install pygame
  2. blackjack_pygame.py 파일을 만들고 두 번째 코드를 복사합니다.
  3. 카드 이미지를 사용하려면 카드 이미지 세트를 다운로드하거나, 이미지 없이도 실행 가능합니다.
  4. 터미널에서 다음 명령어를 실행합니다: python blackjack_pygame.py

코드 설명 📝

공통 클래스 구조

두 버전 모두 다음 클래스를 공유합니다:

  1. Card 클래스: 카드의 모양(suit)과 값(value)을 저장하고, 숫자 값을 계산하는 메서드를 제공합니다.
  2. Deck 클래스: 52장의 카드를 생성, 섞기, 카드 뽑기 기능을 제공합니다.
  3. Hand 클래스: 플레이어나 딜러가 가진 카드를 관리하고 점수를 계산합니다.

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

  1. 클래스와 객체지향 프로그래밍:
    • Card, Deck, Hand, Game 클래스를 통해 실제 세계의 객체를 모델링
    • 캡슐화를 통한 데이터와 기능의 묶음
  2. 리스트 조작:
    • 카드 덱 생성 및 섞기
    • 손에 든 카드 관리
  3. 랜덤화:
    • random.shuffle()을 사용한 카드 덱 섞기
  4. 조건문과 루프:
    • 게임 상태 확인 및 게임 흐름 제어
    • 플레이어 입력에 따른 분기 처리
  5. 사용자 입력 처리:
    • input() 함수를 통한 사용자 명령 처리
    • 입력 유효성 검사
  6. 함수 구현:
    • 각 클래스 내 메소드를 통한 기능 모듈화
    • 코드 재사용성 향상

알고리즘 설명 🧮

  1. 카드 덱 초기화 알고리즘:
    • 4개의 슈트(스페이드, 하트, 다이아몬드, 클럽)와 13개의 랭크(A,2~10,J,Q,K)를 조합하여 52장의 카드 생성
    • Fisher-Yates 알고리즘을 기반으로 한 random.shuffle()을 사용하여 카드 무작위 배치
  2. 카드 점수 계산 알고리즘:
    • 일반 카드: 숫자 그대로 점수 계산
    • 페이스 카드(J,Q,K): 10점으로 계산
    • 에이스(A): 합이 21을 넘지 않는 선에서 11점, 그렇지 않으면 1점으로 계산
    • 에이스 처리를 위해 먼저 모든 에이스를 11점으로 계산한 후, 합이 21을 초과하면 필요한 만큼 에이스를 1점으로 조정
  3. 딜러 AI 알고리즘:
    • 단순하지만 실제 카지노 규칙을 따르는 알고리즘
    • 딜러의 카드 합이 17 미만이면 무조건 카드를 더 받음
    • 17 이상이면 더 이상 카드를 받지 않음
  4. 승패 결정 알고리즘:
    • 플레이어 또는 딜러가 버스트(21 초과)하면 즉시 패배
    • 둘 다 버스트하지 않았다면, 카드 합이 더 높은 쪽이 승리
    • 카드 합이 같으면 무승부(Push)

다음 포스팅 예고 🔮

다음 포스팅에서는 '파이썬으로 만드는 메모리 매칭 게임(Memory Matching Game)'에 대해 알아보겠습니다. Pygame 라이브러리를 활용하여 그래픽 인터페이스를 갖춘 카드 뒤집기 게임을 구현해볼 예정입니다. 게임 로직뿐만 아니라 이미지 처리, 애니메이션 효과, 사운드 추가 등 게임 개발의 다양한 측면을 배워보겠습니다.

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

  •  
반응형