파이썬 강좌 – 실습 – 원카드 게임 만들기 (2)

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

지난 시간까지 대략적으로 핵심적인 기능만 작성해보았습니다.


비슷한 기능을 함수로 묶기

지금의 코드는 플레이어 부분과 컴퓨터 부분이 유사합니다. 완전히 똑같지는 않지만 구조는 비슷하기 때문에 공격 메커니즘을 추가한다고 했을 때 비슷한 코드를 다시 두번 작성해야 합니다. 이러한 귀찮음을 피하기 위해 비슷한 기능을 최대한 함수로 묶어보도록 합시다.

getAvailable

getAvailable은 주어진 상황에서 가능한 카드의 리스트인 available를 반환하는 함수입니다. 어떤 플레이어의 패인 hand와 마지막으로 놓여있는 카드인 last_hand 를 인수로 받습니다.

코드로는 다음과 같습니다.

# 가능한 카드 리스트를 반환
def getAvailable(hand, last_card):
    available = []
    for card in hand:
        if (card[0] == last_card[0]
            or card[1] == last_card[1]
            or card[0] == 'Joker'
            or put[-1][0] == 'Joker'):
            available.append(card)
    return available

이로써 available을 구하는 다소 긴 과정이 다음과 같은 코드로 압축될 수 있습니다.

# 플레이어 부분
available = getAvailable(player, put[-1])

# 컴퓨터 부분
available = getAvailable(computer, put[-1])

turn

비슷한 원리로 전체 과정을 줄여봅시다. turn 함수는 플레이어든, 컴퓨터든 간에 하나의 차례를 통째로 표현합니다. 함수를 정의하기 이전에 우리가 큰 while에서 어떻게 이 함수를 이용하고 싶은지 먼저 적어봅시다. 우리는 차례를 다음과 같이 간단하게 나타내고 싶습니다.

while True:
    turn(player)
    turn(computer)

하지만 turn 함수 입장에서, 들어오는 카드의 리스트가 player 것인지, computer 것인지 판단할 수 없습니다. 그러므로 인수 하나를 추가해서 컴퓨터라면 True, 그렇지 않다면 False를 넣을 수 있게 해봅시다.

while True:
    turn(player, False)
    turn(computer, True)

아직 turn 함수는 정의하지 않았습니다. 하지만 함수를 어떻게 호출할 것인지부터 적는다면 함수 정의를 더 쉽게 할 수도 있습니다.

모든 과정을 turn 안에 넣었다고 가정할 때, 문제는 이 무한루프를 멈출 방법이 없어졌다는 것입니다. 함수 안에서 break 해봤자 함수는 격리된 공간이기 때문에 외부의 while에 영향을 미칠 수 없습니다. 함수는 리턴 값으로 결과를 반환할 수 있습니다. 만약 플레이어가 이겼다면 True를, 그렇지 않다면 False를 반환하도록 합시다. 그렇다면 while을 멈출 수 있는 수단이 생깁니다.

while True:
  
    if turn(player, False):
        break

    if turn(computer, True):
        break

좋습니다. 이제 함수를 어떻게 호출하고 결과 값이 무엇이 나와야 하는지 정해졌습니다. 코드를 작성해봅시다.

def turn(hand, isComputer):

    # 전역 변수 접근
    global put, deck

    # 이름 정하기
    if isComputer:
        name = "컴퓨터"
    else:
        name = "플레이어"

    # 차례
    print(name, "의 차례입니다.")
    if not isComputer:
        print("현재 패 >>", hand)
    print("놓여진 카드 >>", put[-1])

    # 가능한 카드
    available = getAvailable(hand, put[-1])
    if not isComputer:
        print("낼 수 있는 카드:", available)

    # 낼 수 있는 카드가 있는 경우
    if len(available) > 0:
        if isComputer:
            selected = random.choice(available)
            print("컴퓨터가", selected, "를 냈습니다.")
        else:
            i = int(input("몇 번째 카드를 내시겠습니까?"))
            i -= 1
            selected = available[i]
        hand.remove(selected)
        put.append(selected)
    
    # 낼 수 있는 카드가 없는 경우
    else:
        print(name, "가 낼 수 있는 카드가 없어 먹습니다.")
        hand.append(deck.pop())

    if len(hand) == 0:
        print(name, "가 이겼습니다!")
        return True

    else:
        return False

