광주인력개발원

<개복치 키우기> 개발 완료 보고서 [2023-04-25 학습일지]

플광 2023. 5. 30. 08:51

 

금일의 수업은 조동현 교수님께서 진행해주셨다.

 

오늘의 과제는 류홍걸 교수님의 개복치 키우기 문제를 파이썬 CLI 에서 실행시키는 문제였다.

 

요구 조건이 상당하기 때문에, 나는 실제 게임 플레이 양상이 어떤지 유튜브를 통해 작동 모습을 확인하였다.

 

GP를 통해 구매한 아이템들은 개복치가 죽어도 계속 해금되어있는 상태이기 때문에, 부스팅 레벨업이 가능하다는 걸 알게 되었다.

 

또한, 객체지향적인 설계를 위해 1~2시간 가량은 자료형 정의 및 함수를 어떻게 쓸지 고민을 많이했다.

 

그리하여 메모장으로 설계한 내용은 다음과 같다.

 


도메인

[grade]

index:

-name

-min_weight

-max_weight

-first_evolution_mp_reward

-second_evolution_mp_reward

-death_mp_reward

 

[feed]

index:

'name'

'gain_weight'

'need_mp_to_buy

'requires_to_open

'reason_of_death'

'death_description'

 

[adventure]

index:

-name

-gain_weight

-success_mp_reward

-need_mp_to_buy

-requires_to_open

-reason_of_death

-death_description

 

feed_success_probability = [920, 999]

adventure_success_probability = [500, 750, 950, 990]

 

[distributions_of_feed_price]

index:

-count_of_feed

-need_mp_to_buy

 

[user_status]

- retry_count

- now_stat : grade, weight, is_alive, adventure_point

- grade_source

- 0 : [reach_count]

- feed_resource

- count_of_feed

- types

- 0:

- have_been_death

- enable

- advanture_resource

- 0:

- have_been_death

- enable

- holding_mp

- do_not_want_game

 

[store_status]

- feed_resource

0:

- basic_option

- not_been_bought

- price

- adventure_resource

0:

- basic_option

- not_been_bought

- price

 

<선언 함수>

def ask_start_game(): 맨 처음 시작할지 종료할지 물어봄, return True이면 시작, False면 종료, global user_status, shop_status 초기화

def get_random(): 1~1000까지 수 출력

def print_status(user_status) : 현재 등급, 몸무게, adventure_point 표시함

def select_mode(user_status, shop_status) : 1,2,3,4 선택하게함, ap 부족하면 모험 선택 못함.

def get_feed(user_status) : 열려있는 먹이 중 랜덤으로 공급함, 다 먹으면 user_status에 보상

def get_adventure(user_status) : 현재 해방된 모험 시도, 시도 횟수 확인 후 user_status에 보상 또는 죽음 변경

def get_store(user_status, shop_status) : 구매할 수 있는 리스트 나열, 구매하면 mp 차감, 나가기 통해서 나갈 수 있음

def check_alive(user_status) : 살아있으면 넘기고, 죽었으면 현재 grade 비례 mp 보상, now_stat 영역 초기화

def check_grade(user_status) : 체중이 일정 부분 도달했으면 진화 모션, 진화 횟수 확인 후 mp보상


그리하여 실제 메인 구동부는 이것이 전부다.

while ask_start_game():
    while not user_status['is_game_end']:
        print_status(user_status)
        select_mode(user_status, store_status)
        if user_status['do_not_want_game']:
            break
        check_grade(user_status)

print("게임 종료")
 

다음은 선언한 자료형이다.

grade = {
    0: {
        'name': '별사탕',
        'min_weight': 0,
        'max_weight': 19,
        'first_evolution_mp_reward': 0,
        'second_evolution_mp_reward': 0,
        'death_mp_reward': 30
    },
    1: {
        'name': '아기',
        'min_weight': 20,
        'max_weight': 199,
        'first_evolution_mp_reward': 40,
        'second_evolution_mp_reward': 20,
        'death_mp_reward': 120
    },
    2: {
        'name': '어린이',
        'min_weight': 200,
        'max_weight': 899,
        'first_evolution_mp_reward': 70,
        'second_evolution_mp_reward': 60,
        'death_mp_reward': 240
    },
    3: {
        'name': '젊은이',
        'min_weight': 900,
        'max_weight': 3199,
        'first_evolution_mp_reward': 230,
        'second_evolution_mp_reward': 90,
        'death_mp_reward': 560
    },
    4: {
        'name': '사회의 일원',
        'min_weight': 3200,
        'max_weight': 9999,
        'first_evolution_mp_reward': 600,
        'second_evolution_mp_reward': 140,
        'death_mp_reward': 900
    },
    5: {
        'name': '개복치왕',
        'min_weight': 10000,
        'max_weight': 24999,
        'first_evolution_mp_reward': 1950,
        'second_evolution_mp_reward': 360,
        'death_mp_reward': 1950
    },
    6: {
        'name': '수족관 주인',
        'min_weight': 25000,
        'max_weight': 49999,
        'first_evolution_mp_reward': 3340,
        'second_evolution_mp_reward': 680,
        'death_mp_reward': 3340
    }
}

