파이게임 (Pygame) 으로 만드는 푸시 박스 게임

PythonBeginner
지금 연습하기

소개

이 프로젝트는 Python 언어와 Pygame 을 사용하여 고전 게임 Sokoban 을 개발하는 것입니다.

이 프로젝트에서 다루는 지식 포인트는 다음과 같습니다:

  • Python 의 기본 문법
  • Pygame 을 사용한 기본적인 게임 개발

이 과정은 난이도가 중간 정도이며, Python 에 대한 기본적인 이해가 있고 지식을 더 향상시키고 싶은 사용자에게 적합합니다.

소스 코드 sokoban.py.zip는 GNU GPL v3 라이선스 하에 배포되며, 스킨은 Borgar 가 제작했습니다.

👀 미리보기

Sokoban game preview animation

🎯 과제

이 프로젝트에서 다음을 배우게 됩니다:

  • Pygame 을 사용하여 게임을 초기화하는 방법
  • 게임 이벤트 및 키보드 조작을 처리하는 방법
  • 게임 맵을 구현하는 방법
  • 플레이어와 상자의 이동 조작을 구현하는 방법
  • 실행 취소 (undo) 및 다시 실행 (redo) 조작을 구현하는 방법
  • 게임 인터페이스를 테스트하는 방법

🏆 성과

이 프로젝트를 완료하면 다음을 수행할 수 있습니다:

  • Pygame 을 초기화하고 게임 창을 설정합니다.
  • Pygame 에서 게임 이벤트 및 키 입력을 처리합니다.
  • 게임 맵을 구현하고 Pygame 을 사용하여 표시합니다.
  • 플레이어와 상자의 이동 조작을 구현합니다.
  • 게임에서 실행 취소 (undo) 및 다시 실행 (redo) 조작을 구현합니다.
  • 게임 인터페이스를 테스트하고 실행합니다.

게임 설명

Sokoban 게임에서는 불규칙한 다각형 영역을 형성하는 닫힌 벽이 있습니다. 플레이어와 상자는 이 영역 내에서만 이동할 수 있습니다. 영역 내부에는 사람, 여러 개의 상자, 그리고 목표 지점이 있습니다. 게임의 목표는 화살표 키를 사용하여 사람의 움직임을 제어하고 상자를 목표 지점으로 밀어 넣는 것입니다. 한 번에 하나의 상자만 움직일 수 있으며, 상자가 구석에 갇히면 게임을 계속 진행할 수 없습니다.

캐릭터

위의 설명에서, 우리는 게임에서 다음과 같은 캐릭터를 추상화할 수 있습니다:

  1. 벽 (Walls): 이동 경로를 막는 닫힌 영역.
  2. 공간 (Spaces): 사람이 걸어 다니며 상자를 밀 수 있는 영역.
  3. 사람 (Person): 플레이어가 제어하는 캐릭터.
  4. 상자 (Boxes)
  5. 목표 지점 (Target points)

사람, 상자, 그리고 목표 지점은 모두 공간 영역 내에서 초기화되어야 하며, 다른 캐릭터는 벽 영역 내에 나타나서는 안 됩니다.

조작

Sokoban 게임에서 우리가 제어할 수 있는 유일한 캐릭터는 사람입니다. 우리는 화살표 키를 사용하여 사람의 움직임을 제어하며, 이는 사람을 움직이는 것과 상자를 미는 것 모두에 사용됩니다. 사람의 움직임에는 두 가지 유형이 있으며, 각 경우를 별도로 처리해야 합니다:

  1. 사람만 움직이는 경우
  2. 상자를 밀면서 사람을 움직이는 경우

또한, 게임은 다음 두 가지 조작을 지원합니다:

  1. 실행 취소 (Undo): 이전 움직임을 취소하며, 백스페이스 키로 제어합니다.
  2. 다시 실행 (Redo): 이전에 실행 취소된 움직임을 다시 실행하며, 스페이스 바 (space bar) 로 제어합니다.

