初心者向け No.083

迷路ゲーム

キーボード操作でキャラクターをゴールへ導く迷路ゲーム。Canvas描画とキーイベント処理を組み合わせて実装。

🎯 難易度: ★★★ 📦 ライブラリ: tkinter(標準ライブラリ) ⏱️ 制作時間: 30〜90分

1. アプリ概要

キーボード操作でキャラクターをゴールへ導く迷路ゲーム。Canvas描画とキーイベント処理を組み合わせて実装。

このアプリはgamesカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter(標準ライブラリ)、難易度は ★★★ です。

このアプリは「ゲーム」カテゴリです。ゲーム実装は GUI イベント処理・状態管理・描画ループを同時に学べる教材で、画面・入力・ロジックの三層分離の感覚が掴めます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。

コピー&実行で動作確認 → コード解説で構造理解 → カスタマイズで応用、の 3 ステップで進めると、短時間で本質を押さえられます。

アプリを完成させた後は、自分の使い方に合わせて改造するのが学びを定着させる近道です。カスタマイズ章のアイデアを足がかりに、独自機能を一つ追加してみてください。

迷路ゲーム 実行画面(Windows)
実行画面(Windows)
迷路ゲーム 実行画面(Linux Mint)
実行画面(Linux Mint)

2. 機能一覧

  • 「新しい迷路」ボタン(new_game())で操作
  • messagebox.showinfo() によるダイアログ通知

3. 事前準備・環境

ℹ️
動作確認環境

Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)

Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は標準ライブラリの範囲で動作する想定ですが、未検証です。

  • Python 3.10 以上
  • OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)

4. 完全なソースコード

💡
コードのコピー方法

右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。

追加インストール不要(標準ライブラリのみ使用)
app083.py
import random
import tkinter as tk
from tkinter import messagebox, ttk


CELL = 28  # セルのピクセルサイズ


