Pyxel 迷路ゲームの作成3.迷路の自動生成

Pyxel 迷路ゲームの作成(迷路の自動生成)

 Python向けレトロゲームエンジン「Pyxel(ピクセル)」を使ってのゲーム作成を通してプログラミングを学習します。

 次の2点を大きな目標にして,迷路ゲームを作成する例を紹介します。
 ・キャラクターが上下左右に歩くアニメーションを作る
 ・タイルマップの迷路を自動生成する

 前回の記事
 Pyxel 迷路ゲームの作成2.キャラクターの歩き動作 - 勉強ボックス管理者ブログ

 

参考サイト

 迷路の自動生成方法は,Ishida Soさんの自動生成迷路 のページを参考にしました。
 Samayou Oharikui トップページ
  > 雑学的な情報 の 迷路のプログラム
   > 迷路のウンチク
      棒倒し法や穴掘り法や壁延ばし法など、迷路の自動生成の方法の特性に関するメモ

 自動生成方法は複数ありますが,本記事では実装が簡単な棒倒し法を(少し変更して)使います。

 

自動生成の流れ

 今回のプログラムでは正解が一通りの迷路ではなく,つながった通路が多いある程度自由に動けるステージを作りたいと思います。
 

迷路作成の流れ

 

Stageクラスの作成

 本記事で作成するゲームのソースファイルとリソースファイルは下記を参照してください。
 Googleドライブ 迷路ゲーム03 - Google ドライブ
 最終版ソース game/pyxel/maze_random at main · benkyoubox/game · GitHub

 

リソースファイル

 リソースエディタで,イメージバンク1の(1,0)に壁タイルを作成します。
 

  

01 外周を壁にする

 迷路を自動生成するStageクラスを作ります。迷路生成メソッドを呼び出されたら,タイルマップを空きタイルで埋めて外周を壁タイルに設定するようにしてみましょう。

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)

stage.py

import pyxel

# 迷路作成用定義
TM = 0
IMG_NO = 1
TILE_BLANK = (0,0)
TILE_WALL = (1,0)

class Stage:
    ''' 迷路表示 '''

    def __init__(self):
        pyxel.tilemap(TM).refimg = IMG_NO
        return
        
    def initmaze(self):
        for yi in range(self.size):
            for xi in range(self.size):
                pyxel.tilemap(TM).pset(xi,yi,TILE_BLANK)
        return

    def makemaze(self,size):
        ''' 迷路作成 引数:size 一辺のタイル数 '''

        self.size = size
        self.initmaze()

        # 外周を壁にする
        for yi in range(size+1):
            pyxel.tilemap(TM).pset(0,yi,TILE_WALL)
            pyxel.tilemap(TM).pset(size,yi,TILE_WALL)
            if 0 == yi or size == yi:
                for xi in range(1,size):
                    pyxel.tilemap(TM).pset(xi,yi,TILE_WALL)    
        return

        
    def update(self):
        return
    
    def draw(self):
        pyxel.cls(1)
        pyxel.bltm(0,0, TM, 0,0, pyxel.width,pyxel.height, 0)
        return

# テストコード
if __name__ == '__main__':
    pyxel.init(256, 256)
    pyxel.load("alice.pyxres")
    stg = Stage()
    size = 24

    def update():
        if pyxel.btnp(pyxel.KEY_SPACE):
            stg.makemaze(size)

    def draw():
        stg.draw()
        pyxel.text(1,1,"Stage Test",7)

    pyxel.run(update,draw)

実行結果

・__init__()メソッド内のpyxel.tilemap(TM).refimg = IMG_NO
 タイルマップ0で表示するドット絵はイメージバンク1に指定します。(イメージバンク0はキャラクターで使用)

・initmaze()メソッド
 yi,xiはタイルの位置を扱うための変数として index の頭文字を付けてx座標等のドットと区別しています。
 2次元配列のデータは行数分のループの中で列数分のループを行う2重ループで処理することが多いです。

・makemaze()メソッド
 迷路の自動生成を行うメソッドですが,基準点の間隔(壁と通路の幅)を繰り返して最後に壁を置きたいので引数のsize+1で外周を作っています。(そのためsizeは基準点の間隔の倍数で指定する仕様にします)

・draw()メソッド
 Stageクラスの描画がゲーム画面の一番底の表示になる予定なので画面クリアも入れました。(Appクラスでクリアする作りにしても良いと思います)

・テストコードでスペースキーを押すとmakemaze()メソッドを呼び出すようにしました。

 

02 棒倒し法で壁を作る

 内側の壁を作る処理を追加します。

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)

stage.py抜粋

・・・
UNIT = 6 # 迷路1区画のサイズ

DIR_UP = 0
DIR_DOWN = 1
DIR_LEFT = 2
DIR_RIGHT = 3

def isblank(xi,yi,direction):
    ret = False
    if DIR_UP == direction:
        yi-=1
    elif DIR_DOWN == direction:
        yi+=1
    elif DIR_LEFT == direction:
        xi-=1
    elif DIR_RIGHT == direction:
        xi+=1
        
    if TILE_BLANK == pyxel.tilemap(TM).pget(xi,yi):
        ret = True
        
    return ret