요약하면, 우리는 네 개의 화살표 키, 실행 취소를 위한 백스페이스 키, 그리고 다시 실행을 위한 스페이스 바에 대한 키보드 이벤트를 지원해야 합니다. 다음 Pygame 구현 섹션에서, 우리는 이 여섯 개의 키보드 이벤트를 처리해야 합니다.

개발 준비

환경에서 pygame 을 사용하려면, 실험 환경에서 터미널을 열고 다음 명령을 입력하여 pygame 을 설치합니다:

sudo pip install pygame

pygame 에는 마우스, 디스플레이 장치, 그래픽, 이벤트, 글꼴, 이미지, 키보드, 사운드, 비디오, 오디오 등 많은 모듈이 있습니다. Sokoban 게임에서는 다음 모듈을 사용합니다:

  • pygame.display: 이미지를 표시하기 위해 디스플레이 장치에 접근합니다.
  • pygame.image: 스프라이트 시트 (sprite sheet) 를 처리하기 위해 이미지를 로드하고 저장합니다.
  • pygame.key: 키보드 입력을 읽습니다.
  • pygame.event: 이벤트를 관리하고, 게임에서 키보드 이벤트를 처리합니다.
  • pygame.time: 시간을 관리하고 프레임 정보를 표시합니다.

위의 소개에서 스프라이트 시트에 대해 언급했습니다. 스프라이트 시트는 게임 개발에서 흔히 사용되는 이미지 병합 방법으로, 작은 아이콘과 배경 이미지를 하나의 이미지로 병합한 다음, pygame 의 이미지 위치 지정을 사용하여 이미지의 필요한 부분을 표시합니다.

Sokoban 게임에서는 미리 만들어진 스프라이트 시트를 사용합니다. 여기서는 이미지를 자르고 스프라이트 시트를 병합하는 방법에 대해 자세히 설명하지 않겠습니다. 온라인에서 무수히 많은 방법을 찾을 수 있습니다.

이 프로젝트에서 사용되는 Sokoban 스프라이트 시트의 이미지 요소는 borgar에서 가져왔으며, 파일은 ~/project/borgar.png에서 찾을 수 있습니다.

게임 이미지 요소는 다음과 같습니다:

  • 게임 인터페이스 배경색
  • 플레이어
  • 일반 상자
  • 목표 지점
  • 플레이어와 목표 지점의 겹침 효과
  • 상자가 목표 지점에 도달한 겹침 효과

스프라이트 시트의 두 개의 상자 이미지는 구현에 필요하지 않습니다. 이후 구현 부분에서 pygame 의 blit 메서드를 사용하여 스프라이트 시트의 내용을 로드하고 표시하는 방법을 자세히 설명하겠습니다.

게임 개발

먼저, ~/project 디렉토리에 sokoban.py 파일을 생성한 다음, 파일에 다음 내용을 입력합니다:

  1. pygame 초기화
import pygame, sys, os
from pygame.locals import *

from collections import deque


pygame.init()
  1. 디스플레이 객체 설정
## pygame 디스플레이 창의 크기를 가로 400 픽셀, 세로 300 픽셀로 설정합니다.
screen = pygame.display.set_mode((400,300))
  1. 이미지 요소 로드
## 단일 파일에서 이미지 요소 로드
skinfilename = os.path.join('borgar.png')

try:
    skin = pygame.image.load(skinfilename)
except pygame.error as msg:
    print('cannot load skin')
    raise SystemExit(msg)

skin = skin.convert()

## 창의 배경색을 skin 파일의 좌표 (0,0) 에 있는 요소로 설정합니다.
screen.fill(skin.get_at((0,0)))
  1. 시계와 키보드 이벤트의 반복 시간을 설정합니다. key.set_repeat을 사용하여 매개변수 (delay, interval)로 반복 이벤트의 시간 간격을 설정합니다.
clock = pygame.time.Clock()
pygame.key.set_repeat(200,50)
  1. 메인 루프 시작
