Project Euler

프로젝트 오일러 054

포커 게임에서 이긴 횟수 구하기

3분
#project euler #python

점수 체계 정하기

포커의 승패는 다섯장의 카드로 만들 수 있는 계급의 높낮이로 승부를 가르며, 계급이 같은 경우, 더 높은 숫자를 가진 사람이 이기는 규칙이 있습니다. 문제에서는 각 계급이 어떻게 결정되는지를 정의해두었으니, 규칙과 높은 카드를 비교하여 높은 쪽이 승리하도록 한 판의 게임을 판정하는 로직을 구현하면 됩니다.

그런데 아무래도 판단해야 하는 규칙이 많고 복잡하다보니, 각 상황에서 양쪽 선수의 상태를 비교하기 보다는, 개별 선수의 카드가 해당하는 계급을 찾아 점수로 환산하려고 합니다. 특히 포커 계급은 특정 하위 계급의 조합으로 상위 계급이 만들어질 수도 있기 때문에 로직을 단순화하는데 도움이 됩니다. 예를 들어, Two Pair는 두 개의 One Pair이고, Full House는 One Pair + Three of a Kind 의 조합입니다. 따라서 T/P의 점수는 O/P의 점수의 두 배가, F/H의 점수는 O/P + T/K의 점수가 되도록 점수 체계를 구성한다면, 이 두 케이스의 계급을 판정하는 로직은 구현할 필요가 없게 됩니다.

계급의 중간쯤에 있는 Flush를 100점으로 둔다면 다음과 같이 계급별 점수를 정할 수 있을 겁니다. 이 점수 체계는 상대적인 것이므로 보다 더 적절한 체계를 사용해도 좋습니다. 다른 계급 두 개의 조합이 동시에 성립하는 계급의 점수가 이를 구성하는 계급들의 점수의 합이 되고, 전체 계급의 순서가 지켜질 수 있도록만 설정하면 됩니다.

  • One Pair = 25
  • Two Pairs = 50 (25 × 2)
  • Three of a Kind = 80
  • Straight = 90
  • Flush = 100
  • Full House = 105 (25 + 80)
  • Four of a Kind = 110
  • Straight Flush = 190 (90 + 100)
  • Royal Flush = 200

판정

점수 체계를 위와 같이 구성하면 계급 판정에서 불필요한 판정을 계산할 필요가 없습니다. 아래와 같은 순서대로 판정합니다.

  1. 플러시 조건을 만족한다면 점수에 100점을 더합니다.
  2. 로열 플러시 조건이라면 100점을 더하고 종료합니다.
  3. (플러시 여부와 상관없이) 스트레이트 조건을 만족한다면 점수에 90점을 더합니다.
  4. F/K 조건을 만족한다면 110점을 더합니다.
  5. T/K 조건을 만족한다면 80점을 더합니다.
  6. One Pair를 이루는 쌍이 몇 개인지 찾고, 각 쌍마다 25점을 더합니다.

하이 카드를 통해서 비교하기 위해서는 5장의 카드에서 숫자의 개수가 많고, 숫자가 더 큰 순서대로 정렬하여 5개를 나열합니다. 어차피 하이 카드는 같은 계급(점수)에서만 비교하므로, 이렇게 정렬하면, 계급과 그 계급의 숫자가 같은 (예를 들어 두 사람이 모두 5 One Pair 라면) 경우에 남은 카드로 승부를 결정하게 됩니다.

참고로 주의할 점은, A는 K보다 높은 카드이지 1이 아닙니다. 따라서 Q, K, A, 2, 3 은 스트레이트가 아니라는 점입니다.

    # 점수와 카드 숫자를 높은숫자부터 정렬한 리스트를 반환
    # 결과를 그대로 비교하면 승패를 즉시 파악가능

    score:int = 0
    # d: 각 숫자별 카드 수
    d: dict[Number, int] = dict()

    for n, _ in cards:
        d[n] = d.get(n, 0) + 1

    # 높은 카드번호부터 정렬된 카드번호
    ns = sum(
        [
            [x] * y
            for x, y in sorted(d.items(), key=lambda x: (x[1], x[0]), reverse=True)
        ],
        [],
    )

    # flush 판정
    if all(card[1] == cards[0][1] for card in cards):
        score += 80
        if ns == list(range(14, 9, -1)):
            return (200, ns)

    # straight 판정
    if all(x - y == 1 for (x, y) in zip(ns, ns[1:])):
        score += 75

    # kinds 판정
    if any(v == 4 for v in d.values()):
        score += 100
    if any(v == 3 for v in d.values()):
        score += 70

    # pairs 판정
    score += sum(25 for v in d.values() if v == 2)

    return score, ns

각 카드는 (숫자, 종류)의 순서쌍(튜플)로 구성되며, 5장씩 리스트로 전달하면 점수와 하이카드 배열이 리턴됩니다. 튜플의 비교는 앞에 있는 요소부터 순차적으로 비교하면 점수가 높거나, 동일 점수에서 하이카드의 가장 높은 카드부터 비교하여 승패를 알 수 있습니다.

requests.get()을 스트리밍 모드로 연결하면 파일을 행단위로 읽어올 수 있으므로, 설령 파일이 아주 크더라도 안정적으로 처리할 수 있습니다.

def main():
    # 문자로된 카드숫자를 값으로 변환하기 위한 맵핑
    ps = {c: i for (i, c) in enumerate(",.23456789TJQKA")}
    res = 0
    url = "https://euler.synap.co.kr/files/poker.txt"
    response = requests.get(url, stream=True)
    for line in response.iter_lines():
        cards = [(ps[x[0]], x[1]) for x in line.decode().split()]
        a, b = cards[:5], cards[5:]
        x, y = map(evaluate, (a, b))
        if x > y:
            res += 1
    print(res)