[에러 해결] [Python] 텍사스 홀덤 동적 플레이어 역할 관리 문제 원인과 깔끔한 해결 방법

게임 개발에서 플레이어의 역할은 게임의 흐름을 결정하는 핵심 요소입니다. 특히 텍사스 홀덤과 같은 카드 게임에서는 딜러, 스몰 블라인드, 빅 블라인드와 같은 역할이 매 핸드마다 순환하며 게임 규칙과 턴 순서에 직접적인 영향을 미치죠. 하지만 이러한 동적인 역할을 깔끔하고 유연하게 관리하는 것은 생각보다 까다로울 수 있습니다. 오늘은 텍사스 홀덤 게임에서 동적으로 변하는 플레이어 역할을 효과적으로 관리하는 방법에 대해 알아보겠습니다. 개발자가 겪는 일반적인 문제점을 분석하고, 객체 지향적인 해결책을 제시하여 유지보수성과 확장성이 뛰어난 코드를 작성하는 데 도움을 드릴 것입니다.

1. 에러 발생 상황

한 개발자가 텍사스 홀덤 게임의 플레이어 역할을 구현하는 과정에서 난관에 봉착했습니다. 현재 빅 블라인드, 스몰 블라인드, 딜러 역할을 정의해 두었지만, 이 역할들이 매 핸드마다 순환하는 로직을 어떻게 깔끔하게 구현해야 할지 고민 중입니다. 특히 딜러는 단순히 카드를 나누는 주체가 아니라, 포지션 역할을 나타내며 턴 순서를 결정하는 중요한 위치입니다.

현재 개발자는 역할을 관리하기 위해 다음과 같은 정적 딕셔너리를 사용하고 있지만, 이는 단순한 정의에 불과하고 실제 역할을 동적으로 할당하고 순환시키는 로직은 부족합니다. 또한, 향후 2인 게임에서는 한 플레이어가 여러 역할을 동시에 맡을 수 있는 상황까지 고려해야 하므로, 현재의 접근 방식으로는 유연성이 떨어진다고 판단하고 있습니다.

PLAYER_ROLES = {
    "big_blind": "",
    "small_blind": "",
    "dealer": ""
}

class Table():
    def __init__(self, players, player_limit, player_roles):
        # ... (생략) ...
        self.players = players
        self.player_limit = player_limit
        self.player_roles = player_roles # 역할을 초기화하지만, 관리 로직은 부재

2. 명확한 발생 원인

위와 같은 상황이 발생하는 주된 원인은 다음과 같습니다.

  1. 역할 관리 로직의 부재: PLAYER_ROLES 딕셔너리는 단순히 역할의 이름을 정의하고 초기화하는 데 사용될 뿐, 누가 어떤 역할을 맡았는지 “현재 상태”를 추적하거나 다음 핸드에서 역할을 “순환”시키는 구체적인 로직을 포함하고 있지 않습니다.
  2. 정적인 역할 정의: 텍사스 홀덤의 딜러, 스몰 블라인드, 빅 블라인드는 매 핸드마다 시계 방향으로 순환하는 동적인 특성을 가집니다. 이를 위한 명확한 상태 전이(state transition) 메커니즘이 코드에 구현되어 있지 않습니다.
  3. 유연성 및 확장성 부족: 현재 3인 플레이에 맞춰져 있거나 최소 플레이어 수 검증 로직에 오류가 있어, 2인 플레이와 같이 플레이어 수가 변동될 때 역할 할당 규칙(예: 딜러가 스몰 블라인드를 겸임)을 적용하기 어렵습니다. 이는 게임의 다양한 변형에 대한 확장성을 저해합니다.
  4. 책임 분리 원칙 위반: 역할을 딕셔너리에 단순히 저장하는 방식은 역할을 맡은 플레이어의 행동(예: 강제 베팅)과 역할을 누가 맡았는지에 대한 상태 관리가 한곳에서 이루어지지 않아, 코드가 복잡해지고 유지보수가 어려워질 수 있습니다. Table 클래스가 플레이어 목록과 역할 관리에 대한 책임을 모두 가지면서도, 그 책임을 명확히 수행하지 못하고 있습니다.

3. 해결 방법 및 코드 예시

이 문제를 해결하기 위한 가장 깔끔하고 객체 지향적인 방법은 Table 클래스에서 플레이어 목록과 함께 역할의 현재 상태(누가 딜러이고, 스몰/빅 블라인드인지)를 관리하고, 매 핸드 종료 시 역할을 다음 플레이어로 순환시키는 메서드를 구현하는 것입니다. 이 방식은 역할 관리의 책임을 명확히 하고, 유연한 확장을 가능하게 합니다.

