소코반(SOKOBAN) 게임 (2)

ivor·2021년 12월 23일
0

python_toy_sokoban

목록 보기
2/2

https://github.com/ivorrr987/sokoban


게임 실행

sokoban.py와 main.py

sokoban.py

import processing
import main

origin = processing.open_file("maps.txt") # maps.txt에 저장된 정보를 가져옴.
maps_str = processing.divide_stages(origin) # '====='를 기준으로 스테이지 별로 분할.

stages = processing.set_stages(maps_str) # 각 스테이지 지도 정보를 2차원 배열 형태로 저장

main.start_game(stages)

sokoban.py 파일로 게임을 실행할 수 있다.
데이터를 처리하여 사용하기 편한 상태로 가공하고
main.pystart_game함수를 이용하여 게임을 시작한다.

main.py

import copy
import move
import show
import check
import processing
import slot

# 명령어에 따라 동작 수행
def exec_commands(map_list, map_list_init, commands, hole_loc, log, slots, i):
    move_commands = ['w', 'W', 'a', 'A', 's', 'd', 'D'] # 이동 관련된 명령어들. 'S'는 저장용으로 써야 하기 때문에 제외.

    while len(commands)>0: # 입력받은 명령어들을 순차적으로 처리
        if check.is_all_in(map_list): # 대기 중인 명령어가 있더라도 현재 스테이지를 깰 수 있는 상태면 바로 완료.
            return map_list, log, False, i

        if commands[0] in move_commands:
            map_list, success = move.update_map(map_list, commands[0], hole_loc)
            if success==True:
                print(f"(턴수: {len(log)})") # 턴수 출력
                log[len(log)] = map_list # 로그 생성
            commands.pop(0)
            
        elif commands[0] == 'r' or commands[0] == 'R': # 매 스테이지 시작마다 복사해두었던 지도 정보를 이용해 초기화 명령 실행.
            map_list= copy.deepcopy(map_list_init)
            show.show_basic_info(map_list)
            log.clear() # log를 초기화 (+턴수 초기화 효과)
            log = {0 : map_list}
            print("r: 지도를 리셋합니다")
            commands.pop(0)

        elif commands[0] == 'c':
            if check.check_possible(map_list)[0]:
                print("이동이 불가능한 공이 없습니다.")
            else: 
                print("이동이 불가능한 공이 있습니다. 리셋 혹은 종료를 권합니다")
                print("해당 공의 좌표는 다음과 같습니다")
                print(check.check_possible(map_list)[1])
            commands.pop(0)
        
        elif commands[0] == 'q' or commands[0] == 'Q': # 'quit' 명령어 입력 시 게임 종료.
            return map_list, log, True, i

        elif 49<= ord(commands[0]) and ord(commands[0])<=53: # 1~5의 숫자가 입력되었을 때
            slot.show_slot(slots, int(commands[0])-1)
            command = input()

            if command == 'S':
                slots[int(commands[0])-1] = copy.deepcopy(slot.save_slot(map_list, slots, int(commands[0])-1, log, i))
            elif command == 'L':
                map_list, log, i = slot.load_slot(slots, int(commands[0])-1)
            else:
                print("잘못된 입력입니다")
            
            commands.pop(0)

        else : # 올바른 명령이 아닐 시, 아무 동작 없이 command만 지움.
            print("올바른 명령이 아닙니다")
            commands.pop(0)
            
    return map_list, log, False, i


def input_command(): # 커맨드를 입력받아 알파벳만 list 변수에 저장 후 return
    print("SOKOBAN>", end=' ')
    commands = list(input().replace(" ",''))
    
    return commands


def finish_game():
    print("축하합니다. 모든 스테이지를 완료하셨습니다")


# 게임 시작
def start_game(stages):
    i=0

    slots = [[stages[0], {} , 0]]*5
    while i<len(stages):
    # for i in range(len(stages)): # 스테이지1부터 마지막 스테이지까지. 스테이지 지도 정보는 map.txt 내용 수정하여 설정할 수 있음.
        show.show_stage_info(stages[i], i+1) # 각 스테이지 정보 표시
        stage_init = copy.deepcopy(stages[i])
        hole_loc = processing.save_hole_loc(stage_init)
        
        log = {0 : stages[i]} # 턴수 출력과 추후 'undo' 명령을 위해
        quit = False
        while check.is_all_in(stages[i])==False and quit==False: ## 스테이지를 깰 때까지 반복. 'quit' 명령어 입력 시 스테이지 및 게임 종료.
            commands = input_command()
            stages[i], log, quit, i = exec_commands(stages[i], stage_init, commands, hole_loc, log, slots, i)
            
        if quit:
            print("게임을 종료합니다")
            break
        else:
            print(f"\n<Stage {i+1} Clear>\n")

        i+=1
    if quit==False:
        finish_game()

