파이썬 강좌 – 실습 – 원카드 게임 만들기 (3) (작성중)

  1. 프롤로그
  2. 개발 첫걸음
    1. 컴퓨터 구성요소 – 컴퓨터는 어떤 걸 할 수 있나?
    2. 개발과 관련된 용어
    3. 파이썬의 선택 – 왜 파이썬인가?
    4. 파이썬 설치 – Hello World 출력하기
    5. Visual Studio Code 의 편리한 기능
    6. REPL과 콘솔 창 – 파이썬 동작시키기
  3. 파이썬 기초
    1. 기초 입출력 – 소통하기
    2. 변수와 대입 – 기억하기
    3. 연산자 – 계산하기
    4. 조건문 – 분기를 만들기
    5. 반복문 – 비슷한 작업을 반복하기
    6. 반복문 코딩하기
    7. 변수와 리스트 – 비슷한 변수들을 묶기
    8. for, range – 리스트의 항목을 다루기
    9. 함수와 메소드의 호출 – 편리한 기능 이용하기
    10. 모듈 설치와 사용 – 유용한 기능 끌어다 쓰기
    11. 문자열 – 텍스트 다루기
  4. 파이썬 중급
    1. 함수를 직접 만들기 – 자주 쓰는 기능을 묶기
    2. 딕셔너리, 튜플, 세트 – 변수를 다양한 방법으로 묶기
    3. 클래스와 객체 – 변수를 사람으로 진화시키기
    1. 상속 – 클래스를 확장하기
    2. 정체성과 동질성 – 객체의 성질
    3. 특별 메소드와 연산자 – 파이썬의 내부 작동방식 이해하기
    4. 다양한 함수 인수 – 유연한 함수 만들기
    5. 슬라이싱 – 리스트 간편하게 접근하기
    6. 지능형 리스트(List Comprehension) – 리스트 갖고 놀기
    7. namedtuple - 데이터 묶음 손쉽게 만들기
    8. 조건 표현식 (Conditional Expression) - 간단한 분기 나타내기
    9. 코드 스타일 - 코드의 일관성 유지하기
    10. 명령문, 표현식 – 문법을 이루는 것들
    11. 본격적인 검색 해보기
  5. 파이썬 고급
    1. 일급 함수 다루기
    2. NotImplementedError와 NotImplemented
    3. 어노테이션 – 수월하게 프로그래밍하기
    1. 내장 함수 톺아보기
    2. 예외와 에러 – 예상치 못한 상황에 대응하기 (v0.1)
    3. 변수의 범위 – 이름 검색의 범위
  6. 파이썬 심화
    1. 시퀀스와 반복자 – 반복과 순회를 자유자재로 다루기
    2. 데코레이터 – 함수의 기능을 강화하기
    3. 프로퍼티
    4. 제너레이터
    5. async와 await
    6. 객체로서의 클래스 – 클래스를 동적으로 정의하기
  7. 파이썬 프로젝트 실습
    1. 원카드 게임 만들기 (1)
    2. 원카드 게임 만들기 (2)
    3. 원카드 게임 만들기 (3) (작성중)
    4. 턴제 자동 전투 게임 만들기 (작성중)
  8. 실전 (파이썬 외적인 것들)
    1. 정규표현식 – 문자열을 검색하고 치환하기 (작성중)
    2. 유니코드 – 컴퓨터에서 문자를 표기하는 방법
    3. html, css, 인터넷 – 자동화 첫 걸음 내딛기
    4. 네트워크 – 인터넷으로 통신하는 방법
    5. 문서 – 문맥을 읽어보기

이번 시간이 원카드 게임 만들기의 마지막 시간입니다. 이번 시간에는 다음 4가지를 구현하며 완성시키도록 하겠습니다.

  1. 입력을 개선합니다. 현재 상황에서는 올바르지 않은 입력을 했을 시 에러를 뿜으며 프로그램이 종료되지만, 이제는 제대로된 입력을 하라고 메시지를 띄우며 다시 입력받도록 합니다.
  2. 낼 수 있는 카드가 있어도 먹는 행동을 구현하겠습니다. 먹는 행동은 충분히 전략적으로 이용할 수 있기 때문이죠. 0을 입력하면 카드를 먹도록 하겠습니다.
  3. 컴퓨터를 여러 명 추가하여 여러 명이서 게임을 할 수 있도록 해봅시다.
  4. 7, J, Q, K 등의 특수 카드를 구현합니다.

입력 개선하기

지난 시간에 이은 코드를 그대로 실행해보도록 하겠습니다.

