Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

개발자입니다

[클린 코드 파이썬] 14장: 실전 프로젝트: 하노이 탑과 사목 게임 본문

Python/클린 코드, 이제는 파이썬이다

[클린 코드 파이썬] 14장: 실전 프로젝트: 하노이 탑과 사목 게임

끈기JK 2024. 9. 14. 09:02

하노이 탑

"""하노이의 탑, Al Sweigart al@inventwithpython.com
원반 이동 퍼즐 게임.
이 코드는 https://nostarch.com/big-book-small-python-programming 에서 볼 수 있습니다.
태그: 간단한, 게임, 퍼즐"""

import copy
import sys

TOTAL_DISKS = 5  # 더 많은 원반은 더 어려운 퍼즐을 의미합니다.

# 모든 원반을 A 타워에 시작합니다:
COMPLETE_TOWER = list(range(TOTAL_DISKS, 0, -1))


def main():
    print("""하노이의 탑, Al Sweigart al@inventwithpython.com

원반을 하나씩 다른 타워로 옮기세요. 큰 원반은 작은 원반 위에 놓을 수 없습니다.

더 많은 정보는 https://en.wikipedia.org/wiki/Tower_of_Hanoi 에서 확인하세요.
"""
    )

    # 타워를 설정합니다. 리스트의 끝이 타워의 꼭대기입니다.
    towers = {'A': copy.copy(COMPLETE_TOWER), 'B': [], 'C': []}

    while True:  # 단일 턴을 실행합니다.
        # 타워와 원반을 표시합니다:
        displayTowers(towers)

        # 사용자에게 이동할 타워를 묻습니다:
        fromTower, toTower = askForPlayerMove(towers)

        # fromTower의 맨 위 원반을 toTower로 이동합니다:
        disk = towers[fromTower].pop()
        towers[toTower].append(disk)

        # 사용자가 퍼즐을 해결했는지 확인합니다:
        if COMPLETE_TOWER in (towers['B'], towers['C']):
            displayTowers(towers)  # 마지막으로 타워를 표시합니다.
            print('퍼즐을 해결했습니다! 잘하셨습니다!')
            sys.exit()


def askForPlayerMove(towers):
    """사용자에게 이동할 타워를 묻습니다. (fromTower, toTower)를 반환합니다."""

    while True:  # 유효한 이동을 입력할 때까지 계속 묻습니다.
        print('이동할 "from" 타워와 "to" 타워의 문자를 입력하세요, 또는 QUIT을 입력하세요.')
        print('(예: AB는 A 타워에서 B 타워로 원반을 이동합니다.)')
        response = input('> ').upper().strip()

        if response == 'QUIT':
            print('게임을 플레이해주셔서 감사합니다!')
            sys.exit()

        # 사용자가 유효한 타워 문자를 입력했는지 확인합니다:
        if response not in ('AB', 'AC', 'BA', 'BC', 'CA', 'CB'):
            print('AB, AC, BA, BC, CA, 또는 CB 중 하나를 입력하세요.')
            continue  # 사용자에게 다시 이동할 타워를 묻습니다.

        # 가독성을 높이기 위한 변수 이름 설정:
        fromTower, toTower = response[0], response[1]

        if len(towers[fromTower]) == 0:
            # "from" 타워는 비어있으면 안 됩니다:
            print('선택한 타워에 원반이 없습니다.')
            continue  # 사용자에게 다시 이동할 타워를 묻습니다.
        elif len(towers[toTower]) == 0:
            # 원반은 비어 있는 "to" 타워로 옮길 수 있습니다:
            return fromTower, toTower
        elif towers[toTower][-1] < towers[fromTower][-1]:
            print('큰 원반을 작은 원반 위에 놓을 수 없습니다.')
            continue  # 사용자에게 다시 이동할 타워를 묻습니다.
        else:
            # 유효한 이동이므로 선택한 타워를 반환합니다:
            return fromTower, toTower


