Pygame を使った箱詰めゲーム

PythonPythonBeginner
今すぐ練習

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

このプロジェクトは、Python言語とPygameを使って、クラシックゲームのソコバンを開発するものです。

このプロジェクトでカバーされる知識ポイントは以下の通りです。

  • Pythonの基本構文
  • Pygameを使った基本的なゲーム開発

このコースは難易度が適度で、Pythonの基本的な理解を持ち、さらに知識を深めたいユーザーに適しています。

ソースコードsokoban.py.zipはGNU GPL v3ライセンスの下で公開されており、スキンはBorgarによって作成されました。

👀 プレビュー

Sokoban game preview animation

🎯 タスク

このプロジェクトで学ぶことは以下の通りです。

  • Pygameを使ってゲームを初期化する方法
  • ゲームイベントとキーボード操作を処理する方法
  • ゲーム用のマップを実装する方法
  • プレイヤーとボックスの移動操作を実装する方法
  • アンドゥとリドゥ操作を実装する方法
  • ゲームインターフェイスをテストする方法

🏆 成果

このプロジェクトを完了すると、以下のことができるようになります。

  • Pygameを初期化し、ゲームウィンドウをセットアップする
  • Pygameでゲームイベントとキーボード入力を処理する
  • ゲームマップを実装し、Pygameを使って表示する
  • プレイヤーとボックスの移動操作を実装する
  • ゲーム内でアンドゥとリドゥ操作を実装する
  • ゲームインターフェイスをテストして実行する

ゲームの説明

ソコバンゲームでは、不規則な多角形領域を形成する囲いの壁があります。プレイヤーとボックスはこの領域内でのみ移動できます。領域内には、1人の人物といくつかのボックスと目標地点があります。ゲームの目的は、矢印キーを使って人物の移動を制御し、ボックスを目標地点に押し付けることです。一度に1つのボックスのみを移動でき、ボックスが隅に引っかかった場合、ゲームは続行できません。

キャラクター

上記の説明から、このゲームにおける以下のキャラクターを抽象化できます。

  1. 壁:移動経路をブロックする囲まれた領域
  2. スペース:人物が歩き、ボックスを押すことができる領域
  3. 人物:プレイヤーが制御するキャラクター
  4. ボックス
  5. 目標地点

人物、ボックス、目標地点はすべてスペース領域内で初期化する必要があり、壁領域内には他のキャラクターが出現してはいけません。

操作方法

ソコバンゲームでは、操作できる唯一のキャラクターは人物です。矢印キーを使って人物の移動を制御し、人物の移動とボックスの押し付けの両方に使用します。人物の移動には2種類あり、それぞれのケースを個別に処理する必要があります。

  1. 人物のみを移動させる
  2. ボックスを押しながら人物を移動させる

また、このゲームでは以下の2つの操作がサポートされています。

  1. アンドゥ:前の移動を取り消します。バックスペースキーで制御します。
  2. リドゥ:以前取り消された移動をやり直します。スペースバーで制御します。

要するに、4つの矢印キー、アンドゥ用のバックスペースキー、リドゥ用のスペースバーのキーボードイベントをサポートする必要があります。次のPygameを実装するセクションでは、これら6つのキーボードイベントを処理する必要があります。

開発準備

環境でpygameを使用できるようにするには、実験環境のターミナルを開き、次のコマンドを入力してpygameをインストールします。

sudo pip install pygame

pygameには、マウス、表示デバイス、グラフィック、イベント、フォント、画像、キーボード、サウンド、ビデオ、オーディオなど、多くのモジュールがあります。ソコバンゲームでは、以下のモジュールを使用します。

  • pygame.display:画像を表示するための表示デバイスにアクセスします。
  • pygame.image:画像を読み込んで保存し、スプライトシートを処理するために使用します。
  • pygame.key:キーボード入力を読み取ります。
  • pygame.event:イベントを管理し、ゲーム内のキーボードイベントを処理します。
  • pygame.time:時間を管理し、フレーム情報を表示します。

