Site icon Wireframe

오일러 프로젝트 54

포커라는 카드 게임은 다섯 장으로 된 패의 높고 낮음에 따라 승부를 냅니다. (포커 규칙을 이미 아는 분이라면 규칙 설명 부분은 건너 뛰셔도 좋습니다.) 카드 한 장은 아래와 같은 순서대로 값이 높아집니다.

2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K, A

다섯장으로 이루어진 패의 계급은 낮은 것부터 높은 순서로 다음과 같습니다.

  • High Card : 가장 높은 카드의 값 비교
  • One Pair : 한 쌍이 같은 카드
  • Two Pairs: 서로 다른 두 쌍이 같은 카드
  • Three of a Kind: 세 장이 같은 카드
  • Straight : 모든 카드가 연속된 숫자 (Q, K, A, 2, 3 은 스트레이트가 아닙니다)
  • Flush : 모든 카드의 무늬가 같음
  • Full House : 세장이 같고, 또 한쌍이 같음
  • Four of a Kind : 네 장이 같은 카드
  • Royal Flush : 10, J, Q, K, A 가 무늬도 같음 (Royal Straight Flush가 아니냐고 하겠지만, Royal Straight라는 것은 없습니다.)

두 사람의 패가 같은 종류의 계급이라면, 계급을 구성하는 카드 중 높은 쪽을 쥔 사람이 이깁니다. 예를 들면 8 원페어는 5 원페어를 이깁니다. (풀하우스의 경우 Three of a Kind의 숫자를 먼저 비교하고, 원 페어의 숫자를 다시 비교) 계급을 이루는 카드 숫자까지 같으면 (둘 다 Q 원페어인 경우 등) 다른 카드를 높은 순서대로 비교해서 승부를 정합니다.

텍스트 파일 poker.txt에는 두 선수가 벌인 1,000회의 승부가 저장되어 있습니다. 한 줄에는 10장의 카드가 공백으로 분리되어 있는데, 앞의 다섯장은 1번 선수의 것이고 뒤의 다섯장은 2번 선수의 패입니다. 잘못되거나 중복된 데이터는 없으며, 무승부도 없습니다.

카드 숫자는 2, 3, …, 9, T, J, Q, K, A 로(숫자 10은 T로 표시), 무늬는 C, D, H, S로 표시되어 있습니다. 예를 들면 3C 3D 3S 9S 9D의 경우 3 풀하우스가 됩니다.

이 데이터를 분석하고 1번 선수가 이긴 횟수를 구하세요.

https://euler.synap.co.kr?problem=54

접근

먼저 계급을 어떻게 비교하는 게 좋을지 생각해보자. 가장 기본적으로는 각 계급에 인덱스나 점수를 부여해서 그 값을 비교하는 것을 생각해 볼 수 있다. 그런데 9개의 포커 계급 중에서 몇 가지 겹치는 영역이 보인다.

그러면 각 조건에 점수를 적절히 부여한다면, 두 조건을 모두 만족하는 경우에는 각각의 조건에 의한 점수를 합산하는 것으로 최종 점수를 얻는 것으로 구현할 수도 있을 것이다. 각 조건의 점수를 적절하게 분배하는 것이 요령이라 하겠다.

  1. Flush를 검사한다.
  2. Flush 이면서, 10, J, Q, K, A 이면 로열 플러시 판정 (완료)
  3. 스트레이트 여부를 검사한다.
  4. Four of a Kind 를 검사한다.
  5. Three of a Kind를 검사한다.
  6. One Pair는 몇 개나 있는지 검사한다.

점수표

Flush를 100점으로 잡으면 대략 아래와 같이 점수를 분포시킬 수 있다. 다른 계급의 조합으로 만들어지는 계급은 따로 검사하지 않아도 된다.

하이 카드

