용어와 Hand rank에 관한 내용은 이전포스팅을 참고하십시오.

승률

확률적 맥락에 의해 ♠A♣A♥A ◆A의 승률은 똑같습니다. 같은 이유로, ♠A♠K♥A ♥K, ◆A ◆K와 같은 승률을 갖습니다. 따라서, 아래와 같이 13가지 pocket과 suit, off-suit의 gapper 총 169가지 승률만 미리 구해놓을 수 있으면, 모든 경우의 승률을 알 수 있습니다.

A
K
Q
J
T
9
8
7
6
5
4
3
2
A
AA
AKs
AQs
AJs
ATs
A9s
A8s
A7s
A6s
A5s
A4s
A3s
A2s
K
AKo
KK
KQs
KJs
KTs
K9s
K8s
K7s
K6s
K5s
K4s
K3s
K2s
Q
AQo
KQo
QQ
QJs
QTs
Q9s
Q8s
Q7s
Q6s
Q5s
Q4s
Q3s
Q2s
J
AJo
KJo
QJo
JJ
JTs
J9s
J8s
J7s
J6s
J5s
J4s
J3s
J2s
T
ATo
KTo
QTo
JTo
TT
T9s
T8s
T7s
T6s
T5s
T4s
T3s
T2s
9
A9o
K9o
Q9o
J9o
T9s
99
98s
97s
96s
95s
94s
93s
92s
8
A8o
K8o
Q8o
J8o
T8o
98o
88
87s
86s
85s
84s
83s
82s
7
A7o
K7o
Q7o
J7o
T7o
97o
87o
77
76s
75s
74s
73s
72s
K
A6o
K6o
Q6o
J6o
T6o
96o
86o
76o
66
65s
64s
63s
62s
5
A5o
K5o
Q5o
J5o
T5o
95o
85o
75o
65o
55
54s
53s
52s
4
A4o
K4o
Q4o
J4o
T4o
94o
84o
74o
64o
54o
44
43s
42s
3
A3o
K3o
Q3o
J3o
T3o
93o
83o
73o
63o
53o
43o
33
32s
2
A2o
K2o
Q2o
J2o
T2o
92o
82o
72o
62o
52o
42o
32o
22

승자판별

각 player의 hand와 board(5장의 community card)가 공개되면, 각각의 hand로부터 rank를 계산해서 비교합니다. 이 때, split(공동 1등)을 염두해 두어야 합니다. (원본 code에 TC가 포함되어 있습니다.)

"who_is_winner.drawio.png.svg", iseohyun, iseohyun.com, public
def get_winner(players, board):
  ranks = []
  for player in players:
      ranks.append(get_rank(player + board))

  best_rank = ranks[0]  # 0번(p1) 임시 우승
  winner = [0]  # split(동시 우승) 가능성 있음

  for i in range(1, len(ranks)):  # 1번(p2)부터 챌린지
      current_rank = ranks[i]
      if current_rank[0] < best_rank[0]:  # 랭킹 우위
          best_rank = current_rank  # 우승자 변경
          winner = [i]  # 우승자 목록 단독 기록
      elif current_rank[0] == best_rank[0]:  # 동일 랭킹, 하이카드 비교
          for cur, best in zip(current_rank[1], best_rank[1]):
              if values.index(cur) < values.index(best):  # 새 우승자 탄생
                  best_rank = current_rank
                  winner = [i]
                  break
              elif values.index(cur) > values.index(best):
                  break
          else:
              winner.append(i)  # 공동 우승자 탄생

  return winner

승리 횟수 판별

pre-flop에서 board는 공개되지 않았으므로, 모든 경우의 수 \( _{52 - 2 * players}C_5 \)를 고려해야 합니다. heads-up의 경우 \( _{48}C_5 = 1,712,304 \)가지 경우의 수가 존재합니다.