feed = {  # 먹기 전엔 92% 생존확률, 돌연사 이후엔 99.9% 생존확률
    0: {
        'name': '동물성 플랑크톤',
        'gain_weight': 1,
        'need_mp_to_buy': 0,
        'requires_to_open': None
    },
    1: {
        'name': '해파리',
        'gain_weight': 2,
        'need_mp_to_buy': 110,
        'requires_to_open': None,
        'reason_of_death': '비닐봉투',
        'death_description': '개복치는 해파리와 비닐봉투를 착각하여 잘 질식하여 죽는다.',
    },
    2: {
        'name': '오징어',
        'gain_weight': 4,
        'need_mp_to_buy': 220,
        'requires_to_open': None,
        'reason_of_death': '오징어를 너무 먹었어',
        'death_description': '개복치는 내장도 약하여 정말 좋아하는 오징어를 너무 많이 먹어 소화를 전혀 시키지 못해서 죽는다.',
    },
    3: {
        'name': '새우',
        'gain_weight': 7,
        'need_mp_to_buy': 600,
        'requires_to_open': None,
        'reason_of_death': '새우의 껍질',
        'death_description': '개복치는 내장도 약하여 새우를 완전히 삼켜 그 껍질이 식도를 찔러 가끔 죽는다.',
    },
    4: {
        'name': '정어리',
        'gain_weight': 10,
        'need_mp_to_buy': 1200,
        'requires_to_open': None,
        'reason_of_death': '정어리의 뼈',
        'death_description': '개복치는 식도도 약해서 정어리의 작은 뼈가 찔려서 고름이 생겨 죽는다.',
    },
    5: {
        'name': '게',
        'gain_weight': 14,
        'need_mp_to_buy': 3200,
        'requires_to_open': None,
        'reason_of_death': '게의 다리',
        'death_description': '개복치는 내장도 약하여 게의 다리가 찔러서 고름이 생겨 죽는다.',
    },
    6: {
        'name': '가리비',
        'gain_weight': 25,
        'need_mp_to_buy': 6500,
        'requires_to_open': None,
        'reason_of_death': '가리비의 껍질',
        'death_description': '개복치는 내장도 약하여 가리비의 껍질이 찔러 고름이 생겨 죽는다.',
    },
    7: {
        'name': '굴',
        'gain_weight': 40,
        'need_mp_to_buy': 11000,
        'requires_to_open': None,
        'reason_of_death': '굴의 껍질',
        'death_description': '아마 개복치가 아니라도 껍질을 먹으면 내장을 찔러서 고름이 생겨 죽을 것이다.',
    },
    8: {
        'name': '닭새우',
        'gain_weight': 55,
        'need_mp_to_buy': 23000,
        'requires_to_open': None,
        'reason_of_death': '닭새우의 껍질',
        'death_description': '개복치는 수족관에서 닭새우를 먹는 등 아주 비용이 많이 든다. 껍질이 남아있으면, 물론 죽을 것이다.',
    },
}
 