def displayTowers(towers):
    """현재 상태를 표시합니다."""

    # 세 개의 타워를 표시합니다:
    for level in range(TOTAL_DISKS, -1, -1):
        for tower in (towers['A'], towers['B'], towers['C']):
            if level >= len(tower):
                displayDisk(0)  # 원반이 없는 기둥을 표시합니다.
            else:
                displayDisk(tower[level])  # 원반을 표시합니다.
        print()

    # 타워 레이블 A, B, C를 표시합니다.
    emptySpace = ' ' * (TOTAL_DISKS)
    print('{0} A{0}{0} B{0}{0} C\n'.format(emptySpace))


def displayDisk(width):
    """주어진 너비의 원반을 표시합니다. 너비가 0이면 원반이 없음을 의미합니다."""
    emptySpace = ' ' * (TOTAL_DISKS - width)

    if width == 0:
        # 원반이 없는 기둥 부분을 표시합니다:
        print(emptySpace + '||' + emptySpace, end='')
    else:
        # 원반을 표시합니다:
        disk = '@' * width
        numLabel = str(width).rjust(2, '_')
        print(emptySpace + disk + numLabel + disk + emptySpace, end='')


# 프로그램이 실행될 때 (임포트되지 않고), 게임을 실행합니다:
if __name__ == '__main__':
    main()

 

상수를 파일의 맨 위쪽에 정의하여 한데 묶고 전역 변수로 만든다.

 

함수에도 독스트링이 존재할 수 있다. def 문 아래의 main()에 대한 독스트링에 주목하자.

 

일반적으로 "AB", "AC" 등을 매직 값으로 하드코딩하는 구현 방식은 악습으로 여겨지며, 여기서 구현한 방식은 프로그램의 기둥이 세 개만 존재하는 경우에 유효하다. 하지만 TOTAL_DISKS 상수를 변경해 원판 수를 조정하고 싶을 수는 있어도, 게임에 기둥을 더 추가할 가능성은 매우 낮다. 이런 경우라면 조건 행에 가능한 모든 기둥 간 움직임을 명시하는 방식도 괜찮다.

 

print('{emptySpace} A{emptySpace}{emptySpace} B{emptySpace}{emptySpace} C\n') 과 같은 f-문자열을 사용하지 않고, format() 문자열 메소드를 사용한다. 이렇게 하면 연관된 문자열에 {0}이 나타나는 곳이면 어디서든 동일한 emptySpace 인수를 사용할 수 있게 되어 f-문자열 버전보다 더 짧고 가독성 높은 코드가 만들어진다.

 

 

 

사목 게임

"""Four in a Row, Al Sweigart al@inventwithpython.com
'Connect Four'와 유사한, 네 개를 연속으로 맞추는 타일 떨어뜨리기 게임.
이 코드는 https://nostarch.com/big-book-small-python-programming 에서 볼 수 있습니다.
태그: 큰, 게임, 보드 게임, 2인용"""

import sys

# 보드판을 표시할 때 사용할 상수:
EMPTY_SPACE = '.'  # 빈 공간을 나타내는 점은 스페이스보다 세기 쉽습니다.
PLAYER_X = 'X'
PLAYER_O = 'O'

# 주의: BOARD_WIDTH가 변경되면 displayBoard() 및 COLUMN_LABELS를 업데이트해야 합니다.
BOARD_WIDTH = 7
BOARD_HEIGHT = 6
COLUMN_LABELS = ('1', '2', '3', '4', '5', '6', '7')
assert len(COLUMN_LABELS) == BOARD_WIDTH