def cnt_winner(players, board=["", "", "", "", ""]):
    """players, 카드 숫자로 넘어온다.(예)[[0,1],[2,3]]"""
    outs = [i for i in range(52)]
    winner_count = [0 for _ in range(len(players))]

    # players의 hand를 문자열로 바꾼다.
    for player in players:
        for i in range(2):
            outs.remove(player[i])
            player[i] = int2card(player[i])

    tot_match = 0
    for flop1 in range(0, len(outs) - 4):
        board[0] = int2card(outs[flop1])
        for flop2 in range(flop1 + 1, len(outs) - 3):
            board[1] = int2card(outs[flop2])
            for flop3 in range(flop2 + 1, len(outs) - 2):
                board[2] = int2card(outs[flop3])
                for turn in range(flop3 + 1, len(outs) - 1):
                    board[3] = int2card(outs[turn])
                    for river in range(turn + 1, len(outs)):
                        board[4] = int2card(outs[river])
                        winners = get_winner(players, board)
                        for winner in winners:
                            winner_count[winner] += 1

    # players의 hand를 숫자로 바꾼다.
    for player in players:
        for i in range(2):
            player[i] = card2int(player[i])

    # print(test_code) # test Code: 테스트 결과 출력력
    return winner_count

코드 검증

간단한 계산으로 예측가능한 값과 실제로 출력된 갯수를 비교하여 검증하였습니다.

CASE #1.

Hand) [As, Ac] [Ah, Ad]
조건) p2가 p1을 flush vs flush로 이길 경우의 수
출력결과) 1,540

예측) CC 5장이 모두 heart 또는 diamond가 나와야 함.
계산) 1. 모두 diamond가 나오면, p1도 flush가 성립하므로 12(Ad빠짐)에서 5장이 나올 경우의 수는 792가지
  p2가 R. flush가 나오는 경우를 제외[8개] : [Kd, Qd, Jd, Td] + 9d, 8d, 7d, 6d, 5d, 4d, 3d, 2d
  p2가 (back) S. flush [8개] : [5d, 4d, 3d, 2d] + 6d, 7d, 8d, 9d, Td, Jd, Qd, Kd
  (A Top 아닌) S.flush [6개] : (Qd, Jd, Td, 9d, 8d, 7d) Kd->R., 6d-> back
  도합 (792-22)*2개의 case발생 = 1540개
      

CASE #2.

