프로그래밍/생각나는대로 프로젝트

[프로젝트] 03. Text RPG게임 냥이 키우기

Churnobyl 2023. 4. 3. 01:09
728x90
반응형

개요: 파이썬을 이용한 Text RPG. 직업을 가진 고양이 캐릭터들을 골라 多 vs 多 턴제 게임을 구현함

 

 


프로젝트 정보


  • 프로젝트: 냥이 키우기 프로젝트 (파이썬 객체 지향 프로그래밍 세션)
  • 개발기간: 2023.03.29 - 2023.03.31 ( 3일 )
  • 역할
    • 메인 화면 구현
    • 인트로 구현
    • 배틀 시스템 구현
    • 마을 구현
    • 여관 구현
  • 사용 언어: Python
  • 사용 라이브러리:
    • Python(Vanilla)

 


프로젝트 구성


★ 파일구조

각 기능을 통합하는 main.py를 포함해 총 6개 파일로 구성되어 있다.

Cat Game ─┬─ main.py (각 기능 통합 및 게임 실행)
           ├─ battlephase.py (전투 시스템)
           ├─ items.py (아이템 및 장비)
           ├─ object.py (캐릭터 및 몬스터)
           ├─ shop.py (상점)
           └─ utility.py (입력 문자열 체크, 운영체제 별 터미널 클리어)

 

★ 화면구성 및 코드

1. 메인화면 (시스템 변경)

각 기능이 함수 단위로 구현되었기 때문에 게임을 진행하면서 플레이어의 선택에 따라 각 함수로 이동해야 한다

그래서 main.py의 status 변수에 플레이어가 선택한 행동의 상태를 기록하고 각 함수를 실행한 뒤 return으로 status 값을 바꾸는 식으로 게임을 구현했다

 

아래의 코드를 보자

# main.py

while status != 'quit':
    # 어떤 행동 끝나면 마을
    if status == 'town':
        status = town(player_character_list, player_money)

    # 여관
    elif status == 'inn':
        status, player_money = inn(player_character_list, player_money)

    # 상점
    elif status == 'buy_item':
        status, player_money = buy_item(player_character_list, player_money)

    # 일반 전투
    elif status == 'prebattle':
        status, player_money = prebattle(
            player_character_list, player_money, character_skills)

    # 보스 전투
    elif status == 'prebossbattle':
        status, player_money, check_boss_clear = prebossbattle(
            player_character_list, player_money, character_skills, boss_clear)

        # 보스 클리어 확인
        if check_boss_clear == True:
            boss_clear += 1
            check_boss_clear = False

    elif status == 'lose':
        status, player_money = lose(player_money)

    # 총 스테이터스 창

    elif status == 'all_status':
        status = all_status_s(player_character_list)

위와 같이 main.py에서는 while문이 게임이 끝날 때까지 계속해서 돌면서 status에 어떤 값이 들어가는지 체크해 각 기능이 담긴 함수를 실행하게끔 만들었다. 또한 상점이나 전투 같은 함수가 실행되고 나면 플레이어의 소지금에 변화가 생기므로 return에 이러한 값도 담기게끔 했다

 

 


2. 인트로 (캐릭터 선택)

Select characters

 

When I selected one character, show character's name on the first line

게임을 시작했을 때 화면이다.

네마리의 캐릭터 중 세마리를 차례로 고를 수 있도록 했다.

한마리씩 고를 때 마다 첫번째 줄에 현재 고른 캐릭터의 이름이 나오도록 했으며, 만약 이미 선택한 캐릭터를 다시 고른다면 '이미 xxx에게 간택 받았습니다.'라는 메세지가 출력되도록 했다

 

아래의 코드를 보자

 

# main.py

# 캐릭터 선택(3 마리)
while len(player_character_list) < 4:
    screen_clear()
    print("간택 받고 싶은 고양이 세마리를 골라주세요!(현재:", end=" ")
    print(*player_character_list, end=" ")
    print(")")
    select = input("1)냥검사 2)냥법사 3)냥궁수 4)냥힐러 : ")
    if input_check(1, len(characters), select) == False:
        continue
    elif characters[select] in player_character_list:
        print(f"이미 {characters[select]}에게 간택 받았습니다.")
        time.sleep(1)
    else:
        player_character_list.append(characters[select])

        # 3 마리가 다 채워졌다면 break
        if len(player_character_list) == 3:
            break

screen_clear()함수는 게임의 가독성을 위해 화면을 지우고 새로 출력하고자 할 때, windows와 mac의 화면 지우기 명령어가 'cls''clear'로 다르므로 이를 구분해 주기 위해 utility.py에 함수를 작성했다

# utility.py
import platform
import os

def screen_clear():
    os_check = platform.system()
    if os_check == 'Windows':
        os.system("cls")
    else:
        os.system("clear")

 

platform 모듈의 system()함수를 사용했다

 

어쨌든 main.py에서는 if~elif~else문으로 문자열 체크, 선택한 고양이 중복 선택 체크 후에 문제가 없으면 player_character_list에 캐릭터를 append했다

