[python] python3 힘들게 배우기 #18

13 분 소요

ex43. 기본적인 객체지향 프로그래밍의 분석과 디자인

지금부터 파이썬을 이용해 무언가를 만들 때(특히 객체지향 프로그램에서) 필요한 과정을 설명해 보고자 한다. 지금부터 설명하는 과정은 절대적인 순서는 아니며 여러 가지 방법 중에 하나이므로 이것을 하나의 진리로 받아들이는 일은 없도록 하자. 단지 많은 프로그래밍 문제를 해결하는 하나의 좋은 시작 방법이라고 생각하자.

  1. 문제에 대해서 글을 쓰거나, 그림으로 표현한다.
  2. key concept을 추출하고 그것에 대해 조사한다.
  3. 클래스 상속도를 만들고, 컨셉에 맞는 오브젝트 맵을 그린다.
  4. 클래스들을 코딩한 후 실행시킨다.
  5. 반복 실행 하고 정제한다.

지금 보는 과정을 ‘탑-다운(top-down)’방법이라고 하는데, 아주 추상적인 아이디어에서 천천히 구체적인 생각과 코딩이 가능한 수준으로 정제하는 방법이다.

처음 시작할 떄는 노트나 그림으로 묘사한 후, 키-컨셉을 추출한다. 이 때 쓰는 방법이 단순히 명사동사 들을 나열해 보는 것이다. 이것이 나중에 클래스 와 오브젝트들, 나아가 함수 를 만드는 데 도움이 된다.

일단 나열해 본 후에는 그것들이 어떻게 클래스로서 관련이 있는지 관계도를 작성한다. 어떤 게 상위 클래스가 될 지? 하위가 될 지? tree구조로 작성해 보도록 한다.

하이어라키가 완성되면 기본적인 클래스만 존재하는 스켈레톤 코드를 작성한다. 그리고 코드를 돌려서 제대로 동작하는 지 확인해 보고, 조금 더 작성 하고 다시 돌리는 과정을 반복한다.

이 과정을 하나의 텍스트 게임 엔진과 게임을 만들어 보는 과정을 통해 보여주도록 하겠다.

게임에 대해 적어보기

산문형태로 게임에 대해 대략적으로 알 수 있도록 묘사해보자.

“배경은 중세시대. 주인공은 기사로서 미로를 뚫고 지나가며 괴물들을 물리쳐야 한다. 게임은 각 장면으로 연결된 맵을 실행시키는 엔진이 포함될 것이다. 각각의 장면은 주인공이 입장 시 자기자신에 대한 설명을 프린트할 것이며 엔진은 맵에서 다음 장면이 무엇인지 말해줄 것이다.”

이 시점에서 게임에 필요할 것 같은 단어들을 무작위로 적어 보겠다.

  • Death: 플레이어가 죽을 때
  • Corridor: 시작 지점으로서 이미 괴물이 나타나 있음
  • Forge: 주인공이 검을 얻을 수 있음. 성에서 탈출하기 전 신상을 부수기 위한 칼임. 주인공이 숫자를 입력할 수 있도록 키패드가 있다.
  • Chamber: 주인공이 괴물을 무찌르는 곳
  • Escape room: 탈출할 수 있는 방, 하지만 올바른 문을 추측해야만 한다.

key concept을 추출하고 조사해 보기

이제 Class hierachy를 구성하기 위해 명사 를 적어보도록 하겠다.

  • monster
  • player
  • castle
  • maze
  • scene
  • escape room
  • map
  • engine
  • Death
  • Corridor
  • Forge
  • Chamber

일단 배틀 시스템을 만들고 싶지 않으므로(?) monster 와 player는 무시하겠다. castle, maze, map은 전부 게임 전체를 뜻하므로, map으로 통일시킨다. scene밑에 속하는 것들 (escape room, Corridor, forge, chamber)는 하위 클래스로 넣는다. 그럼 아래와 같이 정리될 수 있겠다.

  • Map
  • Engine
  • Scene
    • Death
    • Corridor
    • Forge
    • Chamber
    • Escape room

이후에는 각 클래스에 필요할 것으로 생각되는 동사 를 적어보도록 하겠다. 예를 들면 엔진에는 “실행” 액션이 필요할 것이고.. 맵에는 “다음 장면으로”, “장면 열기”, Scene에서는 “들어가기” 등이 필요할 것이다. 아래와 같이 적용해 보자:

  • Map
    • next_scene
    • opening_scene
  • Engine
    • play
  • Scene
    • enter
    • Death
    • Corridor
    • Forge
    • Chamber
    • Escape room

Scene 에 -enter명령어를 넣었기 때문에, 하위 클래스인 Death, Corridor 등에서 -enter를 상속받아 이용할 수 있을 것이라는 점을 기억해두자.

이제 클래스들을 코딩한 후 테스트 런을 해보자.

class Scene(object):

    def enter(self):
        pass

class Death(Scene):

    def enter(self):
        pass