adventure = {
    0: {
        'name': '몸이 가려워',
        'gain_weight': 26,
        'success_mp_reward': 30,
        'need_mp_to_buy': 0,
        'requires_to_open': None,
        'reason_of_death': '착수 시의 충격',
        'death_description': '개복치는 기생충을 떨쳐내기 위해 점프하지만 그 착수 시의 충격으로 까끔죽는다.'
    },
    1: {
        'name': '해저에는 진수성찬이',
        'gain_weight': 39,
        'success_mp_reward': 30,
        'need_mp_to_buy': 100,
        'requires_to_open': None,
        'reason_of_death': '물이 차가워서',
        'death_description': '개복치는 맛있는 먹이를 찾아서 한 번에 심해로 잠수하지만 물이 너무 차서 쇼크사한다.'
    },
    2: {
        'name': '바위의 그림자에는 보물이',
        'gain_weight': 49,
        'success_mp_reward': 40,
        'need_mp_to_buy': 260,
        'requires_to_open': None,
        'reason_of_death': '바위에 격돌',
        'death_description': '개복치는 거의 앞으로만 헤엄칠 수 있기 때문에 바위를 피하지 못하고 자주 부딪혀 죽는다.'
    },
    3: {
        'name': '햇볕 쬐기',
        'gain_weight': 118,
        'success_mp_reward': 40,
        'need_mp_to_buy': 800,
        'requires_to_open': None,
        'reason_of_death': '바짝 말라버린다',
        'death_description': '개복치는 일광욕한 채로 자버려서 가끔 육지로 떠올라 죽는다.'
    },
    4: {
        'name': '바다거북아, 안녕',
        'gain_weight': 215,
        'success_mp_reward': 50,
        'need_mp_to_buy': 1600,
        'requires_to_open': None,
        'reason_of_death': '바다거북이 무서워서',
        'death_description': '개복치는 바다거북에게 충돌하는 것을 예감하고 패닉이 되어 호흡법을 잊어버려 죽는다.'
    },
    5: {
        'name': '물고기 천국',
        'gain_weight': 337,
        'success_mp_reward': 60,
        'need_mp_to_buy': 4200,
        'requires_to_open': None,
        'reason_of_death': '인간에게 먹힌다',
        'death_description': '개복치는 잘 그물에 걸려 인간에게 먹혀 죽는다.'
    },
    6: {
        'name': '새들은 친구들',
        'gain_weight': 499,
        'success_mp_reward': 110,
        'need_mp_to_buy': 8300,
        'requires_to_open': None,
        'reason_of_death': '새들의 발톰에 고름',
        'death_description': '개복치는 일광욕을 하여 몸의 기생충을 먹게 하지만 자주 새들의 발톱에 상처를 입어 고름이 생겨 죽는다.'
    },
    7: {
        'name': '동료를 구해라!',
        'gain_weight': 874,
        'success_mp_reward': 220,
        'need_mp_to_buy': 15000,
        'requires_to_open': None,
        'reason_of_death': '동료의 죽음',
        'death_description': '개복치는 눈 앞에서 동료가 죽는 모습을 보고 그 스트레스로 죽는다.'
    },
    8: {
        'name': '빛의 방향으로',
        'gain_weight': 1200,
        'success_mp_reward': 300,
        'need_mp_to_buy': 24000,
        'requires_to_open': None,
        'reason_of_death': '아침해가 너무 강해서',
        'death_description': '개복치는 섬세하기 때문에, 아침해가 너무 밝으면 놀라서 죽는다. 그런데도 영문명은 선피쉬.'
    },
}
 

개인적으로 float 형의 사용을 피하기 위해 99.9%의 확률은 999로 표현하였다.

feed_success_probability = [920, 999]
adventure_success_probability = [500, 750, 950, 990]
distributions_of_feed_price = {
    0: {
        'count_of_feed': 7,
        'need_mp_to_buy': 0
    },
    1: {
        'count_of_feed': 8,
        'need_mp_to_buy': 24
    },
    2: {
        'count_of_feed': 9,
        'need_mp_to_buy': 64
    },
    3: {
        'count_of_feed': 10,
        'need_mp_to_buy': 96
    },
    4: {
        'count_of_feed': 11,
        'need_mp_to_buy': 128
    },
    5: {
        'count_of_feed': 12,
        'need_mp_to_buy': 192
    },
    6: {
        'count_of_feed': 13,
        'need_mp_to_buy': 400
    },
    7: {
        'count_of_feed': 14,
        'need_mp_to_buy': 600
    },
    8: {
        'count_of_feed': 15,
        'need_mp_to_buy': 800
    },
    9: {
        'count_of_feed': 16,
        'need_mp_to_buy': 1200
    },
    10: {
        'count_of_feed': 17,
        'need_mp_to_buy': 2080
    },
    11: {
        'count_of_feed': 18,
        'need_mp_to_buy': 3120
    },
    12: {
        'count_of_feed': 19,
        'need_mp_to_buy': 4000
    },
    13: {
        'count_of_feed': 20,
        'need_mp_to_buy': 6000
    },
}
 

basic_user_status 를 선언하였고, 매번 새로운 게임이 시작할 때마다 이 변수를 user_status에 깊은 복사를 하게 만들었다. 죽을 때는 now_stat만 초기화하도록 기능을 구현했다.

basic_user_status = {
    'retry_count': 1,
    'now_stat': {
        'grade': '별사탕',
        'weight': 1,
        'is_alive': True,
        'adventure_point': 0,
    },
    'grade_resource': {
        0: 0,
        1: 0,
        2: 0,
        3: 0,
        4: 0,
        5: 0,
        6: 0,
    },
    'feed_resource': {
        'count_of_feed': 7,
        'types': {
            0: {
                'have_been_death': 0,
                'enable': True
            },
            1: {
                'have_been_death': 0,
                'enable': False
            },
            2: {
                'have_been_death': 0,
                'enable': False
            },
            3: {
                'have_been_death': 0,
                'enable': False
            },
            4: {
                'have_been_death': 0,
                'enable': False
            },
            5: {
                'have_been_death': 0,
                'enable': False
            },
            6: {
                'have_been_death': 0,
                'enable': False
            },
            7: {
                'have_been_death': 0,
                'enable': False
            },
            8: {
                'have_been_death': 0,
                'enable': False
            },

        }
    },
    'adventure_resource': {
        0: {
            'have_been_death': 0,
            'enable': True,
        },
        1: {
            'have_been_death': 0,
            'enable': False,
        },
        2: {
            'have_been_death': 0,
            'enable': False,
        },
        3: {
            'have_been_death': 0,
            'enable': False,
        },
        4: {
            'have_been_death': 0,
            'enable': False,
        },
        5: {
            'have_been_death': 0,
            'enable': False,
        },
        6: {
            'have_been_death': 0,
            'enable': False,
        },
        7: {
            'have_been_death': 0,
            'enable': False,
        },
        8: {
            'have_been_death': 0,
            'enable': False,
        },
    },
    'holding_mp': 0,
    'do_not_want_game': False,
    'is_game_end': False,
    'is_user_want_keep': False
}
 

