
【6日でできるPython入門】一筆書きアプリを作ろう②
「一筆書きアプリを作ろう①」で骨格を組み立てた一筆書きアプリを、今回は 完成形 まで仕上げます。処理ごとのキモをコード片で示しつつ、最終版のフルソースと改造アイデアも紹介します。
全体の処理フロー(ゲームループ)
番号 | 状態 | 主な処理内容 |
---|---|---|
0 | ステージ開始 | stage_data() で地形と開始座標を用意 → draw_bg() で描画 |
1 | プレイ中 | move_pen() で入力反映 → count_tile() が 0 なら idx = 2 |
2 | クリア演出 | 6 秒間メッセージ表示 → 次ステージへ (またはゲームクリア) |
root.after(200, game_main)
で 0.2 秒間隔に再帰呼び出し。これがメインループです。

1.ペンの移動処理 move_pen()
def move_pen():
global ix, iy, key
bx, by = ix, iy # 元の座標
if key == "Left" and maze[iy][ix-1] in (0, 3): ix -= 1
if key == "Right" and maze[iy][ix+1] in (0, 3): ix += 1
if key == "Up" and maze[iy-1][ix] in (0, 3): iy -= 1
if key == "Down" and maze[iy+1][ix] in (0, 3): iy += 1
- 移動先が 未塗り 0 または ゴール 3 なら進行可
- 歩いた直後に
maze[iy][ix] = 2
で塗り済みに更新 - 描画は矩形+ペン画像を再配置し、キャンバスを最小限更新
ギブアップ (g
, G
, Shift
) もここで受け付け、即ステージを再読込できます。
2.ステージクリア判定
def count_tile():
return sum(1 for y in range(H) for x in range(W) if maze[y][x] in (0, 3))
count_tile()
が 0 になった瞬間、すべてのマスが 2(塗り済み)になったと判断。game_main()
内で
if count_tile() == 0:
txt = "STAGE CLEAR" if stage < 3 else "ALL STAGES CLEAR!"
...
idx, tmr = 2, 0
とメッセージを出して次の状態へ遷移します。
3.ゲームクリア判定
idx == 2 の 6 秒演出が終わると…
if stage < 3:
stage += 1
...
else:
if tkinter.messagebox.askyesno(
"Congratulations!", "全ステージクリア!\n最初から遊びますか?"):
stage = 1
...
else:
root.destroy()
3 面目をクリアしたらダイアログで続行可否を確認し、OK なら周回、キャンセルならアプリ終了。ここが ゲームクリア です。
4.プログラム(完全コード)
ゲーム画面イメージ

