【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.pngwall.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() での全面再描画を避け、変更箇所だけ更新すると大型マップに強い。
  • 効果音・BGMpygame.mixer などを併用してステージクリア音を鳴らすと達成感が増します。
  • スマホ対応:タッチ操作用に <Button-1> イベントでタイルを指定し、道筋を自動生成する工夫も可能。

これで「一筆書きアプリ」完成です。ぜひ自分好みにチューンしてみてください。