Hand) [As, Ts] [9s, 2s]
      조건) p2가 p1을 S. flush로이길 경우의 수
      출력 결과) 86개
      
      예측) '♠3456' + wild = 44C1, '♠5678' + wild = 44C1, 88개
      예외) S. flush Top8, Top7 무승부, 2개
      예측 결과) 86개

      p1 hand('As')
      ['Ac', '6s', '5s', '4s', '3s']
      ['Ah', '6s', '5s', '4s', '3s']
      ['Ad', '6s', '5s', '4s', '3s']
      ['Ks', '6s', '5s', '4s', '3s']
      ['Kc', '6s', '5s', '4s', '3s']
      ['Kh', '6s', '5s', '4s', '3s']
      ['Kd', '6s', '5s', '4s', '3s']
      ['Qs', '6s', '5s', '4s', '3s']
      ['Qc', '6s', '5s', '4s', '3s']
      ['Qh', '6s', '5s', '4s', '3s']
      ['Qd', '6s', '5s', '4s', '3s']
      ['Js', '6s', '5s', '4s', '3s']
      ['Jc', '6s', '5s', '4s', '3s']
      ['Jh', '6s', '5s', '4s', '3s']
      ['Jd', '6s', '5s', '4s', '3s']
      p1 hand('Ts')
      ['Tc', '6s', '5s', '4s', '3s']
      ['Th', '6s', '5s', '4s', '3s']
      ['Td', '6s', '5s', '4s', '3s']
      p2 hand('9s')
      ['9c', '6s', '5s', '4s', '3s']
      ['9h', '6s', '5s', '4s', '3s']
      ['9d', '6s', '5s', '4s', '3s']
      ['8s', '6s', '5s', '4s', '3s']
      ['8c', '6s', '5s', '4s', '3s']
      ['8h', '6s', '5s', '4s', '3s']
      ['8d', '6s', '5s', '4s', '3s']
      (S. flush Top7 무승부)
      ['7c', '6s', '5s', '4s', '3s']
      ['7h', '6s', '5s', '4s', '3s']
      ['7d', '6s', '5s', '4s', '3s']
      65432 (p2: S. flush)
      ['6s', '6c', '5s', '4s', '3s']
      ['6s', '6h', '5s', '4s', '3s']
      ['6s', '6d', '5s', '4s', '3s']
      ['6s', '5s', '5c', '4s', '3s']
      ['6s', '5s', '5h', '4s', '3s']
      ['6s', '5s', '5d', '4s', '3s']
      ['6s', '5s', '4s', '4c', '3s']
      ['6s', '5s', '4s', '4h', '3s']
      ['6s', '5s', '4s', '4d', '3s']
      ['6s', '5s', '4s', '3s', '3c']
      ['6s', '5s', '4s', '3s', '3h']
      ['6s', '5s', '4s', '3s', '3d']
      p2 hand('2s')
      ['6s', '5s', '4s', '3s', '2c']
      ['6s', '5s', '4s', '3s', '2h']
      ['6s', '5s', '4s', '3s', '2d']
      
      p1 hans('As')
      ['Ac', '8s', '7s', '6s', '5s']
      ['Ah', '8s', '7s', '6s', '5s']
      ['Ad', '8s', '7s', '6s', '5s']
      ['Ks', '8s', '7s', '6s', '5s']
      ['Kc', '8s', '7s', '6s', '5s']
      ['Kh', '8s', '7s', '6s', '5s']
      ['Kd', '8s', '7s', '6s', '5s']
      ['Qs', '8s', '7s', '6s', '5s']
      ['Qc', '8s', '7s', '6s', '5s']
      ['Qh', '8s', '7s', '6s', '5s']
      ['Qd', '8s', '7s', '6s', '5s']
      ['Js', '8s', '7s', '6s', '5s']
      ['Jc', '8s', '7s', '6s', '5s']
      ['Jh', '8s', '7s', '6s', '5s']
      ['Jd', '8s', '7s', '6s', '5s']
      p1 hand('Ts')
      ['Tc', '8s', '7s', '6s', '5s']
      ['Th', '8s', '7s', '6s', '5s']
      ['Td', '8s', '7s', '6s', '5s']
      p2 hand('9s')
      ['9c', '8s', '7s', '6s', '5s']
      ['9h', '8s', '7s', '6s', '5s']
      ['9d', '8s', '7s', '6s', '5s']
      98765 (p2: S. flush)
      ['8s', '8c', '7s', '6s', '5s']
      ['8s', '8h', '7s', '6s', '5s']
      ['8s', '8d', '7s', '6s', '5s']
      ['8s', '7s', '7c', '6s', '5s']
      ['8s', '7s', '7h', '6s', '5s']
      ['8s', '7s', '7d', '6s', '5s']
      ['8s', '7s', '6s', '6c', '5s']
      ['8s', '7s', '6s', '6h', '5s']
      ['8s', '7s', '6s', '6d', '5s']
      ['8s', '7s', '6s', '5s', '5c']
      ['8s', '7s', '6s', '5s', '5h']
      ['8s', '7s', '6s', '5s', '5d']
      (S. flush Top8 무승부)
      ['8s', '7s', '6s', '5s', '4c']
      ['8s', '7s', '6s', '5s', '4h']
      ['8s', '7s', '6s', '5s', '4d']
      ['8s', '7s', '6s', '5s', '3s']
      ['8s', '7s', '6s', '5s', '3c']
      ['8s', '7s', '6s', '5s', '3h']
      ['8s', '7s', '6s', '5s', '3d']
      p2 hand('2s')
      ['8s', '7s', '6s', '5s', '2c']
      ['8s', '7s', '6s', '5s', '2h']
      ['8s', '7s', '6s', '5s', '2d']
      

CASE #3

HAND) [As Ts] [2s 3s]
          조건) p2가 p1을 S. flush로 이길 경우의 수
          출력결과) 989

          예측) 45C2 = 990
          예외) ['4s', '5s', '6s', '7s', '8s']의 경우 8 Top S. flush로 동점
          예측 결과) 989