개요

  • 한번에 여러 개의 명령어를 입력받고 순차적으로 처리할 수 있도록 구현했다.
  • 이동 명령이 입력되면 알맞은 이동을 수행하고 이동 후의 지도 정보를 출력하도록 구현했다. 이 때 사용한 턴수가 출력된다.
  • 만약 해당 스테이지의 모든 구멍에 공이 들어갔다면 다음 스테이지로 넘어가도록 구현했다.
  • RESET 명령('r')이 입력되면 스테이지를 리셋하도록 구현했다.
    • 이 때 턴수 또한 리셋된다.
  • CHECK 명령('c')이 입력되면 무언가에 막혀 이동시킬 수 없는 공이 있는지 확인하도록 구현했다.
  • QUIT 명령어('q')가 입력되었을 때는 바로 게임을 종료하도록 구현했다.

상세

move.py

import show

def locate_player(map_list): ## 플레이어 위치 확인
    
    for i in range(len(map_list)):
        if 'P' in map_list[i]:
            row = i
            col = map_list[i].index('P')
    
    return [row, col]


#이동하고자 하는 위치가 공백이거나 빈 구멍일 경우
def move_up(map_list, current_row, current_col, hole_loc):
    map_list[current_row-1][current_col] = 'P'
    map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
    show.show_success(map_list, 'w') 

    return map_list

def move_left(map_list, current_row, current_col, hole_loc):
    map_list[current_row][current_col-1] = 'P'
    map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
    show.show_success(map_list, 'a')

    return map_list

def move_down(map_list, current_row, current_col, hole_loc):
    map_list[current_row+1][current_col] = 'P'
    map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
    show.show_success(map_list, 's')

    return map_list

def move_right(map_list, current_row, current_col, hole_loc):
    map_list[current_row][current_col+1] = 'P'
    map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
    show.show_success(map_list, 'd')

    return map_list

# 플레이어가 이동하고자 하는 위치에 공이 놓여있어서 공을 밀어야 할 경우
def push_up(map_list, current_row, current_col, hole_loc):
    success = True
    if map_list[current_row-2][current_col] == ' ':
        map_list[current_row-2][current_col] = 'o'
        map_list[current_row-1][current_col] = 'P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list, 'w')
    elif map_list[current_row-2][current_col]== 'O':
        map_list[current_row-2][current_col] = '0'
        map_list[current_row-1][current_col] = 'P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list, 'w')
    else:
        success = show.show_fail(map_list)

    return map_list, success

def push_left(map_list, current_row, current_col, hole_loc):
    success = True
    if map_list[current_row][current_col-2] == ' ':
        map_list[current_row][current_col-2] = 'o'
        map_list[current_row][current_col-1] = 'P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list, 'a')
    elif map_list[current_row][current_col-2]== 'O':
        map_list[current_row][current_col-2] = '0'
        map_list[current_row][current_col-1] = 'P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list, 'a')
    else:
        success = show.show_fail(map_list)

    return map_list, success

def push_down(map_list, current_row, current_col, hole_loc):
    success = True
    if map_list[current_row+2][current_col] == ' ':
        map_list[current_row+2][current_col] = 'o'
        map_list[current_row+1][current_col] = 'P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list,'s')
    elif map_list[current_row+2][current_col]== 'O':
        map_list[current_row+2][current_col] = '0'
        map_list[current_row+1][current_col] = 'P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list,'s')
    else:
        success=show.show_fail(map_list)

    return map_list, success

def push_right(map_list, current_row, current_col, hole_loc):
    success = True
    if map_list[current_row][current_col+2] == ' ':
        map_list[current_row][current_col+2] = 'o'
        map_list[current_row][current_col+1] = 'P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list,'d')
    elif map_list[current_row][current_col+2]== 'O':
        map_list[current_row][current_col+2] = '0'
        map_list[current_row][current_col+1] = 'P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list,'d')
    else:
        success = show.show_fail(map_list)

    return map_list, success


# 0을 o과 O으로 분리하는 움직임일 때.
def take_out_up(map_list, current_row, current_col, hole_loc):
    success = True
    if map_list[current_row-2][current_col] == ' ':
        map_list[current_row-2][current_col]='o'
        map_list[current_row-1][current_col]='P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list, 'w')
    elif map_list[current_row-2][current_col] == 'O':
        map_list[current_row-2][current_col] = '0'
        map_list[current_row-1][current_col] = 'P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list, 'w')
    else:
        success = show.show_fail(map_list)

    return map_list, success
        
def take_out_left(map_list, current_row, current_col, hole_loc):
    success = True
    if map_list[current_row][current_col-2] == ' ':
        map_list[current_row][current_col-2]='o'
        map_list[current_row][current_col-1]='P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list, 'a')
    elif map_list[current_row][current_col-2] == 'O':
        map_list[current_row][current_col-2] = '0'
        map_list[current_row][current_col-1] = 'P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list, 'a')
    else:
        success = show.show_fail(map_list)

    return map_list, success

def take_out_down(map_list, current_row, current_col, hole_loc):
    success = True
    if map_list[current_row+2][current_col] == ' ':
        map_list[current_row+2][current_col]='o'
        map_list[current_row+1][current_col]='P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list, 's')
    elif map_list[current_row+2][current_col] == 'O':
        map_list[current_row+2][current_col] = '0'
        map_list[current_row+1][current_col] = 'P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list, 's')
    else:
        success = show.show_fail(map_list)

    return map_list, success
    