class Corridor(Scene):

    def enter(self):
        pass

class Forge(Scene):

    def enter(self):
        pass

class Chamber(Scene):

    def enter(self):
        pass

class EscapeRoom(Scene):

    def enter(self):
        pass

class Engine(object):

    def __init__(self, scene_map):
        pass

    def play(self):
        pass

class Map(object):

    def __init__(self, start_scene):
        pass

    def next_scene(self, scene_name):
        pass

    def opening_scene(self):
        pass

a_map = Map('corridor')
a_game = Engine(a_map)
a_game.play()

여기까지 간단한 하이어라키 구성 및 마지막 부분에 실행을 위한 간단한 코드가 들어가 있는 곳까지 구상하였다.

이후 나머지 코드들을 작성해 보도록 하겠다.

# 기본적으로 사용할 모듈들을 임포트하자. 3가지를 임포트하겠다.
from sys import exit
# radnom한 int를 생성해주는 randint
from random import randint
# textwrap.dedent(text)함수는 text 앞 쪽의 공백을 없애주는 함수다. 특히 트리플 쿼트 """
# 에서 사용할 경우, 왼쪽 정렬해 주는 효과가 있다. 파이선 공식 문서 참조.
# https://docs.python.org/3.6/library/textwrap.html#textwrap.dedent
from textwrap import dedent

# Scene과 이하 클래스가 공유하는 enter 함수 정의.
# SubClass와 ParentClass에 같은 이름의 함수가 있을 경우, SubClass를 호출한다.
# Scene 클래스에는 SubClass에 enter()가 정의되지 않을 경우 나올 문구를 작성해 둔다.
class Scene(object):

    def enter(self):
        print("아직 이 Scene이 작성되지 않았습니다.")
        print("Subclass를 정의하신 후 enter()를 수행하십시오.")
        exit(1)

# Death 씬은 죽었을 떄 임의의 문구 중 하나를 출력한다. 
class Death(Scene):

    # 문구를 넣을 list 작성 
    quips = [
        "당신은 죽었습니다.",
        "실력이 형편없네요.",
        "다시 시도해 보세요."
        "ㅠㅠㅠㅠ."
    ]
    def enter(self):
        print(Death.quips[randint(0, len(self.quips) - 1)])
        exit(1)

# 게임 진행 순서는 Corridor>Forge>Chamber>EscapeRoom 순.
# Corridor씬에서는 정답인 문구를 맞추면 통과한다. 
class Corridor(Scene):

    def enter(self):
        print(dedent("""
              당신은 괴물을 무찌르기 위해 걸어나가기 시작했습니다.
              곧 괴물과 마주쳤습니다. 어떤 행동을 취하시겠습니까? 
              1. 때린다. 2. 피한다. 3. 농담을 한다. 
              """))

        action = input("> ")
        
        if action == '1':
            print("괴물은 화가 나서 당신을 죽였습니다.")
            return 'death'
        
        elif  action == '2':
            print("당신은 피할 수 없었습니다.")
            return 'death'

        elif action == '3':
            print("운이 좋군요. 괴물이 웃는 사이 지나갈 수 있었습니다.")
            return 'forge'

        else:
            print("1,2,3 중 하나로 입력하세요.")
            return 'corridor'

# Forge씬에서는 11~19 중 10번(..) 안에 맞추면 검을 얻도록 설정한다. 
class Forge(Scene):

    def enter(self):
        print("검을 얻고 싶다면 11~19 숫자 중 하나를 10번안에 맞추시오.")
        code = f"{randint(1,1)}{randint(1,9)}" # code 변수는 string
        guess = input("> ")
        guesses = 0

        while guess != code and guesses < 10:
            rest = 10 - guesses
            print(f"다시 시도해 보세요. 남은 횟수는 {rest}회 입니다." )
            guesses += 1
            guess = input("> ")

        if code == guess:
            print("정답입니다.")
            return 'chamber'
        else:
            print("안됐지만, 실패했습니다.")
            return 'death'

# Chamber씬에서는 올바른 액션을 해야 넘어갈 수 있다. 
class Chamber(Scene):

    def enter(self):
        print(dedent("""
              당신은 최종보스와 마주쳤습니다. 검을 휘둘러 무찔러야 합니다.
              어느 곳을 공격하겠습니까?
              1. 목 2. 가슴 3. 발바닥 
              """))

        action = input("> ")
        
        if action == '1':
            print("보스의 목은 튼튼해서 검이 들어가지 않습니다.")
            return 'death'
        
        elif action == '2':
            print("보스의 가슴은 튼튼해서 검이 들어가지 않습니다.")
            return 'death'

        elif action == '3':
            print("발바닥을 찔린 보스는 고통에 몸부림치다 죽었습니다.")
            return 'escape_room'

