[TIL #8] Python 텍스트게임 리뷰

안떽왕·2023년 3월 28일
1

Today I Learned

목록 보기
8/76

오늘 오후 5시까지 마감이였던 텍스트게임 개인과제가 마무리 되었습니다.

넣고싶었던 기능은 한가득이지만 이틀만에 해결한다는게 저한테는 아직 버거운 일이였던 것 같습니다.

코드리뷰

이번 프로젝트는 클래스를 담아둔 파이썬 파일과 게임 내 행동 함수와 실행을 담당하는 코드가 모여있는 파이썬 파일로 총 2개가 있습니다.

원래 행동 함수와 실행을 담당하는 부분을 쪼개 총 3개의 파일로 구성할 생각이였으나 양쪽을 서로 import하는 순환참조가 이루어져서 급하게 수정하려 노력했으나, 이미 짜놓은 판이 있어서 차마 다 수정하지 못하고 두 파일을 합치게 되었습니다.

클래스 파일

플레이어 클래스

# 플레이어 클래스의 구성
class Player():
    name = ""
    hp = 0
    current_hp = 0
    mp = 0
    current_mp = 0
    power = 0
    magic_power = 0

    # 클래스가 불러와질때 자동 실행, 파라미터로 넣은 값을 가지고 객체를 생성
    def __init__(self, name):   # 스탯이 랜덤이라 이름만 파라미터로 받음
        self.name = name
        self.hp = random.randint(30, 50)
        self.current_hp = self.hp
        self.mp = random.randint(10, 20)
        self.current_mp = self.mp
        self.power = random.randint(10, 15)
        self.magic_power = random.randint(20, 25)    
        print(f"\n{self.name}이(가) 생성 되었습니다.")
        print(f"\nHP: {self.hp}\nMP: {self.mp}\n힘: {self.power}\n마력: {self.magic_power}")
        
        # 랜덤 스탯에 불만이 있거나 이름을 잘못 지정했을 경우 Y/N을 작성해 재분배 선택
        while True:
            choice = input("진행하려면 Y / 다시하려면 N 을 입력해주세요: ")
            if choice not in ["Y", "N", "y", "n"]:  # 입력값이 Y/N이 아니라면 continue로 다시 반복
                print("Y 또는 N을 입력해주세요")
                continue
            elif choice in ["N", "n"]:
                p1 = Player(input("당신의 이름은 무엇입니까?: ")) # N을 입력할 경우 p1 재선언
            break   # Y가 들어오면 함수 종료

    # 플레이어의 일반 공격
    def nomal_attack(self, enemy):
        damage = int(self.power * (random.randrange(8, 12) / 10)) # damage가 float형식이라 int로 형변환
        enemy.current_hp = enemy.current_hp - damage # 적의 현재 hp는 현재hp - 데미지
        print(f"{self.name}의 공격! {enemy.name}에게 {damage}의 물리데미지를 입혔습니다.")
        time.sleep(0.5) # sleep함수를 사용해 텍스트가 바로 띄워지는걸 방지, 좀 더 게임처럼 느껴짐
        if enemy.current_hp <= 0:   # 적의 현재 hp가 0보다 작거나 같을경우 적 사망
            print(f"{enemy.name}이(가) 쓰러졌습니다.")
            time.sleep(0.5)

    # 플레이어의 마법 공격
    def magic_attack(self, enemy):
        damage = int(self.magic_power * (random.randrange(8, 12) / 10))
        enemy.current_hp = enemy.current_hp - damage
        self.current_mp = self.current_mp - 5
        print(f"{self.name}의 공격! {enemy.name}에게 {damage}의 마법데미지를 입혔습니다.")
        time.sleep(0.5)
        if enemy.current_hp <= 0:
            print(f"{enemy.name}이(가) 쓰러졌습니다.")
            time.sleep(0.5)
    
    # 플레이어의 상태
    def status(self):
        print(f"\n==={self.name}의 상태===\nHP: {self.current_hp} / {self.hp}\nMP: {self.current_mp} / {self.mp}")
        time.sleep(0.5)

게임 사용자가 되는 플레이어의 클래스입니다.

본래 __init__함수에 파라미터를 각각 다 넣어주는 방향을 취했었는데 팀원분들과 코드 리뷰를 진행하며 랜덤으로 스탯을 부여해주는 것이 훨씬 재밌을 것 같다는 의견이 많아 반영하게 되었습니다.

__init__함수 뒤에 while문이 하나 있는데 이것도 랜덤한 스탯부여를 만들고 나서 생긴 기능으로 플레이어가 생성된 능력치를 보고 맘에 들지 않거나 이름을 잘못입력 했을 경우 다시 설정할 수 있도록 만들었습니다.

if choice not in ["Y", "N", "y", "n"]:  # 입력값이 Y/N이 아니라면 continue로 다시 반복
    print("Y 또는 N을 입력해주세요")
    continue

while문 안에 있는 이 if문을 만들 때 조금 고생한게 있는데 맨 처음 구성은 choise != "Y" or "N" 이런 식이였습니다. 다 만들고 실행했을 때 예외인 경우를 입력해보니 아주 잘 작동해서 흡족하고 있던 찰나 YN을 입력해도 다시 입력해달라는 텍스트를 보고 뭐지 싶었습니다.

찾아보니 저런 식으로 구성하게되면 문자열을 false로 인식해 저런 상황이 벌어짐을 알게 되었고 리스트를 만들어 안에 없을 경우로 다시 만들었습니다.

attack 함수에 랜덤식은 어제 TIL에서 언급한 내용인데 전역변수로 랜덤한 코드를 구성해 집어넣으니 이미 할당이 되버려서 때릴때마다 랜덤한 값이 나오지 않고 새 게임을 키는 경우에만 랜덤이 적용되길래 전역변수를 없애버리고 공격 함수에다가 직접 random을 널게 되었습니다.

중간 중간마다 time.sleep(0.5)가 있는 걸 확인할 수 있는데 저 코드 없이 실행시켜도 아무 이상없이 게임은 진행되지만, 게임의 몰입감이 달라집니다. 저 코드 기준으로는 0.5초 쉬어간다는 뜻인데, 텍스트가 한번에 팍 올라오는걸 보다가 하나하나씩 입력되는 텍스트를 보면 비로소 게임 같아졌다는 생각이 들게 됩니다.

그리고 클래스 맨 위에 있는 코드

class Player():
    name = ""
    hp = 0
    current_hp = 0
    mp = 0
    current_mp = 0
    power = 0
    magic_power = 0

코드 리뷰 전까지만 하더라도 딕셔너리 형태를 정해놓는 것처럼 무조건 해놓는거라 생각했는데 코드 리뷰할 때 저렇게 해놓지 않고 바로 __init__함수를 작성해도 아무 지장이없다는 것을 알게 되었습니다..

몬스터 클래스는 저기서 마법공격만 없앤 것이기 때문에 생략하고 넘어가도록 하겠습니다.

터미널 새로고침 함수

다음은 터미널 창을 새로고침하는 함수입니다.

def new_cmd():
    com_os = platform.system()  # 사용자 시스템의 os가 무엇인지 탐색
    input("\nENTER 키를 눌러 다음으로 진행하세요")  # 새로고침이 되기 전 나온 정보들을 사용자가 읽고 넘어갈 수 있도록 엔터 입력 후 창 새로고침 진행
    if com_os == 'windows': # 환경이 윈도우라면 cls 입력
        os.system('Cls')
    else:   # 환경이 리눅스나 맥이라면 clear 입력
        os.system('Clear')

하나씩 코드가 나오게 코드를 짠 다음 구현안 함수인데 터미널 창에 지저분하게 텍스트가 쌓이는 현상을 해결해줍니다. 먼저 platform.system()함수를 이용해 사용자의 시스템이 윈도우인지 리눅스인지 맥인지 검사하고 각 시스템에 맞는 명령어를 입력하게끔 만들어 놨습니다.

VScode에서 실행했을 때는 굉장히 잘 적용되는 함수였는데, powershell에서 돌릴때는 먹히지않아 참으로 슬펐습니다. 해결하려했으나 제가 못 찾은것이기도 하고 제출시간이 다가와서 넘어가게 되었습니다.

행동 함수 및 실행 파일

플레이어 행동 함수

# 플레이어의 행동 함수
def player_turn():
    while True: # while문을 사용해 break를 걸 때 까지 계속 실행
        try:    
            print("\n===공격 방식 선택===\n1. 물리 공격\n2. 마법 공격")
            player_skill_input = int(input("\n공격할 방식의 번호를 입력해주세요: "))
            if player_skill_input == 0 or player_skill_input > 2:   # 1,2 외에 다른 숫자가 들어오면 다시 입력
                print("공격방식을 다시 입력해주세요")
                continue  # while문을 처음부터 다시 시작함
            
            if player_skill_input == 2:
                if p1.current_mp < 5:
                    print("스킬을 사용하는데 필요한 MP가 모자랍니다.")
                    continue 
        except ValueError:  # ValueError가 나왔을때 execpt문 실행
            print("공격방식을 숫자로 입력해주세요")
            continue 
            
        break  # while문을 빠져나옴

    print("\n===공격 대상 선택===")
    for i, m in enumerate(monster_list):    # enumerate함수를 사용해 i는 순서를 m은 m1,m2와 같은 몬스터 변수를 입력
        print(f"{i+1}. {m.name} (HP: {m.current_hp} / {m.hp})")

    while True: # while문을 사용해 break를 걸 때 까지 계속 실행
        try:    
            player_target_input = int(input("\n공격할 대상의 번호를 입력해주세요: ")) 
            if player_target_input <= 0 or player_target_input > len(monster_list): # 0 또는 리스트의 갯수보다 더 많은 숫자 입력 시 다시 입력
                print("잘못된 대상을 선택하셨습니다. 다시 선택해주세요.")
                continue 
        except ValueError:  # ValueError가 나왔을때 execpt문 실행
            print("대상 번호를 숫자로 입력해주세요.")
            continue
            
        break

    if player_skill_input == 1: # 공격방식의 번호 입력이 1(노말어택)이라면
        game_class.Player.nomal_attack(p1, monster_list[player_target_input -1])    # game_class 파일 Player 클래스의 nomal_attack함수 실행
    elif player_skill_input == 2: # 공격방식의 번호 입력이 2(매직어택)이라면
        game_class.Player.magic_attack(p1, monster_list[player_target_input -1])

while문은 공격방식을 선정하고 공격할 상대를 정하는게 메인이고 예외 입력이 들어왔을때 다시 입력하게끔 만들었습니다.

공격 대상 선택 부분에 for문에서 enumerate를 이용했습니다. 본래 코드는 단순히 모든 몬스터를 프린트하다보니 이미 죽은 몬스터도 공격 대상에 남아있어서 시체를 때리는 불상사가 있었습니다. 이를 enumerate를 이용해 리스트의 번호를 매겨 for문의 반복으로 몬스터의 번호 체력을 나타내고 죽은 몬스터는 monster_list에서 지워지기 때문에 대상선택에 남아있지 않게됩니다.

밑에 if문은 사용자의 입력 값에 따라 클래스 파일에 있던 공격함수로 공격하는 코드입니다. 뒤에 입력 값 -1 이라고 되어있는 걸 확인할 수 있는데, 이는 리스트의 인덱스는 0부터 시작하고 입력값은 1부터 시작하기 때문에 추가하게 되었습니다.

몬스터 행동 함수

# 몬스터의 행동함수
def monster_turn():
    for monster in monster_list:
        game_class.Monster.nomal_attack(monster, p1)    # 반복문을 돌면서 한마리씩 플레이어 어택

# 몬스터 리스트 제거 함수(반복문으로 간략화 가능해 보임)
def monster_death():  
    for m in monster_list:
        if m.current_hp <= 0:
            monster_list.remove(m)

몬스터는 플레이어와 다르게 마법공격이 없이 물리 공격만 있습니다. 공격 대상도 플레이어 하나로 한정적이기 때문에 for문을 이용해 간략한 코드를 구성할 수 있었습니다.

monster_death함수는 몬스터가 죽었을 떄 리스트에서 제거시키는 함수입니다. 본래는 3마리 모두 if문으로 삭제시켰지만 코드를 간결하게 하기 위해 for문을 이용해 현재hp가 0보다 작거나 같다면 리스트에서 제거하는 방법을 사용했습니다.

실행 부분

객체 생성

p1 = game_class.Player(input("당신의 이름은 무엇입니까?: "))    # Player 클래스 __init__에서 많은 숫자가 랜덤으로 부여시켜서 name만 받음
m1 = game_class.Monster("주황버섯", 15, 4)  # Monster 클래스 __init__에서 요구하는 (name, hp, power)
m2 = game_class.Monster("초록버섯", 25, 5)
m3 = game_class.Monster("파랑버섯", 35, 6)

monster_list = [m1, m2, m3] # 몬스터 리스트

플레이어는 이름을 제외한 모든 스탯들이 랜덤이기 때문에 파라미터로 이름만을 받습니다
몬스터는 마법공격을 하지않아 마나와 마력이 없습니다. 받는 파라미터는 이름, 체력, 힘 입니다.
몬스터들은 monster_list에 들어갑니다.

실행

# 게임 실행 부분
while True: # break가 걸릴 때 까지 반복 실행
    game_class.new_cmd()
    print(f"\n====={turn}번째 턴====")  # f_string을 이용
    time.sleep(0.3) # 텍스트가 한번에 입력되는 것을 방지함, 좀 더 게임처럼 즐길 수 있음
    p1.status() # p1 = 플레이어, 플레이어의 상태를 보여주는 status함수를 사용
    time.sleep(0.3)
    for m in monster_list:  # 몬스터 리스트에 있는 몬스터들을 m이라는 변수에 순차 저장
        m.status()  # m에 m1,m2,m3 순서로 들어오고 status함수 사용, 몬스터가 죽으면 리스트에서 삭제되기에 m2가 죽으면, m1,m3가 순차적으로 들어옴
        time.sleep(0.3)

    print("\n===플레이어 턴===")
    player_turn()   # 플레이어 행동함수 실행
    monster_death() # 몬스터 리스트 제거 함수 실행

    if monster_list == []:  # 몬스터가 모두 죽어서 리스트가 비어버렸다면
        print("\n====clear!====\n당신이 승리했습니다.")
        break   # 반복문 종료
    
    print("\n===몬스터 턴===")
    time.sleep(0.3)
    monster_turn()  # 몬스터 행동함수 실행

    if p1.current_hp <= 0:  # 플레이어의 체력이 0보다 같거나 작다면
        print("\n당신이 사망하였습니다.\n ====게임 오버====")
        break   # 반복문 종료

    turn += 1   # 턴 + 1
    print("\n====턴 종료====")    # 위의 break에 걸리지 않았다면 다시 반복문으로 돌아감
    time.sleep(0.3)

while문을 통해 플레이어가 사망하거나 몬스터들이 모두 토벌되어 monster_list가 공백이 될 때까지 반복합니다.
while문의 첫 코드는 창을 새로고침하는 함수로 시작전 플레이어를 생성하며 생기는 텍스트들을 없애줍니다.
게임에 진입하면 플레이어와 몬스터의 상태창을 먼저 띄워주고 플레이어가 무조건 선공으로 시작합니다.
플레이어 턴이 지나면 리스트내 몬스터를 죽었는지 판별하고 죽었다면 제거합니다.

느낀 점

정말 재밌는 프로젝트였습니다. 아쉬운게 있다면 역시나 기간... 이틀은 너무 짧아요!! 프로젝트 제출하고 다른 분들 코드를 보는데 생전 처음보는것들이 많더군요 rich함수라던가 아스키코드를 이용한 텍스트 그래픽? 솔직히 제가 들은 게 맞는지 모르겠습니다. 저희 조 내에서는 나름 조언도 주고 코드 수정과 버그 잡기 등 여러 역할을 하며 성장했다고 생각했는데 허허 역시 아직 한참 멀었습니다. 내일은 팀프로젝트가 있으니 내일을 위해 다시 달려보겠습니다.

profile
이제 막 개발 배우는 코린이

0개의 댓글