初心者向け No.083

迷路ゲーム

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

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

1. アプリ概要

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

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

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

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

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

2. 機能一覧

  • 迷路ゲームのメイン機能
  • 直感的なGUIインターフェース
  • 入力値のバリデーション
  • エラーハンドリング
  • 結果の見やすい表示
  • クリア機能付き

3. 事前準備・環境

ℹ️
動作確認環境

Python 3.10 以上 / Windows・Mac・Linux すべて対応

以下の環境で動作確認しています。

  • Python 3.10 以上
  • OS: Windows 10/11・macOS 12+・Ubuntu 20.04+

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クラスにアプリの全機能をまとめています。__init__でウィンドウ設定、_build_ui()でUI構築、process()でメイン処理を担当します。責任の分離により、コードが読みやすくなります。

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

UIレイアウトの構築

LabelFrameで入力エリアと結果エリアを視覚的に分けています。pack()で縦に並べ、expand=Trueで結果エリアが画面いっぱいに広がるよう設定しています。

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

イベント処理

ボタンのcommand引数でクリックイベントを、bind('')でEnterキーイベントを処理します。どちらの操作でも同じprocess()が呼ばれ、コードの重複を避けられます。

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

Textウィジェットでの結果表示

tk.Textウィジェットをstate=DISABLED(読み取り専用)で作成し、更新時はNORMALに変更してinsert()で内容を書き込み、再びDISABLEDに戻します。

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

例外処理とエラーハンドリング

try-exceptでValueErrorとExceptionを捕捉し、messagebox.showerror()でエラーメッセージを表示します。予期しないエラーも処理することで、アプリの堅牢性が向上します。

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

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

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

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

    新しいファイルを作成して app083.py と保存します。

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

    App083クラスを定義し、__init__とmainloop()の最小構成を作ります。

  3. 3
    タイトルバーを作る

    Frameを使ってカラーバー付きのタイトルエリアを作ります。

  4. 4
    入力フォームを実装する

    LabelFrameとEntryウィジェットで入力エリアを作ります。

  5. 5
    処理ロジックを実装する

    _execute()メソッドにメインロジックを実装します。

  6. 6
    結果表示を実装する

    TextウィジェットかLabelに結果を表示する_show_result()を実装します。

  7. 7
    エラー処理を追加する

    try-exceptとmessageboxでエラーハンドリングを追加します。

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

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

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

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

💡 データの保存機能

処理結果をCSV・TXTファイルに保存する機能を追加しましょう。filedialog.asksaveasfilename()でファイル保存ダイアログが使えます。

💡 設定ダイアログ

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

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

❌ 日本語フォントが表示されない

原因:システムに日本語フォントが見つからない場合があります。

解決法:font引数を省略するかシステムに合ったフォントを指定してください。

❌ ライブラリのインポートエラー

原因:必要なライブラリがインストールされていません。

解決法:pip install コマンドで必要なライブラリをインストールしてください。

❌ ウィンドウサイズが合わない

原因:画面解像度や表示スケールによって異なる場合があります。

解決法:root.geometry()で適切なサイズに調整してください。

9. 練習問題

アプリの理解を深めるための練習問題です。

  1. 課題1:機能拡張

    迷路ゲームに新しい機能を1つ追加してみましょう。

  2. 課題2:UIの改善

    色・フォント・レイアウトを変更して、より使いやすいUIにカスタマイズしましょう。

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

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

🚀
次に挑戦するアプリ

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