:: last put card ::  [[[[♥5]
:: player's hand ::  [♣5] [♣3] [◆6] [♥6] [♥Q] [♠J] [♠7]
::   available   ::  [♣5] [♥6] [♥Q]
------------------------------
[  1] 플레이어의 차례입니다.
------------------------------
몇 번째 카드를 내시겠습니까?4
Traceback (most recent call last):
  File "c:/Users/tooth/Desktop/test2.py", line 190, in <module>
    if turn(player, False):
  File "c:/Users/tooth/Desktop/test2.py", line 123, in turn    
    selected = available[i]
IndexError: list index out of range

위 상황에서는 [♣5] [♥6] [♥Q]를 낼 수 있기 때문에 1~3의 입력이 들어와야 합니다. 하지만 4를 입력하게 되면 IndexError를 내뿜으며 프로그램이 종료됩니다. 만약 숫자가 아닌 그냥 문자를 입력하게 되면 어떻게 될까요? 이 또한 마찬가지로 에러를 내뿜게 되겠지요. 우리는 아래 수정할 코드를 보다시피, 입력 값을 곧바로 int()에 넣고 있는데, 여기에 숫자가 아닌 문자열이 들어가면 바로 ValueError를 일으킵니다. 이렇듯 잘못된 입력이 들어왔을 때 친절하게 메시지를띄워주고 다시 입력하도록 로직을 변경하려고 합니다.


이제 한번 순서도를 짜볼까요? 우선 카드를 낼 수 있다! 부터 시작하여 해당하는 카드를 뽑는다. 를 끝으로 하는 순서도입니다.

graph TD o[카드를 낼 수 있다!] --> a a[입력을 받는다] --> b{입력이<br>숫자인가?} b --> |Yes|c{입력이<br>범위 내인가?} c --> |Yes|d[해당하는 카드를 뽑는다.] b --> |No|a c --> |No|a

조건에 맞게 카드를 뽑기

범위 내인지 아닌지 체크하는 코드를 먼저 한번 생각해봅시다. 아마 ilen(available)을 비교하면 될 것 같은데, 사실 비교가 가능하려면 둘 다 숫자형이어야 하기 때문에 입력이 숫자인지 아닌지부터 판별하는 절차를 먼저 밟았습니다. 어쨌건 지난 시간에 공격 메커니즘을 짰을 때보다 훨씬 간단해 보이는군요.

우선 반복을 해야겠습니다. 사용자가 계속해서 올바르지 않은 입력을 할 가능성이 있으니까요. 그렇다면 조건을 어떻게 해야 할까요? 우리는 루프를 끝내는 조건이 명확하게 딱 변수 하나로 정할 수 없습니다. 숫자인지 판별한 다음 범위내에 있는지도 판별해야 하니 그 과정이 다소 길다 할 수 있습니다.


그렇다면 while True:로 해놓고 적당한 시점에 break를 하여 입력값이 올바르다는 것을 보증하면 되지 않을까요? 그렇다면 아래 두 가지 순서도로 할 수 있을 것입니다.

graph TD aa["시작 (1안)"]-->a a{while True:} a-->|무조건 True|b[입력을 받는다.] b-->c{숫자가 아닌가?<br>아니면 범위에<br>벗어났나?} c-->|Yes|d[잘못된 원인을 알려주고 continue]-->a c-->|No|e[항목을 뽑아내고<br>break하여 <br>루프를 끝냄.] e-->f[계속 진행]

continuebreak를 이용하는 방법

graph TD aa["시작 (2안)"]-->a a{while True:} a-->|무조건 True|b[입력을 받는다.] b-->c{숫자인가?<br> 그리고 범위<br>내인가?} c-->|Yes|d[항목을 뽑아내고 break]-->f[계속 진행] c-->|No|e[잘못된 원인을 알려준다<br>루프의 끝] e-->a

break 만을 이용하는 방법

천천히 화살표를 따라가다 보면 논리적으로 이해할 수 있으실 것입니다.


이제 이것을 코드로 옮겨봅시다. 우선 대략적인 구조를 먼저 짜보도록 합시다.

# 1번째 안
while True:
    if 올바른가?:
        입력값이 올바를 때 처리
        break
    입력값이 올바르지 않을 때 처리

# 2번째 안
while True:
    if 올바르지 않은가?:
        입력값이 올바르지 않을 때 처리
        continue
    입력값이 올바를 때 처리
    break

문자열이 숫자인지 아닌지 판별하는 방법

이제 조건을 생각해봅시다. 어떻게 문자열이 숫자인지 아닌지 판별할 수 있을까요? 당장 드는 생각은, 문자열을 처음부터 검사하여 하나라도 0에서 9까지가 아닌 숫자가 나온다면, 숫자가 아니라고 판별해버리는 것입니다. 아래 코드는 완전히 새로운 임시 파이썬 파일을 만들어, 독자적으로 구현해보았습니다.

i = input()
is_number = False
for c in i:
    if c not in "0123456789":
        break
else:
    is_number = True

print(is_number)
(1번째 실행)
3
True

(2번째 실행)
2r
False

(3번째 실행)
12312314578768769
True

위 코드를 총 3번 실행시켜서 기능을 테스트해보니 만족할 만 합니다. 문자열과 아닌 것을 구분을 잘 하네요.


눈물겹게 기능을 하나하나 구현하고 있습니다. 하지만, 문자열로 들어온 입력 값이 숫자인지 아닌지 판별하는 문제는 아마 코딩을 시작하는 인구의 99%가 겪었을 문제일 것입니다. 아주 흔한 상황에서 쓰이는 도구들은 누군가가 만들어놓았을 가능성이 높습니다. 실제로 파이썬에는 이 문제를 효율적으로 해결해주는 도구를 제공합니다. 물론 처음이니까 어떻게 쓰는지 모르는 건 당연합니다. 한번 검색해봅시다. 대충 검색해보셔도 됩니다!

파이썬 문자열 숫자 판별이라고 구글에 검색한 모습

오.. 정말로 있군요!! 여러 개의 글을 살펴보니 .isdigit()이 우리의 목적에 부합합니다. 문자열의 메소드인 isdigit을 호출하면 그 문자열이 우리가 흔히 아는 숫자라면 True를 반환하고, 아니라면 False를 반환합니다.


한번 더 실험을 해봅시다. 이것도 마찬가지로 그냥 독자적인 파일입니다.

i = input()
print(i.isdigit())
(1번째 실행)
3
True

(2번째 실행)
2r
False

(3번째 실행)
12312314578768769
True

오! 정말로 잘 작동합니다. 어쨌든 여기까지 하고, 최종적인 입력 개선 구현으로 가기 전에, 한번 스스로의 힘으로 코딩하여 완성해보도록 합시다. 스스로 코딩해야 실력이 늡니다. 정말 애를 써도 무엇이 잘못되었는지 모르겠다면 아래 최종 구현으로 넘어가주세요.


최종 구현

이제 정말로 코드로 작성해보도록 합시다. 아까 만들었던 두 가지 순서도를 기억해주세요. 바로 아래에 코드는 원래 작성되어 있던 부분입니다.

i = int(input("몇 번째 카드를 내시겠습니까?"))
i -= 1
selected = available[i]

이 부분을 다음과 같이 수정해보도록 하겠습니다.

# 1안
while True:
    i = input("몇 번째 카드를 내시겠습니까?")
    if not i.isdigit():
        print_message("숫자를 입력해주세요.")
        continue
    i = int(i) - 1
    if i >= len(available):
        print_message("범위 내 숫자를 입력해주세요.")
        continue
    selected = available[i]
    break

# 2안
while True:
    i = input("몇 번째 카드를 내시겠습니까?")
    if i.isdigit():
        i = int(i) - 1
        if i < len(available):
            selected = available[i]
            break
        else:
            print_message("범위 내 숫자를 입력해주세요.")
    else:
        print_message("숫자를 입력해주세요.")
:: last put card ::  [[[[◆A]
:: player's hand ::  [♣10] [♥Q] [♣A] [♣5] [♣8] [◆Q] [◆9]
::   available   ::  [♣A] [◆Q] [◆9]
------------------------------
[  1] 플레이어의 차례입니다.
[  2] 범위 내 숫자를 입력해주세요.
[  3] 숫자를 입력해주세요.
------------------------------
몇 번째 카드를 내시겠습니까?

위 예제에서는 4a를 차례로 입력했습니다. 4는 범위를 벗어났고, a는 숫자 자체가 아닙니다. 의도한 대로 잘 동작한다는 것을 알 수 있습니다.


어떤 기능을 구현할 때 쓸 수 있는 방법은 굉장히 많습니다. 명쾌한 정답 하나가 있는 상황은 생각보다 흔하지 않습니다. 하지만 생각해낸 대로 구현한다는 경험은 중요하기에, 우리가 생각했던 두 가지 방법을 모두 구현해보았습니다.

둘 중 하나만 선택한다 하면 저는 1안입니다. 왜냐하면 조건에 맞지 않는 부분과 조건에 부합하는 부분이 더 명확하게 구분되어 있기 때문입니다. 특히 break 만을 사용한 2안 구현에서는 if i.isdigit():print_message("숫자를 입력해주세요.")가 멀리 떨어져있지만, 1안에서는 if not i.isdigit():print_message("숫자를 입력해주세요.")가 딱 붙어있어 상대적으로 더 읽기 편합니다. 이건 저의 선택인 것이고, 여러분들은 여러분들이 편한 방법대로 하시면 되겠습니다. 이를테면 while 문 전체를 통째로 함수로 만들어 분리시키는 방법도 아주 좋을 것 같습니다!

다음 구현은 1안을 구현한 상태로 진행하도록 하겠습니다.


낼 수 있는 카드가 있어도 먹는 행동 구현

기능 정의

낼 수 있는 카드가 있음에도 의도적으로 먹는 행위는 원카드에서 금지되지 않습니다. 들고 있는 패가 아주 많은데 공격 카드가 몇 개 없다면, 공격 카드를 아끼기 위해 그냥 덱에 있는 카드 하나를 먹을 수도 있습니다. 이러한 전략적 행동을 구현해보도록 합시다. 우선 기능을 다음과 같이 정의합니다.

카드를 낼 수 있어도, 0이 입력된다면 카드를 먹는다.

카드를 낼지 말지 결정하는 순간은 카드를 선택하는 순간입니다. 그러므로 “카드를 안낸다”라는 의사 표현을 하기 위해서 약속된 예외 입력을 새로 만들어야 합니다.여기서는 0으로 정했습니다. 0이 입력되든 무엇이 입력되든 상관이 없겠습니다만, 단순히 선택하지 않겠다는 간략한 의미를 나타나기 위해 0을 사용했습니다.


문제 인식

자, 이제 무엇을 어떻게 수정해야 할까요? 막막합니다. 우선 지금까지 작성되어있는 코드에서, 언제 내고 언제 먹는지를 다시 한번 살펴봅시다. 이는 다음과 같습니다.

  • 낼 수 있는 카드가 있다면? 무조건 낸다.
  • 낼 수 있는 카드가 없다면?먹는다.

밑줄 쳐져있는 부분을 확장해야 합니다. 그래서 다음과 같이 수정해봅시다.

  • 낼 수 있는 카드가 있는데 먹고 싶다면? 먹는다.
  • 낼 수 있는 카드가 있고, 카드를 내고 싶다면? 낸다.
  • 낼 수 있는 카드가 없다면? 먹는다.

좋긴 합니다만, 이렇게 된다면 한가지 치명적인 단점이 생깁니다. 우리는 입력이 0인지 아닌지 판단하는 것은 if로 쉽게 할 수 있습니다만, 그 분기에서 카드를 먹는 행위를 작성한다면 코드가 상당히 중복되어져 버립니다! 게다가 예상하지 못한 오류까지 발생할 수 있네요! 다음 코드에서 주석이 달린 부분을 면밀하게 살펴보도록 합시다.

# ---------------- 카드 고르기 ---------------
is_available = len(available) > 0
if is_available: # 낼 수 있는 카드가 있을 때
    if isComputer:
        selected = random.choice(available)
    else:
        while True:
            i = input("몇 번째 카드를 내시겠습니까?")
            ### (중략) ###
            if i == 0 : # 카드 먹는 걸 선택하는 건지 판단 (예시)
                # -------------- 카드 먹기? ------------- ##b_1##
                print_message(f'{name}가 낼 수 있는 카드가 없어 {damage}장 먹습니다.')
                if not isComputer:
                    input("계속 하려면 엔터를 누르세요")
                is_attack = False
                for i in range(damage):
                    draw(hand)
                damage = 1
                break
            selected = available[i]
            break
    hand.remove(selected) # 카드를 그냥 먹는 경우에는 오류!  ##b_2##
    put.append(selected) # 카드를 그냥 먹는 경우에는 오류!
    ### (공격 카드 처리 중략) ###
else: # 낼 수 있는 카드가 없을때
# -------------- 카드 먹기? -------------
    print_message(f'{name}가 낼 수 있는 카드가 없어 {damage}장 먹습니다.')
    if not isComputer:
        input("계속 하려면 엔터를 누르세요")
    is_attack = False
    for i in range(damage):
        draw(hand)
    damage = 1

(이 코드는 잘못된 코드이므로 독자들이 코드를 구현하여 결과를 확인할 필요는 없습니다.)

일단 코드가 훨씬 못생겨집니다. 카드 먹기? 부분의 대략 7줄 부분이 중복 코드로 생겨버립니다. 중복된 코드는 보기 불편한 걸 넘어서서 코드를 이해하기도, 수정하기도 힘들게 합니다. 당장 성급하게 0을 입력했다고 해서 바로 카드를 먹으면 안될 것 같습니다. 공통적으로 적용할 수 있는 솔루션이 필요합니다.

그리고 더욱 중대한 오류가 있습니다. 카드를 고르는 if is_avilable: 블록 내부의 거의 마지막에, hand.remove(selected)put.append(selected)가 등장하는 부분을 주목해주세요. 이 부분에 다다르면, 우리는 selected가 무조건 어떤 카드로 선택되어있다고 간주합니다. 하지만, 우리는 카드를 선택하지 않는다는 선택은 구현해놓지 않았습니다! 위 프로그램을 그대로 실행하여 0을 입력한다 했을 때 다음 결과를 볼 수 있습니다.

:: last put card ::  [[[[♣5]
:: player's hand ::  [♠3] [♣K] [♠K] [♣9] [♣Q] [◆3] [♣J]
::   available   ::  [♣K] [♣9] [♣Q] [♣J]
------------------------------
[  1] 플레이어의 차례입니다.
[  2] 플레이어가 낼 수 있는 카드가 없어 1장 먹습니다.
------------------------------
계속 하려면 엔터를 누르세요
Traceback (most recent call last):
  File "c:/Users/tooth/Desktop/test2.py", line 208, in <module>
    if turn(player, False):
  File "c:/Users/tooth/Desktop/test2.py", line 142, in turn
    hand.remove(selected) # 카드를 고르지 않고 먹는다고 했을때, 오류!    
UnboundLocalError: local variable 'selected' referenced before assignment

카드를 선택하지 않고 그냥 먹는다면 selectedNone이 되어 삭제도 못하고 추가도 못하고 프로그램은 에러를 내뿜으며 죽습니다. 이렇게 꼬여버린 상황을 어떻게 타개할까요?


문제를 나누기

다음과 같이 구현하고 싶다고 했습니다.

  • 낼 수 있는 카드가 있는데 먹고 싶다면? ⇒ 먹는다.
  • 낼 수 있는 카드가 있고, 카드를 내고 싶다면? 낸다.
  • 낼 수 있는 카드가 없다면? 먹는다.

그리고, 굵게 밑줄 친 부분이 문제의 암덩어리라고 했습니다. 코드 중복과 에러가 발생합니다. 우리는 카드를 선택하고, 내고, 먹는 부분을 일일히 나눠야 합니다. 이번 절에서의 가장 핵심 내용입니다.

  • 낼 수 있는 카드가 있는데 카드를 내고 싶다면? 카드를 선택해둔다.
  • 0을 입력한다면 ? 선택해둔 카드는 없다.
  • 낼 수 있는 카드가 없다면? 선택하둔 카드는 없다.
  • 선택해둔 카드가 있다면? 카드를 낸다.
  • 선택해둔 카드가 없다면? 먹는다.

최종 구현

좋습니다. 이제 코드를 작성해봅시다. 카드를 내는 부분은 선택을 완전히 완료한 뒤에 내므로, 코드를 통째로 옮겨야 합니다. 시작은 카드를 선택하는 부분부터 입니다. 아래 코드를 참조해주세요! ## 3 ## 등의 링크를 클릭하면 설명으로 이동합니다.

# ----------- 카드 선택하기 ---------------------
selected = None ##a_1##
is_available = len(available) > 0
if is_available:
    if isComputer:
        selected = random.choice(available)
    else:
        while True:
            i = input("몇 번째 카드를 내시겠습니까? "
                "카드를 먹고 싶다면 0을 눌러주세요.") ##a_2##
            if not i.isdigit():
                print_message("숫자를 입력해주세요.")
                continue
            i = int(i) - 1
            if i >= len(available):
                print_message("범위 내 숫자를 입력해주세요.")
                continue
            if i != -1: ##a_3##
                selected = available[i]
            break
else: ##a_4##
    print_message(f'{name}가 낼 수 있는 카드가 없습니다.') 

# ------------선택한 카드 내기 ----------------------- #
if selected is not None: ##a_5##
    hand.remove(selected)
    put.append(selected)

    if is_attack_card(selected):
        if not is_attack:
            damage = get_damage(selected)
        else:
            damage += get_damage(selected)

        is_attack = True

    print_message(f'{name}가 {selected}를 냈습니다."') 

# ------------ 카드 먹기 -----------------------
else: ##a_6##
    print_message(f'{name}가 {damage}장 먹습니다.')  ##a_7##
    if not isComputer:
        input("계속 하려면 엔터를 누르세요")
    is_attack = False
    for i in range(damage):
        draw(hand)
    damage = 1

a_1(1.) selected 정의

우선 is_available를 정의할 때 같이 selectedNone으로 정의해둡니다. selected를 미리 정의해둔 이유는, 추후 카드를 선택하려고 했을 때, 먹는다는 선택이면 selected에 아무런 행동도 안함으로써 None으로 남아있도록 하기 위함입니다. selected를 미리 정의해놓지 않으면 selected에 접근하려는 시도조차 실패하여 에러가 뜹니다.

a_2(2.) 입력 메시지 변경

input의 메시지를 바꾸어줍니다. 0을 입력하게 되면 아무런 선택도 하지 않고 먹는다는 걸 명시합니다. 참고로, 단순한 문자열 리터럴이 이어져있으면 하나의 문자열로 인식합니다. 그래서 위와 같이 줄 하나가 너무 길어질 때 저렇게 줄을 구분하여 작성할 수 있습니다.

a_3(3.) 먹는다는 선택을 인식

i를 판별하여 먹는다는 선택을 인식합니다. 0이 아닌 -1과 비교한 이유는 우리가 입력값을 보정할 때 i = int(i) - 1를 하기 때문입니다. 만약 사용자가 0을 입력했다면 최종적으로는 -1이 됩니다. 만약 어떤 카드를 정말로 선택했다면, 거기에 대해서 selected에 선택한 카드를 설정합니다. 어떠한 카드도 선택하지 않았다면, 아무런 행동도 하지 않습니다. 왜냐하면 처음부터 selectedNone이었으니까요.

a_4(4.) 출력 메시지 변경 (1)

여기는 if is_available: 절에 이은 else 절이므로 낼 수 있는 카드 자체가 없는 상황입니다. 낼 수 있는 카드가 없다고 메시지를 추가해줍시다.

a_5(5.) 카드 내기 통째로 이동

카드를 내는 부분입니다. 카드 선택에 바짝 붙어있었던 카드 내기를 통째로 떨구어 놓았습니다. 이제 selected가 있는 경우에만 카드를 낼 수 있도록 합니다.

a_6(6.) 카드 먹기 부분

카드를 먹는 부분입니다. selectedNone일 경우에만 카드를 먹도록 합니다.

a_7(7.) 출력 메시지 변경 (2)

기존에는 무조건 카드를 낼 수 없어서 먹었는데, 이제는 플레이어가 원해서 카드를 먹을 수도 있으므로, 단순히 카드 몇 장 먹었다 하도록 메시지를 수정하도록 합니다.


중간 정리

지금까지 작성한 전체 코드는 다음과 같습니다.


컴퓨터 여러 명 추가하며 턴 시스템 개선

이번 절부터는 클래스에 대한 지식이 필요합니다. 클래스를 쓰지 않고서도 구현할 수 있지만 더 나은 가독성과 유지보수성을 위해 클래스를 사용합니다.

문제 파악

사용자에게 숫자 입력을 받아서 컴퓨터의 수를 조정해본다고 가정해봅시다. 우선은 감이 오지 않으니까 하드코딩으로 한 명을 추가하여 봅시다. 이름은 computer2 로 정합니다.

# 플레이어에게 카드 나누기

player = []
computer = []
computer2 = []

for i in range(7):
    player.append(deck.pop())
    computer.append(deck.pop())
    computer2.append(deck.pop())


# 낸 카드에 하나 올려놓기
put = []
put.append(deck.pop())

# 게임 시작
while True:

    if turn(player, False):
        break

    if turn(computer, True):
        break

    if turn(computer2, True):
        break

반복되는 부분이 보이시나요? 아래 내용은 그 반복되는 내용을 표로 정리한 것입니다.

반복되는 부분 설명
player = [] 패를 새롭게 만들어내는 부분
player.append(deck.pop()) 덱에서 7장을 나눠주는 부분
if turn(player, False): ... 턴을 시작하는 부분

컴퓨터의 수를 유동적으로 갖고가기 위해 반복되는 부분을 유연하게 바꿀 필요가 있습니다. 가장 먼저 와닿는 생각은, 플레이어와 컴퓨터를 담는 리스트를 하나 더 만들어서 관리하면 어떨까요? 겉으로 보았을 때에는 중첩 리스트가 되겠네요. 한번 시도해보도록 합시다.


일단 아래 코드는 작성하지 말고 눈으로만 봐주세요.

com_count = int(input('컴퓨터의 수를 입력해주세요. --> '))
people = []

# 플레이어 설정
people.append([])

# 컴퓨터 설정
for i in range(com_count):
    people.append([])

# 플레이어에게 카드 나누기
for i in range(7):
    for person in people:
        person.append(deck.pop())


# 낸 카드에 하나 올려놓기
put = []
put.append(deck.pop())

# 게임 시작
while True:
    for person in people:
        if turn(person, ...): ##g_1##
            break ##g_2##

people 리스트를 새롭게 정의했습니다. 이 리스트에는 기존의 playercomputer 들이 들어갑니다. 비슷한 것들을 묶음에 따라 플레이어에게 카드를 나눈거나 턴을 진행시킬 때에 for문을 이용해 편하게 반복 작업을 할 수 있게 되었습니다!


하지만 위 코드에는 두 가지 큰 문제점이 있습니다.

g_1(1.) turn 에 넘길 인수가 달라짐

플레이어와 컴퓨터는 turn 함수에 들어갈 인수가 다릅니다. 어떤 패가 플레이어의 패인지 컴퓨터의 패인지 확실히 구분짓기 위해 True, False로 나뉘었었는데, 지금 당장은 여기에 넣을 값이 애매합니다. 두개를 분리시켜서 작성해도 되지만 그렇게 하면 통일성이 사라지지요. 플레이어 및 컴퓨터의 패, 그리고 컴퓨터인지 여부를 결합시킬 방법이 필요해집니다.

우리는 플레이어 및 컴퓨터의 패와 컴퓨터인지 여부를 결합시킬 방법이 필요합니다. 그렇게 해야 게임 진행의 for에서 동일한 인터페이스로 turn 함수를 이용할 수 있습니다. 여기에 조금 더 나아가서, 컴퓨터의 이름을 컴퓨터마다 다르게 하기 위해 이름 정보도 추가해보고자 합니다. 즉 이제 새롭게 만들고자 하는 어떤 묶음은 다음 세 가지 정보를 포함해야 합니다.

  • 화면 상에 나타날 이름인 name
  • 카드를 들고 있는 패인 hand
  • 컴퓨터인지 여부인 is_computer

g_2(2.) break 가 쓸모 없어짐

여기서의 break는 안타깝게도 for 루프에 대응되는 break 입니다. 그래서 만약 어떤 플레이어가 승리하여 turn 함수가 true를 반환하더라도 제일 바깥의 while 문에는 닿지 않기 때문에 게임이 종료되지 않습니다! 바로 게임이 종료되도록 exit() 함수를 쓰거나 하는 방법도 있겠지만 그렇게 유용한 작업은 아닙니다. 이를 어떻게 해결해야 할까요?

중첩된 조건문과 break, 명확하지 않은 turn 반환값 등 문제를 다같이 수정해봅시다.


구현

namedtuple은 튜플의 항목에 이름으로 접근할 수 있도록 만든 것입니다. 플레이어의 정보를 묶어서 관리하기 위해 namedtuple을 이용할 것이기에, 아직 사용법을 모른다면 링크로 들어가셔서 관련 내용을 익히고 와주세요. 그리하여 namedtuple은 아래와 같이 호출될 것이고, Person을 이용해 세 가지 정보를 가지고 있는 객체를 만들 수 있게 됩니다.

Person = collections.namedtuple('Person', 'name hand is_computer') 

또한 턴이 넘어가는 구조를 아래와 같이 좀 더 고치도록 하겠습니다.

  • turn 함수에 전달하는 인수를 Person 으로 만든 객체 하나로 제한합니다. 만약 def turn(person): 과 같이 선언되어있다고 가정했을 때, turn 함수 내에서는 person.name, person.hand, person.is_computer 등과 같이 정보에 접근할 수 있습니다.
  • 특정 플레이어가 이기는지 어떤지 확인하기 위하여 우리는 turn 함수 내에서 hand의 길이를 검사했습니다. 하지만 이 로직을 turn 함수 바깥으로 빼와서 무한 루프 내에 구현하도록 하겠습니다. 즉 이제 turn 함수는 아무것도 반환하지 않습니다.
  • 플레이어를 순회하기 위해 for 루프를 쓰지 않습니다. 왜냐하면 for 루프는 단 한번만 순회하기 때문에, 잘 동작하는 무한한 순회를 구현하려면 추가 작업이 많아지기 때문입니다. 그리하여 아래와 같은 작업이 필요합니다.
    • 인덱스 변수 i를 두어 people[i]와 같이 Person 객체로 직접 접근하도록 합니다. 초기값은 0으로 합니다.
    • IndexError가 일어나지 않도록 i의 범위를 자동으로 잘 조절하기 위한 장치가 필요합니다.
    • 매 루프마다 i를 증감시켜줍니다.

이제 코드를 작성해봅시다. 코드의 가장 기초적인 부분을 바꾸었으므로 고칠 것이 은근히 많아집니다. 우리는 원카드 프로젝트를 진행하면서 게임의 끝과 시작을 제일 먼저 구현했었는데요, 기초가 얼마나 튼튼하게 잘 잡혀져 있냐에 따라 앞으로의 개발이 얼마나 순탄할지가 좌우된다는 점을 깨닫게 되실 것입니다.

import collections ##h_1##
# ... 중략 ...

def print_message(message): ##h_2##
    global put, deck, is_attack, messages, message_count, people
    player = people[0].hand

# ... 중략 ...

def turn(player): ##h_3##
    name = player.name
    hand = player.hand
    isComputer = player.is_computer

# ... 중략 ...

    # 삭제 ##h_4##
    # if len(hand) == 0:
    #     print_message(f"{name}가 이겼습니다!")
    #     return True
    # 
    # else:
    #     return False

# ... 중략 ...

# 플레이어 정보를 담는 namedtuple 생성
Person = collections.namedtuple('Person', 'name hand is_computer') ##h_5##
people = [] 

# 플레이어 설정
people.append(Person('플레이어', [], False)) ##h_6##

# 컴퓨터 설정
com_count = int(input('컴퓨터의 수를 입력해주세요. --> '))
for i in range(com_count): 
    people.append(Person(f'컴퓨터{i}', [], True)) 

# 플레이어에게 카드 나누기
for i in range(7):
    for person in people:
        person.hand.append(deck.pop())

# 낸 카드에 하나 올려놓기
put = []
put.append(deck.pop())

# 게임 시작
i = 0 ##h_7##
while True:
    current_person = people[i % len(people)] ##h_8##
    turn(current_person) ##h_9##
    if len(current_person.hand) == 0: ##h_10##
        print_message(f"{current_person.name}가 이겼습니다!")
        break
    i += 1 ##h_11##

h_1(1.) namedtuple을 사용하기 위한 import

collectionsimport 합니다.

h_2(2.) print_message 수정

player 전역 변수가 사라졌으므로 플레이어에 해당하는 people[0]을 이용해야 하는데요, 하위 내용을 수정하지 않기 위해 player에 새로 대입해주도록 합시다.

h_3(3.) turn 수정

Person 객체 하나만 받을 수 있도록 선언부를 수정합니다. 하위 내용을 모두 수정하는 건 귀찮으니 기존에 사용했던 변수인 name, hand, isComputer를 적절하게 대입해주도록 합니다.

h_4(4.) 승리 판단 보류

누군가 이겼는지 판단하기 위한 코드를 turn 함수 바깥으로 빼내기 위해 삭제합니다.

h_5(5.) namedtuplePerson 클래스 생성

새로운 클래스를 정의합니다. Person 으로 만든 객체는 name, hand, is_computer 항목을 지니고 있게 됩니다. 이 Person 객체들을 담고 있을 people이라는 빈 리스트도 만듭니다.

h_6(6.) 플레이어 추가

플레이어를 추가하기 위한 코드입니다. 플레이어를 만들면서 Person 객체를 만들고 있습니다. 컴퓨터를 추가할 때에도 동일한 방법을 사용합니다.

h_7(7.) 인덱스 변수 초기화

people[i]와 같이 직접 접근하기 위한 인덱스 변수 i를 정의합니다. 이러한 방법은 우리가 초기에 반복문에 대해 배웠을 때와 유사합니다.

h_8(8.) 인덱스 값 보정

i는 반드시 0에서 len(people)-1 사이에 존재해야 합니다. 0이 첫번째 요소를 나타내고 len(people)-1 이 마지막 요소를 나타내기 때문입니다. 이 범위를 넘어가게 되면 IndexError를 일으키며 프로그램은 종료됩니다. (음수 인덱스는 끝에서부터 시작하지만 일단 생각하지 맙시다.) 우리는 매번 if문을 통해 i의 값이 범위 내에 있는지 없는지 체크한 후 값을 보정해줄 수 있지만, 그렇게 되면 더 복잡해집니다. 복잡한 것은 피하는 게 상책입니다. 좋은 방법이 없을까요?

한 가지 기가 막힌 방법이 있네요. i % len(people) 의 값은 0 이상 len(people)-1 사이에 있다는 것이 보장됩니다. i의 값이 아무리 크든, 작든, 심지어 음수이든 이 조건은 충족됩니다. 이를 이용해 people에 직접 접근을 했습니다.

h_9(9.) turn 호출

turn 함수는 이제 아무것도 반환하지 않으므로 단순히 호출만 합니다. 그리고 인수를 Person 객체 하나만 받을 수 있도록 설계가 변경되었기 때문에, 그에 따라 current_person 하나만 인수로 넣어주도록 합니다.

h_10(10.) 승리 판단 추가

current_person으로 현재 플레이어가 턴을 마친 후 승리했는지 안했는지 판단할 수 있습니다. 본래 turn 함수 내에 있었던 내용과 유사합니다.

h_11(11.) 인덱스 증감

다음 플레이어를 나타나게 하기 위해 인덱스 변수 i를 변화시켜줍니다. 단순히 1만 증감해주도록 합니다. 수가 굉장히 커진다고 해도 people에 접근하는 과정에서 보정 과정을 거치므로 별다른 작업을 할 필요는 없습니다.


중간 코드


각종 특수 카드 구현


프로그래밍 문제: 모든 카드를 namedtuple로 변경하기

그냥 과제입니다.

  1. 프롤로그
  2. 개발 첫걸음
    1. 컴퓨터 구성요소 – 컴퓨터는 어떤 걸 할 수 있나?
    2. 개발과 관련된 용어
    3. 파이썬의 선택 – 왜 파이썬인가?
    4. 파이썬 설치 – Hello World 출력하기
    5. Visual Studio Code 의 편리한 기능
    6. REPL과 콘솔 창 – 파이썬 동작시키기
  3. 파이썬 기초
    1. 기초 입출력 – 소통하기
    2. 변수와 대입 – 기억하기
    3. 연산자 – 계산하기
    4. 조건문 – 분기를 만들기
    5. 반복문 – 비슷한 작업을 반복하기
    6. 반복문 코딩하기
    7. 변수와 리스트 – 비슷한 변수들을 묶기
    8. for, range – 리스트의 항목을 다루기
    9. 함수와 메소드의 호출 – 편리한 기능 이용하기
    10. 모듈 설치와 사용 – 유용한 기능 끌어다 쓰기
    11. 문자열 – 텍스트 다루기
  4. 파이썬 중급
    1. 함수를 직접 만들기 – 자주 쓰는 기능을 묶기
    2. 딕셔너리, 튜플, 세트 – 변수를 다양한 방법으로 묶기
    3. 클래스와 객체 – 변수를 사람으로 진화시키기
    1. 상속 – 클래스를 확장하기
    2. 정체성과 동질성 – 객체의 성질
    3. 특별 메소드와 연산자 – 파이썬의 내부 작동방식 이해하기
    4. 다양한 함수 인수 – 유연한 함수 만들기
    5. 슬라이싱 – 리스트 간편하게 접근하기
    6. 지능형 리스트(List Comprehension) – 리스트 갖고 놀기
    7. namedtuple - 데이터 묶음 손쉽게 만들기
    8. 조건 표현식 (Conditional Expression) - 간단한 분기 나타내기
    9. 코드 스타일 - 코드의 일관성 유지하기
    10. 명령문, 표현식 – 문법을 이루는 것들
    11. 본격적인 검색 해보기
  5. 파이썬 고급
    1. 일급 함수 다루기
    2. NotImplementedError와 NotImplemented
    3. 어노테이션 – 수월하게 프로그래밍하기
    1. 내장 함수 톺아보기
    2. 예외와 에러 – 예상치 못한 상황에 대응하기 (v0.1)
    3. 변수의 범위 – 이름 검색의 범위
  6. 파이썬 심화
    1. 시퀀스와 반복자 – 반복과 순회를 자유자재로 다루기
    2. 데코레이터 – 함수의 기능을 강화하기
    3. 프로퍼티
    4. 제너레이터
    5. async와 await
    6. 객체로서의 클래스 – 클래스를 동적으로 정의하기
  7. 파이썬 프로젝트 실습
    1. 원카드 게임 만들기 (1)
    2. 원카드 게임 만들기 (2)
    3. 원카드 게임 만들기 (3) (작성중)
    4. 턴제 자동 전투 게임 만들기 (작성중)
  8. 실전 (파이썬 외적인 것들)
    1. 정규표현식 – 문자열을 검색하고 치환하기 (작성중)
    2. 유니코드 – 컴퓨터에서 문자를 표기하는 방법
    3. html, css, 인터넷 – 자동화 첫 걸음 내딛기
    4. 네트워크 – 인터넷으로 통신하는 방법
    5. 문서 – 문맥을 읽어보기

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

Scroll to top