def main():
    print("""Four in a Row, Al Sweigart al@inventwithpython.com

두 플레이어가 번갈아가며 타일을 일곱 개 열 중 하나에 떨어뜨려
가로, 세로 또는 대각선으로 네 개를 연속으로 맞추는 게임입니다.
""")

    # 새로운 게임을 시작합니다:
    gameBoard = getNewBoard()
    playerTurn = PLAYER_X

    while True:  # 각 플레이어의 턴을 실행합니다.
        # 보드를 표시하고 플레이어의 이동을 받습니다:
        displayBoard(gameBoard)
        playerMove = askForPlayerMove(playerTurn, gameBoard)
        gameBoard[playerMove] = playerTurn

        # 승리 또는 무승부 여부를 확인합니다:
        if isWinner(playerTurn, gameBoard):
            displayBoard(gameBoard)  # 마지막으로 보드를 표시합니다.
            print('플레이어 ' + playerTurn + '이 승리했습니다!')
            sys.exit()
        elif isFull(gameBoard):
            displayBoard(gameBoard)  # 마지막으로 보드를 표시합니다.
            print('무승부입니다!')
            sys.exit()

        # 플레이어의 턴을 변경합니다:
        if playerTurn == PLAYER_X:
            playerTurn = PLAYER_O
        elif playerTurn == PLAYER_O:
            playerTurn = PLAYER_X


def getNewBoard():
    """네 개 맞추기 보드를 나타내는 딕셔너리를 반환합니다.

    키는 (columnIndex, rowIndex) 두 개의 정수 튜플이며, 
    값은 'X', 'O' 또는 '.' (빈 공간)을 나타내는 문자열입니다."""
    board = {}
    for columnIndex in range(BOARD_WIDTH):
        for rowIndex in range(BOARD_HEIGHT):
            board[(columnIndex, rowIndex)] = EMPTY_SPACE
    return board


def displayBoard(board):
    """화면에 보드와 타일을 표시합니다."""

    '''format() 문자열 메서드에 전달할 리스트를 준비합니다.
    이 리스트는 왼쪽에서 오른쪽, 위에서 아래로 모든 보드의
    타일(및 빈 공간)을 포함합니다:'''
    tileChars = []
    for rowIndex in range(BOARD_HEIGHT):
        for columnIndex in range(BOARD_WIDTH):
            tileChars.append(board[(columnIndex, rowIndex)])

    # 보드를 표시합니다:
    print("""
     1234567
    +-------+
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    |{}{}{}{}{}{}{}|
    +-------+""".format(*tileChars))


def askForPlayerMove(playerTile, board):
    """플레이어가 타일을 떨어뜨릴 보드 열을 선택하게 합니다.

    타일이 떨어질 (column, row)의 튜플을 반환합니다."""
    while True:  # 유효한 이동을 입력할 때까지 계속 묻습니다.
        print('플레이어 {}, 열을 입력하거나 QUIT을 입력하세요:'.format(playerTile))
        response = input('> ').upper().strip()

        if response == 'QUIT':
            print('게임을 플레이해주셔서 감사합니다!')
            sys.exit()

        if response not in COLUMN_LABELS:
            print('{}에서 선택한 숫자를 입력하세요.'.format(BOARD_WIDTH))
            continue  # 플레이어에게 다시 묻습니다.

        columnIndex = int(response) - 1  # 인덱스를 0 기준으로 변환.

        # 열이 가득 차 있으면 다시 이동을 묻습니다:
        if board[(columnIndex, 0)] != EMPTY_SPACE:
            print('해당 열이 가득 찼습니다. 다른 열을 선택하세요.')
            continue  # 플레이어에게 다시 묻습니다.

        # 맨 아래부터 시작해 첫 번째 빈 공간을 찾습니다.
        for rowIndex in range(BOARD_HEIGHT - 1, -1, -1):
            if board[(columnIndex, rowIndex)] == EMPTY_SPACE:
                return (columnIndex, rowIndex)