class App083:
    """迷路ゲーム"""

    def __init__(self, root):
        self.root = root
        self.root.title("迷路ゲーム")
        self.root.configure(bg="#f8f9fc")
        self.cols = 21
        self.rows = 15
        self.maze = []  # 1=壁, 0=通路
        self.player = (1, 1)
        self.goal = (self.cols - 2, self.rows - 2)
        self.steps = 0
        self.cleared = False
        self._build_ui()
        self.new_game()

    def _build_ui(self):
        """UI を構築する"""
        title = tk.Frame(self.root, bg="#3776ab", pady=10)
        title.pack(fill=tk.X)
        tk.Label(title, text="迷路ゲーム", font=("Noto Sans JP", 16, "bold"),
                 bg="#3776ab", fg="white").pack()

        ctrl = tk.Frame(self.root, bg="#f8f9fc")
        ctrl.pack(fill=tk.X, pady=6)
        ttk.Label(ctrl, text="サイズ:").pack(side=tk.LEFT, padx=(20, 4))
        self.size_var = tk.StringVar(value="中(21x15)")
        cb = ttk.Combobox(ctrl, textvariable=self.size_var, state="readonly", width=12,
                          values=["小(15x11)", "中(21x15)", "大(27x21)"])
        cb.pack(side=tk.LEFT)
        ttk.Button(ctrl, text="新しい迷路", command=self.new_game).pack(side=tk.LEFT, padx=10)
        self.status = tk.Label(ctrl, text="", bg="#f8f9fc",
                               font=("Noto Sans JP", 11))
        self.status.pack(side=tk.RIGHT, padx=20)

        self.canvas = tk.Canvas(self.root, bg="white", highlightthickness=0)
        self.canvas.pack(padx=10, pady=10)

        # キーバインド (矢印 / WASD)
        for k in ("Up", "Down", "Left", "Right", "w", "a", "s", "d", "W", "A", "S", "D"):
            self.root.bind(f"<{k}>", self._on_key)
        self.root.focus_set()

    def _set_size_from_combo(self):
        """コンボボックスからサイズを決定する"""
        v = self.size_var.get()
        if v.startswith("小"):
            self.cols, self.rows = 15, 11
        elif v.startswith("大"):
            self.cols, self.rows = 27, 21
        else:
            self.cols, self.rows = 21, 15

    def new_game(self):
        """新しい迷路を生成する"""
        self._set_size_from_combo()
        self.maze = self._generate_maze(self.cols, self.rows)
        self.player = (1, 1)
        self.goal = (self.cols - 2, self.rows - 2)
        self.steps = 0
        self.cleared = False
        self.canvas.config(width=self.cols * CELL, height=self.rows * CELL)
        self._draw()
        self.status.config(text="矢印キーで操作 / 歩数: 0", fg="#333")

    def _generate_maze(self, w, h):
        """深さ優先探索で迷路を生成する (奇数サイズ前提)"""
        if w % 2 == 0:
            w += 1
        if h % 2 == 0:
            h += 1
        self.cols, self.rows = w, h
        m = [[1] * w for _ in range(h)]

        def carve(x, y):
            m[y][x] = 0
            dirs = [(2, 0), (-2, 0), (0, 2), (0, -2)]
            random.shuffle(dirs)
            for dx, dy in dirs:
                nx, ny = x + dx, y + dy
                if 0 < nx < w - 1 and 0 < ny < h - 1 and m[ny][nx] == 1:
                    m[y + dy // 2][x + dx // 2] = 0
                    carve(nx, ny)

        carve(1, 1)
        m[h - 2][w - 2] = 0
        return m

    def _draw(self):
        """迷路を描画する"""
        self.canvas.delete("all")
        for y, row in enumerate(self.maze):
            for x, v in enumerate(row):
                x0, y0 = x * CELL, y * CELL
                color = "#1f2933" if v == 1 else "#f8f9fc"
                self.canvas.create_rectangle(x0, y0, x0 + CELL, y0 + CELL,
                                             fill=color, outline="")
        # ゴール
        gx, gy = self.goal
        self.canvas.create_rectangle(gx * CELL + 4, gy * CELL + 4,
                                     (gx + 1) * CELL - 4, (gy + 1) * CELL - 4,
                                     fill="#ffd43b", outline="")
        # プレイヤー
        px, py = self.player
        self.canvas.create_oval(px * CELL + 4, py * CELL + 4,
                                (px + 1) * CELL - 4, (py + 1) * CELL - 4,
                                fill="#3776ab", outline="")

    def _on_key(self, event):
        """キー入力でプレイヤーを移動する"""
        if self.cleared:
            return
        key = event.keysym.lower()
        dx, dy = 0, 0
        if key in ("up", "w"):
            dy = -1
        elif key in ("down", "s"):
            dy = 1
        elif key in ("left", "a"):
            dx = -1
        elif key in ("right", "d"):
            dx = 1
        if dx == 0 and dy == 0:
            return
        x, y = self.player
        nx, ny = x + dx, y + dy
        if 0 <= nx < self.cols and 0 <= ny < self.rows and self.maze[ny][nx] == 0:
            self.player = (nx, ny)
            self.steps += 1
            self._draw()
            self.status.config(text=f"歩数: {self.steps}")
            if self.player == self.goal:
                self.cleared = True
                self.status.config(text=f"クリア! 歩数: {self.steps}", fg="green")
                self.root.after(200,
                                lambda: messagebox.showinfo(
                                    "クリア!", f"{self.steps} 歩でゴールしました"))


if __name__ == "__main__":
    root = tk.Tk()
    app = App083(root)
    root.mainloop()

5. コード解説

迷路ゲームのコードを、実際に書かれている実装に沿って解説します。

クラス設計とコンストラクタ

App083 クラスにアプリの全機能をまとめています(メソッド7個・全152行)。__init__ ではタイトル「迷路ゲーム」 を設定します。あわせて状態を保持する変数(self.colsself.rowsself.stepsself.cleared)を初期化し、そのあと _build_ui()new_game() の順に呼び出して初期状態を作ります。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

ウィジェット構成

画面は tk.Frame×2・tk.Label×2・ttk.Labelttk.Comboboxttk.Buttontk.Canvas で構成しています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

イベント処理とボタンの接続

「新しい迷路」ボタン(new_game())を command= で接続しています。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

例外処理を書いていない理由

このコードには try-except による例外処理がありません。テキスト入力欄がなくボタン操作だけで完結し、ファイルやネットワークへのアクセスも無い設計のため、例外を投げうる処理(型変換・ファイル/通信)がそもそも無いからです。入力欄やファイル保存を追加する改造をするときは、例外処理もセットで検討しましょう。

※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。

6. ステップバイステップガイド

このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。

  1. 1
    ファイルを作成する

    新しいファイルを作成して app083.py と保存します。使うのは randomtkinter だけなので、追加インストールは不要です。

  2. 2
    クラスの骨格を作る

    App083 クラスを定義し、__init__ と、末尾の root = tk.Tk()mainloop() の最小構成を書いて、まず空のウィンドウが出ることを確認します。

  3. 3
    ウィンドウを設定する

    title("迷路ゲーム")configure(bg="#f8f9fc") をコンストラクタで設定します。

  4. 4
    画面部品を並べる

    _build_ui() の中で、tk.Frame×2・tk.Label×2・ttk.Labelttk.Comboboxttk.Buttontk.Canvas を作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。

  5. 5
    イベントを接続する

    「2. 機能一覧」で挙げた各ボタンを command= で対応するメソッド(new_game())につなぎます。

  6. 6
    中心になるメソッドを実装する

    アプリの本体である new_game()(11行)・_on_key()(29行)・_generate_maze()(22行) を実装します。

  7. 7
    動作確認する

    python app083.py で起動し、各ボタンが反応することを確認します。

7. カスタマイズアイデア

基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。

💡 ダークモードを追加する

bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。

💡 結果をファイルに保存する

このコードに保存処理はないため、表示中の数値や結果はアプリを閉じると消えます。テキストファイルへ書き出して、次回起動時に読み込む機能を追加してみましょう。

💡 設定ダイアログ

フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。

8. よくある問題と解決法

❌ 文字のフォントが崩れる・見た目が違う

原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。

解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。

9. 練習問題

アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。

  1. 課題1:迷路のサイズに「特大」を足す

    サイズ欄は小・中・大の3つだけです。もっと広い盤面で遊べるように、4つ目の「特大(33x25)」を足しましょう。

    期待結果:サイズ欄に「特大(33x25)」が増える。選んで「新しい迷路」を押すと、盤面が横33マス・縦25マスに広がる。

    合格条件

    • サイズ欄の選択肢が4つになる
    • 「特大(33x25)」で新しい迷路を作ると盤面が縦横とも広がる
    • 「中(21x15)」に戻すと元の大きさに戻る

    ヒント①(どこを触るか): 触るのは2か所です。_build_ui()values=["小(15x11)", "中(21x15)", "大(27x21)"] と、_set_size_from_combo() の分岐です。new_game() は先頭で _set_size_from_combo() を呼ぶので触りません。
    ヒント②(使うもの): 分岐は elif v.startswith("特大"): を作り、その中に self.cols, self.rows = 33, 25 と書きます。Canvas の大きさは new_game()self.canvas.config(width=self.cols * CELL, height=self.rows * CELL) が合わせてくれます。
    つまずきやすい点: values に足しただけでは大きさが変わらず、エラーも出ません。_set_size_from_combo() が見ているのは startswith("小")startswith("大") だけで、「特大(33x25)」は先頭が「特」なのでどちらにも当たらず else の21×15になるからです。CELL は28なので特大の Canvas は 924×700 ピクセルになり、画面の狭いノートPCでは縦がはみ出ることがあります。

  2. 課題2:ゴールまでの残りマス数をステータスに出す

    いまのステータス欄は歩数だけです。ゴールまであと何マスかを一緒に出して、近づいている実感が持てるようにしましょう。

    期待結果:「中(21x15)」で新しい迷路を作ると、ステータスが「矢印キーで操作 / 歩数: 0 / 残り 30」になる。ゴールに近づく向きへ1歩動かすと「残り 29」に減る。

    合格条件

    • 迷路を作った直後は 小=20・中=30・大=42 と出る
    • ゴールの1つ手前で「残り 1」になる
    • 壁にぶつかって進めなかったときは数が変わらない

    ヒント①(どこを触るか): 触るのは2か所です。new_game()self.status.config(text="矢印キーで操作 / 歩数: 0", fg="#333") と、_on_key()self.status.config(text=f"歩数: {self.steps}") です。
    ヒント②(使うもの): self.playerself.goal はどちらも (x, y) のタプルです。gx, gy = self.goal のように展開し、abs(px - gx) + abs(py - gy) で残りマス数を求めます。
    つまずきやすい点: ゴールに着いた瞬間の「残り 0」は画面に残りません。歩数を書いた直後にクリア判定が走り、self.status.config(text=f"クリア! 歩数: {self.steps}", fg="green") が同じラベルを上書きするからです。またこの計算はまっすぐ進んだときの距離なので、壁を避ける実際の歩数はこれより多くなります。

  3. 課題3:保存機能の追加

    処理結果をファイルに保存する機能を追加しましょう。

🚀
次に挑戦するアプリ

このアプリをマスターしたら、次のアプリに挑戦しましょう。

🐛
エラーが出て動かないときは

写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。

📖
独学を1冊で体系化するなら

写経しながら「なぜこう書くのか」が気になり始めたら、入門書を1冊通して読むと断片的な知識がつながります。Python入門書のおすすめ2冊(当サイトの参考書ランキング総合1〜2位)で、独学者向けの最初の1冊を比較しています。