해결 전략

  1. `Table` 클래스에서 역할 상태 관리: Table 클래스가 게임에 참여하는 Player 객체들의 리스트를 가지고, 현재 딜러, 스몰 블라인드, 빅 블라인드 역할을 맡은 플레이어의 인덱스를 추적합니다.
  2. 초기 역할 할당 메서드 구현: 게임 시작 시 플레이어 수에 따라 초기 역할을 할당하는 내부 메서드를 Table 클래스에 추가합니다.
  3. 역할 순환 메서드 구현: 매 핸드가 끝날 때마다 역할을 다음 플레이어에게 순환시키는 rotate_roles() 메서드를 구현합니다. 이때 2인 게임과 3인 이상 게임의 규칙 차이를 고려합니다.
  4. 역할 플레이어 조회 메서드: 특정 역할을 맡은 플레이어 객체를 반환하는 헬퍼 메서드를 추가하여 외부에서 쉽게 현재 역할자를 파악할 수 있도록 합니다.

코드 예시

import random

# 상수 정의
SUITS = ("Clubs", "Diamonds", "Hearts", "Spades")
RANKS = ("2","3","4","5","6","7","8","9","10","J","Q","K","A")
PLAYER_LIMIT = 12 # 최대 플레이어 수

# Card 클래스 (변동 없음)
class Card:
    __slots__ = ("rank", "suit")
    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit
    def __repr__(self):
        return f"{self.rank} of {self.suit}"

# Deck 클래스 (간결한 __repr__ 수정)
class Deck:
    def __init__(self, ranks, suits):
        self.suits = suits
        self.ranks = ranks
        self.deck = [Card(rank, suit) for rank in ranks for suit in suits]
    def shuffle(self):
        random.shuffle(self.deck) # 반환 없이 리스트를 제자리에서 섞음
    def draw(self, num):
        if num > len(self.deck):
            raise ValueError(f"Tried to draw {num}, only {len(self.deck)} remain")
        cards = self.deck[-num:]
        del self.deck[-num:]
        return cards
    def __repr__(self):
        return f"Deck with {len(self.deck)} cards" # 남은 카드 수 표시

# Player 클래스 (변동 없음)
class Player:
    def __init__(self, name, balance):
        self.name = name
        self.balance = balance
        self.hand = []

# Table 클래스: 플레이어 역할 관리 로직 추가
class Table:
    def __init__(self, players, player_limit=PLAYER_LIMIT):
        # 플레이어 수 유효성 검사 (텍사스 홀덤은 최소 2인 이상)
        if not (2 <= len(players) <= player_limit):
            raise ValueError(f"Cannot seat {len(players)} players. Minimum is 2, Maximum is {player_limit}")
        
        self.players = list(players) # 플레이어 목록 (mutable list로 복사)
        self.player_limit = player_limit
        self.num_players = len(self.players)

        # 역할 인덱스 초기화
        self.dealer_index = 0
        self.small_blind_index = 0
        self.big_blind_index = 0

        # 첫 핸드의 역할 할당
        self._assign_initial_roles()

    def _assign_initial_roles(self):
        """
        첫 핸드 시작 시 역할을 할당합니다.
        텍사스 홀덤 규칙에 따라 2인 게임과 3인 이상 게임의 역할 할당 방식이 다릅니다.
        """
        if self.num_players == 2:
            # 2인 게임 (Heads-Up): 딜러가 스몰 블라인드를 겸하고 먼저 행동합니다.
            # 딜러(SB) -> BB -> 딜러(SB)
            self.dealer_index = 0 # 편의상 첫 번째 플레이어를 딜러로 시작
            self.small_blind_index = self.dealer_index
            self.big_blind_index = (self.dealer_index + 1) % self.num_players
        else: # 3인 이상 게임
            # 딜러 -> 스몰 블라인드 -> 빅 블라인드
            self.dealer_index = 0 # 편의상 첫 번째 플레이어를 딜러로 시작
            self.small_blind_index = (self.dealer_index + 1) % self.num_players
            self.big_blind_index = (self.small_blind_index + 1) % self.num_players
        self._print_current_roles()

    def rotate_roles(self):
        """
        다음 핸드를 위해 플레이어 역할을 순환시킵니다.
        딜러 버튼은 항상 시계 방향으로 이동합니다.
        """
        # 딜러 버튼 이동
        self.dealer_index = (self.dealer_index + 1) % self.num_players

        if self.num_players == 2:
            # 2인 게임 규칙에 따라 스몰 블라인드와 빅 블라인드 인덱스 조정
            self.small_blind_index = self.dealer_index
            self.big_blind_index = (self.dealer_index + 1) % self.num_players
        else:
            # 3인 이상 게임 규칙에 따라 스몰 블라인드와 빅 블라인드 인덱스 조정
            self.small_blind_index = (self.dealer_index + 1) % self.num_players
            self.big_blind_index = (self.small_blind_index + 1) % self.num_players
        self._print_current_roles()

    def get_role_player(self, role: str):
        """
        주어진 역할(예: 'dealer', 'small_blind', 'big_blind')을 맡은 Player 객체를 반환합니다.
        """
        if role == "dealer":
            return self.players[self.dealer_index]
        elif role == "small_blind":
            return self.players[self.small_blind_index]
        elif role == "big_blind":
            return self.players[self.big_blind_index]
        else:
            raise ValueError(f"Unknown role: {role}")

    def _print_current_roles(self):
        """현재 역할 할당 상태를 출력 (디버깅 및 시연용)"""
        print("--- 현재 역할 할당 ---")
        print(f"딜러 (Dealer): {self.get_role_player('dealer').name}")
        print(f"스몰 블라인드 (Small Blind): {self.get_role_player('small_blind').name}")
        print(f"빅 블라인드 (Big Blind): {self.get_role_player('big_blind').name}")
        print("---------------------\n")