그리하여, 각각의 먹이, 어드벤처로 인해 죽을 경우를 카운트하고 있어서 이에 따라 죽을 확률이 함수 내부에서 결정되게 했다.

basic_store_status = {
    'feed_resource': {
        0: {
            'basic_option': True,
            'not_been_bought': True,
            'price': feed[0]['need_mp_to_buy'],
            'name': feed[0]['name'],
        },
        1: {
            'basic_option': False,
            'not_been_bought': False,
            'price': feed[1]['need_mp_to_buy'],
            'name': feed[1]['name'],
        },
        2: {
            'basic_option': False,
            'not_been_bought': False,
            'price': feed[2]['need_mp_to_buy'],
            'name': feed[2]['name'],
        },
        3: {
            'basic_option': False,
            'not_been_bought': False,
            'price': feed[3]['need_mp_to_buy'],
            'name': feed[3]['name'],
        },
        4: {
            'basic_option': False,
            'not_been_bought': False,
            'price': feed[4]['need_mp_to_buy'],
            'name': feed[4]['name'],
        },
        5: {
            'basic_option': False,
            'not_been_bought': False,
            'price': feed[5]['need_mp_to_buy'],
            'name': feed[5]['name'],
        },
        6: {
            'basic_option': False,
            'not_been_bought': False,
            'price': feed[6]['need_mp_to_buy'],
            'name': feed[6]['name'],
        },
        7: {
            'basic_option': False,
            'not_been_bought': False,
            'price': feed[7]['need_mp_to_buy'],
            'name': feed[7]['name'],
        },
        8: {
            'basic_option': False,
            'not_been_bought': False,
            'price': feed[8]['need_mp_to_buy'],
            'name': feed[8]['name'],
        },
    },
    'adventure_resource': {
        0: {
            'basic_option': True,
            'not_been_bought': True,
            'price': adventure[0]['need_mp_to_buy'],
            'name': adventure[0]['name'],
        },
        1: {
            'basic_option': False,
            'not_been_bought': False,
            'price': adventure[1]['need_mp_to_buy'],
            'name': adventure[1]['name'],
        },
        2: {
            'basic_option': False,
            'not_been_bought': False,
            'price': adventure[2]['need_mp_to_buy'],
            'name': adventure[2]['name'],
        },
        3: {
            'basic_option': False,
            'not_been_bought': False,
            'price': adventure[3]['need_mp_to_buy'],
            'name': adventure[3]['name'],
        },
        4: {
            'basic_option': False,
            'not_been_bought': False,
            'price': adventure[4]['need_mp_to_buy'],
            'name': adventure[4]['name'],
        },
        5: {
            'basic_option': False,
            'not_been_bought': False,
            'price': adventure[5]['need_mp_to_buy'],
            'name': adventure[5]['name'],
        },
        6: {
            'basic_option': False,
            'not_been_bought': False,
            'price': adventure[6]['need_mp_to_buy'],
            'name': adventure[6]['name'],
        },
        7: {
            'basic_option': False,
            'not_been_bought': False,
            'price': adventure[7]['need_mp_to_buy'],
            'name': adventure[7]['name'],
        },
        8: {
            'basic_option': False,
            'not_been_bought': False,
            'price': adventure[8]['need_mp_to_buy'],
            'name': adventure[8]['name'],
        },
    },
}
 

basic_store_status에는 상점의 상태를 저장했다. 만약 상점에서 어떤 상품을 구매했다면? 구매한 것으로 처리를 하고, 이후에는 매입을 못하게 했다. 또한 기본 상태를 이렇게 저장해두었기 때문에, 게임을 새로 시작하면 유저 및 상점 정보가 공장 초기화 되듯 깔끔하게 진행된다.

user_status = {
    'now_stat': {
        'weight': 1,
    },
}

store_status = {}
 

다만, 게임을 진행하려면 weight를 요구하게 하여 처음에만, 데이터가 일부 들어가있다. 게임 시작한 이후로는 계속 데이터셋이 들어가 있다.