def take_out_right(map_list, current_row, current_col, hole_loc):
    success = True
    if map_list[current_row][current_col+2] == ' ':
        map_list[current_row][current_col+2]='o'
        map_list[current_row][current_col+1]='P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list, 'd')
    elif map_list[current_row][current_col+2] == 'O':
        map_list[current_row][current_col+2] = '0'
        map_list[current_row][current_col+1] = 'P'
        map_list[current_row][current_col] = treat_hole(map_list, hole_loc, current_row, current_col)
        show.show_success(map_list, 'd')
    else:
        success = show.show_fail(map_list)

    return map_list, success


# 플레이어가 있던 자리가 원래 구멍이었는지 아닌지에 따라 플레이어가 해당 자리에서 이동했을 때 해당 위치 정보(' ' or 'O') 표시.
def treat_hole(map_list, hole_loc, current_row, current_col): 
    if [current_row, current_col] in hole_loc:
        map_list[current_row][current_col] = 'O'
    else:
        map_list[current_row][current_col] = ' '

    return map_list[current_row][current_col]


# map 정보를 커맨드에 따라 갱신.
def update_map(map_list, command, hole_loc): 

    current_row = locate_player(map_list)[0]
    current_col = locate_player(map_list)[1]
    success = True

    if command=='w' or command=='W':
        if map_list[current_row-1][current_col] == ' ' or map_list[current_row-1][current_col] == 'O': # 공백일 경우 P 위치 변경
            map_list = move_up(map_list, current_row, current_col, hole_loc)
        elif map_list[current_row-1][current_col] == 'o':
            map_list, success = push_up(map_list, current_row, current_col, hole_loc)
        elif map_list[current_row-1][current_col] == '0':
            map_list, success = take_out_up(map_list, current_row, current_col, hole_loc)
        else:
            success = show.show_fail(map_list)
            
    elif command=='a' or command== 'A':
        if map_list[current_row][current_col-1] == ' ' or map_list[current_row][current_col-1] == 'O':
            map_list = move_left(map_list, current_row, current_col, hole_loc)
        elif map_list[current_row][current_col-1] == 'o':
            map_list, success = push_left(map_list, current_row, current_col, hole_loc)
        elif map_list[current_row][current_col-1] == '0':
            map_list, success = take_out_left(map_list, current_row, current_col, hole_loc)
        else:
            success = show.show_fail(map_list)
            
    elif command=='s':
        if map_list[current_row+1][current_col] == ' ' or map_list[current_row+1][current_col] == 'O':
            map_list = move_down(map_list, current_row, current_col, hole_loc)    
        elif map_list[current_row+1][current_col] == 'o':
            map_list, success = push_down(map_list, current_row, current_col, hole_loc)
        elif map_list[current_row+1][current_col] == '0':
            map_list, success = take_out_down(map_list, current_row, current_col, hole_loc)
        else:
            success = show.show_fail(map_list)
            
    elif command=='d' or command == 'D':
        if map_list[current_row][current_col+1] == ' ' or map_list[current_row][current_col+1] == 'O':
            map_list = move_right(map_list, current_row, current_col, hole_loc)
        elif map_list[current_row][current_col+1] == 'o':
            map_list, success = push_right(map_list, current_row, current_col, hole_loc)
        elif map_list[current_row][current_col+1] == '0':
            map_list, success = take_out_right(map_list, current_row, current_col, hole_loc)
        else:
            success = show.show_fail(map_list)

    return map_list, success

플레이어 이동을 다음의 경우들로 분리했다.

  • 플레이어가 단순히 비어있는 공간으로 이동하는 경우
    • 아무것도 없는 공간으로 이동할 때
    • 구멍이 있는 공간으로 이동할 때
  • 플레이어가 이동하려는 방향에서 공과 맞닿아 있을 때 공을 미는 경우
    • 공을 아무것도 없는 공간으로 밀 때
    • 공을 구멍으로 밀어넣을 때
  • 플레이어가 구멍에 있던 공을 밀어서 구멍 밖으로 빼내는 경우
    • 밀려서 빠져나온 공이 또다시 옆 구멍으로 들어가는 경우
    • 밀려서 빠져나온 공이 아무것도 없던 공간에 위치하는 경우

각 경우에 따라서 올바른 이동을 수행하도록 했고, 만약 벽에 가로막히는 등 이동이 불가능할 경우 아무것도 실행되지 않도록 했다.

플레이어가 구멍이 있던 위치로 이동했을 경우 출력되는 지도 정보에는 구멍에 대한 심벌('0') 없이 플레이어 심벌('P')만 존재한다.
때문에 구멍 위에 서있던 플레이어가 다른 곳으로 이동했을 때 구멍 심벌을 다시 출력해줘야 했다.

save_hole_loc 함수를 이용해 각 스테이지마다 구멍의 위치를 저장해두고
treat_hole 함수를 이용해 매 이동마다 플레이어가 구멍 위에 서있었다면 이동 후 구멍 심벌이 다시 출력되도록 했다.

profile
BEST? BETTER!

0개의 댓글