7 minute read

Byte degree 미니 프로젝트

  • 숫자 퍼즐 게임 완성하기
  • 아래와 같이 숫자 퍼즐을 만들고 숫자를 이동시켜 순서대로 맞추는 게임
  • 퍼즐이미지
  • 이미지 출처
import random

게임 로직 구현하기

  1. 퍼즐 생성하기
  2. 퍼즐 랜덤하게 섞기
  3. 퍼즐 출력
  4. 사용자 입력(움직일 숫자 입력 받기)
  5. 퍼즐 완성 확인하기
    • 완성? 완료 메시지와 함께 종료
    • 미완성? 3번으로 이동

퍼즐 생성하기

  • 2차원 리스트 형태로 생성
  • 퍼즐의 크기(size)를 파라미터로 받아, 동적으로 size*size의 리스트로 생성
  • 퍼즐이 생성되면 1부터 차례대로 행방향으로 숫자를 나열
    • 사이즈가 3인 경우의 생성 예
    • [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
  • 퍼즐의 가장 마지막 아이템(마지막 행의 마지막 열 아이템)은 ‘’ 빈문자열로 처리
    • 이유는? 숫자퍼즐의 목표는 빈공간을 이용해 각 이동하고자 하는 숫자를 빈공간으로 움직여 숫자들을 순서대로 다시 맞추는 것이 목적이므로, 빈공간을 표현하기 위한 방법으로 빈문자열을 사용
  • pure python 버젼
def initiate_puzzle(size):
    '''
    파라미터
     size: 퍼즐의 크기
    리턴
     생성된 퍼즐 리스트
    '''
    # 퍼즐을 리스트의 리스트로 표현하려고 합니다.
    # 즉 3x3 퍼즐이라면 아래와 같은 리스트의 리스트로 표현이 되게 하려고 합니다
    # [[1, 2, 3], [4, 5, 6], [7, 8, '']]
    # 마지막 아이템을 ''로 바꾼 이유는 퍼즐의 빈 공간을 표현하기 위해서이구요.
    puzzle = [] # 전체 퍼즐을 담을 리스트
    for i in range(size):
        row = [] 
        for j in range(size):
            row.append((i*size)+j+1)
        puzzle.append(row)

    # 일단 퍼즐을 생성 한 뒤, 마지막 리스트의 마지막 아이템을 ''로 변경합니다.
    puzzle[-1][-1] = ''
    return puzzle

  • 리뷰
    • 아주 잘해주셨습니다.
    • i, j, 그리고 size를 이용해 연속된 숫자를 잘 만들어주셨습니다.
    • 또한 문제 요구사항에 맞게 1부터 시작하도록 +1도 아주 잘해주셨어요
  • numpy 버젼
  • numpy를 추가한 이유는 더 간결하게 작성하는 것을 보여주기 위해서 입니다. 심화 과정이니 numpy를 모르시는 분은 위의 pure python버젼으로 확인하시면 됩니다.
import numpy as np
def initiate_puzzle(size):
    '''
    파라미터
     size: 퍼즐의 크기
    리턴
     생성된 퍼즐 리스트
    '''
    puzzle = np.arange(1, (size*size)+1).reshape(size, size)
    puzzle = puzzle.tolist() 
    puzzle[-1][-1] = ''
    return puzzle

  • 리뷰
    • 여기도 잘해주셨습니다.
    • 이 부분은 위의 pure python 부분과 동일한 결과가 나와야 합니다.
    • np.arange는 python의 range와 거의 동일하다고 보시면 되기에
    • size x size 만큼의 숫자를 만듣기 위해서 위와 같이 size * size +1
    • 잘해주셨습니다.
puzzle = initiate_puzzle(4)
print(puzzle)
[[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 14, 15, '']]

퍼즐 출력하기

  • 생성된 퍼즐(puzzle)을 파라미터로 받아 화면에 출력
  • 이때, 퍼즐은 2차원 형태이므로 2중 loop을 이용
def show_puzzle(puzzle):
    '''
    파라미터
     puzzle: 퍼즐 
    리턴
     None
    '''
    # 3*3 퍼즐을 예를 들면 
    # [[1, 2, 3],   -> 1열
    #  [4, 5, 6],   -> 2열
    #  [7, 8, 9]]   -> 3열
    # 와 같은 형태로 출력하게 하는 것입니다.
    # for문 2개를 중첩하여 출력을 하면 되는데
    # 외부의 for는 각 열에 접근하기 위해서이고
    # 내부의 for는 열의 각각 아이템에 접근하기 위해서 입니다.
    for index_r, r_num in enumerate(puzzle):
        # HDJ[20-04-16]:자릿 수 정렬(2자리수만 감안)
        if index_r == 0:
            print('[[', end='')
        else:
            print(' [', end='')
        for index_c, c_num in enumerate(r_num):
            if type(c_num) == int and c_num < 10:
                print(' ', end='')
            elif type(c_num) == str:
                print('  ', end='')
            
            if index_c==len(r_num)-1:
                if index_r == len(puzzle)-1:
                    print(c_num, end=']]')
                else:
                    print(c_num, end='],')
            else:
                print(c_num, end=', ')
        print()
show_puzzle(puzzle)
[[ 1,  2,  3,  4],
 [ 5,  6,  7,  8],
 [ 9, 10, 11, 12],
 [13, 14, 15,   ]]

  • 리뷰
    • 아주 잘해주셨습니다.
    • 사실 [는 꼭 들어가지 않아도 되지만
    • 간격까지 맞춰 아주 잘해주셨네요!

퍼즐 섞기(shuffling)

  • 생성할때부터 랜덤하게 숫자를 배열하지 않고, 완성된 상태에서 퍼즐을 섞어야 함
    • 이유? 랜덤하게 배열하는 경우, 퍼즐이 완성되지 못하는 경우의 수가 수학적으로 존재하기 때문
    • 퍼즐을 완성시킬 수 없는 예
    • [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12], [13, 15, 14, ‘’]]
    • 출처: 네이버 블로그
def get_index(puzzle, n):
    '''
    파라미터
      puzzle: 퍼즐
      n: 퍼즐 내에서 찾으려는 숫자 혹은 빈공간('') 값
    리턴
      퍼즐에서 해당 숫자나 빈공간을 찾았다면 해당 인덱스를 반환
      찾지 못했다면 None, None 반환
    '''
    for i in range(len(puzzle)):
        # HDJ[20-04-16]:퍼즐 내 해당 숫자가 있는지 먼저 확인
        if n in puzzle[i]:
            index = puzzle[i].index(n) # 리스트의 내장 함수를 활용하여 주어진 숫자 n의 인덱스 찾기
            return i, index
    
    return None, None

  • 리뷰
    • list에 없는 값을 index로 찾으면 ValueError가 나지요
    • 하지만 위와 같이 if로 먼저 하게 되면 Error가 날 염려는 하지 않아도 되겠습니다.
    • 값을 못 찾은 경우의 return 값도 아주 잘해주셨습니다.
def shuffle_puzzle(puzzle, shuffle_count):
    '''
    파라미터
     puzzle: 퍼즐
     shuffle_count: 섞을 횟수
    리턴
     None
    '''
    
    # 각각 섞을 때마다 빈공간을 기준으로 상하좌우의 방향으로 섞기 위해
    # 방향 리스트 생성
    # 순서대로 상 우 하 좌 
    dxs = [1, 0, -1,  0]
    dys = [0, 1,  0, -1]

    cnt = 0 # 섞을 횟수를 카운팅 할 변수
    while cnt <= shuffle_count: # shuffle_count에 도달할 때까지 퍼즐을 섞으려고 합니다.
        
        # 퍼즐을 섞으려면 빈 공간을 기준으로 좌 우 상 하 
        # 중 한 곳으로 움직여 섞을 수 있습니다.
        # 이것을 랜덤하게 하기 위해서 0부터 3까지 임의의 인덱스 값을 찾는데요
        # 예를들어서 랜덤한 값으로 1이 선택되었을 경우 
        # 아래의 코드에서 dxs, dys에 1번째 값을 선택하게 되는데 
        # 각각 dx, dy로 (0, 1)을 선택하게 됩니다.
        # 이 것이 의미하는 것은 새로 바뀌게 될 인덱스를 의마하게 되는데요
        # i인덱스는(각 row의 인덱스를 나타냄)는 0이 더해지고(즉 변화가 없고)
        # j인덱스는(각 row내에서의 인덱스를 나타냄)는 1이 더해지기 때문에
        # 오른쪽으로 움직이게 된다는 것을 알 수 있습니다.
        rnd = random.randint(0, 3) 
        dx = dxs[rnd]
        dy = dys[rnd]

        # 빈공간의 index를 계산합니다.
        i, j = get_index(puzzle, '')     
        
        # 각각 dx, dy를 더하여 새로 업데이트 될 인덱스를 계산합니다.
        ni = i + dx
        nj = j + dy

        # 새로 얻은 인덱스가 유효한 범위인지 확인합니다. 
        # 예를들어 이미 가장 오른쪽에 있던 빈 공간을 한번 더 오른쪽으로 옮기는 것은 
        # 불가능하기 때문에, 이렇게 아래와 같이 새 인덱스가 유효한지 체크 해주는 것입니다.
        # 유효하다면, 기존의 빈공간의 값과 새로운 인덱스의 값을 교환하면 됩니다.
        if 0 <= ni < len(puzzle) and 0 <= nj < len(puzzle[0]):
            puzzle[ni][nj], puzzle[i][j] = puzzle[i][j], puzzle[ni][nj]
        
        cnt += 1
shuffle_puzzle(puzzle, 10)
show_puzzle(puzzle)
[[ 1,  2,  3,  4],
 [ 5,  6,  7,  8],
 [ 9, 10, 11, 12],
 [13, 14,   , 15]]

퍼즐이 완성되었는지 확인하기

  • 퍼즐이 완성된 형태인지 확인
  • puzzle 퍼즐로 활용할 리스트, completed 완성된 형태의 퍼즐 리스트
  • 완성되었다면 True, 아니라면 False 반환
# puzzle은 현재 풀고자 하는 퍼즐을 담은 리스트
# completed는 미리 생성한 완성된 퍼즐을 담은 리스트
def is_puzzle_completed(puzzle, completed):
    # 두개의 리스트가 완전히 동일한 지 비교하면 됩니다.
    if puzzle == completed:
        return True
    else:
        return False

  • 리뷰
    • 잘해주셨습니다.
    • puzzle과 completed같은 경우 True를 아니면 False를 주면 되겠네요
    • 이 코드를 조금 더 단순화한다면
    • return에 바로 비교문을 넣으셔도 되겠습니다.
    • 비교문의 결과가 바로 True / False 이니까요
complete = [row[:] for row in puzzle]

퍼즐 이동하기

  • 퍼즐 내의 숫자를 이동
  • 이때 이동이 가능한 경우는 해당 숫자가 빈공간 상하좌우에 위치한 경우에만 가능
# puzzle 내에서 숫자 n을 움직이는 코드
# n옆에 빈 공간이 있으면 n과 빈공간의 위치를 바꾼다
# 그렇게 하기 위해서는 n의 인덱스를 찾아야 하고(get_index) 
# 그 이후, move_by_index 함수를 이용해서 n과 빈공간을 바꾼다.
def move_by_number(puzzle, n):
    # 숫자가 위치한 index
    i, j = get_index(puzzle, n)
    
    # HDJ[20-04-16]:퍼즐 내 해당 숫자가 있을 때만 이동
    if i != None and j != None:
        # index를 이용하여 숫자 이동
        move_by_index(puzzle, i, j)
# shuffle_puzzle 함수의 코드를 참고하면 됩니다.


# 퍼즐에서 현재 i,j인덱스에 있는 아이템을 옮기는 함수입니다.
def move_by_index(puzzle, i, j):
    # 좌우위아래 한방향중 하나가 '' 값이라면 이동 가능
    for dx, dy in ((1, 0), (0, 1), (-1, 0), (0, -1)):
        new_i = i + dx
        new_j = j + dy

        # boundary 체크(갈 수 없는 곳이면 패스)
        if not (0 <= new_i < len(puzzle) and 0 <= new_j < len(puzzle[0])):
            continue

        # 옆에 빈 공간인 경우에는 퍼즐의 위치를 빈공간과 바꿈(swap)
        if puzzle[new_i][new_j] == '':
            puzzle[new_i][new_j], puzzle[i][j] = puzzle[i][j], puzzle[new_i][new_j]
            return 

  • 리뷰
    • 어려운 부분이었는데 정확하게 잘 풀어주셨습니다.
    • boundary 값을 체크하기 위해선 가로, 세로가 넘어가는지를 보면 되겠습니다.
    • swap도 위와 같이 한줄로 할 수 있다는 것이 python의 장점입니다

사용자 프롬프트 입력

  • 게임의 진행을 위해 동적으로 키보드 입력을 받을 필요가 있음
    • 퍼즐의 크기, 이동할 수 지정
  • 이를 위해 input 함수 사용
    • 원하는 값 입력후, Enter
value = input('입력하세요')
print(value)
입력하세요r
r
  • 입력받은 값을 숫자형태로 변경
value = int(input('숫자를 입력하세요'))
print(value)
숫자를 입력하세요1
1

퍼즐, 퍼즐 완성본 생성 및 셔플링

  • 퍼즐 사이즈 입력
size = int(input('-> 퍼즐 사이즈를 입력하세요: '))
print('퍼즐 사이즈: ', size)
-> 퍼즐 사이즈를 입력하세요: 4
퍼즐 사이즈:  4
  • 퍼즐 생성
puzzle = initiate_puzzle(size)
  • 퍼즐 완성본 생성
  • 기존 퍼즐을 복사하여 생성
  • 아래와 같이 deep copy본으로 생성
    • 그렇지 않으면, 항상 puzzle과 complete가 동일한 객체가 됨
# 퍼즐의 완성본을 미리 복사해둡니다.
complete = [row[:] for row in puzzle]
show_puzzle(puzzle)
[[ 1,  2,  3,  4],
 [ 5,  6,  7,  8],
 [ 9, 10, 11, 12],
 [13, 14, 15,   ]]
  • 퍼즐 섞기
shuffle_puzzle(puzzle, 300)
show_puzzle(puzzle)
[[14,   ,  9, 11],
 [ 5,  2,  6,  3],
 [ 4, 15,  8,  7],
 [ 1, 13, 10, 12]]

게임 루프

  • 퍼즐이 완성되었나 확인
    • 완성되었다면 종료
    • 완성되지 않았다면 사용자 입력 대기 및 퍼즐 출력
# output을 clear하기 위해 사용
from IPython.display import clear_output
show_puzzle(puzzle)

# 퍼즐이 완성되지 않았다면 계속 while 구문을 수행.
while not is_puzzle_completed(puzzle, complete):
    # 숫자를 입력하지 않은 경우에 대한 예외 처리
    try:
        num = int(input(' -> 움직일 숫자를 입력하세요 : '))
    except:
        print('숫자가 아닙니다.')
        continue

    # 사용자가 선택한 num을 움직임
    # 앞서서 구현한 함수중 하나를 호출
    move_by_number(puzzle, num)

    # 화면 clear
    clear_output()

    # 움직인 이후 퍼즐 상태 보기
    show_puzzle(puzzle)

# 루프의 종료는 곧 퍼즐의 완성을 의미!
print('\n퍼즐 완성!')
[[14,  2,  9, 11],
 [ 5, 15,  6,  3],
 [ 4,   ,  8,  7],
 [ 1, 13, 10, 12]]

  • 리뷰
    • 위의 함수도 그렇고 마지막 마무리도 아주 잘해주셨습니다.
    • 외부인자도 알맞게 넣어주셨습니다.
    • 추가적으로 퍼즐을 도중에 끝낼 수 있도록 코드를 추가해보는 것도 기회가 되신다면 해보시길 권해드립니다.