게임 개발에서 플레이어의 역할은 게임의 흐름을 결정하는 핵심 요소입니다. 특히 텍사스 홀덤과 같은 카드 게임에서는 딜러, 스몰 블라인드, 빅 블라인드와 같은 역할이 매 핸드마다 순환하며 게임 규칙과 턴 순서에 직접적인 영향을 미치죠. 하지만 이러한 동적인 역할을 깔끔하고 유연하게 관리하는 것은 생각보다 까다로울 수 있습니다. 오늘은 텍사스 홀덤 게임에서 동적으로 변하는 플레이어 역할을 효과적으로 관리하는 방법에 대해 알아보겠습니다. 개발자가 겪는 일반적인 문제점을 분석하고, 객체 지향적인 해결책을 제시하여 유지보수성과 확장성이 뛰어난 코드를 작성하는 데 도움을 드릴 것입니다.
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. 명확한 발생 원인
위와 같은 상황이 발생하는 주된 원인은 다음과 같습니다.
-
역할 관리 로직의 부재:
PLAYER_ROLES딕셔너리는 단순히 역할의 이름을 정의하고 초기화하는 데 사용될 뿐, 누가 어떤 역할을 맡았는지 “현재 상태”를 추적하거나 다음 핸드에서 역할을 “순환”시키는 구체적인 로직을 포함하고 있지 않습니다. - 정적인 역할 정의: 텍사스 홀덤의 딜러, 스몰 블라인드, 빅 블라인드는 매 핸드마다 시계 방향으로 순환하는 동적인 특성을 가집니다. 이를 위한 명확한 상태 전이(state transition) 메커니즘이 코드에 구현되어 있지 않습니다.
- 유연성 및 확장성 부족: 현재 3인 플레이에 맞춰져 있거나 최소 플레이어 수 검증 로직에 오류가 있어, 2인 플레이와 같이 플레이어 수가 변동될 때 역할 할당 규칙(예: 딜러가 스몰 블라인드를 겸임)을 적용하기 어렵습니다. 이는 게임의 다양한 변형에 대한 확장성을 저해합니다.
-
책임 분리 원칙 위반: 역할을 딕셔너리에 단순히 저장하는 방식은 역할을 맡은 플레이어의 행동(예: 강제 베팅)과 역할을 누가 맡았는지에 대한 상태 관리가 한곳에서 이루어지지 않아, 코드가 복잡해지고 유지보수가 어려워질 수 있습니다.
Table클래스가 플레이어 목록과 역할 관리에 대한 책임을 모두 가지면서도, 그 책임을 명확히 수행하지 못하고 있습니다.
3. 해결 방법 및 코드 예시
이 문제를 해결하기 위한 가장 깔끔하고 객체 지향적인 방법은 Table 클래스에서 플레이어 목록과 함께 역할의 현재 상태(누가 딜러이고, 스몰/빅 블라인드인지)를 관리하고, 매 핸드 종료 시 역할을 다음 플레이어로 순환시키는 메서드를 구현하는 것입니다. 이 방식은 역할 관리의 책임을 명확히 하고, 유연한 확장을 가능하게 합니다.
해결 전략
-
`Table` 클래스에서 역할 상태 관리:
Table클래스가 게임에 참여하는Player객체들의 리스트를 가지고, 현재 딜러, 스몰 블라인드, 빅 블라인드 역할을 맡은 플레이어의 인덱스를 추적합니다. -
초기 역할 할당 메서드 구현: 게임 시작 시 플레이어 수에 따라 초기 역할을 할당하는 내부 메서드를
Table클래스에 추가합니다. -
역할 순환 메서드 구현: 매 핸드가 끝날 때마다 역할을 다음 플레이어에게 순환시키는
rotate_roles()메서드를 구현합니다. 이때 2인 게임과 3인 이상 게임의 규칙 차이를 고려합니다. - 역할 플레이어 조회 메서드: 특정 역할을 맡은 플레이어 객체를 반환하는 헬퍼 메서드를 추가하여 외부에서 쉽게 현재 역할자를 파악할 수 있도록 합니다.
코드 예시
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. 향후 예방을 위한 팁
동적인 플레이어 역할 관리를 포함하여 게임 로직을 설계할 때, 다음과 같은 원칙들을 따르면 더 견고하고 유지보수하기 쉬운 코드를 작성할 수 있습니다.
-
책임 분리 (Separation of Concerns): 각 클래스는 하나의 명확한 책임을 가지도록 설계합니다. 위 예시에서는
Table클래스가 플레이어 목록과 역할 관리에 대한 모든 책임을 지도록 하여,Player클래스는 자신의 속성(이름, 잔액, 카드 패)에만 집중할 수 있게 했습니다. 이는 코드의 가독성과 유지보수성을 크게 향상시킵니다. -
상태 캡슐화 (State Encapsulation): 역할 인덱스(
dealer_index등)와 같은 게임의 중요한 상태 정보는 해당 클래스(Table) 내부에 캡슐화하고, 외부에서는get_role_player()와 같은 공개 메서드를 통해서만 접근하도록 합니다. 이는 데이터의 무결성을 보호하고, 의도치 않은 상태 변경을 방지합니다. - 유연성을 위한 상수 및 규칙 관리: 게임 규칙(예: 최소/최대 플레이어 수, 2인 게임 시 역할 할당 규칙)을 코드 내부에 하드코딩하기보다, 상수로 정의하거나 별도의 규칙 관리 클래스를 통해 유연하게 변경할 수 있도록 설계하는 것이 좋습니다. 이는 향후 새로운 포커 변형을 추가하거나 규칙을 수정할 때 유리합니다.
- 단위 테스트 (Unit Testing): 역할 순환 로직과 같은 핵심 게임 메커니즘은 다양한 플레이어 수(2명, 3명, 최대 인원)에서 올바르게 작동하는지 확인하는 단위 테스트를 반드시 작성해야 합니다. 이는 버그를 조기에 발견하고 코드 변경 시 회귀를 방지하는 데 필수적입니다.
- 명확한 주석 및 문서화: 복잡한 게임 로직, 특히 역할 순환 규칙이나 특정 플레이어 수에 대한 예외 처리 로직에는 명확한 주석을 달아 다른 개발자(혹은 미래의 자신)가 코드를 쉽게 이해하고 유지보수할 수 있도록 해야 합니다.
이러한 원칙들을 적용함으로써, 텍사스 홀덤과 같은 동적인 게임에서 플레이어 역할을 보다 깔끔하고 효율적으로 관리할 수 있으며, 장기적으로 안정적이고 확장 가능한 게임 시스템을 구축할 수 있을 것입니다.
![[데이터 수집] 페이스북 영상 조회수, Graph API 없이 1000개 추적하기: yt-dlp 및 Headless 브라우저 한계 돌파 전략 [데이터 수집] 페이스북 영상 조회수, Graph API 없이 1000개 추적하기: yt-dlp 및 Headless 브라우저 한계 돌파 전략](https://dev-error.com/wp-content/plugins/contextual-related-posts/default.png)