- 프롤로그
- 개발 첫걸음
- 파이썬 기초
- 파이썬 중급
- 파이썬 고급
- 내장 함수 톺아보기
- 예외와 에러 – 예상치 못한 상황에 대응하기 (v0.1)
- 변수의 범위 – 이름 검색의 범위
- 파이썬 심화
- 시퀀스와 반복자 – 반복과 순회를 자유자재로 다루기
- 데코레이터 – 함수의 기능을 강화하기
- 프로퍼티
- 제너레이터
- async와 await
- 객체로서의 클래스 – 클래스를 동적으로 정의하기
- 파이썬 프로젝트 실습
- 원카드 게임 만들기 (1)
- 원카드 게임 만들기 (2)
- 원카드 게임 만들기 (3) (작성중)
- 턴제 자동 전투 게임 만들기 (작성중)
- 실전 (파이썬 외적인 것들)
- 정규표현식 – 문자열을 검색하고 치환하기 (작성중)
- 유니코드 – 컴퓨터에서 문자를 표기하는 방법
- html, css, 인터넷 – 자동화 첫 걸음 내딛기
- 네트워크 – 인터넷으로 통신하는 방법
- 문서 – 문맥을 읽어보기
지난 시간까지 대략적으로 핵심적인 기능만 작성해보았습니다.
비슷한 기능을 함수로 묶기
지금의 코드는 플레이어 부분과 컴퓨터 부분이 유사합니다. 완전히 똑같지는 않지만 구조는 비슷하기 때문에 공격 메커니즘을 추가한다고 했을 때 비슷한 코드를 다시 두번 작성해야 합니다. 이러한 귀찮음을 피하기 위해 비슷한 기능을 최대한 함수로 묶어보도록 합시다.
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
put
과 deck
은 함수의 외부에 있기 때문에 접근할 수 없지만, global
키워드를 이용하여 접근을 허용하고 있습니다. 또한 isComputer
로 컴퓨터의 유무를 확인할 수 있도록 하였으나 True
와 False
만으로는 어떤 뜻인지 알 수 없으므로 명시적으로 주체의 이름을 정해줍니다. 이 이름은 나중에 출력할 때 쓰입니다.
함수 내에서 필요할 때마다 if isComputer:
를 작성하여 컴퓨터로서 처리되어야 할 부분과 플레이어로서 처리되어야 할 부분을 구분합니다. 전체 코드의 줄 변화는 크게 없지만 우리는 이제 코드를 수정하기 용이해졌습니다. 전체 코드는 다음과 같습니다.
공격 메커니즘 만들기
공격 메커니즘 구상하기
대략적인 공격 메커니즘을 먼저 생각해봅시다.
공격 메커니즘
위의 메커니즘은 아주 러프하게 작성한 것입니다. 본격적으로 코드를 작성하기 전에 조금 더 생각을 정리해봅시다. 상황이 더 복잡해졌고, 중복되는 코드가 많아질 예정입니다. 무작정 코드를 작성하다가는 밀려드는 코드에 정신을 차리지 못할 것입니다!!
우선 게임을 좌지우지하는 두 가지 상황이 있습니다. 첫 번째는 공격 상황인지의 여부이고 두 번째는 그에 따라 낼 수 있는 카드가 존재하는 지의 여부입니다. 이를 각각 is_attack
과 is_available
이라는 이름으로 부르겠습니다. 이 변수를 어떻게 다룰지는 후술하겠습니다. 일단 이 변수에 따라서 영역을 분할해 봅시다.
영역 | 영향 받는 것 | 영향을 주는 것 |
---|---|---|
턴 시작 | – | – |
낼 수 있는 카드 고르기 | is_attack 에 따라 낼 수 있는 카드가 다름. |
낼 수 있는 카드가 없다면 is_avilable = False , 낼 수 있다면 is_available = True |
카드 내기 | is_available 이 True 라면 실행 |
공격 카드일시 is_attack 를 True 로 설정 |
카드 먹기 | is_available 이 False 라면 실행 |
is_attack 이 True라면, is_attack 를 False 로 설정 |
턴 끝 | – | – |
이렇게 분할하는 이유는, 실제로 코딩을 할 때 어떤 부분에서 무엇을 처리하는지 확실히 하기 위함입니다. if
문을 그대로 따라가면서 모든 기능을 집어넣다 보면 유지보수가 굉장히 어려워집니다. 그리고 영역을 나눠놓으면 나중에 함수로 기능을 분리시키기도 편리합니다.
위 표에서는 아직 하나의 기능을 고려하지 않았습니다. 바로 카드를 먹을 때, 공격을 받는 상황이었다면 공격받은 만큼 먹었다는 것이죠. 우리는 bool
형의 두 가지 변수밖에 가지지 않았습니다. 공격을 얼마나 받았는지를 저장하는 변수가 또 하나 더 필요해졌습니다. 그 변수의 이름은 damage
로 설정하고 타입은 숫자형으로 하겠습니다. 그리고 이제 여러 방면에서 damage
가 다루어집니다.
영역 | 영향 받는 것 | 영향을 주는 것 |
---|---|---|
턴 시작 | – | – |
낼 수 있는 카드 고르기 | is_attack 에 따라 낼 수 있는 카드가 다름. |
낼 수 있는 카드가 없다면 is_avilable = False , 낼 수 있다면 is_available = True |
카드 내기 | is_available 이 True 라면 실행 |
공격 카드일시 is_attack 를 True 로 설정. 처음 공격이라면 damage 를 공격만큼만 설정하고, 이어지는 공격이라면 damage 를 증감한다. |
카드 먹기 | is_available 이 False 라면 실행, is_attack 에 따라 얼마나 먹는지 달라짐 |
공격 상황이라면 damage 만큼 카드를 먹고 damage 를 0 으로 초기화 후 공격 상황 종료. 공격 상황이 아니라면 카드를 1 개 먹음. |
턴 끝 | – | – |
우리는 이제 코드를 직접 짜지 않고도 머릿속으로(!) 코드를 최적화할 수도 있습니다. 코드를 어디서 어떻게 효율적으로 만드는가는 많이 코딩해봐야 감이 생깁니다. 카드 먹기 부분을 조금 더 손을 봅시다. 우리는 조건에 따라 분기해야 하는 비슷한 부분을, 묶어줄 수 있습니다. 저것만 따로 순서도를 만들어봅시다.
카드 먹기 부분
곰곰히 생각해봅시다. 왼쪽은 damage
만큼 카드를 먹고, 오른쪽은 1
만큼 카드를 먹습니다. 근데 만약에 damage
가 1
이라면 분기를 나눌 필요도 없이 damage
만큼만 먹는 코드 하나만 있으면 충분하지 않을까요? 그렇다면 공격상황이 아니어도 처음부터 damage
가 1
이도록 만들면 될까요? 그런데 그게 가능할까요? 한 번 해봅시다.
카드 먹기 부분 수정 1
그럴싸합니다. damage
를 초기화 할 때 1
로 하니까 상관없을 것 같고, 어차피 나중에 공격을 시작할 때에는 1
에서 증감하는 것이 아니라 정확한 값으로 공격을 대입하는 것이니 1
이라는 값이 자연스럽게 버려질 것입니다.
계속해서 수정해봅시다. 사실 공격 상황이든 상황이 아니든 일단 먹는다면, 공격이 종료된다고 보아도 됩니다. 공격 상황이 아닐 때에도 굳이 is_attack = False
라고 한다면 자원의 낭비라고 생각될 수도 있으나 코드 효율성을 생각한다면 훨씬 지구에 이득입니다. damage = 1
구문도 마찬가지입니다. 이렇게 모든 항목을 조건에서 제외하면 분기 자체가 사라지는 기적이 생깁니다!
카드 먹기 부분 수정 2
수정한 것을 바탕으로 영역 표를 다시 한번 수정해봅시다. is_attack
에 따른 분기가 사라지니까 영향 받는 것에서도 is_attack
이 사라집니다.
영역 | 영향 받는 것 | 영향을 주는 것 |
---|---|---|
턴 시작 | – | – |
낼 수 있는 카드 고르기 | is_attack 에 따라 낼 수 있는 카드가 다름. |
낼 수 있는 카드가 없다면 is_avilable = False , 낼 수 있다면 is_available = True |
카드 내기 | is_available 이 True 라면 실행 |
공격 카드일시 is_attack 를 True 로 설정. 처음 공격이라면 damage 를 공격만큼만 설정하고, 이어지는 공격이라면 damage 를 증감한다. |
카드 먹기 | is_available 이 False 라면 실행, is_attack 에 따라 얼마나 먹는지 달라짐 |
무조건 공격 상황을 종료하고 damage 만큼 카드를 먹은 뒤 damage 를 1로 초기화함. |
턴 끝 | – | – |
카드 내기, 카드 먹기 구현
너무 머리만 쓰니까 머리가 지끈지끈합니다. 이제 코드로 적어봅시다. 우선 영역을 주석으로 확실하게 표시해줍니다.
# ----------- 낼 수 있는 카드 고르기 ---------------
available = getAvailable(hand, put[-1])
if not isComputer:
print("낼 수 있는 카드:", available)
### 중략 ###
# ----------- 카드 고르기 ---------------------
if len(available) > 0:
### 중략 ###
# ------------ 카드 먹기 -----------------------
else:
print(name, "가 낼 수 있는 카드가 없어 먹습니다.")
hand.append(deck.pop())
그 다음 변수를 정의해줍니다. 특히 is_attack
과 damage
변수는 턴이 지나도 계속해서 기억이 되어야 하므로 turn
함수의 바깥에서 정의해주고, global
로 땡겨옵니다. is_available
또한 turn
함수 내에서 조건에 맞게 정의해줍니다.
is_attack = False
damage = 1
def turn(hand, isComputer):
# 전역 변수 접근
global put, deck, is_attack, damage
### 중략 ###
# ----------- 카드 고르기 ---------------------
is_available = len(available) > 0
if is_available:
낼 수 있는 카드를 구하는 부분부터 생각해봅시다. 근데 또 머리가 아픕니다. 공격 상황일때 덱 위에 올라가 있는 카드에 따라 낼 수 있는 카드가 한정되어 있습니다. 예를 들어 2
가 올라가 있으면 Joker
를 낼 수 있지만, Joker
가 올라가 있다면 a
를 낼 수 없습니다. 이 상황은 조금 있다가 다시 살펴보도록 합시다. 쉬운 것부터 먼저 구현해보도록 하자구요!
우선 카드를 내는 부분부터 설정해봅시다. 우리는 여기서 새로운 함수 2개를 만들 것입니다. 바로 카드가 공격 카드인지 판별하는 is_attack_card
함수와, 카드의 공격력을 계산하는 함수인 get_damage
함수입니다. 이런 기능은 다른 곳에서도 쓰일 가능성이 많으므로 처음부터 함수로 만들도록 합시다.
def is_attack_card(card):
return card[0] == 'Joker' or card[1] in ['A', '2']
def get_damage(card):
damage = 0
if card[0] == 'Joker':
if card[1] == 'colored':
damage = 10
else:
damage = 5
elif card[1] == 'A':
damage = 3
elif card[1] == '2':
damage = 2
return damage
좋습니다. 이제 카드 내는 부분을 수정해봅시다.
# ----------- 카드 고르기 ---------------------
is_available = len(available) > 0
if is_available:
### 중략 ###
if is_attack_card(selected):
if not is_attack:
damage = get_damage(selected)
else:
damage += get_damage(selected)
is_attack = True
처음 공격했는지를 판단하기 위해 not is_attack
으로 처음에 먼저 체크를 하여 damage
를 설정해주는 모습입니다. 마지막으로 is_attack
을 True
로 설정합니다.
이제 카드를 먹는 부분을 수정해봅시다. 더 쉽습니다!
# ------------ 카드 먹기 -----------------------
else:
print(name, "가 낼 수 있는 카드가 없어", damage, "장 먹습니다.")
is_attack = False
for i in range(damage):
hand.append(deck.pop())
damage = 1
혹시 눈치 빠르신 분이 계실 지는 모르겠지만, 카드 먹는 부분에는 치명적인 오류가 있습니다. 바로 deck
에서 모든 카드를 뽑았을 때, 빈 deck
에서 pop
가 실행되는 에러입니다. 아직 직접적으로 문제가 나오지는 않았지만 언젠가 반드시 터질 문제입니다. 일단 낼 수 있는 카드를 구하는 부분을 먼저 구현하도록 하겠습니다.
낼 수 있는 카드 고르기 구현
현재 상황이 공격 상황인지 아닌지에 따라 낼 수 있는 카드가 달라지는데, 이 부분을 더 자세히 알아보겠습니다.
공격 상황 | 일반 상황 |
---|---|
put[-1] 의 카드는 무조건 공격 카드임. 이 카드보다 더 높은 공격력을 가진 카드이면서 모양이 같아야함. 단, 조커는 색깔과 공격력에 상관없이 낼 수 있음. |
put[-1] 이 조커인 경우 아무 카드나 낼 수 있음. 조커가 아니라면 모양이 같거나, 숫자가 같거나 해야 함. |
저 상황을 코딩할 수 있는 상황으로 간단하게 만들어야 합니다. 변수는 다양합니다. 공격 상황과 일반 상황을 저렇게 나누어놓은 것 이외에도, hand
조커인지, 덱에 놓여져 있는 카드가 조커인지에 따라 또 달라질 수 있습니다. 쉬운 것부터 차근차근 해나가봅시다.
우선 일반 상황일 때 놓여져 있는 카드가 조커라면, 이 말인 즉슨 내가 조커를 내고 상대가 다 카드를 와장창 받아먹고 그 다음 내 턴이 왔다는 뜻이므로, 내 hand
에 있는 어떤 카드든지 전부 다 낼 수 있습니다. 이 경우를 가장 먼저 처리합니다. 이제 hand
의 카드 한장 한장이 가능한지 살펴보아야겠습니다. 우선 hand
에 있는 카드가 조커면 공격 상황이든 아니든 무조건 낼 수 있으므로, 무조건 포함시킵니다.
그 다음으로는, 공격 상황이든 일반 상황이든 모양과 숫자가 모두 다른 카드는 절대 낼 수 없으므로 우선 제외시킵니다. 그 다음 공격 상황에 대해서만, 놓여져 있는 카드보다 공격력이 높은 카드를 낼 수 있도록 포함시킵니다. 그러면 완성입니다.
이를 순서로 표현하면 다음과 같습니다.
수정된 getAvailable
의 순서 1
이제 한번 정리되었으니 코딩을 해봅시다. 공격력을 얻는 부분은 우리가 get_damage
함수를 구현했으니 손쉽게 할 수 있을 것입니다. 그리고 공격 상황인지를 알기 위해 변수를 얻어와야 하는데, global is_attack
하는 방법이 있겠지만, 간단한 변수이기 때문에 인수로 하나 추가해 주었습니다. 리스트의 extend
함수는 인수로 들어가는 또 다른 리스트를 그대로 확장한다는 의미입니다.
def getAvailable(hand, last_card, is_attack):
available = []
if not is_attack and last_card[0] == 'Joker':
available.extend(hand)
return available
for card in hand:
if card[0] == 'Joker':
available.append(card)
elif (card[0] != last_card[0]
and card[1] != last_card[1]):
continue
elif is_attack:
if get_damage(card) >= get_damage(last_card):
available.append(card)
else:
available.append(card)
return available
함수의 정의 부분을 바꾸었으니 이제 호출 부분도 바꾸어줍시다.
# ----------- 낼 수 있는 카드 고르기 ---------------
available = getAvailable(hand, put[-1], is_attack)
if not isComputer:
print("낼 수 있는 카드:", available)
이제 비로소 제대로 작동하는지 확인할 수 있습니다! 한번 실행해봐서 문제가 없는지 테스트를 해봅시다. 아래는 지금까지 작성한 모든 코드입니다.
연습 문제: 카드 먹기 보완
아까 카드 먹기에 대한 심각한 결함이 있다고 하였죠? 실제로 게임을 진행하다 보면 다음과 같은 에러를 맞닥뜨립니다.
( ... 중략 )
플레이어 가 낼 수 있는 카드가 없어 5 장 먹습니다.
Traceback (most recent call last):
File "c:/Users/tooth/Desktop/test.py", line 144, in <module>
if turn(player, False):
File "c:/Users/tooth/Desktop/test.py", line 99, in turn
hand.append(deck.pop())
IndexError: pop from empty list
이 문제를 해결하기 위해 카드를 먹는 draw
함수를 새롭게 정의하세요. 이 함수는 deck
에 카드가 없을 때 put
에 가장 위에 있는 카드 한 장만 남기고 섞어 다시 deck
에 넣는 기능을 포함합니다. 이 기능이 있으면 절대 에러가 나지 않겠지요. draw
함수는 인수로 어떤 hand
를 가져옵니다. draw
함수에서 deck
과 put
에 대한 접근은, turn
함수가 그래왔던 것처럼 함수 내부에서 global
로 선언하여 접근할 수 있도록 합니다. 함수를 정의한 후 호출 부분까지 완전히 구현해보도록 합시다.
풀이
deck
에서 모든 카드를 먹었기 때문에 deck
은 텅텅 비어있게 되었는데, 이를 어떻게 처리해야 합니다. put
에서 제일 위에 있는 카드 하나를 제외하고, 모든 카드를 다시 섞에 deck
에 놓는 작업을 해야 합니다. draw
함수를 만들어봅시다.
def draw(hand):
global put, deck
hand.append(deck.pop())
if len(deck) == 0:
print("카드를 다시 섞습니다!")
last_card = put.pop()
random.shuffle(put)
put, deck = deck, put
put.append(last_card)
리스트는 가변 객체이므로 단순한 대입문은 이름을 추가시켜주는 것과 같습니다. 그러니까 ls1 = ls2
을 하게 되면, ls2
을 가리키는 리스트의 이름이 ls1
, ls2
두 개가 된다는 뜻입니다. 그러므로 put, deck = deck, put
구문은 실제 데이터를 바꾸는 게 아니라 그냥 이름만 떼서 서로 바꾸는 작업입니다. 커다란 창고의 출입문에 푯말만 서로 바꾼다고 상상하시면 됩니다. 함수의 정의 부분을 새로 만들었으니 호출할 부분을 수정해 봅시다.
# ------------ 카드 먹기 -----------------------
else:
print(name, "가 낼 수 있는 카드가 없어", damage, "장 먹습니다.")
is_attack = False
for i in range(damage):
draw(hand)
damage = 1
좋습니다! 원하는 대로 카드가 잘 섞입니다.
연습 문제: print
개선하기
지금은 너무 보기가 힘듭니다. 카드를 보여주는 방식이 너무 파이썬에서 보여주는 방식 같습니다. ('♣', '10')
말고 ♣10
이렇게 간단하게 표현하면 되지 않을까요?, 그리고 계속 아래에서 새로운 메시지가 올라오니 정신이 사납습니다. 중요한 건 고정시키는 방법은 없을까요? 위쪽에 현재 나와있는 카드, 플레이어의 패, 플레이어가 낼 수 있는 패를 고정시키고 아래쪽에는 메시지가 계속 업데이트 되는 식으로 만드는 겁니다! 하는 김에 메시지에 숫자를 매겨서 얼마나 게임이 진행됐는지도 체크해봅시다. 아래는 실행 결과를 먼저 구상해본 것입니다.
:: last put card :: [[[[JokerBlack]
:: player's hand :: [♣10] [♥9] [◆8]
:: available ::
---------------------------------------------------
[ 23] 컴퓨터 의 차례입니다.
[ 24] 컴퓨터 가 낼 수 있는 카드가 없어 3 장 먹습니다.
[ 25] 플레이어 의 차례입니다.
[ 26] 몇 번째 카드를 내시겠습니까?1
[ 27] 컴퓨터 의 차례입니다.
[ 28] 컴퓨터가 ('Joker', 'black') 를 냈습니다.
[ 29] 플레이어 의 차례입니다.
[ 30] 플레이어 가 낼 수 있는 카드가 없어 8 장 먹습니다.
이제 순차적으로 과제를 부여하도록 하겠습니다.
- 우선 카드를 좀 더 간편하게 출력할 수 있도록 바꾸어봅시다.
card_str
함수는 카드 하나를 인수로 받고,[♣10]
형식의 문자열을 반환합니다. 이 함수를 이용해서 출력까지 해보세요. - 이제 완전하게 출력하는
hand_str
함수를 만들어봅시다. 이 함수는 패가 담긴 리스트인hand
하나를 입력받고, 안의 내용을 모두 문자열로 반환합니다. 반드시card_str
함수를 이용하는 내용을 포함시키세요. 가능하다면str
의join
기능을 활용하세요.
함수 내부에서print
를 쓰지 않는 이유는, 앞으로 우리가 고정위치로 출력할 것임에 따라 실제 값이 필요하기 때문입니다. 이 또한 제대로 작동하는지까지 테스트해보시기 바랍니다. - 화면의 특정 위치에 어떻게 문자열을 고정시킬지 고민해봅시다. 기나긴 사색 보다는 인터넷 검색을 적극적으로 이용해봅시다.
- 메시지에 번호를 매겨야 합니다. 새로운 변수가 필요할 것 같습니다. 이것도 어떻게 구현할지 고민해봅니다.
- 3번과 4번을 구현합니다.
풀이
card_str
함수부터 먼저 구현합니다.
def card_str(card):
return f'[{card[0]}{card[1]}]'
그렇게 어렵지 않습니다. f-string을 이용하면 손쉽게 해결할 수 있습니다. 실제로 출력하는 것을 테스트해봅시다. 아직 엉성합니다. 뒤에서 깔끔하게 만들어봅시다.
# 차례
print(name, "의 차례입니다.")
if not isComputer:
print("현재 패 >> ", end="")
for card in hand:
print(card_str(card) + " ", end="")
print("")
### 후략 ###
플레이어 의 차례입니다.
현재 패 >> [♥9][♠8][◆8][♥J][♥A][◆3][◆10]
그 다음 hand_str
함수를 구현합니다. str
의 join
기능은 배열의 여러 요소에서 하나의 문자열로 변환시킬 때 아주 편리한 도구입니다. 이것을 활용할 것입니다.
def hand_str(hand):
ls = []
for card in hand:
ls.append(card_str(card))
return " ".join(ls)
파이썬에서 제공하는 map
이라는 함수를 이용하면 함수의 내용을 단 한줄로도 구현할 수 있습니다.
def hand_str(hand):
return " ".join(map(card_str, hand))
함수를 호출할 수 있도록 turn
함수의 내부를 수정해줍시다. 만든 card_str
과 hand_str
두 가지 함수를 적절히 사용합니다.
# 차례
print(name, "의 차례입니다.")
if not isComputer:
print("플레이어의 패 >> ", hand_str(hand))
print("놓여진 카드 >>", card_str(put[-1]))
# ----------- 낼 수 있는 카드 고르기 ---------------
available = getAvailable(hand, put[-1], is_attack)
if not isComputer:
print("낼 수 있는 카드:", hand_str(available))
아래는 결과입니다. 보기만 해도 눈이 편해졌습니다!
플레이어의 패 >> [◆K] [◆8] [♥10] [♣A] [♥A] [♠9]
놓여진 카드 >> [◆6]
낼 수 있는 카드: [◆K] [◆8]
자, 이제 고민의 시간입니다. 어떻게 화면에 고정시킬 수 있을까요? 화면이 무슨 리스트도 아니고 대괄호로 특정한 자리를 집어낼 수 있는 것도 아니고, 어떻게 아래 쪽만 변하도록 할까요? 이럴 때에는 발상의 전환을 해야 합니다. 특정 부분만 움직이게 만드는 것은 화면을 모두 삭제하고 고정된 요소는 똑같은 위치에 출력하고 바뀌는 요소는 다른 위치에 출력하라는 것과 일맥상통합니다!
이것을 어떻게 가능하게 할 수 있을까요? 콘솔 화면 삭제 등으로 인터넷에 검색해봅시다. 검색 과정은 생략하도록 할게요. 방법은 os.system("cls")
(맥은 os.system("clear")
)를 이용하는 것입니다. os
모듈에서는 시스템 명령어를 사용할 수 있는 system
함수를 가지고 있습니다. cls
명령어는 콘솔에 출력되어 있는 모든 텍스트를 삭제하라는 뜻입니다.
매번 메시지를 화면에 띄울 때마다 전부 다 지우고 처음부터 작성하는 로직이 필요해졌습니다. 다음은 그 역할을 하는 print_message
함수를 구현한 모습입니다.
message_count = 0
messages = []
def print_message(message):
global put, deck, player, is_attack, messages, message_count
os.system("cls")
output = []
output.append(f":: last put card :: [[[{card_str(put[-1])}")
output.append(f":: player's hand :: {hand_str(player)}")
output.append(f":: available :: {hand_str(getAvailable(player, put[-1], is_attack))}")
output.append("-" * 30)
message_count += 1
messages.append(message)
if len(messages) == 16:
messages.pop(0)
for i, m in enumerate(messages) :
output.append(f'[{message_count - len(messages) + i + 1:>3}] {m}')
output.append("-" * 30)
print("\n".join(output))
이제 위 구현에 대해 설명하도록 하겠습니다.
- 제일 먼저, 사용할 수 있는 변수를
global
로 땡겨옵니다. - 우선 처음에
os.system("cls")
로 화면을 깨끗이 정리합니다. 코드의 최상단에import os
집어넣는 걸 잊지 마세요! - 코드에서는
output
이라는 리스트를 만들어서 출력 내용을 저장해놓았다가 한번에print("\n".join(output))
으로 출력합니다.output.append
하지 않고 그때 그때 바로print
로 출력할 수도 있지만,print
를 한 번에 많이 쓰게 되면 속도가 조금 느려지므로 한번에 처리하기 위해 위와 같이 구현했습니다. - 메시지를 처리하는 부분에서는,
message_count
를 증감시켜주고,messages
에message
를 넣어줍니다.messages
의 길이가 16가 되면 하나를 삭제하여 길이가 15로 유지되도록 했습니다. - 메시지 번호를 계산하기 위해
message_count - len(messages) + i + 1
라는 요상한 식을 이용했습니다.i
는 인덱스를 의미합니다. 표를 통해서 보면 훨씬 이해가 잘 갈 것입니다. (message_count는 250라 가정
)
len(messages) |
i |
message_count - len(messages) + i + 1 |
---|---|---|
15 | 0 | 236 |
15 | 1 | 237 |
15 | 2 | 238 |
15 | 3 | 239 |
15 | 4 | 240 |
15 | 5 | 241 |
15 | 6 | 242 |
15 | 7 | 243 |
15 | 8 | 244 |
15 | 9 | 245 |
15 | 10 | 246 |
15 | 11 | 247 |
15 | 12 | 248 |
15 | 13 | 249 |
15 | 14 | 250 |
enumerate
라는 요상한 게 등장했습니다! 이건 무엇일까요? 이건for
루프를 돌 때 한 껏 간편하게 돌 수 있게 해주는 도구입니다.enumerate
를 사용하면(index, value)
식으로 리스트 내 요소를 사용할 수 있게 됩니다. 코드로 설명하면 다음과 같습니다.
i = 0
for m in messages :
output.append(f'[{message_count - len(messages) + i + 1:>3}] {m}')
i += 1
## 이걸 enumerate 로 고치면?
for i, m in enumerate(messages) :
output.append(f'[{message_count - len(messages) + i + 1:>3}] {m}')
## 이렇게 됩니다!
이제 print_message
함수를 호출할 수 있도록 turn
함수 내부를 수정해볼까요? 일단 기존의 print
함수 호출을 대체해줍니다. print_message
함수는 print
함수와 달리 인수를 하나밖에 받지 못합니다. 그러므로 호출하는 부분도 전부 하나의 문자열을 받도록 수정해줍니다. f-string 이 간단하겠지요?
print_message(f'{name}의 차례입니다.')
print_message(f'{name}가 {selected}를 냈습니다."')
print_message(f'{name}가 낼 수 있는 카드가 없어 {damage}장 먹습니다.')
print_message(f"{name}가 이겼습니다!")
작업이 완료되면 기존의 현재 놓여져 있는 카드, 가지고 있는 패, 낼 수 있는 카드를 출력하는 부분을 없애줍니다. 이 부분은 print_message
에서 자동으로 계속 출력합니다.
추가 조치
더 깔끔하게 보이기 위해, 카드를 골랐을 때에는 플레이어나 컴퓨터나 상관없이 화면에 출력하도록 합시다. 그리고 플레이어가 카드를 먹을 때에는 플레이어가 확인하고 진행할 수 있도록 input
을 이용해 흐름을 멈추어줍시다. 아래 두 코드 블록은 조치한 것들입니다.
# ----------- 카드 고르기 ---------------------
is_available = len(available) > 0
### 중략 ###
print_message(f'{name}가 {selected}를 냈습니다."')
# ------------ 카드 먹기 -----------------------
else:
print_message(f'{name}가 낼 수 있는 카드가 없어 {damage}장 먹습니다.')
if not isComputer:
input("계속 하려면 엔터를 누르세요")
### 중략 ###
연습 문제: get_damage
개선
get_damage
함수를 완전히 뒤엎습니다. 파이썬의 딕셔너리와 딕셔너리의 get 메소드를 활용하는 방법으로 수정할 예정입니다. 딕셔너리인 damage_map
은 키로 카드의 숫자(card[1]
에 해당하는 내용)를 가지고 값으로 공격력을 가집니다. 숫자에 대응되는 공격력 데이터를 가진 damage_map
으로 손쉽게 get_damage
를 구현할 수 있습니다. damage_map
은 전역으로 정의하고, get_damage
내부에서는 global
로 damage_map
에 접근하여 나머지 내용을 작성하도록 합니다.
풀이
damage_map = {
'colored': 15,
'black': 10,
'A': 3,
'2': 2
}
def get_damage(card):
global damage_map
return damage_map.get(card[1], 0)
딕셔너리는 리스트와 비슷하게 [key]
이렇게 값에 접근할 수 있습니다. 하지만 없는 키 값에 접근하게 되면 에러가 발생하는데요, 그것을 방지하기 위해 get
을 씁니다. get
에서 첫번째 인수를 키로 하여 값을 찾는데요, 만약 키가 없다면 인수의 두번째를 기본 값으로 하여 가져옵니다.
최종 코드
아래는 최종 코드입니다. 다음 시간에는 이 코드 기준으로 설명할 것입니다.
마무리
다음 시간에는 입력방법 개선, 낼 수 있는 카드가 있어도 먹는 행동 구현, 컴퓨터 여러 개 추가하면서 턴 시스템 개선, 7 J Q K와 같은 특수 카드를 구현해보도록 하겠습니다.
- 프롤로그
- 개발 첫걸음
- 파이썬 기초
- 파이썬 중급
- 파이썬 고급
- 내장 함수 톺아보기
- 예외와 에러 – 예상치 못한 상황에 대응하기 (v0.1)
- 변수의 범위 – 이름 검색의 범위
- 파이썬 심화
- 시퀀스와 반복자 – 반복과 순회를 자유자재로 다루기
- 데코레이터 – 함수의 기능을 강화하기
- 프로퍼티
- 제너레이터
- async와 await
- 객체로서의 클래스 – 클래스를 동적으로 정의하기
- 파이썬 프로젝트 실습
- 원카드 게임 만들기 (1)
- 원카드 게임 만들기 (2)
- 원카드 게임 만들기 (3) (작성중)
- 턴제 자동 전투 게임 만들기 (작성중)
- 실전 (파이썬 외적인 것들)
- 정규표현식 – 문자열을 검색하고 치환하기 (작성중)
- 유니코드 – 컴퓨터에서 문자를 표기하는 방법
- html, css, 인터넷 – 자동화 첫 걸음 내딛기
- 네트워크 – 인터넷으로 통신하는 방법
- 문서 – 문맥을 읽어보기
멀티로는 만들수 있나요?
ㅠㅠ네 그 내용은 여기 없어요.. 네트워크 관련된 거는 다른 공부가 필요하기에~