PySimpleGui (Python GUI) でスライディングパズル(15パズル)作成

Python

はじめに

PySimpleGUIは、Python用のGUIライブラリです。

このライブラリを使用したサンプルとして、スライディングパズル(15パズル)を作成しました。Python の GUI プログラミングの参考になればと思います。

インストールと実行方法

ダウンロード

ソースは、https://github.com/tostos5963/PySimpleGUI-Sliding15Puzzle で公開していますので以下のどちらかの手順でダウンロードしてください。

git clone

git clone https://github.com/tostos5963/PySimpleGUI-Sliding15Puzzle.git

URL部は github のサイトで、①[Code]押下 → ②URL コピー を押下したらクリップボードにコピーできます。

PySimpleGUI-Sliding15Puzzle の git clone 用 URLコピー

ZIPファイル

①[Code]押下 → ② [Download ZIP]押下で、ZIPファイルをダウンロードできます。ダウンロードしたら任意のフォルダに展開してください。

PySimpleGUI-Sliding15Puzzle の ZIP ファイルダウンロード

Windowsの場合

pip install pysimplegui
python Sliding15Puzzle.py

Linux (ubuntu)の場合

pip3 install pysimplegui
python3 Sliding15Puzzle.py

以下のエラーが出た場合

ModuleNotFoundError: No module named 'tkinter'

python3-tk をインストールしてください

sudo apt-get install python3-tk

Macの場合

すみません。動作すると思いますがMacを所持していないため確認できていません。

ソースコード

主な処理の説明です。

初期処理

class Sliding15Puzzle():
    def __init__(self):
        # 0 - 15 をシャッフル
        self.board = [(no+1) for no in range(15)]
        self.board.append(0)
        self.shuffle()

        # 数字ボタンを作成し、二次元配列(4行 x 4列)に保持
        self.no_buttons = []
        for row in range(4):
            row_btns = []
            for col in range(4):
                # ボタンの数字を取得(シャッフルした board の値を取り出す)
                no = self.board[row * 4 + col]

                # 数字ボタンを作成。キーはタプル(行,列)
                no_button = sg.Button(image_filename = self.image_fname(no), key=(row, col))

                # 作成したボタンを1行用のリストに追加
                row_btns.append(no_button)

            # 1行の数字ボタン(4つ)を、二次元配列(4 x 4)に追加
            self.no_buttons.append(row_btns)

        # フレーム(数字ボタン(4 x 4))と「終了」ボタンでレイアウト作成
        self.layout = [ [sg.Frame('', self.no_buttons)] ] + [ [sg.Button('終了', expand_x=True)] ]

        # ウィンドウ作成
        self.window = sg.Window('スライディングパズル', self.layout)

images は、ボタンに表示する画像ファイル名のリストです。スペースボタン用と、1~15の数字ボタン用の画像ファイル名を設定しています。

board は、0~15 の数を保持しているリストです。0はスペース、1~15は各数字ボタンに対応します。リスト作成後、後述する shuffle() を呼び出します。

no_buttons は、PySimpleGUI の Button を保持する二次元配列(4 x 4)です。スペースボタンと 1~15の数字ボタンをboardの順番(シャッフルした順番)で作成し保持しています。Buttonの Key はボタン位置(行, 列)にしています。

layout はウィンドウに表示する エレメント(ウィジェット)のリストです。Frame(no_buttons) と 終了ボタンを保持してします。

window は、PySimpleGUIのWindow です。タイトル名とlayoutを設定しています。

シャッフル

    def shuffle(self):
        random.seed()

        # 0=スペースのidxを取得
        sp_idx = -1
        for idx in range(len(self.board)):
            if self.board[idx] == 0:
                sp_idx = idx
                break
        if sp_idx == -1:
            return

        # 「スペース」ボタンの(行, 列)
        sp_row, sp_col = divmod(sp_idx, 4)

        # 「スペース」を上下左右に動かす(移動回数はランダムに決める)
        n_loops = random.randrange(200, 300)
        for n in range(n_loops):
            target_idx = -1
            while target_idx == -1:
                # 移動方向(上下左右)をランダムに決める
                direction = random.randrange(0, 99) % 4
                target_row = sp_row
                target_col = sp_col

                if direction == 0:      # 上
                    if sp_row >= 1:
                        target_row = sp_row - 1
                        target_idx = target_row * 4 + target_col

                elif direction == 1:    # 下
                    if sp_row < 3:
                        target_row = sp_row + 1
                        target_idx = target_row * 4 + target_col

                elif direction == 2:    # 左
                    if sp_col >= 1:
                        target_col = sp_col - 1
                        target_idx = target_row * 4 + target_col

                elif direction == 3:    # 右
                    if sp_col < 3:
                        target_col = sp_col + 1
                        target_idx = target_row * 4 + target_col

                if target_idx != -1:
                    # 決定した方向(上下左右)に「スペース」ボタン動かすことが可能なら動かす
                    self.board[sp_idx] = self.board[target_idx]
                    self.board[target_idx] = 0
                    sp_row = target_row
                    sp_col = target_col
                    sp_idx = sp_row * 4 + sp_col