그리고 3마리가 다 채워지면 while문을 빠져 나가도록 해 캐릭터를 선택하도록 했다.

 

 


3. 마을

마을로 들어왔을 때 화면이다

어떤 행동을 하고 난 뒤에는 항상 마을로 돌아오도록 시스템을 구성했다

각 캐릭터의 스테이터스와 소지금, 그리고 인벤토리를 간단하게 표시하고 총 다섯개의 선택지로 이동할 수 있게끔 했다

'1) 길거리'는 일반전투, '2) 높은 탑'은 보스 전투다

 

마을의 코드를 한번 살펴보자

# shop.py

# 마을
def town(character_list, money):
    # 마을에서 행동 리스트
    town_action = {
        '1': 'prebattle',
        '2': 'prebossbattle',
        '3': 'inn',
        '4': 'buy_item',
        '5': 'all_status'
    }

    while True:
        screen_clear()
        for i in character_list:
            print(i.status())

        print("---------------------------------------------------")
        print(f"소지금: {money}$")
        print("- 인벤토리 -")
        print(f"{potion1.name} : {potion1.num}개")
        print(f" {potion2.name}  : {potion2.num}개")
        print(f"{potion3.name} : {potion3.num}개")
        print(f" {potion4.name}  : {potion4.num}개")

        if bool(pet_list) == True:
            print('현재 pet : ', end=" ")
            print(*pet_list)

        action = input("어디로 갈까요? [ 1)길거리 2)높은 탑 3)여관 4)상점 5)총 스테이터스] : ")
        if input_check(1, 5, action) == False:
            continue
        else:
            return town_action[action]

town_action이라는 딕셔너리 안에는 각 행동을 함수로 만들어 정리해두었다

그리고 맨 아래의 input으로 1~5까지의 값을 받아 함수를 실행하도록 했다

input_check()함수는 입력받은 문자열이 validate한지 체크하는 함수이다

 

# utility.py
import re

def input_check(x, y, text):
    pattern = "[{}-{}]".format(x, y)
    if text.isdigit() is False:
        print("정수를 입력해주세요.")
        return False
    elif bool(re.fullmatch(pattern, text)) is False:
        print("잘못 입력했습니다. 다시 시도하세요.")
        return False
    else:
        return True

input_check()함수는 위에서 보듯이 isdigit()함수로 입력받은 문자열이 정수값인지 체크하고 re모듈의 fullmatch()함수로 특정 값이면 True를 리턴하도록 했다

 

 


4. 일반 전투( 길거리 )

마을에서 1번을 입력했을 때 실행되는 일반전투 창이다

뱀, 쥐, 바퀴벌레, 까치 4 종류의 몬스터 중에 랜덤으로 1~3마리의 몬스터가 출현한다

현재 기절하지 않은 캐릭터만 전투에 참여할 수 있도록 코드를 짰고 마지막 줄에서 보듯이 고양이들과 몬스터들의 평균 속도를 비교해 더 빠른 진영이 선공할 수 있도록 했다.

 

본격적인 전투에 들어가기 전에 전투에 관련한 정보를 세팅하는 prebattle()함수부터 살펴보자

 

# battlephase.py

# 일반배틀 준비
def prebattle(character_list, money, character_skills):
    battle_character = []
    for i in character_list:
        if i.HP != 0:
            battle_character.append(i)

    if len(battle_character) == 0:
        print("싸울 수 있는 고양이가 없습니다. 마을로 돌아갑니다.")
        time.sleep(2)
        return 'town', money

    # 일반 던전 몬스터 가중치
    sum_ = 0
    for i in character_list:
        sum_ += i.level
    avg_lv = sum_ // 3 + random.randint(1, 3)

    # Monster List        HP / MP / ATK / DEF / SPD
    monster_list = [
        Monster("쥐", avg_lv, 70, 0, 13, 15, 10),
        Monster("까치", avg_lv, 100, 0, 18, 18, 7),
        Monster("바퀴벌레", avg_lv, 110, 0, 15, 30, 12),
        Monster("뱀", avg_lv, 80, 0, 20, 20, 10)
    ]

    # 전투에 나올 몬스터
    battle_monster = []

    # 몬스터 뽑기
    for i in range(random.randint(1, 3)):
        battle_monster.append(monster_list.pop(
            random.randint(0, len(monster_list)-1)))

    # 전투 시작
    return battle(battle_character, battle_monster, money, character_skills)

전투 전에 for문을 돌면서 캐릭터의 HP가 0이면 전투에 참가할 수 없도록 했다.

그리고 전부 HP가 0이라면 싸울 수 있는 고양이가 없다는 뜻이므로 중단하고 다시 마을로 돌아가도록 했다

그리고 현재 캐릭터의 레벨에 비례해서 몬스터의 레벨을 변화시키는 avg_lv 변수를 만들었고, 그중 랜덤으로 몬스터가 나오게끔 했다

 