素材のダウンロード
以下のリンクから使用する素材をダウンロードできます。
pen.png | wall.png |
---|---|
![]() | ![]() |
ファイル名:maze_app.py
import os
import sys
import tkinter
import tkinter.messagebox
# ── 基本設定 ───────────────────────────
TS = 60
W, H = 12, 10
COL_FLOOR = "#2c3e50" # 未塗りタイル
COL_PAINT = "#00bcd4" # 塗り済みタイル
COL_TEXT = "#222" # ステージ文字
COL_WALL = "#888" # 壁
# ───────────────────────────────────
os.chdir(os.path.dirname(os.path.abspath(sys.argv[0])))
idx = 0
tmr = 0
stage = 1
ix = iy = 0
key = 0
def key_down(e):
global key
key = e.keysym
def key_up(e):
global key
key = 0
def stage_data():
"""ステージマップと開始位置を設定(開始マスは 2 = 塗り済み)"""
global ix, iy, maze
if stage == 1:
ix, iy = 1, 1
maze = [
[9,9,9,9,9,9,9,9,9,9,9,9],
[9,0,0,0,0,0,0,0,0,0,0,9],
[9,0,0,9,0,0,0,0,0,0,0,9],
[9,0,0,9,0,0,0,0,0,0,0,9],
[9,0,3,9,0,0,0,0,0,0,0,9],
[9,0,0,9,0,0,0,0,0,0,0,9],
[9,0,0,9,0,0,0,0,0,0,0,9],
[9,0,0,9,9,9,9,9,3,0,0,9],
[9,0,0,0,0,0,0,0,0,0,0,9],
[9,9,9,9,9,9,9,9,9,9,9,9],
]
elif stage == 2:
ix, iy = 1, 1
maze = [
[9,9,9,9,9,9,9,9,9,9,9,9],
[9,0,0,0,0,0,0,0,0,0,0,9],
[9,0,0,0,9,0,0,0,0,0,3,9],
[9,0,0,0,9,0,0,0,0,0,0,9],
[9,0,0,0,9,0,0,0,0,0,0,9],
[9,0,0,0,9,0,0,0,0,0,0,9],
[9,0,0,0,9,0,0,0,0,0,0,9],
[9,0,0,0,9,0,0,0,0,0,3,9],
[9,0,0,0,0,0,0,0,0,0,0,9],
[9,9,9,9,9,9,9,9,9,9,9,9]
]
elif stage == 3:
ix, iy = 1, 1
maze = [
[9,9,9,9,9,9,9,9,9,9,9,9],
[9,0,9,0,0,0,9,0,0,0,0,9],
[9,0,9,0,9,0,9,0,9,0,0,9],
[9,0,9,0,9,0,9,0,9,0,9,9],
[9,0,9,0,9,0,9,0,9,3,0,9],
[9,0,9,0,9,0,9,0,9,0,0,9],
[9,0,9,0,9,0,9,0,9,0,9,9],
[9,0,9,0,9,0,9,3,9,3,0,9],
[9,0,0,0,9,0,0,0,0,0,0,9],
[9,9,9,9,9,9,9,9,9,9,9,9],
]
else:
maze = [[9]*W for _ in range(H)]
# 開始マスを塗り済みにする
maze[iy][ix] = 2
def draw_bg():
"""マップ全体を描画"""
cvs.delete("BG")
cvs.delete("PEN")
for y in range(H):
for x in range(W):
gx, gy = TS*x, TS*y
v = maze[y][x]
if v == 0 or v == 3: # 未塗り
cvs.create_rectangle(gx, gy, gx+TS, gy+TS,
fill=COL_FLOOR, width=0, tag="BG")
if v == 2: # 塗り済み
cvs.create_rectangle(gx, gy, gx+TS, gy+TS,
fill=COL_PAINT, width=0, tag="BG")
if v == 9: # 壁
cvs.create_rectangle(gx, gy, gx+TS, gy+TS,
fill=COL_WALL, width=0, tag="BG")
cvs.create_image(gx+TS//2, gy+TS//2, image=wall, tag="BG")
if v == 3: # ゴール地点
cvs.create_oval(gx+15, gy+15, gx+TS-15, gy+TS-15,
fill="#e53935", width=0, tag="BG")
# ステージ番号
cvs.create_text(110, 30, text=f"STAGE {stage}",
fill=COL_TEXT, font=("Helvetica", 24, "bold"), tag="BG")
# ペン
gx, gy = TS*ix, TS*iy
cvs.create_image(gx+TS//2, gy+TS//2, image=pen, tag="PEN")
def move_pen():
"""方向キーでペンを移動し、踏んだマスを 2 にする"""
global ix, iy, key
bx, by = ix, iy
if key == "Left" and maze[iy][ix-1] in (0, 3): ix -= 1
if key == "Right" and maze[iy][ix+1] in (0, 3): ix += 1
if key == "Up" and maze[iy-1][ix] in (0, 3): iy -= 1
if key == "Down" and maze[iy+1][ix] in (0, 3): iy += 1
if (ix, iy) != (bx, by): # 移動したら現在マスを塗る
maze[iy][ix] = 2
gx, gy = TS*ix, TS*iy
cvs.create_rectangle(gx, gy, gx+TS, gy+TS,
fill=COL_PAINT, width=0, tag="BG")
cvs.delete("PEN")
cvs.create_image(gx+TS//2, gy+TS//2, image=pen, tag="PEN")
if key in ("g", "G", "Shift_L"): # ギブアップ
key = 0
if tkinter.messagebox.askyesno("ギブアップ", "このステージをやり直しますか?"):
stage_data()
draw_bg()
root.focus_force()
def count_tile():
"""未塗り (0) とゴール (3) を数える"""
return sum(1 for y in range(H) for x in range(W) if maze[y][x] in (0, 3))
def game_main():
"""ゲームのメインループ"""
global idx, tmr, stage
if idx == 0: # ステージ開始
stage_data()
draw_bg()
idx = 1
elif idx == 1: # プレイ中
move_pen()
if count_tile() == 0: # 全塗りでクリア
txt = "STAGE CLEAR" if stage < 3 else "ALL STAGES CLEAR!"
cvs.create_text(W*TS//2, H*TS//2,
text=txt, fill=COL_TEXT,
font=("Helvetica", 32, "bold"), tag="BG")
idx = 2
tmr = 0
elif idx == 2: # クリア演出
tmr += 1
if tmr == 30: # 0.2s×30 = 6 秒後
if stage < 3:
stage += 1
stage_data()
draw_bg()
idx = 1
else: # 全面クリア
if tkinter.messagebox.askyesno(
"Congratulations!", "全ステージクリア!\n最初から遊びますか?"):
stage = 1
stage_data()
draw_bg()
idx = 1
else:
root.destroy()
root.after(200, game_main)
# Tkinter 初期化
root = tkinter.Tk()
root.title("一筆書きアプリ")
root.resizable(False, False)
root.bind("<KeyPress>", key_down)
root.bind("<KeyRelease>", key_up)
cvs = tkinter.Canvas(root, width=W*TS, height=H*TS)
cvs.pack()
pen = tkinter.PhotoImage(file="pen.png")
wall = tkinter.PhotoImage(file="wall.png")
game_main()
root.mainloop()
5.改造のポイント
- ステージエディタ:二次元リストを手入力せず、CSV や JSON から読み込むと量産が楽になります。
- 移動アニメーション:1 マスを数フレームで滑らかに動かすと見栄えアップ。
Canvas.move()
を活用。 - パフォーマンス最適化:
draw_bg()
での全面再描画を避け、変更箇所だけ更新すると大型マップに強い。 - 効果音・BGM:
pygame.mixer
などを併用してステージクリア音を鳴らすと達成感が増します。 - スマホ対応:タッチ操作用に
<Button-1>
イベントでタイルを指定し、道筋を自動生成する工夫も可能。
これで「一筆書きアプリ」完成です。ぜひ自分好みにチューンしてみてください。