def isFull(board):
    """`board`에 빈 공간이 없으면 True를 반환하고, 그렇지 않으면 False를 반환합니다."""
    for rowIndex in range(BOARD_HEIGHT):
        for columnIndex in range(BOARD_WIDTH):
            if board[(columnIndex, rowIndex)] == EMPTY_SPACE:
                return False  # 빈 공간을 찾으면 False를 반환.
    return True  # 모든 공간이 가득 찼습니다.


def isWinner(playerTile, board):
    """`playerTile`이 `board`에서 네 개를 연속으로 맞췄으면 True를 반환하고,
    그렇지 않으면 False를 반환합니다."""

    # 전체 보드를 살펴보며 네 개 연속을 찾습니다:
    for columnIndex in range(BOARD_WIDTH - 3):
        for rowIndex in range(BOARD_HEIGHT):
            # 오른쪽으로 가로 네 개 맞추기 확인:
            tile1 = board[(columnIndex, rowIndex)]
            tile2 = board[(columnIndex + 1, rowIndex)]
            tile3 = board[(columnIndex + 2, rowIndex)]
            tile4 = board[(columnIndex + 3, rowIndex)]
            if tile1 == tile2 == tile3 == tile4 == playerTile:
                return True

    for columnIndex in range(BOARD_WIDTH):
        for rowIndex in range(BOARD_HEIGHT - 3):
            # 아래로 세로 네 개 맞추기 확인:
            tile1 = board[(columnIndex, rowIndex)]
            tile2 = board[(columnIndex, rowIndex + 1)]
            tile3 = board[(columnIndex, rowIndex + 2)]
            tile4 = board[(columnIndex, rowIndex + 3)]
            if tile1 == tile2 == tile3 == tile4 == playerTile:
                return True

    for columnIndex in range(BOARD_WIDTH - 3):
        for rowIndex in range(BOARD_HEIGHT - 3):
            # 오른쪽 아래로 대각선 네 개 맞추기 확인:
            tile1 = board[(columnIndex, rowIndex)]
            tile2 = board[(columnIndex + 1, rowIndex + 1)]
            tile3 = board[(columnIndex + 2, rowIndex + 2)]
            tile4 = board[(columnIndex + 3, rowIndex + 3)]
            if tile1 == tile2 == tile3 == tile4 == playerTile:
                return True

            # 왼쪽 아래로 대각선 네 개 맞추기 확인:
            tile1 = board[(columnIndex + 3, rowIndex)]
            tile2 = board[(columnIndex + 2, rowIndex + 1)]
            tile3 = board[(columnIndex + 1, rowIndex + 2)]
            tile4 = board[(columnIndex, rowIndex + 3)]
            if tile1 == tile2 == tile3 == tile4 == playerTile:
                return True
    return False


# 프로그램이 실행될 때 (임포트되지 않고), 게임을 실행합니다:
if __name__ == '__main__':
    main()

 

PLAYER_X와 PLAYER_O 상수를 정의하면 프로그램 전체에 걸쳐 "X"와 "O"라는 문자열을 사용할 필요가 없어지므로 에러를 더 쉽게 잡을 수 있다.

 

프로그램이 실행되는 동안 상수는 변경되지 않아야 한다. 그러나 프로그래머는 다음 버전의 프로그램에서 상수 값을 변경할 수도 있다. 따라서, BOARD_WIDTH 값을 변경하는 경우, BOARD_TEMPLATE와 COLUMN_LABELS 상수도 갱신해야 한다는 메모를 프로그래머들에게 남긴다.

 

게임이 끝나지 않았다면, 다음 코드가 playerTurn을 다른 플레이어로 설정한다.

# 플레이어의 턴을 변경합니다:
if playerTurn == PLAYER_X:
    playerTurn = PLAYER_O
elif playerTurn == PLAYER_O:
    playerTurn = PLAYER_X

elif문을 조건 없이 단순 else 문으로 만들 수도 있었다. 그러나 "명시적인 것이 암시적인 것보다 낫다"는 파이썬의 선 지침을 떠올려보자.