파일로 저장

def print_file(my_hand):
    # 1. 저장할 파일 불러오기
    filename = f"{my_hand[0]}{my_hand[1]}.txt"

    with open(filename, "r") as f:
        lines = f.readlines()

    # 2. 저장된 내용 불러오기(중단부분이 있다면), 시작할 지점 세팅
    players = [[card2int(my_hand[0]), card2int(my_hand[1])], [0, 0]]
    if lines:
        last_record = lines[-1]
        print("last record: ", last_record)
        last_record = ast.literal_eval(f"[{last_record}]")
        players[1] = [card2int(last_record[0][0]), card2int(last_record[0][1])]
        print(players)
    else:
        players[1] = [0, 0]

    start_processing = False # 중간 시작을 위한 flag

    # 3. draw된 카드 풀 삭제
    outs = [x for x in range(52)]
    outs.remove(players[0][0])
    outs.remove(players[0][1])

    # 4. player2의 hand 생성
    for i in range(50):
        if (not start_processing) and outs[i] < players[1][0]:
            continue

        patterns = [] # hand패턴을 기억해서, 동일한 계산을 생략
        for j in range(i + 1, 50):
            if not start_processing:
                if outs[j] <= players[1][1]:
                    if j == 49: # player2의 마지막 hand까지('2d') 계산이 완료된 경우
                        start_processing = True
                        print(">> Start process: ", players)
                    continue
                    
                start_processing = True
                print(">> Start process: ", players)

            # 5. 패턴 검사
            players[1] = [outs[i], outs[j]]
            cur_pattern = get_pattern(players)
            message_out = f"\n['{int2card(players[1][0])}', '{int2card(players[1][1])}'], "

            for k, pattern in enumerate(patterns):
                if cur_pattern == pattern: # 5-1: 이전 계산값을 파일에서 찾는다.
                    search_text = f"{int2card(outs[i])}', '{int2card(outs[j] -len(patterns) + k)}"
                    with open(filename, "r") as f:
                        for line in f:
                            if search_text in line:
                                winner_count = ast.literal_eval(f"[{line}]")[1]
                                message_out += f"{winner_count}, '*'" # 이 계산은 복사되었음을 표시한다.
                                break
                    break
            else:
                # 6. 1,712,304가지의 승부를 계산한다.
                winner_count = cnt_winner(players)
                message_out += f"{winner_count}"

            # 7. 파일에 기록한다.
            with open(filename, "a") as f:
                f.write(message_out)

            # 8. 패턴 정보를 업데이트 한다.
            if outs[j] % 4 == 3:
                patterns = []
            else:
                patterns.append(cur_pattern)

저장된 파일(예시)

AsAc.txt

아래 코드는 ♠A♣A hand가 다른 hand와 붙었을 때, 승리 횟수입니다. 예를들어 ♥A♥K와 붙었을 때, 1,515,162번 이기고, 218,634번 진다는 의미입니다. 총 전적수 1,712,304보다 21,492가 많은 이유는 draw가 발생하기 때문입니다.

승률은 약 88.4867%이며, 동점을 제외하면 87.2316% 확률로 승리합니다.

['Ah', 'Ad'], [1675094, 1675094]
['Ah', 'Ks'], [1612022, 123343]
['Ah', 'Kc'], [1612022, 123343]
['Ah', 'Kh'], [1515162, 218634] # 예시
['Ah', 'Kd'], [1595877, 137865]
['Ah', 'Qs'], [1604389, 130734]
['Ah', 'Qc'], [1604389, 130734]
['Ah', 'Qh'], [1508243, 225365]
['Ah', 'Qd'], [1588400, 145101]
['Ah', 'Js'], [1596756, 138125]
['Ah', 'Jc'], [1596756, 138125]
['Ah', 'Jh'], [1501324, 232096]
['Ah', 'Jd'], [1580923, 152337]
['Ah', 'Ts'], [1589123, 145516]