上記の説明ではスプライトシートについて言及しました。スプライトシートは、ゲーム開発における一般的な画像マージ方法であり、小さなアイコンと背景画像を1つの画像にマージし、その後、pygameの画像位置指定を使用して画像の必要な部分を表示します。

ソコバンゲームでは、既製のスプライトシートを使用しています。ここでは、画像の切り抜き方法やスプライトシートのマージ方法について詳しく説明しません。オンライン上には数え切れない方法があります。

このプロジェクトで使用するソコバンのスプライトシートの画像要素はborgarからのもので、ファイルは~/project/borgar.pngにあります。

ゲーム画像要素には、以下が含まれます。

  • ゲームインターフェイスの背景色
  • プレイヤー
  • 通常のボックス
  • 目標地点
  • プレイヤーと目標地点の重なり効果
  • ボックスが目標地点に到達した重なり効果

スプライトシート内の2つのボックス画像は、この実装では必要ありません。後続の実装部分で、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. ゲームイベントとキーボード操作を処理する。メインループでは、キーボードイベントを処理する必要があり、前述の通り、上、下、左、右、バックスペース、スペースの6つのキーをサポートする必要がある。
## ゲームイベントを取得する
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をベースとしたゲームフレームワークが完成しました。次に、ゲームロジックの実装に移りましょう。

✨ 解答を確認して練習

マップの実装

まず、ソコバンオブジェクトを定義する必要があります。すべてのゲーム関連のロジックを含むクラスを使用します。

class Sokoban:

    ## ソコバンゲームを初期化する
    def __init__(self):
        pass

ソコバンゲームには操作領域が必要で、それがマップ領域です。マップを表すために文字のリストを使用し、異なる文字がゲーム内の異なる要素を表します。

  1. 壁:## 記号
  2. スペース:- 記号
  3. プレイヤー:@ 記号
  4. ボックス:$ 記号
  5. 目標地点:. 記号
  6. 目標地点にいるプレイヤー:+ 記号
  7. 目標地点に置かれたボックス:* 記号

ゲームが始まるとき、マップにデフォルトの文字リストを設定する必要があります。同時に、マップの幅と高さを知る必要があり、この1次元のリストから2次元のマップを生成するためです。

マップの表現は以下のコードに似ています。このコードを元にゲームを開始したときの様子が想像できますか?

class Sokoban:

    ## ソコバンゲームを初期化する
    def __init__(self):
        ## マップを設定する
        self.level = list(
            "----#####----------"
            "----#---#----------"
            "----#$--#----------"
            "--###--$##---------"
            "--#--$-$-#---------"
            "###-#-##-#---######"
            "#---#-##-#####--..#"
            "#-$--$----------..#"
            "#####-###-#@##--..#"
            "----#-----#########"
            "----#######--------")

        ## マップの幅と高さと、マップ内のプレイヤーの位置(マップリスト内のインデックス値)を設定する
        ## 合計19列
        self.w = 19

        ## 合計11行
        self.h = 11

        ## プレイヤーの初期位置はself.level[163]にある
        self.man = 163

マップは文字リストを走査し、文字に基づいて対応する位置に異なる要素を表示することで表示されます。

表示は2次元であるため、幅と高さを使用して2次元表示領域内の各文字の位置を決定します。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))
✨ 解答を確認して練習

移動操作の実装

移動操作では、矢印キーを使って左、右、上、下の4方向に移動を制御します。移動方向を指定するために、4つの文字 'l'(左)、'r'(右)、'u'(上)、'd'(下)を使用します。

やり直し操作と移動操作に必要な処理は似ているため、ソコバンクラス内で内部関数 _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
✨ 解答を確認して練習

まとめ

このプロジェクトでは、倉庫番ゲームの基本機能のみを実装しました。この実験を基に、以下のようにコードを拡張することができます。

  1. 書かれたコードからマップデータを抽出し、ファイルに保存する方法を考える。
  2. マウス操作を実装して、キャラクターを迅速に特定の位置に移動させる。
  3. マップが解けるかどうかを自動的に判断するアルゴリズムを開発する。