迷路ゲーム
キーボード操作でキャラクターをゴールへ導く迷路ゲーム。Canvas描画とキーイベント処理を組み合わせて実装。
1. アプリ概要
キーボード操作でキャラクターをゴールへ導く迷路ゲーム。Canvas描画とキーイベント処理を組み合わせて実装。
このアプリはgamesカテゴリの実践的なPythonアプリです。使用ライブラリは tkinter(標準ライブラリ)、難易度は ★★★ です。
このアプリは「ゲーム」カテゴリです。ゲーム実装は GUI イベント処理・状態管理・描画ループを同時に学べる教材で、画面・入力・ロジックの三層分離の感覚が掴めます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。
コピー&実行で動作確認 → コード解説で構造理解 → カスタマイズで応用、の 3 ステップで進めると、短時間で本質を押さえられます。
アプリを完成させた後は、自分の使い方に合わせて改造するのが学びを定着させる近道です。カスタマイズ章のアイデアを足がかりに、独自機能を一つ追加してみてください。
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. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
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.cols・self.rows・self.steps・self.cleared)を初期化し、そのあと _build_ui() → new_game() の順に呼び出して初期状態を作ります。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×2・tk.Label×2・ttk.Label・ttk.Combobox・ttk.Button・tk.Canvas で構成しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「新しい迷路」ボタン(new_game())を command= で接続しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
例外処理を書いていない理由
このコードには try-except による例外処理がありません。テキスト入力欄がなくボタン操作だけで完結し、ファイルやネットワークへのアクセスも無い設計のため、例外を投げうる処理(型変換・ファイル/通信)がそもそも無いからです。入力欄やファイル保存を追加する改造をするときは、例外処理もセットで検討しましょう。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app083.py と保存します。使うのは
random・tkinterだけなので、追加インストールは不要です。 -
2クラスの骨格を作る
App083クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("迷路ゲーム")・configure(bg="#f8f9fc")をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×2・tk.Label×2・ttk.Label・ttk.Combobox・ttk.Button・tk.Canvasを作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(new_game())につなぎます。 -
6中心になるメソッドを実装する
アプリの本体である
new_game()(11行)・_on_key()(29行)・_generate_maze()(22行) を実装します。 -
7動作確認する
python app083.pyで起動し、各ボタンが反応することを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 結果をファイルに保存する
このコードに保存処理はないため、表示中の数値や結果はアプリを閉じると消えます。テキストファイルへ書き出して、次回起動時に読み込む機能を追加してみましょう。
💡 設定ダイアログ
フォントサイズや色などの設定をユーザーが変更できるオプションダイアログを追加しましょう。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。
-
課題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:ゴールまでの残りマス数をステータスに出す
いまのステータス欄は歩数だけです。ゴールまであと何マスかを一緒に出して、近づいている実感が持てるようにしましょう。
期待結果:「中(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.playerとself.goalはどちらも(x, y)のタプルです。gx, gy = self.goalのように展開し、abs(px - gx) + abs(py - gy)で残りマス数を求めます。
つまずきやすい点: ゴールに着いた瞬間の「残り 0」は画面に残りません。歩数を書いた直後にクリア判定が走り、self.status.config(text=f"クリア! 歩数: {self.steps}", fg="green")が同じラベルを上書きするからです。またこの計算はまっすぐ進んだときの距離なので、壁を避ける実際の歩数はこれより多くなります。 -
課題3:保存機能の追加
処理結果をファイルに保存する機能を追加しましょう。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
写経しながら「なぜこう書くのか」が気になり始めたら、入門書を1冊通して読むと断片的な知識がつながります。Python入門書のおすすめ2冊(当サイトの参考書ランキング総合1〜2位)で、独学者向けの最初の1冊を比較しています。