## 게임 메인 루프
while True:
    clock.tick(60)
    pass
  1. 게임 이벤트 및 키보드 조작 처리. 메인 루프에서 키보드 이벤트를 처리해야 합니다. 앞서 언급했듯이, 우리는 여섯 개의 키, 즉 위, 아래, 왼쪽, 오른쪽, 백스페이스 및 스페이스를 지원해야 합니다.
## 게임 이벤트 가져오기
for event in pygame.event.get():
    ## 게임 종료 이벤트
    if event.type == QUIT:
        pygame.quit()
        sys.exit()
    ## 키보드 조작
    elif event.type == KEYDOWN:
        ## 왼쪽으로 이동
        if event.key == K_LEFT:
            pass
        ## 위로 이동
        elif event.key == K_UP:
            pass
        ## 오른쪽으로 이동
        elif event.key == K_RIGHT:
            pass
        ## 아래로 이동
        elif event.key == K_DOWN:
            pass
        ## 실행 취소 조작
        elif event.key == K_BACKSPACE:
            pass
        ## 다시 실행 조작
        elif event.key == K_SPACE:
            pass

이제 pygame 기반의 게임 프레임워크를 완성했습니다. 게임 로직을 구현해 보겠습니다.

맵 구현

먼저, Sokoban 객체를 정의해야 합니다. 게임과 관련된 모든 로직을 포함하는 클래스를 사용합니다.

class Sokoban:

    ## Sokoban 게임 초기화
    def __init__(self):
        pass