boardに保持している 0~15の数字をシャッフルする処理です。単純にシャッフルすると、パズルが解けない配置になることがあるため、「スペース」を上下左右にランダムに動かしてシャッフルします。

「スペース」を動かす回数は、200回~300回の範囲からランダムに決めています。

メインループ

    def main_loop(self):
        while True: # Event Loop
            event, values = self.window.read()
            if event in (None, '終了'):
                # 「終了」ボタンが押下されたらループを抜ける
                break

            # イベントがタプルであれば数字ボタンが押下されたと判断する
            if type(event) is tuple:
                self.touch(event)

        # アプリ終了
        self.window.close()

event, values = self.window.read() でイベントを読み取ります。

イベントが ‘終了’ だったらメインループを抜けてアプリを終了します。

イベントの型がタプルだったら数字orスペースボタンが押下された(イベントが(行, 列))と判断して、後述する touch() を呼び出します。

数字ボタン押下時に呼び出される処理

    # クリックした数字ボタンに隣接する「スペース」があれば入れ替え
    def touch(self, pos):
        no = self.pos2no(pos)
        if no == 0:
            # 「スペース」ボタン押下
            return

        # クリックした数字ボタンを「上」に移動
        if self.up_swap(pos):
            pass

        # クリックした数字ボタンを「下」に移動
        elif self.down_swap(pos):
            pass

        # クリックした数字ボタンを「左」に移動
        elif self.left_swap(pos):
            pass

        # クリックした数字ボタンを「右」に移動
        elif self.right_swap(pos):
            pass

        if self.is_complete():
            sg.popup('完成!')

引数posは、押下されたボタンの位置(行, 列)です。

pos2no(pos) はposに表示している数字を取り出す処理です。0だったらスペースボタンと判断して処理を抜けます。

up_swap(pos)は押下された数字ボタンの「上」がスペースだったら、数字ボタンとスペースボタンを入れ替えて結果(戻り値)Trueを返します。スペースでなければ何も行わず結果(戻り値)Falseを返します。

down_swap(pos)=下、left_swap(pos)=左、right_swap(pos)=右 は、方向が違いますが処理内容はup_swap(pos)と同じです。

is_complete()は、数字ボタンが1~15まで並んでいるかチェックします。並んでいたらポップアップで「完成!」を表示します。

「スペース」と「スペースに隣接する数字ボタン」を入れ替える処理

    # 「スペース」と「スペースに隣接する数字ボタン」を入れ替え
    def swap(self, my_pos, target_pos):
        ret = False
        sp_no = self.pos2no(target_pos)
        if sp_no == 0:
            sp_idx = self.pos2idx(target_pos)
            my_idx = self.pos2idx(my_pos)

            self.board[sp_idx] = self.board[my_idx]
            self.board[my_idx] = 0

            if self.window != None:
                self.window[my_pos].Update(image_filename = self.image_fname(0))
                self.window[target_pos].Update(image_filename = self.image_fname(self.board[sp_idx]))

            ret = True

        return ret

引数my_posは 押下した数字ボタンの位置(行, 列)です。引数target_posは隣接(上下左右)ボタンの位置(行, 列)です。

pos2no(target_pos)は、隣接(上下左右)ボタンの数字です。0(スペース)でなければ処理を抜けます。

pos2idx(pos) は位置(行, 列)から boardのインデックス(0~15)に変換します。変換したインデックスを使用して board の数字を入れ替えます。

window[KEY]は window内のエレメント(ウィジェット)の内、KEYが一致するものを返します。各ボタンのKEYは、初期化処理でボタン位置(行, 列)にしているため、my_pos と target_pos を渡せば対象の Button を取得できます。

取得したボタンの画像(数字、スペース)をUpdate()で入れ替えます。

まとめ

今回は、PySimpleGUIを使って、スライディングパズル(15パズル)を作成しました。Python用のGUIライブラリはいろいろありますが、その中でも PySimpleGUIはシンプルで使いやすいように感じました。

コメント

タイトルとURLをコピーしました