# Escape Room에서는 1~5중 올바를 숫자를 찍어야 탈출 가능하다. 
class EscapeRoom(Scene):

    def enter(self):
        print(dedent("""
              당신의 앞에 탈출 통로로 보이는 문이 5개 있습니다.
              어떤 문으로 통과하시겠습니까?
              """))

        number = randint(1, 5)
        guess = input("> ")

        if number == int(guess):
          print("정답입니다.")
          return 'finished'
        else:
          print("함정이 나타났습니다.")
          return 'death'


# 위에서는 작성하지 못했지만, 게임을 끝냈을 때의 씬도 클래스로 만들어 놓자.
class Finished(Scene):

    def enter(self):
        print("축하합니다. 모든 시련이 끝났습니다.")
        return exit(0)


# Engine class는 게임이 돌아가기 위해, 씬을 이동시키는 기능이 있는 클래스다. 
# 씬 클래스에 들어있는 enter()함수를 실제로 구동시키는 클래스 
class Engine(object):

    # Map클래스의 인스턴스를 인수로 받아 인스턴스화한다.
    def __init__(self, scene_map):
        self.scene_map = scene_map

    # play() 함수는, Map클래스의 함수들을 이용하여
    # current_scene변수에 시작 씬(opening_scene)을 넣고,
    # last_scene변수에 'finished'씬을 넣어서
    # 두 변수값이 같을 때까지 각씬의 enter()를 실행시킴으로써
    # 플레이어로 하여금 마지막 씬에 다다르게 만든다.  
    def play(self):
        current_scene = self.scene_map.opening_scene()
        last_scene = self.scene_map.next_scene('finished')

        while current_scene != last_scene:
            next_scene_name = current_scene.enter()
            current_scene = self.scene_map.next_scene(next_scene_name)

        current_scene.enter()


class Map(object):

    # 각 씬들을 실행시키는 클래스를 dict를 통해 만들어 두자.
    # key는 string값이고 value는 클래스 인스턴스화.
    scenes = {
      'corridor': Corridor(),
      'forge': Forge(),
      'chamber': Chamber(),
      'escape_room': EscapeRoom(),
      'finished': Finished(),
      'death': Death(),
    }

    # Map클래스는 start_scene 인수를 받으며 인스턴스화.
    # 인스턴스변수 start_scene에 인수 start_scene을 넣자.
    def __init__(self, start_scene):
        self.start_scene = start_scene

    # next_scene 함수는 scene_name을 인수로 받아서 key값으로 사용,
    # Map 클래스에 정의되어 있는 scenes dict 에서 value를 추출해서 reuturn한다.
    # 이후 리턴된 클래스()에 Engine에 마지막 줄에 있는 .enter()가 붙어서 
    # 씬이 실행된다. 
    def next_scene(self, scene_name):
        val = Map.scenes.get(scene_name)
        return val

    # opening_scene 함수는 인수를 받지 않고,
    # 실행될 경우 Map 클래스의 next_scene함수를 실행시켜서 리턴하는데
    # 이 때 next_scene함수의 인수는 init시킬 때 넣었던 start_scene값이다.
    def opening_scene(self):
        return self.next_scene(self.start_scene)

    # opening_scene()과 next_scene()의 역할은?
    # next_scene()은 scene_name으로 씬을 가져오는 함수.
    # opening_scene()은 next_scene()함수를 이용해서,
    # Map 클래스를 인스턴스화 할 때 썼던 start_scene을 넣은 것 뿐인 함수.


a_map = Map('corridor')
a_game = Engine(a_map)
a_game.play()

실행화면

당신은 괴물을 무찌르기 위해 걸어나가기 시작했습니다.
곧 괴물과 마주쳤습니다. 어떤 행동을 취하시겠습니까? 
1. 때린다. 2. 피한다. 3. 농담을 한다. 

> 3
운이 좋군요. 괴물이 웃는 사이 지나갈 수 있었습니다.
검을 얻고 싶다면 11~19 숫자 중 하나를 10번안에 맞추시오.
> 11
다시 시도해 보세요. 남은 횟수는 10회 입니다.
> 12
다시 시도해 보세요. 남은 횟수는 9회 입니다.
> 13
다시 시도해 보세요. 남은 횟수는 8회 입니다.
> 14
다시 시도해 보세요. 남은 횟수는 7회 입니다.
> 15
다시 시도해 보세요. 남은 횟수는 6회 입니다.
> 16
다시 시도해 보세요. 남은 횟수는 5회 입니다.
> 17
다시 시도해 보세요. 남은 횟수는 4회 입니다.
> 18
정답입니다.

당신은 최종보스와 마주쳤습니다. 검을 휘둘러 무찔러야 합니다.
어느 곳을 공격하겠습니까?
1. 목 2. 가슴 3. 발바닥 

> 3
발바닥을 찔린 보스는 고통에 몸부림치다 죽었습니다.

당신의 앞에 탈출 통로로 보이는 문이 5개 있습니다.
어떤 문으로 통과하시겠습니까?

> 4
정답입니다.
축하합니다. 모든 시련이 끝났습니다.

댓글남기기