하이 카드는 두 가지 경우에 비교를 하는데, 두 선수가 특정 계급인 경우에는 그 계급을 구성하는 카드 숫자를 비교하며, 두 선수가 같은 계급의 그 숫자까지 같다면, 계급을 이루지 못하고 남는 카드에서 비교한다. 파이썬에서 튜플이나 리스트의 대소를 비교하면 기본적으로 앞의 원소끼리 비교하고, 두 원소가 같으면 그 다음 원소끼리 비교하는 식이 된다. 따라서 모든 카드의 숫자를 다음의 순서로 나열한다.

  1. 개수가 가장 많은 숫자가 우선
  2. 같은 개수에서는 숫자가 큰 것이 우선

이 리스트를 비교하면 같은 계급에서 (즉, 산출된 점수가 같을 때) 계급을 구성하는 카드의 숫자와 남는 숫자를 차례로 비교하게 된다.

점수 계산 함수

번호가 같거나, 연속된 번호인 것을 찾는 것이 번거로울 수 있으니 사전을 이용해서 각 숫자가 몇 개씩 있는지를 분류하면 보다 편하게 점수를 계산할 수 있다. 각 숫자를 카운트한 후에는 하이카드 규칙에 따라 숫자를 정렬해두고, 점수와 함께 리턴하도록 한다.

점수 계산 함수는 (숫자값, 종류문자열) 로 구성된 튜플의 리스트를 입력으로 받고 (계급의 의한 점수, 하이카드 리스트)를 리턴하도록 한다. 두 선수의 계산 결과를 단순히 비교만 하더라도 승패를 쉽게 알 수 있다. 문제에서는 무승부는 없다고 했지만, 이 함수를 사용하면 무승부도 판정이 가능하다.

def calc(cards: list[tuple[int, str]]) -> tuple[int, list[int]]:
  score = 0
  d = dict()
  for n, _ in cards:
    d[n] = d.get(n, 0) + 1

  # 하이카드 리스트
  # sum([ ... ], []) 을 사용하면 리스트를 flatten 할 수 있다. 
  ns = sum([ [x] * y for x, y in\
             sorted(d.items(), key=lambda x: (x[1], x[0]), reverse=True)
           ],[])
  
  # 플러시 여부 체크, 플러시인 경우에 로열 플러시라면 바로 리턴
  if all(card[1] == cards[0][1] for card in cards):
    score = 80
    if ns == list(range(14, 9, -1)):
      return (200, ns)

  # 스트레이트 체크. ns 가 역순으로 정렬된 연속된 숫자임에 유의
  x = list(range(15, 0, -1))
  if x[-ns[-1]:-ns[0] + 1] == ns:
    score += 75

  # 4/3/2 검사
  if any(v == 4 for v in d.values()):
    score += 100
  if any(v == 3 for v in d.values()):
    score += 70
  score += sum(25 for v in d.values() if d == 2)

  return score, ns

승패 비교함수

텍스트 파일의 각 라인을 입력으로 받아서, 승패를 비교하는 함수를 작성해보자. 양 선수가 입력으로 받아야 하는 데이터의 형식은 똑같으므로 먼저 (숫자, 카드종류) 의 튜플로 분해한 리스트를 만들고 이를 절반씩 끊어서 계산하고 그 결과를 비교한다.

def testline(line: str) -> bool:
  N = "0123456790TJQKA"
  tx = [(N.index(x), y) for (x, y) in line.split()]
  return calc(tx[:5]) > calc(tx[5:])

최종 검사

이제 파일을 열고 각각의 라인에 대해서 testline() 에 전달하고 얻은 결과가 참인 경우만 카운트하면 된다. open() 함수로 텍스트 파일을 연 경우, 이 때의 파일 디스크립터는 문자열의 반복자(iterator) 역할을 겸하므로 아래와 같이 제너레이터 축약문법으로 간단하게 표현 가능하다. 대략 수십 ms 내에 답을 계산할만큼 효율이 좋다.

def main():
  with open('poker.txt') as f:
    print(sum(1 for line in f if testline(line)))

Exit mobile version