Numpy 숫자퍼즐
Byte degree 미니 프로젝트
- 숫자 퍼즐 게임 완성하기
- 아래와 같이 숫자 퍼즐을 만들고 숫자를 이동시켜 순서대로 맞추는 게임
- 이미지 출처
import random
게임 로직 구현하기
- 퍼즐 생성하기
- 퍼즐 랜덤하게 섞기
- 퍼즐 출력
- 사용자 입력(움직일 숫자 입력 받기)
- 퍼즐 완성 확인하기
- 완성? 완료 메시지와 함께 종료
- 미완성? 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]]
- 리뷰
- 위의 함수도 그렇고 마지막 마무리도 아주 잘해주셨습니다.
- 외부인자도 알맞게 넣어주셨습니다.
- 추가적으로 퍼즐을 도중에 끝낼 수 있도록 코드를 추가해보는 것도 기회가 되신다면 해보시길 권해드립니다.