하단 소스의 verified_user_input_number는 유저 입력을 받을 때 문자를 걸러내는 함수다. 그리고 숫자라면 num을 리턴하는데, 각각 사용처에서 필요한 숫자만 받고 아니면 다시 입력을 받게 하였다.

def verified_user_input_number():
    while True:
        num = input("[번호 입력] : ")
        if not num.isdigit():
            print('잘못된 형식의 입력입니다.')
            continue
        break
    num = int(num)
    return num


def get_random():
    return random.randrange(1, 1001)


def ask_start_game():
    print("개복치 게임입니다.")

    if globals()['user_status']['now_stat']['weight'] != 1:
        print("[ 1. 새로 시작 ] [ 2. 계속하기 ] [ 3. 그만하기 ] ")
        while True:
            answer = verified_user_input_number()
            if answer not in [1, 2, 3]:
                print('올바른 번호를 입력해주세요.')
                continue
            break
        if answer == 1:
            globals()['user_status'] = copy.deepcopy(globals()['basic_user_status'])
            globals()['store_status'] = copy.deepcopy(globals()['basic_store_status'])
            return True
        elif answer == 2:
            globals()['user_status']['do_not_want_game'] = False
            return True
        else:
            return False

    else:
        print("[ 1. 새로 시작 ] [ 2. 그만하기 ] ")
        while True:
            answer = verified_user_input_number()
            if answer not in [1, 2]:
                print('올바른 번호를 입력해주세요.')
                continue
            break
        if answer == 1:
            globals()['user_status'] = copy.deepcopy(globals()['basic_user_status'])
            globals()['store_status'] = copy.deepcopy(globals()['basic_store_status'])
            return True
        else:
            return False
 

ask_start_game함수에선 user_status의 weight가 1이 아닌 경우엔 게임을 한적이 있다는 뜻이므로, 계속하기를 선택할 수 있게 구현했고, 새로하기를 선택할 경우엔, 공장초기화를 구현했다.

하단의 print_status는 개복치의 상태에 대해 출력하도록 했다. get_feed에서는 어드벤처 포인트를 올리고, 현재 보유하고 있는 먹이 타입 중에서 랜덤하게 먹이를 구성하게 하였고, 매번 먹이를 먹을 때 마다 확률 값에 따라 돌연사 여부를 결정하고 있다. 인게임 구동 상에는 "어 혹시? 죽는건가?" 그런 이벤트들이 중간 중간 있는데, 실제는 안죽었다는 기믹이 있어, 이도 같이 구현했다.

def print_status(user_status):
    os.system('cls')
    print(f"{'=' * 40}")
    print(f"{user_status['retry_count']}대 째 개복치 \t\t MP : {user_status['holding_mp']}")
    print(f"현재 등급 : {user_status['now_stat']['grade']}")
    print(f"몸무게 : {user_status['now_stat']['weight'] / 10:.1f}kg")
    print(f"어드벤처 포인트 : {user_status['now_stat']['adventure_point']}")


def get_feed(user_status):
    print('[ 먹이 먹기 ]를  선택하였습니다.')
    if user_status['now_stat']['adventure_point'] < 3:
        user_status['now_stat']['adventure_point'] += 1

    enable_feed_set = set()
    for (k, v) in user_status['feed_resource']['types'].items():
        if v['enable']:
            enable_feed_set.add(k)
    enable_feed_set = list(enable_feed_set)
    feed_list = []
    for f in range(user_status['feed_resource']['count_of_feed']):
        feed_list.append(enable_feed_set[random.randrange(0, len(enable_feed_set))])

    for f in feed_list:
        probability = get_random()
        death_of_probability = 1000
        feed_name = globals()['feed'][f]['name']
        gain_weight = globals()['feed'][f]['gain_weight']
        death_count = user_status['feed_resource']['types'][f]['have_been_death']
        if f != 0:
            if death_count == 0:
                death_of_probability = 920
            elif death_count > 1:
                death_of_probability = 999
            if probability > 800:
                print('오잉...? 개복치의 상태가..?')
                input('확인 하시겠습니까? press <Enter> ')
                if probability > death_of_probability:
                    print('돌.연.사')
                    print(globals()['feed'][f]['reason_of_death'])
                    print(globals()['feed'][f]['death_description'])
                    user_status['feed_resource']['types'][f]['have_been_death'] += 1
                    user_status['now_stat']['is_alive'] = False
                    return
                else:
                    print('그냥 기분탓인가 봅니다.')
        print(f"먹이 {feed_name}을(를) 먹었습니다. 몸무게가 {gain_weight / 10:.1f}kg 증가합니다.")
        user_status['now_stat']['weight'] += gain_weight
 

하단의 get_adventure 함수도 get_feeding 과 비슷한 로직이라 금방 구현했다.

