간단한 보드게임 만들기

yun·2023년 9월 3일
10

Python

목록 보기
4/13
post-thumbnail

클래스 개념을 실습하기 위해 간단한 보드게임을 만들어 보기로 했다.
결과물은 위에 보이는 표지 이미지이다.

게임의 규칙

  • 플레이어의 말은 흰색과 검은색이다.
    • Player라는 베이스 클래스를 바탕으로, 그를 상속하는 클래스는 WhitePlayer와 BlackPlayer라는 이름으로 만든다.
  • 게임을 시작할 때 말의 색상을 고른다.
  • '나'와 적의 현재 위치를 list를 활용해서 화면에 표시한다.
  • 주사위를 굴리는 순서는 게임 초기에 랜덤으로 컴퓨터가 골라준다.
  • 매 턴마다 1부터 6사이의 값을 가지는 주사위를 굴린다.
  • 숫자 20에 먼저 도착하는 플레이어가 이기고, 이때 게임이 종료된다.

클래스 작성

class Player:
    def __init__(self):
        self.idx = 0

    def show_you(self):
        print("\n")
        print("Let's get started!")
        print("This is you: " + self.icon)
        print("This is where you are now: " + str(self.idx))

    def show_enemy(self):
        print("\n")
        print("FYI, this is your enemy: " + self.icon)
        print("This is where the enemy is now: " + str(self.idx))

    def set_the_order(self, n):
        self.order = n

    def roll_the_dice(self):
        dice_number = random.randrange(1, 7)
        print("The dice number of " + self.name + ": " + str(dice_number))

        return dice_number
    
    def move(self, n):
        self.idx += n


class WhitePlayer(Player):
    def __init__(self, name):
        self.idx = 0
        self.icon = '○'
        self.name = name  # you or enemy


class BlackPlayer(Player):
    def __init__(self, name):
        self.idx = 0
        self.icon = '●'
        self.name = name  # you or enemy
  • 사실 흰말과 검은말은 '나'와 적을 구분하기 위한 것으로, 두 플레이어의 기능은 거의 같다. 따라서 내장함수 init()을 override하여 icon과 name만 다른 값을 갖게 했다.

remarks 1

파이썬의 클래스는 자바와 달리 모두 public 접근이 가능하고, property에 직접 접근을 막는 방법이 없다.

그렇다고 해도 변수를 private으로 명시하는 방법은 있다.
변수 앞에 _를 붙여서 private 변수임을 동료 개발자에게 표시할 수 있고, 언더바를 두 개 붙이면 name mangling이 적용되어 _클래스명__속성명 형태로만 접근할 수 있게 된다.

private 변수에 대해서는, 아래와 같이 어노테이션(@)을 사용해 getter/setter를 따로 만들어준다.

    @property
    def order(self):
        return self.__order
    
    @order.setter
    def order(self, n):
        self.__order = n

