소코반(SOKOBAN) 게임 (1)

ivor·2021년 12월 10일
0

python_toy_sokoban

목록 보기
1/2

과제로 간단한 소코반 게임을 만들어 보았다.

소코반 게임이란?

각 스테이지의 모든 박스(공)를 정해진 위치(구멍)에 올려놓는(넣는) 것을 목표로 하는 게임이다. 링크를 따라가거나 검색해보면 아마 언젠가 한번 정도는 해봤을 게임일거라 생각한다.

처음엔 '금방 만들지 않을까?'하는 마음이었다. 로직 자체가 단순했다고 생각했기 때문이다. 하지만 웬걸, 생각보다 만들기 쉽지 않았다. (아니면 내 실력이 부족한걸지도 🥲)

자바와 파이썬 중에 고민했다. 각 언어에 대한 실력은 고만고만하지만 그나마 파이썬이 낫겠다 싶어 파이썬으로 만들기로 했다. 자바를 좀 더 공부하고 자바로도 만들어 보자고 스스로 다짐했다.


https://gist.github.com/ivorrr987/43694b65e390d8bfb202ea9f9a7fa809
(과제를 수행하면서 gist를 이용하여 버전 관리를 했다. 추후에 좀 더 정리하여 github repository에 올리고자 한다.)

https://github.com/ivorrr987/sokoban


구현 계획

본격적인 구현에 앞서 코드를 어떻게 짤지 생각해보았다.
기본적으로 반드시 필요하다 생각한 것들은 다음과 같다.

  • 우선 스테이지 별로 map 정보를 저장하고 해당 정보를 가져와서 화면에 보여줘야 한다.
  • 사용자의 입력을 받아 명령어로 사용한다. (방향키? wasd? 번호?)
  • 해당 명령어에 따라 적절한 동작을 수행해야 한다.
  • 동작 수행의 결과를 출력으로 보여줘야 한다.

Map과 출력

Map

심벌들을 설정하고 그걸로 맵을 구성해야 했다. 그래야 사용자 화면에 출력하고 사용자가 명령어를 통해 게임을 할 수 있을테니까.
심벌은 다음처럼 정했다.

  • '#' : 벽
  • 'o' : 박스
  • 'O' : 박스 목표 위치
  • 'P' : 플레이어
  • '0' : 목표 위치에 올라간 박스

수정 및 관리가 용이하도록 txt파일에 각 스테이지의 맵을 저장하고 그 정보를 가져와 사용하기로 했다.

maps.txt

Stage 1
#####
#OoP#
#####
---
Stage 2
  #######  
###  O  ###
#    o    #
#  OoPoO  #
#    o    #
###  O  ###
  #######    
---
Stage 3
  #### 
###  # 
#P Oo##
#   o #
# #O  #
#     #
#######
---
Stage 4
 ###### 
##    ##
# OO   #
# # ooo#
#   #PO#
########
---
Stage 5
######  
#P   ## 
# Oo0  #
# #oO  #
#      #
########

각 스테이지에 대한 정보를
스테이지명
맵 정보 로 나누고, 각 스테이지는 ---로 나누었다.

텍스트 파일로부터 데이터를 뽑아내고 전처리

maps.txt에는 모든 스테이지의 정보가 한 데 뭉쳐 저장되어 있다. 게임 시 사용자는 자신이 플레이 중인 스테이지의 정보만 보면 되므로 각 스테이지 별로 데이터를 분리하고자 했다.

즉, 전체 데이터 추출 → 스테이지 별로 데이터 분리

def open_file(filename):
    with open(filename) as maps_file:
        origin = maps_file.read()
    
    return origin
    
def divide_stages(origin)
    return origin.split("---\n")

maps.txt 파일을 불러와 읽고 그 내용을 origin 함수에 담아 전달하도록 했다. 변수명 짓는 게 어려웠다. '더 적절한 단어가 있을 것 같은데'하는 생각이 코딩하는 내내 들었다.
divide_stages 함수는 전체 데이터를 가져와 '---\n'를 기준으로 나누는 역할을 하게 했다.

origin = open_file("maps.txt")
divided = divide_stages(origin)
print(divided)

위의 코드를 실행한 결과는 다음과 같다.