def get_adventure(user_status):
    probability = get_random()
    adventure_index = 0
    for (k, v) in user_status['adventure_resource'].items():
        if v['enable']:
            adventure_index = k

    have_been_death_count = user_status['adventure_resource'][adventure_index]['have_been_death']
    death_probability = 990
    if have_been_death_count == 0:
        death_probability = globals()['adventure_success_probability'][0]
    elif have_been_death_count == 1:
        death_probability = globals()['adventure_success_probability'][1]
    elif have_been_death_count == 2:
        death_probability = globals()['adventure_success_probability'][2]
    if probability > death_probability:
        print('돌.연.사')
        print(globals()['adventure'][adventure_index]['reason_of_death'])
        print(globals()['adventure'][adventure_index]['death_description'])
        user_status['adventure_resource'][adventure_index]['have_been_death'] += 1
        user_status['now_stat']['is_alive'] = False
        return
    else:
        adventure_name = globals()['adventure'][adventure_index]['name']
        gain_weight = globals()['adventure'][adventure_index]['gain_weight']
        gain_mp = globals()['adventure'][adventure_index]['success_mp_reward']
        print(f"모험 {adventure_name}을(를) 성공했습니다. 몸무게가 {gain_weight / 10:.1f}kg 증가합니다.")
        user_status['now_stat']['weight'] += gain_weight

 

상점 구현이 제일 어려웠다. 현재 갖고 있는 MP 포인트를 이용하여 먹이, 모험, 먹이 개수를 구입하는 것인데, 현재 상태를 불러와 비교하여 구매 가능 리스트를 출력했다. 1번만 구매하고 끝나는게 아니라, 다른 구매도 할 수 있게 상점 화면에서 나가지 않게 구현했다. 또한, 뒤로가기 기능을 넣어 유저 편의를 개선했다.

def get_store(user_status, store_status):
    is_want_buying = True
    while is_want_buying:
        is_want_back = False
        while True:
            print(f'\t[[ 상 점 ]]')
            print(f"현재 MP : [ {user_status['holding_mp']:>10d} ] points")
            print('[ 1. 먹이 구입 ] [ 2. 모험 구입 ] [ 3. 먹이 개수 구입 ] [ 4. 뒤로 가기 ] ')
            answer = verified_user_input_number()
            if answer not in [1, 2, 3, 4]:
                print('다시 선택해주십시요.')
                continue
            break
        if answer == 1:
            print('[ 먹이 ] 를 구입합니다.')
            enable_buy_index = [0]
            list_for_print = []
            for (k, v) in store_status['feed_resource'].items():
                name = v['name']
                price = v['price']
                index = k
                if k == 0:
                    index = '*'
                option = ''
                if v['basic_option']:
                    option = '기본'
                elif v['not_been_bought'] is False:
                    option = '구매하기'
                    enable_buy_index.append(k)
                else:
                    option = '구매완료'
                list_for_print.append(f"{index}. {name} {price}mp {option}")
            for l in list_for_print:
                print(l)
            while not is_want_back:
                print('몇 번을 구매하시겠습니까? [ 0. 나가기 ]')
                num = verified_user_input_number()
                if num not in enable_buy_index:
                    print('다시 선택해주세요.')
                    continue
                if store_status['feed_resource'][num]['price'] > user_status['holding_mp']:
                    print('금액이 부족합니다. 다시 선택해주세요')
                    continue
                if num == 0:
                    is_want_back = True
                    break
                # num에 따라 feed_resource 구매 처리
                user_status['feed_resource']['types'][num]['enable'] = True
                user_status['holding_mp'] -= store_status['feed_resource'][num]['price']
                store_status['feed_resource'][num]['not_been_bought'] = True
                break

        elif answer == 2:
            print('[ 모험 ] 을 구입합니다.')
            enable_buy_index = [0]
            list_for_print = []
            for (k, v) in store_status['adventure_resource'].items():
                name = v['name']
                price = v['price']
                index = k
                if k == 0:
                    index = '*'
                option = ''
                if v['basic_option']:
                    option = '기본'
                elif v['not_been_bought'] is False:
                    option = '구매하기'
                    enable_buy_index.append(k)
                else:
                    option = '구매완료'
                list_for_print.append(f"{index}. {name} {price}mp {option}")
            for l in list_for_print:
                print(l)
            while not is_want_back:
                print('몇 번을 구매하시겠습니까? [ 0. 나가기 ]')
                num = verified_user_input_number()
                if num not in enable_buy_index:
                    print('다시 선택해주세요.')
                    continue
                if store_status['adventure_resource'][num]['price'] > user_status['holding_mp']:
                    print('금액이 부족합니다. 다시 선택해주세요')
                    continue
                if num == 0:
                    is_want_back = True
                    break
                # num에 따라 adventure 구매 처리
                user_status['adventure_resource'][num]['enable'] = True
                user_status['holding_mp'] -= store_status['adventure_resource'][num]['price']
                store_status['adventure_resource'][num]['not_been_bought'] = True
                break

        elif answer == 3:
            print('[ 먹이 개수 ] 를 구입합니다.')
            enable_buy_index = [0]
            list_for_print = []
            for (k, v) in globals()['distributions_of_feed_price'].items():
                count_of_feed = v['count_of_feed']
                price = v['need_mp_to_buy']
                index = k
                if k == 0:
                    index = '*'
                option = ''
                now_count = user_status['feed_resource']['count_of_feed']
                if now_count < count_of_feed:
                    option = '구매하기'
                    enable_buy_index.append(k)
                elif now_count == globals()['distributions_of_feed_price'][0]:
                    option = '기본'
                else:
                    option = '구매완료'
                list_for_print.append(f"{index}. {count_of_feed}개 {price}mp {option}")
            for l in list_for_print:
                print(l)
 
            while not is_want_back:
                print('몇 번을 구매하시겠습니까? [ 0. 나가기 ]')
                num = verified_user_input_number()
                if num not in enable_buy_index:
                    print('다시 선택해주세요.')
                    continue
                if globals()['distributions_of_feed_price'][num]['need_mp_to_buy'] > user_status['holding_mp']:
                    print('금액이 부족합니다. 다시 선택해주세요')
                    continue
                if num == 0:
                    is_want_back = True
                    break
                # num에 따라 먹이 개수 구매 처리
                feed_count = globals()['distributions_of_feed_price'][num]['count_of_feed']
                price = globals()['distributions_of_feed_price'][num]['need_mp_to_buy']
                user_status['feed_resource']['count_of_feed'] = feed_count
                user_status['holding_mp'] -= price
                break

        else:
            is_want_buying = False
            print('[ 뒤로가기 ] 를 선택했습니다.')
 

