Pygame – TexasHoldem 만들기 – 멀티 플레이 Version

This entry is part 3 of 3 in the series Texas Holdem Pocker

이번 포스트는 지난번 포스트에 이어 Pygame 에 대해서 좀 더 공부한 뒤에 Client/Server 통신을 통한 다자 플레이 기능을 추가한 버전이다. (Multi playing Game) 배팅을 추가하고, 화면별로 접속 유저 중심의 View 를 유지하려다 보니, 난이도도 말도 안 되게 올라간 덕분에 삽질은 엄청 늘었다. ㅠㅠ

이 게임에 사용된 배경 그림과 카드 이미지는 여기 에서 다운로드 받았다.

먼저 Client / Server 통신 부분부터 살펴보도록 하자.


1. Client/ Server Communication

client/server 통신 부분은 이전 포스트에서도 언급한, https://techwithtim.net/tutorials/python-online-game-tutorial/

을 참조했다. socket 을 사용하여 connection 을 맺고, client 는 network class 을 생성하여 server 와의 통신을 수행한다. 여기서는 server에서 client 로의 broadcasting 은 없이, client 가 server에 요청하면 그 응답을 받는 형식이다.

server code
client code
network code

2. Class Diagram

.py 파일 기준으로 아래와 같이 구성되어 있다.

  • Game.py : Game 진행을 위한 내용들로 구성되어 있다. Card, Pot, Deck, Player 와 같은 Game 구성요소들과 주요 Game 의 진행 및 Betting Turn 을 제어하기 위한 Game Class 로 구성되어 있다. Betting 에 관한 자세한 부분은 뒤에서 다루도록 한다.
  • CardCommon.py : 이전 포스트에서도 사용된 Poker 패를 비교하는 로직이 담긴 PockerHelper class 와 결과를 담는 Result class 를 갖고 있다.
  • server.py : client 의 요청을 접수하여 Game class 로 전달해 주는 역할을 한다.
  • client.py : 개별 Player 의 thread 를 의미하며, 각 client 들은 server 로 요청을 보내 Game 의 진행 상태를 확인하고, 자신의 화면에 필요한 그리기(Drawing) 객체를 그린다.
  • network.py : client 에서 server 접속에 사용할 network 객체
  • view.py : pygame 라이브러리를 사용하여 그려질 객체를 정의하고 애니메이션 효과를 미리 지정해 둔다. 이에 대해서는 뒤에서 자세히 설명하도록 한다.
class diagram

3. Pygame View 객체의 구현

기본적으로 여기에 사용된 그리기 객체들은 모두 IsVisible 속성을 갖는다. 이 속성이 True 인 경우에만 그리고 False 이면 그리지 않도록 제어한다. 그리고 vel (velocity) 라는 속성을 추가하여 애니메이션 효과시에 점진적으로 위치나 각도가 변하는 효과를 내는데 사용한다.

1) card 객체 Drawing

card 객체는 다음과 같은 특징을 갖는다.

  • 처음 시작 위치 및 종료 위치를 갖는다.
  • 너비와 높이를 갖는다.
  • y 축을 따라서 종료 지점 까지 점진적으로 이동하는 애니메이션 효과를 가진다.
  • set_pos 함수를 통해 처음 지정되었을 때만 위치 정보가 반영되고 이후에는 반영되지 않는다. (client 에서 반복적으로 set_pos 를 호출하지 않도록 함)

2) chip 객체 Drawing

chip 객체는 다음과 같은 특징을 갖는다.

  • 센터 (원의 중심) 위치 및 반지름 값과 선 두께 값을 갖는다.
  • chip 의 금액을 표시할 text 값을 갖는다.
  • 칩의 형태로 꾸며질 arc 의 반지름 값과 선 두께 값을 갖는다.
  • 원을 따라 1/10 씩 나눠서 arc 를 그리고, 점진적으로 arc 의 각도가 변경되는 애니메이션을 준다.
  • text 도 자연스럽게 회전이 되도록 rotation 하는 애니메이션을 준다.

3) btn 객체 Drawing