Sokoban 게임에는 작동 영역, 즉 맵 영역이 필요합니다. 문자 목록을 사용하여 맵을 나타내며, 여기서 다른 문자는 게임의 다른 요소를 나타냅니다:

  1. 벽: ## 기호
  2. 공간: - 기호
  3. 플레이어: @ 기호
  4. 상자: $ 기호
  5. 목표 지점: . 기호`
  6. 목표 지점에 있는 플레이어: + 기호
  7. 목표 지점에 있는 상자: * 기호

게임이 시작될 때, 맵에 대한 기본 문자 목록을 설정해야 합니다. 동시에, 이 1 차원 목록에서 2D 맵을 생성하기 위해 맵의 너비와 높이를 알아야 합니다.

맵 표현은 다음 코드와 유사합니다. 이 코드를 기반으로 시작 후 어떤 모습일지 상상할 수 있습니까?

class Sokoban:

    ## Sokoban 게임 초기화
    def __init__(self):
        ## 맵 설정
        self.level = list(
            "----#####----------"
            "----#---#----------"
            "----#$--#----------"
            "--###--$##---------"
            "--#--$-$-#---------"
            "###-#-##-#---######"
            "#---#-##-#####--..#"
            "#-$--$----------..#"
            "#####-###-#@##--..#"
            "----#-----#########"
            "----#######--------")

        ## 맵의 너비와 높이, 맵에서 플레이어의 위치 (맵 목록의 인덱스 값) 설정
        ## 총 19 개 열
        self.w = 19

        ## 총 11 개 행
        self.h = 11

        ## 플레이어의 초기 위치는 self.level[163] 에 있습니다.
        self.man = 163

맵은 문자 목록을 스캔하고 문자를 기반으로 해당 위치에 다른 요소를 표시하여 표시됩니다.

표시는 2D 이므로 너비와 높이를 사용하여 2D 표시 영역에서 각 문자의 위치를 결정합니다. pygame 에서 언급한 screenskin을 드로잉 함수 draw의 매개변수로 전달해야 합니다.

구현한 드로잉 함수는 pygame 의 blit을 사용하여 스프라이트 시트에서 이미지를 추출하여 지정된 위치에 표시한다는 점에 유의해야 합니다:

screen.blit(skin, (i*w, j*w), (0,0,w,w))

draw 함수의 전체 구현은 다음과 같습니다. 먼저 스캔을 수행한 다음, 스프라이트 시트를 기반으로 각 문자에 해당하는 이미지를 표시합니다:

class Sokoban:

    ## 맵 레벨을 기반으로 pygame 창에 맵을 그립니다.
    def draw(self, screen, skin):

        ## 각 이미지 요소의 너비를 가져옵니다.
        w = skin.get_width() / 4

        ## 맵 레벨의 각 문자 요소를 반복합니다.
        for i in range(0, self.w):
            for j in range(0, self.h):

                ## 맵에서 j 번째 행과 i 번째 열의 문자를 가져옵니다.
                item = self.level[j*self.w + i]

                ## 이 위치에 벽 (#) 으로 표시합니다.
                if item == '#':
                    ## pygame 의 blit 메서드를 사용하여 지정된 위치에 이미지를 표시합니다.
                    ## 위치 좌표는 (i*w, j*w) 이고, skin 의 이미지 좌표와 길이 - 너비는 (0,2*w,w,w) 입니다.
                    screen.blit(skin, (i*w, j*w), (0,2*w,w,w))
                ## 이 위치에 공간 (-) 으로 표시합니다.
                elif item == '-':
                    screen.blit(skin, (i*w, j*w), (0,0,w,w))
                ## 이 위치에 플레이어 (@) 로 표시합니다.
                elif item == '@':
                    screen.blit(skin, (i*w, j*w), (w,0,w,w))
                ## 이 위치에 상자 ($) 로 표시합니다.
                elif item == '$':
                    screen.blit(skin, (i*w, j*w), (2*w,0,w,w))
                ## 이 위치에 목표 지점 (.) 으로 표시합니다.
                elif item == '.':
                    screen.blit(skin, (i*w, j*w), (0,w,w,w))
                ## 목표 지점에 있는 플레이어 효과로 표시합니다.
                elif item == '+':
                    screen.blit(skin, (i*w, j*w), (w,w,w,w))
                ## 목표 지점에 배치된 상자 효과로 표시합니다.
                elif item == '*':
                    screen.blit(skin, (i*w, j*w), (2*w,w,w,w))

이동 연산 구현

이동 조작은 화살표 키를 사용하여 왼쪽, 오른쪽, 위쪽 및 아래쪽의 네 방향으로 움직임을 제어합니다. 네 개의 문자 'l' (왼쪽), 'r' (오른쪽), 'u' (위쪽) 및 'd' (아래쪽) 를 사용하여 이동 방향을 지정합니다.

다시 실행 조작과 이동 조작에 필요한 프로세스가 유사하므로, Sokoban 클래스에서 이동을 처리하기 위해 내부 함수 _move() 를 정의합니다:

class Sokoban:

    ## 내부 이동 함수: 이동 조작 후 맵에서 요소의 위치 변경을 업데이트하는 데 사용되며, 여기서 d 는 이동 방향을 나타냅니다.
    def _move(self, d):
        ## 이동에 대한 맵의 변위를 가져옵니다.
        h = get_offset(d, self.w)

        ## 이동의 대상 영역이 빈 공간이거나 목표 지점인 경우 플레이어만 이동해야 합니다.
        if self.level[self.man + h] == '-' or self.level[self.man + h] == '.':
            ## 플레이어를 대상 위치로 이동합니다.
            move_man(self.level, self.man + h)
            ## 이동 후 플레이어의 원래 위치를 설정합니다.
            move_floor(self.level, self.man)
            ## 플레이어의 새 위치
            self.man += h
            ## 이동 조작을 솔루션에 추가합니다.
            self.solution += d

        ## 이동의 대상 영역이 상자인 경우 상자와 플레이어 모두 이동해야 합니다.
        elif self.level[self.man + h] == '*' or self.level[self.man + h] == '$':
            ## 상자와 플레이어 위치의 변위
            h2 = h * 2
            ## 다음 위치가 빈 공간이거나 목표 지점인 경우에만 상자를 이동할 수 있습니다.
            if self.level[self.man + h2] == '-' or self.level[self.man + h2] == '.':
                ## 상자를 목표 지점으로 이동합니다.
                move_box(self.level, self.man + h2)
                ## 플레이어를 목표 지점으로 이동합니다.
                move_man(self.level, self.man + h)
                ## 플레이어의 현재 위치를 재설정합니다.
                move_floor(self.level, self.man)
                ## 플레이어의 새 위치를 설정합니다.
                self.man += h
                ## 이 단계에서 상자가 밀렸음을 나타내기 위해 이동 조작을 대문자로 표시합니다.
                self.solution += d.upper()
                ## 상자를 미는 단계 수를 증가시킵니다.
                self.push += 1

_move 함수에서는 다음 함수를 사용해야 합니다:

  • get_offset(d, width): 맵에서 이동의 변위를 가져옵니다. d는 이동 방향을 나타내고, width는 게임 창의 너비를 나타냅니다.
  • move_man(level, i): 맵에서 플레이어의 위치를 이동합니다. level은 맵 목록이고, i는 플레이어의 위치입니다.
  • move_floor(level, i): 이동 후 위치를 재설정합니다. 플레이어가 위치에서 이동한 후에는 빈 공간 또는 목표 지점으로 재설정해야 합니다.
  • move_box(level, i): 맵에서 상자의 위치를 이동합니다. level은 맵 목록이고, i는 상자의 위치입니다.

이러한 함수의 구현은 전체 코드에서 확인할 수 있습니다. 각 요소를 이동할 때 대상 위치의 원래 요소가 무엇인지 고려하여 이동 후 설정해야 하는 요소를 결정하는 것이 중요합니다.

이동 조작을 수행하려면 _move를 호출하고 todo[]를 비워둡니다 (다시 실행 목록은 실행 취소 조작을 수행할 때만 활성화됩니다).

실행 취소 구현

실행 취소는 이동의 반대 조작입니다. solution에서 이전 단계를 검색하여 반대 조작을 수행합니다. 자세한 코드는 다음과 같습니다:

class Sokoban:

    ## 실행 취소 조작: 이전 이동을 실행 취소합니다.
    def undo(self):
        ## 이동 기록이 있는지 확인합니다.
        if self.solution.__len__()>0:
            ## 다시 실행 조작을 위해 이동 기록을 todo 목록에 저장합니다.
            self.todo.append(self.solution[-1])
            ## 이동 기록을 삭제합니다.
            self.solution.pop()

            ## 실행 취소 조작을 위해 이동할 오프셋을 가져옵니다: 마지막 이동의 오프셋의 음수
            h = get_offset(self.todo[-1],self.w) * -1

            ## 이 조작이 상자를 밀지 않고 문자만 이동하는지 확인합니다.
            if self.todo[-1].islower():
                ## 문자를 원래 위치로 다시 이동합니다.
                move_man(self.level, self.man + h)
                ## 문자의 현재 위치를 설정합니다.
                move_floor(self.level, self.man)
                ## 맵에서 문자의 위치를 설정합니다.
                self.man += h
            else:
                ## 이 단계에서 상자를 밀면 _move 에서 문자와 상자를 이동하고 관련 조작을 수행합니다.
                move_floor(self.level, self.man - h)
                move_box(self.level, self.man)
                move_man(self.level, self.man + h)
                self.man += h
                self.push -= 1

다시 실행 연산

실행 취소 명령이 실행되면 내용이 solution[]에서 todo[]로 이동하며, _move 함수를 추출하여 호출하기만 하면 됩니다.

    ## 다시 실행 조작: 실행 취소 조작이 실행되고 활성화되면 실행 취소 전 위치로 다시 이동합니다.
    def redo(self):
        ## 실행 취소 조작이 기록되었는지 확인합니다.
        if self.todo.__len__() > 0:
            ## 실행 취소된 단계를 다시 이동합니다.
            self._move(self.todo[-1].lower())
            ## 이 기록을 삭제합니다.
            self.todo.pop()

위의 단계를 통해 게임의 주요 내용이 완료되었습니다. 완전한 게임 코드를 독립적으로 완성하고, 스크린샷을 테스트하고, 실험실 Q&A 섹션에서 명확하지 않은 부분이 있으면 질문하십시오. 실험실 팀과 선생님은 질문에 즉시 답변해 드릴 것입니다.

추가 기능 및 코드 리팩토링

이제 기본적인 게임이 있지만 완벽하지 않습니다. 더 플레이하기 쉽도록 몇 가지 추가 기능을 추가해야 합니다.

또한 코드를 더 읽기 쉽고 유지 관리 가능하도록 리팩토링해야 합니다.

전체 코드 보기
import pygame, sys, os
from pygame.locals import *

from collections import deque


def to_box(level, index):
    if level[index] == "-" or level[index] == "@":
        level[index] = "$"
    else:
        level[index] = "*"


def to_man(level, i):
    if level[i] == "-" or level[i] == "$":
        level[i] = "@"
    else:
        level[i] = "+"


def to_floor(level, i):
    if level[i] == "@" or level[i] == "$":
        level[i] = "-"
    else:
        level[i] = "."


def to_offset(d, width):
    d4 = [-1, -width, 1, width]
    m4 = ["l", "u", "r", "d"]
    return d4[m4.index(d.lower())]


def b_manto(level, width, b, m, t):
    maze = list(level)
    maze[b] = "#"
    if m == t:
        return 1
    queue = deque([])
    queue.append(m)
    d4 = [-1, -width, 1, width]
    m4 = ["l", "u", "r", "d"]
    while len(queue) > 0:
        pos = queue.popleft()
        for i in range(4):
            newpos = pos + d4[i]
            if maze[newpos] in ["-", "."]:
                if newpos == t:
                    return 1
                maze[newpos] = i
                queue.append(newpos)
    return 0


def b_manto_2(level, width, b, m, t):
    maze = list(level)
    maze[b] = "#"
    maze[m] = "@"
    if m == t:
        return []
    queue = deque([])
    queue.append(m)
    d4 = [-1, -width, 1, width]
    m4 = ["l", "u", "r", "d"]
    while len(queue) > 0:
        pos = queue.popleft()
        for i in range(4):
            newpos = pos + d4[i]
            if maze[newpos] in ["-", "."]:
                maze[newpos] = i
                queue.append(newpos)
                if newpos == t:
                    path = []
                    while maze[t] != "@":
                        path.append(m4[maze[t]])
                        t = t - d4[maze[t]]
                    return path

    return []


class Sokoban:
    def __init__(self):
        self.level = list(
            "----#####--------------#---#--------------#$--#------------###--$##-----------#--$-$-#---------###-#-##-#---#######---#-##-#####--..##-$--$----------..######-###-#@##--..#----#-----#########----#######--------"
        )
        self.w = 19
        self.h = 11
        self.man = 163
        self.hint = list(self.level)
        self.solution = []
        self.push = 0
        self.todo = []
        self.auto = 0
        self.sbox = 0
        self.queue = []

    def draw(self, screen, skin):
        w = skin.get_width() / 4
        offset = (w - 4) / 2
        for i in range(0, self.w):
            for j in range(0, self.h):
                if self.level[j * self.w + i] == "#":
                    screen.blit(skin, (i * w, j * w), (0, 2 * w, w, w))
                elif self.level[j * self.w + i] == "-":
                    screen.blit(skin, (i * w, j * w), (0, 0, w, w))
                elif self.level[j * self.w + i] == "@":
                    screen.blit(skin, (i * w, j * w), (w, 0, w, w))
                elif self.level[j * self.w + i] == "$":
                    screen.blit(skin, (i * w, j * w), (2 * w, 0, w, w))
                elif self.level[j * self.w + i] == ".":
                    screen.blit(skin, (i * w, j * w), (0, w, w, w))
                elif self.level[j * self.w + i] == "+":
                    screen.blit(skin, (i * w, j * w), (w, w, w, w))
                elif self.level[j * self.w + i] == "*":
                    screen.blit(skin, (i * w, j * w), (2 * w, w, w, w))
                if self.sbox != 0 and self.hint[j * self.w + i] == "1":
                    screen.blit(
                        skin, (i * w + offset, j * w + offset), (3 * w, 3 * w, 4, 4)
                    )

    def move(self, d):
        self._move(d)
        self.todo = []

    def _move(self, d):
        self.sbox = 0
        h = to_offset(d, self.w)
        h2 = 2 * h
        if self.level[self.man + h] == "-" or self.level[self.man + h] == ".":
            ## move
            to_man(self.level, self.man + h)
            to_floor(self.level, self.man)
            self.man += h
            self.solution += d
        elif self.level[self.man + h] == "*" or self.level[self.man + h] == "$":
            if self.level[self.man + h2] == "-" or self.level[self.man + h2] == ".":
                ## push
                to_box(self.level, self.man + h2)
                to_man(self.level, self.man + h)
                to_floor(self.level, self.man)
                self.man += h
                self.solution += d.upper()
                self.push += 1

    def undo(self):
        if self.solution.__len__() > 0:
            self.todo.append(self.solution[-1])
            self.solution.pop()

            h = to_offset(self.todo[-1], self.w) * -1
            if self.todo[-1].islower():
                ## undo a move
                to_man(self.level, self.man + h)
                to_floor(self.level, self.man)
                self.man += h
            else:
                ## undo a push
                to_floor(self.level, self.man - h)
                to_box(self.level, self.man)
                to_man(self.level, self.man + h)
                self.man += h
                self.push -= 1

    def redo(self):
        if self.todo.__len__() > 0:
            self._move(self.todo[-1].lower())
            self.todo.pop()

    def manto(self, x, y):
        maze = list(self.level)
        maze[self.man] = "@"
        queue = deque([])
        queue.append(self.man)
        d4 = [-1, -self.w, 1, self.w]
        m4 = ["l", "u", "r", "d"]
        while len(queue) > 0:
            pos = queue.popleft()
            for i in range(4):
                newpos = pos + d4[i]
                if maze[newpos] in ["-", "."]:
                    maze[newpos] = i
                    queue.append(newpos)

        t = y * self.w + x
        if maze[t] in range(4):
            self.todo = []
            while maze[t] != "@":
                self.todo.append(m4[maze[t]])
                t = t - d4[maze[t]]

        self.auto = 1

    def automove(self):
        if self.auto == 1 and self.todo.__len__() > 0:
            self._move(self.todo[-1].lower())
            self.todo.pop()
        else:
            self.auto = 0

    def boxhint(self, x, y):
        d4 = [-1, -self.w, 1, self.w]
        m4 = ["l", "u", "r", "d"]
        b = y * self.w + x
        maze = list(self.level)
        to_floor(maze, b)
        to_floor(maze, self.man)
        mark = maze * 4
        size = self.w * self.h
        self.queue = []
        head = 0
        for i in range(4):
            if b_manto(maze, self.w, b, self.man, b + d4[i]):
                if len(self.queue) == 0:
                    self.queue.append((b, i, -1))
                mark[i * size + b] = "1"

        while head < len(self.queue):
            pos = self.queue[head]
            head += 1

            for i in range(4):
                if mark[pos[0] + i * size] == "1" and maze[pos[0] - d4[i]] in [
                    "-",
                    ".",
                ]:
                    if mark[pos[0] - d4[i] + i * size] != "1":
                        self.queue.append((pos[0] - d4[i], i, head - 1))
                        for j in range(4):
                            if b_manto(
                                maze,
                                self.w,
                                pos[0] - d4[i],
                                pos[0],
                                pos[0] - d4[i] + d4[j],
                            ):
                                mark[j * size + pos[0] - d4[i]] = "1"
        for i in range(size):
            self.hint[i] = "0"
            for j in range(4):
                if mark[j * size + i] == "1":
                    self.hint[i] = "1"

    def boxto(self, x, y):
        d4 = [-1, -self.w, 1, self.w]
        m4 = ["l", "u", "r", "d"]
        om4 = ["r", "d", "l", "u"]
        b = y * self.w + x
        maze = list(self.level)
        to_floor(maze, self.sbox)
        to_floor(
            maze, self.man
        )  ## make a copy of working maze by removing the selected box and the man
        for i in range(len(self.queue)):
            if self.queue[i][0] == b:
                self.todo = []
                j = i
                while self.queue[j][2] != -1:
                    self.todo.append(om4[self.queue[j][1]].upper())
                    k = self.queue[j][2]
                    if self.queue[k][2] != -1:
                        self.todo += b_manto_2(
                            maze,
                            self.w,
                            self.queue[k][0],
                            self.queue[k][0] + d4[self.queue[k][1]],
                            self.queue[k][0] + d4[self.queue[j][1]],
                        )
                    else:
                        self.todo += b_manto_2(
                            maze,
                            self.w,
                            self.queue[k][0],
                            self.man,
                            self.queue[k][0] + d4[self.queue[j][1]],
                        )
                    j = k

                self.auto = 1
                return
        print("not found!")

    def mouse(self, x, y):
        if x >= self.w or y >= self.h:
            return
        m = y * self.w + x
        if self.level[m] in ["-", "."]:
            if self.sbox == 0:
                self.manto(x, y)
            else:
                self.boxto(x, y)
        elif self.level[m] in ["$", "*"]:
            if self.sbox == m:
                self.sbox = 0
            else:
                self.sbox = m
                self.boxhint(x, y)
        elif self.level[m] in ["-", ".", "@", "+"]:
            self.boxto(x, y)


## start pygame
pygame.init()
screen = pygame.display.set_mode((400, 300))

## load skin
skinfilename = os.path.join("borgar.png")
try:
    skin = pygame.image.load(skinfilename)
except pygame.error as msg:
    print("cannot load skin")
    raise SystemExit(msg)
skin = skin.convert()

## screen.fill((255,255,255))
screen.fill(skin.get_at((0, 0)))
pygame.display.set_caption("sokoban.py")

## create Sokoban object
skb = Sokoban()
skb.draw(screen, skin)

clock = pygame.time.Clock()
pygame.key.set_repeat(200, 50)

## main game loop
while True:
    clock.tick(60)

    if skb.auto == 0:
        for event in pygame.event.get():
            if event.type == QUIT:
                pygame.quit()
                sys.exit()
            elif event.type == KEYDOWN:
                if event.key == K_LEFT:
                    skb.move("l")
                    skb.draw(screen, skin)
                elif event.key == K_UP:
                    skb.move("u")
                    skb.draw(screen, skin)
                elif event.key == K_RIGHT:
                    skb.move("r")
                    skb.draw(screen, skin)
                elif event.key == K_DOWN:
                    skb.move("d")
                    skb.draw(screen, skin)
                elif event.key == K_BACKSPACE:
                    skb.undo()
                    skb.draw(screen, skin)
                elif event.key == K_SPACE:
                    skb.redo()
                    skb.draw(screen, skin)
            elif event.type == MOUSEBUTTONUP and event.button == 1:
                mousex, mousey = event.pos
                mousex /= skin.get_width() / 4
                mousey /= skin.get_width() / 4
                skb.mouse(mousex, mousey)
                skb.draw(screen, skin)
    else:
        skb.automove()
        skb.draw(screen, skin)

    pygame.display.update()
    pygame.display.set_caption(
        skb.solution.__len__().__str__() + "/" + skb.push.__str__() + " - sokoban.py"
    )

실행 및 테스트

터미널에서 실행하려면:

cd ~/project
python sokoban.py

모든 것이 정상이라면 다음과 같은 게임 인터페이스를 볼 수 있습니다.

Sokoban game interface preview

요약

이 프로젝트는 Sokoban 게임의 기본적인 기능만 구현했습니다. 실험을 바탕으로, 다음을 통해 이 코드를 확장하는 것을 고려할 수 있습니다.

  1. 작성된 코드에서 맵 데이터를 추출하여 파일에 저장하는 방법을 파악합니다.
  2. 마우스 컨트롤을 구현하여 캐릭터를 특정 위치로 빠르게 이동시킵니다.
  3. 맵의 해결 가능성을 자동으로 판단하는 알고리즘을 개발합니다.
✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습✨ 솔루션 확인 및 연습