하단의 check_alive 함수는 모드 행동(먹이, 모험) 이후에 살아있으면 그냥 지나지만, 죽어있는 상태라면 초기화하고 다시 시작할지 여부를 물어본다. 만약 그만한다고 할 경우, 맨 처음 [새로시작하기, 계속하기, 그만하기] 화면으로 돌려보내고, 실수로 그만했을 경우도 고려하여, 계속하기 기능을 넣게된 이유이다.

 

하단의 check_grade 함수는 check_alive 가 위에 있어 로직 상 살아있는 상태에서만 실행된다. 현재 체중을 기준으로, 체중 등급과 현재 등급이 다를 경우, 등급이 올랐다는 뜻이므로, 이에 대한 출력과 MP보상 로직이 내용이다. 또한, 최고 레벨인 '수족관 주인'을 달성할 경우엔, 게임이 클리어 했음을 고지하고, 이후로도 플레이를 더 하고 싶은지 물어보게 하였다. 매번 행동을 할때마다 물어보는게 아니라, 클리어 이후에 계속할지는 1번만 물어보게 했다.

def check_alive(user_status):
    if not user_status['now_stat']['is_alive']:
        user_status['retry_count'] += 1
        user_status['now_stat']['grade'] = '별사탕'
        user_status['now_stat']['weight'] = 1
        user_status['now_stat']['is_alive'] = True
        user_status['now_stat']['adventure_point'] = 0

        print("다시 새로 키우시겠습니까?")
        print("[ 1. 새로 키운다 ] [ 2. 그만 한다 ]")
        while True:
            answer = verified_user_input_number()
            if answer not in [1, 2]:
                print('다시 선택해주십시요.')
                continue
            break
        if answer == 1:
            pass
        else:
            user_status['do_not_want_game'] = True


def check_grade(user_status):
    now_weight = user_status['now_stat']['weight']
    now_grade = user_status['now_stat']['grade']
    for (k, grade_) in globals()['grade'].items():
        if grade_['min_weight'] <= now_weight < grade_['max_weight'] and grade_['name'] != now_grade:
            print('오잉..? 개복치의 상태가...?')
            input('확인 하시겠습니까? press <Enter> ')
            print(f"상태가 {now_grade}에서 {grade_['name']}으로 진화하였습니다.")
            user_status['now_stat']['grade'] = grade_['name']
            if user_status['grade_resource'][k] == 0:
                reward = globals()['grade'][k]['first_evolution_mp_reward']
                user_status['holding_mp'] += reward
                print(f"처음 진화 보너스로 {reward}mp를 드립니다!")
            elif user_status['grade_resource'][k] == 1:
                reward = globals()['grade'][k]['second_evolution_mp_reward']
                user_status['holding_mp'] += reward
                print(f"두번째 진화 보너스로 {reward}mp를 드립니다!")
            user_status['grade_resource'][k] += 1
            input('확인하셨습니까? press <Enter> ')
            return

    if now_grade == '수족관 주인' and user_status['is_user_want_keep'] is False:
        print('게임을 클리어 하셨습니다.!!')
        input('확인하셨습니까? press <Enter> ')
        print('더 이상의 진화는 없습니다. 그래도 게임을 더 하시겠습니까?')
        print("[ 1. 계속 하기 ] [ 2. 그만 하기 ]")
        while True:
            answer = verified_user_input_number()
            if answer not in [1, 2]:
                print('올바른 번호를 입력해주세요.')
                continue
            if answer == 1:
                user_status['is_game_end'] = False
                user_status['is_user_want_keep'] = True
            else:
                user_status['is_game_end'] = True
            break

 