함수명이 get_order, set_order 같이 서로 다를 경우 name 'order' is not defined 에러가 발생했다. (참고: https://stackoverflow.com/questions/598077/why-does-foo-setter-in-python-not-work-for-me)

remarks 2

파이썬의 클래스 함수를 사용할 때, 필요한 param이 없더라도 def에서 self라는 param은 표시해주어야 한다. 만약 다음과 같이 작성하면

def show_you():
	...

함수를 사용할 때 show_you()로 작성해도 다음과 같은 에러가 발생한다.

TypeError: show_you() takes 0 positional arguments but 1 was given

0개의 argument가 있어야 하는데 1개의 argument가 주어졌다는 뜻이다.

remarks 3

함수 선언 시 @staticmethod 어노테이션이 있으면 self를 사용하지 않으며, 위와 같이 param을 표시하지 않아도 된다. 다만 클래스/인스턴스 property에 접근하거나 클래스/인스턴스 메서드를 호출하는 것도 불가하다.

@staticmethod
def show_you():
	...

게임의 순서

intro_game()
you = pick_your_color()
enemy = set_the_enemy(you)
show_the_map(you, enemy)
set_the_order(you, enemy)
play(you, enemy)
show_the_winner(you, enemy)
goodbye()

intro_game

  • 먼저, 사용자에게 게임의 규칙을 설명한다.
def intro_game():
    print("Welcome to the Simple Board Game!\n")
    print("The Rule is simple:\n")
    print("1. Pick the color, then your enemy will get the other color.")
    print("The computer will make the order for each of you to roll the dice.\n")
    print("2. You start from zero, and move just as the dice number.")
    print("When you reach 20 first, you win.\n")

pick_your_color

  • 사용자에게 말을 선택하게 한다.
  • 혹시 사용자가 B나 W를 제외한 값을 입력하면, 입력값을 보여주고 다시 선택하도록 반복문을 실행한다.
def pick_your_color():
    your_color = input("If you are ready, pick the color. [B: black / W: white]")

    while your_color != 'chosen':
        if your_color == 'B':
            your_color = 'chosen'
            you = BlackPlayer('you')
        elif your_color == 'W':
            your_color = 'chosen'
            you = WhitePlayer('you')
        else:
            print(your_color)
            your_color = input("Sorry, the choice has to be B or W. Try again.")

    you.show_you()
    
    return you

set_the_enemy(you)

  • 사용자가 정한 색을 기준으로, 남은 색을 enemy로 정해준다.
def set_the_enemy(you):
    if isinstance(you, BlackPlayer):
        enemy = WhitePlayer('enemy')
    else:
        enemy = BlackPlayer('enemy')

    enemy.show_enemy()

    return enemy

show_the_map(you, enemy)

  • '나'와 적의 위치를 표시해준다.
  • 이미 이동한 말이 이전 위치에 표시되지 않도록 draw_map 함수로 매번 새로 지도를 그린다.
def draw_map():
    board_map = []

    for i in range(21):
        board_map.append(str(i))

    return board_map
def show_the_map(you, enemy):
    
    # 기존 icon을 지워주는 작업
    board_map = draw_map()

    # idx 가 20보다 큰 경우 20으로 만들어줌
    if you.idx > 20:
        you.idx = 20
    if enemy.idx > 20:
        enemy.idx = 20

    # you와 enemy가 같은 위치인 경우
    two_icons = you.icon + enemy.icon
    if enemy.idx == you.idx:
        board_map[you.idx] = two_icons
    
    # you와 enemy가 다른 위치인 경우
    else:
        board_map[you.idx] = you.icon
        board_map[enemy.idx] = enemy.icon

    print('you: ' + str(you.idx))
    print('enemy: ' + str(enemy.idx))
    print("Let's see the map.")
    print(board_map)

set_the_order(you, enemy)

  • 주사위를 던지는 순서를 정한다.
def set_the_order(you, enemy):
    queue = [you, enemy]
    
    random.shuffle(queue)

    for i, v in enumerate(queue):
        if queue[i] == you:
            you.order = i
        if queue[i] == enemy:
            enemy.order = i

    print("\n")
    print("The computer set the order for you and the enemy.")
    if you.order == 0:
        print("Lucky you! You go first!")
    else:
        print("Your enemy goes first, and you are the second one.")
  • 자료구조 중 queue를 실습하기 좋을 거라고 생각했는데, list.pop()은 O(n)의 시간 복잡도를 가지고, Queue를 import해서 사용하기엔 list처럼 index접근이 안 된다는 게 아쉽다.
  • list 타입을 사용하기로 하고, random.shuffle로 정해진 순서를 각 player의 order property에 할당한다.

play(you, enemy)

  • 게임 시작!
def play(you, enemy):
    while you.idx < 20 and enemy.idx < 20:
        roll_the_dice(you, enemy)

        if you.idx == 20 or enemy.idx == 20:
            break
        elif input("Will you roll the dice again? [Y/N]") == "N":
            break
  • '나' 또는 적의 위치가 20이 될 때까지 (또는 사용자가 게임을 중단하고 싶을 때까지) 계속한다.

roll_the_dice(you, enemy)

  • 순서대로 주사위를 굴리고, 현재 맵을 보여준다.
def roll_the_dice(you, enemy):
    if you.order == 0:
        go(you)
        show_the_map(you, enemy)
        
        if you.idx < 20:
            go(enemy)
            show_the_map(you, enemy)

    else:
        go(enemy)
        show_the_map(you, enemy)
        
        if enemy.idx < 20:
            go(you)
            show_the_map(you, enemy)

go(player)

  • 플레이어를 변수로, you 또는 enemy의 주사위를 굴리고 이동한다.
def go(player):
    print("\n")
    dice_number = player.roll_the_dice()
    player.move(dice_number)

show_the_winner(you, enemy)

  • 위 반복문이 종료된 후, 승자를 알려준다.
def show_the_winner(you, enemy):
    if max(you.idx, enemy.idx) == you.idx:
        print("Bravo! You won!")
    else:
        print("The winner is your enemy.")
  • '나'와 적의 idx를 비교하여 max값이 '나'의 idx이면 내가 승자
  • 그렇지 않으면 적이 이긴 것으로 표시

goodbye

  • 이겼든 졌든, 다음에 또 봐요!
def goodbye():
    print("Well played! See you again :)")

개선할 점

  • DRY(Do not Repeat Yourself) 규칙을 지키지 않고 있다.
  • 지금은 말이 2개지만, 말이 4개라면?
    • if/else로 구분하기엔 경우의 수가 많을 것이다. 케이스 구분 방법을 다르게 고민해 보자.
    • 플레이어가 많다면, 함수의 param, args도 players와 같이 하나로 작성해야 코드 관리가 용이할 것이다.
  • 클래스 필드에 직접 접근하지 말고 setter, getter 사용하기
    • 고민의 흔적으로 Player 클래스에 set_the_order가 있는데 아래에서 코드를 작성할 때는 order에 직접 접근해서 set/get 처리하고 있다.
  • 현재 위의 모든 코드가 한 py 파일에 작성되어 있다. 벌써 200라인이 넘고, 어디에 무슨 코드가 있는지 찾기 번거롭다. 파일을 분리해서 작성해 보자.

2개의 댓글

comment-user-thumbnail
2023년 9월 4일

파이썬의 클래스가 무엇인지 공부하기 좋은 글이네요. 멋집니다.

1개의 답글