・指定方向に壁がないことを確認するisblank()関数を定義します。
(2023-03-12記載修正 名前をckとしていた変数は処理内容がわかるようにdirectionに変更しました)

・上下左右の数値定義や壁タイルの定義をPlayerクラスとStageクラスとの両方のファイルに記述していますが,定義値だけのモジュールを作るなどして一カ所で管理できると良いかと思います。(大きなプログラムを作るときなどは,同じ記載があちこちにあると修正が大変になります)

class Stage:
    ''' 迷路表示 '''
・・・
    def makemaze(self,size):
・・・
        # 壁の生成
        r_min = DIR_UP
        for yi in range(UNIT,size,UNIT):
            if UNIT < yi:
                r_min = DIR_DOWN
            for xi in range(UNIT,size,UNIT):
                if pyxel.rndi(1,100) < 16:
                    continue
                while True:
                    direction = pyxel.rndi(r_min,DIR_RIGHT)
                    if isblank(xi,yi,direction) :
                        tmpx = xi
                        tmpy = yi
                        pyxel.tilemap(TM).pset(tmpx,tmpy,TILE_WALL)
                        for i in range(UNIT):
                            if DIR_UP == direction:
                                tmpy -= 1
                            elif DIR_DOWN == direction:
                                tmpy += 1
                            elif DIR_LEFT == direction:
                                tmpx -= 1
                            elif DIR_RIGHT == direction:
                                tmpx += 1
                            pyxel.tilemap(TM).pset(tmpx,tmpy,TILE_WALL)
                        break # while loop break

        return

・makemaze()メソッドに壁作成のループを追加します。
 外側の2重forループに使っているrange()ではとびとびの数値データを作成しています。
range(start, stop[, step])

>>> list(range(10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> list(range(0,10,2))
[0, 2, 4, 6, 8]
>>> list(range(6,24,6))
[6, 12, 18]

・if pyxel.rndi(1,100) < 16:
 15%の確率でcontinue文を実行します。(continue文はループ内の以降の処理をスキップして次のループへ進みます)

・pyxel.rndi(r_min,DIR_RIGHT)
 上下左右を乱数で決めますが,上から1段目の処理はr_minの値が0で,2段目以降はr_minの値が1になってDIR_UP 0 が選ばれないようにしています。

・tmp は(temporarily の意味で)その処理内だけで使うような臨時の変数などに使われることが多い名前です。
(2023-03-12記載修正 tmpの他に名前をwkとしていた変数は処理内容がわかるようにdirectionに変更しました)

・・・
# テストコード
if __name__ == '__main__':
    pyxel.init(512, 512, display_scale=1,capture_scale=1)
    pyxel.load("alice.pyxres")
    stg = Stage()
    size = 42
・・・

・動作確認のためアプリのサイズを広げています。

実行結果

 

03 ゴールの位置を決める

 迷路内のどこかにゴールを設定します。Stageクラスでゴール位置を保持するように変数を追加し,表示処理も追加します。

【ソースコード】はこちらのリンクから確認してください(Googleドキュメント)

stage.py抜粋

・・・
class Stage:
    ''' 迷路表示 '''

    def __init__(self):
        self.gx = 40
        self.gy = 32
        pyxel.tilemap(TM).refimg = IMG_NO
        return
・・・
    def makemaze(self,size):
・・・
        self.setgoal()
        return

・ゴールのx座標とy座標を追加

・makemaze()メソッドの最後に新規のsetgoal()メソッド呼び出しを追加

    def setgoal(self):
        ''' ゴール作成 '''
        while True:
            gxi = pyxel.rndi(1,self.size-1)
            gyi = pyxel.rndi(1,self.size-1)
            if (
                    TILE_BLANK == pyxel.tilemap(TM).pget(gxi,gyi)
                and TILE_BLANK == pyxel.tilemap(TM).pget(gxi+1,gyi)
                and TILE_BLANK == pyxel.tilemap(TM).pget(gxi,gyi+1)
                and TILE_BLANK == pyxel.tilemap(TM).pget(gxi+1,gyi+1)
                ):
                self.gx = gxi * 8
                self.gy = gyi * 8
                break;

        return

・setgoal()メソッド
 タイルの位置をランダムで決めて,4タイル分空いているか判定してから,ドットの座標にしてgxとgyに代入しています。

・・・
    def draw(self):
        pyxel.cls(1)
        pyxel.bltm(0,0, TM, 0,0, pyxel.width,pyxel.height, 0)
        # ゴール表示
        pyxel.blt(self.gx,self.gy, 0, 0,112, 16,16, 0)

        return
・・・

・ゴールにはイメージバンク0のタルトの絵を指定しました。

実行結果


 棒倒し法を利用した迷路の自動生成と,ゴールをランダムに決めることができました。
 次回の記事でゴール到着判定処理とスタート位置を決める処理を作ります。

次の記事
kinutani.hateblo.jp

関連記事

kinutani.hateblo.jp