['Stage 1\n#####\n#OoP#\n#####\n', 'Stage 2\n ####### \n### O ###\n# o #\n# OoPoO #\n# o #\n### O ###\n ####### \n', 'Stage 3\n #### \n### # \n#P Oo##\n# o #\n# #O #\n# #\n#######\n', 'Stage 4\n ###### \n## ##\n# OO #\n# # ooo#\n# #PO#\n########\n', 'Stage 5\n###### \n#P ## \n# Oo0 #\n# #oO #\n# #\n########']

보다시피 각 스테이지 별 데이터가 문자열로 저장되어 있다. 나중에 명령어에 따라 P나 o 등을 옮겨야 할텐데 각 스테이지 별 데이터가 문자열로 저장되어 있으면 다루기 힘들거라 판단했다. 명령어에 대한 동작을 구현하기 위해 각 심벌을 하나씩 따로 떼서 list에 담아야 했다.

def save(map_str): # 문자열 'stage_info'를 list로 변환하여 필요한 값만 저장
    
    map_str= map_str.split("\n")
    map_list= []
    for info in map_str:
        if 'Stage' not in info and info!='':
            map_list.append(list(info))
    
    return map_list
    
# 문자열로 저장된 각 스테이지 정보를 2차원 배열로 변환.
def set_stages(maps_str):
    stages=[]

    for i in range(len(maps_str)):
        stages.append(save(maps_str[i]))

    return stages  
    
maps_list = set_stages(divided)
print(maps_list)

divided의 각 원소, 즉 문자열 타입의 각 스테이지 정보는 set_stages 함수에서 분리되고 save함수를 통해 list로 변환되어 2차원 배열이 된다. set_stages 함수는 2차원 배열(각 스테이지 데이터)의 list를 반환하고, 그 반환값에 해당하는maps_list를 출력하면 다음의 결과를 얻을 수 있다.

[[['#', '#', '#', '#', '#'], ['#', 'O', 'o', 'P', '#'], ['#', '#', '#', '#', '#']], [[' ', ' ', '#', '#', '#', '#', '#', '#', '#', ' ', ' '], ['#', '#', '#', ' ', ' ', 'O', ' ', ' ', '#', '#', '#'], ['#', ' ', ' ', ' ', ' ', 'o', ' ', ' ', ' ', ' ', '#'], ['#', ' ', ' ', 'O', 'o', 'P', 'o', 'O', ' ', ' ', '#'], ['#', ' ', ' ', ' ', ' ', 'o', ' ', ' ', ' ', ' ', '#'], ['#', '#', '#', ' ', ' ', 'O', ' ', ' ', '#', '#', '#'], [' ', ' ', '#', '#', '#', '#', '#', '#', '#', ' ', ' ', ' ', ' ']], [[' ', ' ', '#', '#', '#', '#', ' '], ['#', '#', '#', ' ', ' ', '#', ' '], ['#', 'P', ' ', 'O', 'o', '#', '#'], ['#', ' ', ' ', ' ', 'o', ' ', '#'], ['#', ' ', '#', 'O', ' ', ' ', '#'], ['#', ' ', ' ', ' ', ' ', ' ', '#'], ['#', '#', '#', '#', '#', '#', '#']], [[' ', '#', '#', '#', '#', '#', '#', ' '], ['#', '#', ' ', ' ', ' ', ' ', '#', '#'], ['#', ' ', 'O', 'O', ' ', ' ', ' ', '#'], ['#', ' ', '#', ' ', 'o', 'o', 'o', '#'], ['#', ' ', ' ', ' ', '#', 'P', 'O', '#'], ['#', '#', '#', '#', '#', '#', '#', '#']], [['#', '#', '#', '#', '#', '#', ' ', ' '], ['#', 'P', ' ', ' ', ' ', '#', '#', ' '], ['#', ' ', 'O', 'o', '0', ' ', ' ', '#'], ['#', ' ', '#', 'o', 'O', ' ', ' ', '#'], ['#', ' ', ' ', ' ', ' ', ' ', ' ', '#'], ['#', '#', '#', '#', '#', '#', '#', '#']]]

굵게 표시한 부분은 stage1에 해당하는 데이터이다. 행렬로 구성해보면 maps.txt 속 stage1 정보와 일치함을 알 수 있다.

이렇게 소코반 게임 구현에 이용할 데이터 전처리 과정을 마쳤다.

profile
BEST? BETTER!

0개의 댓글