# 예제 사용
def main():
    player1 = Player("앨리스", 1000)
    player2 = Player("밥", 1000)
    player3 = Player("찰리", 1000)
    player4 = Player("데이빗", 1000)

    print("--- 3인 플레이 게임 시나리오 ---")
    table_3_players = Table([player1, player2, player3]) # 초기 역할 할당
    print("다음 핸드 진행...")
    table_3_players.rotate_roles() # 역할 순환
    print("다음 핸드 진행...")
    table_3_players.rotate_roles() # 역할 순환

    print("\n" + "="*30 + "\n")

    print("--- 2인 플레이 게임 시나리오 (Heads-Up) ---")
    table_2_players = Table([player1, player2]) # 초기 역할 할당
    print("다음 핸드 진행...")
    table_2_players.rotate_roles() # 역할 순환
    print("다음 핸드 진행...")
    table_2_players.rotate_roles() # 역할 순환

if __name__ == "__main__":
    main()

4. 향후 예방을 위한 팁

동적인 플레이어 역할 관리를 포함하여 게임 로직을 설계할 때, 다음과 같은 원칙들을 따르면 더 견고하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.

  1. 책임 분리 (Separation of Concerns): 각 클래스는 하나의 명확한 책임을 가지도록 설계합니다. 위 예시에서는 Table 클래스가 플레이어 목록과 역할 관리에 대한 모든 책임을 지도록 하여, Player 클래스는 자신의 속성(이름, 잔액, 카드 패)에만 집중할 수 있게 했습니다. 이는 코드의 가독성과 유지보수성을 크게 향상시킵니다.
  2. 상태 캡슐화 (State Encapsulation): 역할 인덱스(dealer_index 등)와 같은 게임의 중요한 상태 정보는 해당 클래스(Table) 내부에 캡슐화하고, 외부에서는 get_role_player()와 같은 공개 메서드를 통해서만 접근하도록 합니다. 이는 데이터의 무결성을 보호하고, 의도치 않은 상태 변경을 방지합니다.
  3. 유연성을 위한 상수 및 규칙 관리: 게임 규칙(예: 최소/최대 플레이어 수, 2인 게임 시 역할 할당 규칙)을 코드 내부에 하드코딩하기보다, 상수로 정의하거나 별도의 규칙 관리 클래스를 통해 유연하게 변경할 수 있도록 설계하는 것이 좋습니다. 이는 향후 새로운 포커 변형을 추가하거나 규칙을 수정할 때 유리합니다.
  4. 단위 테스트 (Unit Testing): 역할 순환 로직과 같은 핵심 게임 메커니즘은 다양한 플레이어 수(2명, 3명, 최대 인원)에서 올바르게 작동하는지 확인하는 단위 테스트를 반드시 작성해야 합니다. 이는 버그를 조기에 발견하고 코드 변경 시 회귀를 방지하는 데 필수적입니다.
  5. 명확한 주석 및 문서화: 복잡한 게임 로직, 특히 역할 순환 규칙이나 특정 플레이어 수에 대한 예외 처리 로직에는 명확한 주석을 달아 다른 개발자(혹은 미래의 자신)가 코드를 쉽게 이해하고 유지보수할 수 있도록 해야 합니다.

이러한 원칙들을 적용함으로써, 텍사스 홀덤과 같은 동적인 게임에서 플레이어 역할을 보다 깔끔하고 효율적으로 관리할 수 있으며, 장기적으로 안정적이고 확장 가능한 게임 시스템을 구축할 수 있을 것입니다.

댓글 남기기