btn (button) 객체는 다음과 같은 특징을 갖는다.

  • 센터 (원의 중심) 위치 및 반지름 값과 바깥 원의 속성 값을 갖는다.
  • 마우스가 올려졌을 때 highlight 되도록, 두 가지 컬러 값을 갖는다.
  • 마우스 버튼을 클릭 했을 때, 마우스의 위치가 버튼 내부의 위치인지 계산하도록 함수를 갖고 있다.
  • IsVisible 속성을 켰다 끌 수 있도록 Toggle 함수를 갖는다.

Animation 예시>


4. Betting Control

가장 어렵고도 많은 수정이 이뤄진 부분이 바로 이 부분이 아닌가 싶다. 우선 Betting 을 하려면 Turn 관리를 해야 하고 Turn 관리를 하려면 매번 죽는 (Fold) 사람을 알고 있어야 한다. 그리고 언제 Betting 이 끝날 것인가가 아주 중요한데, Betting 이 모두 끝나야지 다음 단계(Stage) 으로 이동하거나 Round 를 끝내서 승자를 확정할 수 있기 때문이다. 한편, 끝까지 가지 않고도 한 명을 빼고 모두 죽어 버리면 해당 Round 가 끝나기 때문에 그런 것 까지 고려를 해야 하는 어려운 문제이다. 좀 더 자세히 살펴보자.

이 게임은 절대적인 Game 상태를 Client 가 수신해서 자신 만의 화면에 그려야 하는 형태로 작동하기 때문에 Game 의 상태를 Client 에게 정확히 알려주는 것이 매우 중요하다. 각 Betting 의 단계에서도 그런데, 용어의 정리부터 해 보자.

  • Round : Deck 이 새로 섞이고 (Shuffle) Player 에게 새로운 Card 가 전달되는 새로운 한판. 단, 파산한 Player (Dead) 는 참여할 수 없다.
  • Stage : InitRound -> PreFlop -> Flop -> Turn -> River ->ShowDown 으로 이어지는 일련의 단계
  • Turn : 각 Stage 내의 Player 의 betting 순번. 모든 Betting 의 종료되는 순간 다음 단계가 진행될 수 있다. PreFlop, Flop, Turn, River 등 총 4 단계에서 Betting 이 가능하다.

Game class 는 Betting 의 Player turn 을 정확히 알려주기 위해서 curr_bet_idx 라는 ‘현재 turn 의 player index‘ 정보를 갖는다. 한편 각 단계의 betting 의 종료되었는지 여부를 확인하기 위해서 IsBetFinished 라는 속성을 갖고 있다. Betting 중간에 죽는 (Fold) player 들의 index 를 curr_fold_player_list 에 담고 있어서 curr_bet_idx 를 갱신할 때 사용한다. (다음 Turn 의 player 가 누군지 계산할 때, fold player 는 건너뛴다. ) 한편, betting 이 수행되는것을 기록하는 bet_list 를 갖고 있는데, ‘Check’, ‘Fold’, ‘Raise/Call/Allin’ 을 모두 기록한다. 이 bet_list 와 last_raise_idx 를 갖고서 Betting 이 종료되었는지 등을 판단한다.

[ Betting 의 예시]

4명의 플레이어가 있다고 가정했을 때, bet_list 는 4개의 원소를 가지며 다음 처럼 기록된다. [‘Raise’, ‘Call’, ‘Call’, ‘Raise’]
하나의 예를 들어보면, 한 단계의 Betting Turn 이 모두 끝나는 경우는 다음과 같다.

““““““““““““““““““““““““““↓         ↓         ↓         ↓

첫 번째 Betting → [‘Raise’,    ‘Call’,      ‘Call’,    ‘Raise’]
두 번째 Betting → [‘Fold’,      ‘Call’,   ‘Raise’,    ‘Raise‘]
세 번째 Betting → [‘Fold’,     ‘Fold’,    ‘Call‘,             ]

즉, 최종 Raise 한 player 를 기억해 두고 있다가 (last_raise_idx), 해당 player 바로 이전 player 가 ‘Fold’ 나 ‘Call’ 이 되면 해당 단계(Stage) 의 Betting 은 종료된다. Game 에도 동일한 Betting 종료 판별 로직을 적용하였다.