하단의 select_mode 함수는 위에 작성한 함수를 토대로 어떤 행동을 할지 결정하도록 하는 함수다.

테스트를 위해 555번을 입력할 경우, 치트가 발동하여 돈과 먹이를 올려주어 손쉽게 클리어를 유도했다.

def select_mode(user_status, store_status):
    print("[ 1. 먹이 먹기 ] [ 2. 모험 하기 ] [ 3. 상점 ] [ 4. 종료 ] ")
    while True:
        answer = verified_user_input_number()
        if answer not in [1, 2, 3, 4, 555]:
            print('올바른 번호를 입력해주세요.')
            continue
        if answer == 1:
            get_feed(user_status)
            break
        elif answer == 2:
            if user_status['now_stat']['adventure_point'] < 3:
                print('모험을 떠나기엔 어드벤처 포인트가 부족합니다.')
                continue
            else:
                user_status['now_stat']['adventure_point'] = 0
                get_adventure(user_status)
            break
        elif answer == 3:
            get_store(user_status, store_status)
            break
        elif answer == 555:  # for test
            user_status['now_stat']['adventure_point'] = 3
            user_status['holding_mp'] = 100000
            user_status['feed_resource']['types'][0]['enable'] = False
            user_status['feed_resource']['types'][8]['enable'] = True
            user_status['feed_resource']['count_of_feed'] = 200
        else:
            user_status['do_not_want_game'] = True
            break

    check_alive(user_status)


def print_all_status():  # for debug function
    print("globals()['user_status']", globals()['user_status'])
    print("globals()['store_statue']", globals()['user_status'])
    print("globals()['basic_user_status']", globals()['basic_user_status'])
    print("globals()['basic_store_status']", globals()['basic_store_status'])

 

 

 

 

 

구동화면은 이렇다.

 

다행히.. 어찌저찌 5pm이전까지 완료하여, 조동현 교수님께 피드백을 받을 수 있었다.

나는 딕셔너리 타입이 과도하게 많아 비효율적이게 많은 메모리를 사용하고 있었다. 세이브 파일을 불러올 수 있거나, 만약 클라이언트와 오해가 있어 먹이의 종류를 상점에서 토글(toggle)형식으로 구매하는게 아닌 개수를 구매하는 형식이었다면, 어떻게 할 것인가 물으셨다. 세이브 파일은 user_status를 따로 관리하고 있는 중이었기 때문에, 다행히 파일로 저장하면 쉽게 불러올 수 있는 형태였고(거의 json상태다), 먹이 개수는 시간이 좀 걸리지만 불가능한 상태는 아니였다. 그리하여 수업시간 이후에 피드백을 반영하여 version 2를 작성했다.

    ver2 수정 부분:
        "먹이를 개수별로 구매합니다"
        상점에서 먹이를 구매하면, 그 개수만큼 먹이가 출현한다. -> 고로 먹이 구매 개수 만큼 구매할 수 있음.
        소스 수정 부분 :
            'basic_user_status' , 'feed_resources', 'types'에 enable 이 boolean 대신 int 가 들어감 (0) 부터 시작
            get_store() 가능한 먹이 리스트 검증 시  user_status의 count_of_feed 를 확인하게 함.
            get_store() 먹이 구매 시 user_status의 enable False -> True  대신 값을 1씩 증가하게 함
 

이렇게 계획을 구성하여 함수를 손봐야했다.

 

(ver2의 구동 모습까지는 중요한 사항이 아닌 것 같아 소스 코드로 대체합니다)

 

 

개발 완료 후기)

제한 시간 내에 굵직굵직한 로직들을 구현하는게 애를 많이 먹었는데, 다행히 시간 내에 완료할 수 있어 운이 좋았다고 생각했다. 다만 미흡한 부분도 있었기 때문에, 추후에 어떻게 보완해야할지 고민해보았다. 결국은 클래스를 사용해야 저런 복잡한 dictionary 타입을 쓰지 않아도 될 것이라 생각했다. list만 가지고 쓰기엔 index의 정보를 매번 암기해야하고, 또한, 코드를 작성할 때, 내가 지금 쓰는 변수가 무엇인지..? 맞게 하고 있는건지 구분이 잘 안될 때가 많았다. 언젠가는 파이썬에서 OOP를 자유자재로 다루길 기원하며, 오늘의 학습일지는 여기에서 마무리한다.

 

감사합니다.