전투에 들어가는 battle()함수는 너무 길어 생략하고 아래에 github 주소를 남겨놓도록 하겠다.

 

 


5. 보스 전투 ( 높은 탑 )

보스 전투 역시 본 전투 전에 prebossbattle()함수를 통해 전투에 필요한 데이터를 산출한다

보스는 슈뢰딩거, 잼민이, 제리 세 보스가 구현되어 있으며, 한 보스를 처치하고 나면 달성도를 기억해 두었다가 다시 탑에 가면 다음 보스가 나오는 식이다

각 보스의 스텟은 고정이며 일반전투에서 레벨과 능력치를 쌓은 뒤 보스에 도전할 수 있도록 구성했다

 

prebossbattle()함수를 보자

# battlephase.py

# 보스배틀 준비
def prebossbattle(character_list, money, character_skills, boss_clear):
    battle_character = []
    for i in character_list:
        if i.HP != 0:
            battle_character.append(i)

    # 고양이 셋 다 기절해 있다면
    if len(battle_character) == 0:
        print("싸울 수 있는 고양이가 없습니다. 마을로 돌아갑니다.")
        time.sleep(2)
        return 'town', money, boss_clear

    # 고양이 셋 중 하나라도 기절해 있으면 의사 물어보기
    if len(battle_character) != len(character_list):
        question = input(
            "고양이들 중에 싸울 수 없는 고양이가 있는 것 같아요. 그래도 싸우시겠습니까? [ 1) 네 2) 마을로 돌아갈래요 ]")
        if question == '1':
            print("좋은 자신감이에요")
            time.sleep(2)
        else:
            return 'town', money, boss_clear

    # 나올 보스
    if boss_clear == 0:
        battle_monster = [bosses[0]]
    elif boss_clear == 1:
        battle_monster = [bosses[1], bosses[2]]
    elif boss_clear == 2:
        battle_monster = [bosses[3]]

    # 전투 시작
    return battle(battle_character, battle_monster, money, character_skills)

일반전투 준비와 크게 다른 부분은 없지만 보스 전투이기 때문에 고양이 중 하나라도 기절해 있을 경우 진짜 도전할 건지 의사를 묻는 코드를 추가로 짰고, 어떤 보스를 출현시킬 건지에 관한 boss_clear변수를 매개변수로 받아 일반전투와 같은 battle()함수를 실행시키게끔 했다

 

 


6. 여관

여관은 기절했거나 HP, MP가 부족한 고양이들을 부활시켜주는 곳으로 만들었다

 

# shop.py

def inn(character_list, money):
    while True:
        screen_clear()
        print("어서오세요~고양이 카페입니다. 쉬고 가실건가요?")
        time.sleep(1)
        print("요금은 100$입니다")
        time.sleep(1)
        print(f"소지금: {money}$")
        action = input("1)쉬기 2)돌아가기 : ")
        if input_check(1, 2, action) == False:
            continue
        elif money < 100:
            print("돈도 없는 주제에. 저리 꺼져")
            time.sleep(2)
            print("마을로 돌아갑니다")
            time.sleep(2)
            return 'town', money
        else:
            if action == '1':
                return recovery_in_inn(character_list, money)
            elif action == '2':
                return 'town', money


def recovery_in_inn(character_list, money):

    money -= 100

    for i in character_list:
        i.HP = i.max_HP
        i.MP = i.max_MP
        i.faint = False

    print("고양이 카페에서 푹 쉬었더니 고양이들의 체력이 회복되었습니다")
    time.sleep(3)
    print("마을로 돌아갑니다")
    time.sleep(3)

    return 'town', money

inn()함수에서 소지금이 충분한지 체크하고 recovery_in_inn()함수에서 회복 기능을 할 수 있도록 만들었다.

지금 생각해보니 굳이 나눌 필요가 없었다.

 

 


프로젝트 결과물

https://github.com/barryjung/CatGame

 

GitHub - barryjung/CatGame

Contribute to barryjung/CatGame development by creating an account on GitHub.

github.com

 

 

 


프로젝트 완료 후의 감상

더 효율적으로 코드를 짜지 못한 게 아쉬움으로 남는다.

처음 코드를 짤 때는 시간도 부족하고 프로젝트 방향 자체도 모호해 의식의 흐름대로 코드를 짰는데 프로젝트를 완성하고 다시 되돌아보니 비효율적인 부분들이 곳곳에서 보인다.

예를 들어 일반전투준비와 보스전투준비에서 코드가 겹치는 부분이라던가, 각 인스턴스들의 위치라던가.

 

항상 느끼는 거지만 프로젝트 계획 단계가 가장 중요하다.

시작부터 방향성이 확실했다면 팀원들도 더 효율적인 코드가 나왔을 것 같기도 하다

 

팀장님이 예비군으로 인한 부재로 이번 프로젝트도 피치 못하게 팀장대리처럼 프로젝트를 계획하고 진행했다

팀을 이끄는 건 재밌지만 생각해야 할 것도 너무 많다

반응형