이 밖에도 모두 Check 를 할 경우 다음 Stage 로 넘어가야 한다든지, 한 명의 Player 외에 모두 죽었을 경우 (Fold) 승자를 가리고 다음 Round 로 진행하는 부분, Check 가 가능한 상태인지, Call Betting 이 가능한 상태인지등의 정보를 갖고 있어야 하는 부분등이 추가적으로 구현되었다.


5. Round 갱신과 Client 관점

매 Round 가 새로 시작되면 bet_order_list 를 새로 갱신하게 되는데 기본적으로는 dealer 를 시계 방향으로 하나 증가시켜서 얻는다. dealer 의 위치가 이동하면서 Small Blind 와 Big Blind 의 위치도 같이 이동하며, 최초 Betting 및 Card 를 받는 순서는 SBlind -> BBlind -> … -> Dealer 순이다. bet_order_list 는 player 의 index 이므로 0부터 n-1 player 까진인데, 이 순서가 시계 방향으로 순환하는 형태이다. player 의 index 는 매우 중요한데, 왜냐하면 client 단에서는 화면의 중앙 하단이 자기 자신이 되고 상대방의 위치는 상대적으로 계산되기 때문이다. (즉, 각 player 에서 player index 들의 배열 위치가 각각 다르다. Game 은 player index 정보로만 진행되며 client 가 이 정보를 받아 상대적을 위치를 재 배열하여 그린다.)

아래 그림은각 Player 관점에서 ‘James’ player 의 위치가 어떻게 변동되는지 나타내 준다. James 는 ‘0’ 번 index 를 갖는 player 이지만, 각 player 의 상대적 위치에 따라 재배열 된다.
[0, 1, 2, 3] -> [1, 2, 3, 0] -> [2, 3, 0, 1] -> [3, 0, 1, 2]  (시계 방향)


6. 소스 코드 및 Play 동영상

소스 코드는 Github 에 올려두었다.

screen shot 01
screen shot 02


7. 얻은 점, 느낀 점

발견1.
Client 단에서 Loop 안에서 Rendering 을 조절하는 것이 아니라, Drawing 객체들은 외부에서 정의를 다 해놓는다. 단지 Loop 안의 상황에 따라 보이고 안 보이고 언제부터 보이기 시작할 지를 정하는 것일뿐. 각 객체들은 보이는 시점부터, 혹은 특정 조건에 의해서만 특정 Animation 효과가 등장하도록 미리 코딩해 놓는다.

발견2.
유저들이 모두 연결되면 자동으로 InitRound 를 하게 되어 있는데, InitRound 에서 정지 후, 이를 여러 Client 에서 PreFlop 요청을 할 때 어떻게 해당 단계로 넘어가게 하는 것이 숙제였다. 우선 각 단계별로 set 를 둬서 모든 player 가 참여했을 때 다음 단계로 넘어가도록 하는 것을 생각했다. 하지만 Client 에서 호출해도 너무 빨리 여러 유저들이 요청해서 실제적으로 넘어가는 시간이 찰나여서 Client 에서 pygame.time.delay 를 줘서 해결 했다.

발견3.
위에서 Client 단에서 time.delay 를 준 것으로도 해결이 안 되는 부분이 있는데, Client 요청 만으로 단계를 전환하고자 할 때, Betting 이 끝나서 Game 의 단계는 이미 변경되었으나 이전 상태기준의 Client 요청이 발생하므로 Game 에도 상태 변경 후, Sleep 을 줬다. 그리고 각 단계별 Client 의 상태를 동기화 하는 방편으로 Client 의 단계 종료 요청을 받아서 이 수가 채워져야 넘어가도록 했다.

발견4.
Client/Server 통신 과정에서 pickle 로 serialization 하는데, Game class 자체를 serialization 하니 용량이 너무 커서 계속 에러가 발생하였다. 이의 해결책으로 Client 단에서 사용하는 정보만 별도 딕셔너리에 담아서 리플라이 되도록 하였다.