putdeck은 함수의 외부에 있기 때문에 접근할 수 없지만, global 키워드를 이용하여 접근을 허용하고 있습니다. 또한 isComputer로 컴퓨터의 유무를 확인할 수 있도록 하였으나 TrueFalse 만으로는 어떤 뜻인지 알 수 없으므로 명시적으로 주체의 이름을 정해줍니다. 이 이름은 나중에 출력할 때 쓰입니다.

함수 내에서 필요할 때마다 if isComputer: 를 작성하여 컴퓨터로서 처리되어야 할 부분과 플레이어로서 처리되어야 할 부분을 구분합니다. 전체 코드의 줄 변화는 크게 없지만 우리는 이제 코드를 수정하기 용이해졌습니다. 전체 코드는 다음과 같습니다.


공격 메커니즘 만들기

대략적인 공격 메커니즘을 먼저 생각해봅시다.

graph TD st[턴이 시작된다] a{공격받는<br>상황인가?} am{낼 카드가<br>있는가?} b[더 상위의<br>공격카드를<br>낸다] c[공격을 계속한다] bn[공격받은 만큼<br>카드를 먹는다] a-->|Yes|am-->|Yes|b-->c am-->|No|bn an{낼 카드가<br>있는가?} any[카드를<br>고른다] ann[카드를 한장<br>먹는다] an-->|Yes|any an-->|No|ann a-->|No|an st-->a bn-->bn1[공격 상황을<br>종료한다] any-->any1{공격카드인가?} any1-->|Yes|sta["공격 상황을<br>시작한다"]

공격 메커니즘

하지만 찬찬히 생각해봅시다. 위 과정을 모두 코드로 짜게 된다면 코드의 길이가 어마어마하게 길어질 것입니다. 왜냐하면 다음 세 가지 행동이 완전히 똑같지는 않지만 많은 부분이 같음에도 불구하고 별도의 코드로 작성되어야 할 것이기 때문입니다.

코드 표현 공격받는 상황 일반 상황
낼 수 있는 카드를 체크 (available 불러내기) 일반 상황과 동일하되, 상위 공격 카드만 추가적으로 걸러내면 됨 이미 작성한 코드와 동일
카드 내기 일반 상황과 동일하되, 낼 카드가 공격 카드이면 공격 상황으로 전환 이미 작성한 코드와 동일
카드 먹기 일반 상황(1장)과 같은 코드를, 누적된 공격만큼 반복하면 됨. 이미 작성한 코드와 동일

그러므로 우리는 다음과 같은 흐름으로 고쳐볼 것입니다.

graph TD st[턴이 시작된다]-->s1["낼 수 있는 카드를 구해본다<br>(일반적인 상황)"] s1-->s2{공격받는<br>상황인가?} s2-->|Yes|a1[낼 수 있는 카드 중<br>가능한 공격카드를<br>한번 더 찾는다] a1-->s2 s2-->|No|s3{낼 카드가<br>있는가?} s3-->|Yes|s4{공격받는<br>상황인가?} a{공격받는<br>상황인가?} am{낼 카드가<br>있는가?} b[더 상위의<br>공격카드를<br>낸다] c[공격을 계속한다] bn[공격받은 만큼<br>카드를 먹는다] a-->|Yes|am-->|Yes|b-->c am-->|No|bn an{낼 카드가<br>있는가?} any[카드를<br>고른다] ann[카드를 한장<br>먹는다] an-->|Yes|any an-->|No|ann a-->|No|an st-->a bn-->bn1[공격 상황을<br>종료한다] any-->any1{공격카드인가?} any1-->|Yes|sta["공격 상황을<br>시작한다"]

수정한 공격 메커니즘


여러분은 이제 중복된 코드가 보인다면 불편하셔야 합니다. 중복된 코드를 없애고 순서를 더 원활하게 조정할 수 있기 위해 플레이어들을 담은 리스트 players와 순서 변수 i를 추가하여 더 똑똑하게 관리해봅시다.

# 게임 시작
players = [player, computer]
i = 0
while True:

    # i가 0이 아니면 컴퓨터라고 간주
    if i != 0:
        isComputer = True
    else:
        isComputer = False

    if turn(players[i], isComputer):
        break

    # 차례 조정
    i += 1
    if i >= len(players):
        i -= len(players)

뭔가가 더 복잡해진 것 같나요?

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다

Scroll to top