발견5.
Chip 이나 Card 의 경우 Animation 에 위치 변경이 포함되어 있어, Client Tick 내부에서 계속 포지션 변경에 대한 함수를 호출할 경우 지속적으로 Animation 되면서 위치도 바뀌어서 사용할 수 없게 된다. 따라서 Client 단에서 최초 위치를 한번만 설정할 수 있도록 Trick 을 사용해야 한다. (제어가 필요하다) 또, Betting Btn 같은 경우 Visible 을 Toggle 로 해 놓아야 하는데 Button 을 Visible = False 로 했다가 다시 Visible = True 로 하니 Tick 마다 살아났다 없어졌다를 반복해서 최초 단계에 들어오는지 여부도 관리해야 했다.


8. 미 해결 문제

문제1.
Showdown 으로 가지 못하고 중간에 한 명 빼고 모두 죽었을 때 (Fold), 승자 결정 후 다음 Round 로 진입한다. 이 때 승자 표현하는 부분이 제대로 표시되지 않는 문제를 해결하지 못했다.

문제2.
Mousemotion Event 와 Mousebuttondown Event 를 같이 조건절에 넣었을 때 motion event 가 Queue 에 남아서 마우스 Button Click Event 가 잘 먹지 않는 현상이 있다. Tick 을 최대한 낮춰서 해결하긴 했으나 한 번 클릭으로 안 되고 여러 번 눌러야 제대로 동작하는 경우가 많아 개선이 필요하다.

문제3.
Betting 의 마지막에 Chip Drawing 이 표시가 제대로 되지 않고 다음 단계로 넘어가는 것을 해결하지 못했다.

문제4.
Play 중에 특정 사용자가 loop 를 벗어나는 현상이 나타났다. 원인이 어떤 것인지 파악이 안 되었다.

문제5.
AllIn 의 구현 로직을 정하지 못하여, All In 이 동작하지 않는다.

Series Navigation<< Pygame – TexasHoldem 만들기 – 싱글 플레이 Version

2 thoughts on “Pygame – TexasHoldem 만들기 – 멀티 플레이 Version”

  1. 파이썬을 배우면서 현재 사이트까지 오게되었습니다.
    github에 올리신 소스코드를 위에 스샷처럼 실행시키기 위해서는 어떤 프로그램이 필요하고 어떤 작업이 필요하나요?
    유튜브에서도 봤는데 anaconda와 vs code를 이용하시거 같은데 그 이상은 모르겠어서 질문드림니다 ㅠㅜ 코로나 조심하시고 부디 알려주시면 감사하겠습니다ㅎ

    답변하기
    • 먼저, 질문 감사드립니다.
      Github Source 를 받으시면 실행시켜야 할 파일은 크게 2가지 입니다.

      server.py 와 client.py 입니다.

      먼저 server.py 를 “python server.py” 와 같이 command 창에서 실행시키시고 (따옴표 제외),
      그 후에 client.py 를 “python client.py” 와 같이 command 창에서 실행시키시면 됩니다.

      server 는 하나만 띄우면 되나, client 는 기본(default) 4개로 설정되어 있기 때문에 동일한 실행을 4번 하셔야 합니다.
      영상에서도 확인되지만, client 실행시에는 player name 을 입력해야 합니다. (영어로 입력하시기 바랍니다.)

      혹시 client 나 server 코드시 실행이 안 된다면 대부분 관련 library 들이 설치 되지 않았기 때문입니다.
      대표적으로 pygame library 가 설치되어 있어야 합니다.
      물어보신 vscode 는 설치되어 있어도 되고 안 되어도 상관없습니다. 하지만 python 3은 설치되어 있어야 합니다.
      Anaconda 설치를 하셔도 되고, 직접 python 3 만 설치되어 있어도 관계는 없습니다.

      혹시 python 설치 및 기본 문법이 궁금하시다면, 제 다른 유투브 채널의 다음 영상을 참고해 주셔도 되고 다른 영상을 참조하셔도 되겠습니다.
      https://www.youtube.com/watch?v=fce61TnAUMs&list=PL73qGQ0nG_q10EKzft6GweEDk2lHcF1hX

      답변하기

ruiz에 답글 남기기 답글 취소하기