Pythonでクリスマスツリー点灯ゲームを作った

# 作ったもの

![xmastree_s.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/470446/5f3d5104-c997-6b23-0517-2039ebd6a395.gif)
※ビデオは２倍速です。
電気の通り道をつなげて、クリスマスツリーの電球を点灯させるゲームをPythonで作りました。GUIはTkinterを使っています。
元ネタは、数年前にNORADのサンタ追跡Webサイトに出ていたゲームです。これが大好きでずーっとやっていたのを思い出して、最近勉強して使えるようになったPythonで作ってみることにしました。

# 環境

Python 3.6.8 on linux
依存パッケージ：PIL (Pillow 5.1.0)

# ソースコード
[GitHub](https://github.com/bluejenga/xmastree)で公開しています。以下ではポイントだけ紹介します。

# UI作成のポイント
## 画像の表示
PNG画像を表示するため、PILパッケージのImageモジュールとImageTkモジュールを使います。
はじめに、一通りの画像を`Image.open()`で読み込んでいます。
`resize()`しているのは、今後ラズパイのタッチディスプレイでも動かしたいと考えているので、表示サイズを柔軟に変えられるようにしているためです。

```app_tk.py
IMG_STAR_OFF = Image.open('image/star_off.png').resize(STAR_SIZE, Image.BILINEAR)
IMG_STAR_ON = Image.open('image/star_on.png').resize(STAR_SIZE, Image.BILINEAR)
```

読み込んだ画像は、Tkinterのcanvasに表示します。このときに`PhotoImage()`コンストラクタを使います。
PhotoImageオブジェクトは保持しておく必要があるようです。ローカル変数を使うと表示されませんでした。
`create_image()`の戻り値はcanvas上のオブジェクトのIDです。ゲーム中に画像を切り替えたり回転のアニメーションを表示したりするときに必要になります。

```app_tk.py
self.canvas = tk.Canvas(self, width=w, height=h)
self.star_img = ImageTk.PhotoImage(IMG_STAR_ON)
self.star_id = self.canvas.create_image(STAR_OFFSET_X,
                                        STAR_OFFSET_Y,
                                        image=self.star_img,
                                        anchor=tk.NW)
```

画像を変更するには、新たにPhotoImageを作成し、canvasの`itemcomfigure()`を使います。

```app_tk.py
if tree.is_complete():
    self.star_img = ImageTk.PhotoImage(IMG_STAR_ON)
else:
    self.star_img = ImageTk.PhotoImage(IMG_STAR_OFF)
self.canvas.itemconfigure(self.star_id, image=self.star_img)
```

## 回転アニメーション

Tkinterにはアニメーションメソッドのような便利なものはないので、自前で定期的に画像を更新してアニメーションを実現します。
定期的に処理を呼び出すには、Tkinterのwidget共通のメソッドである`after()`が使えます。

```app_tk.py
def rotate_cell(self, x, y):
    info = self.img_info[x][y]
    info.angle -= 15   # 15度回転
    info.photo_img = ImageTk.PhotoImage(info.img.rotate(info.angle))   # 回転したPhotoImage作成
    self.canvas.itemconfigure(info.id, image = info.photo_img)   # 画像を差し替え
    if info.angle % 90 == 0:
        # アニメーション完了
        # ゲームの状態更新処理など
    else:
        self.after(15, self.rotate_cell, x, y)   # 15ミリ秒後に再度画像を更新する
```

## canvas上にwidgetを配置

canvasの`create_window()`メソッドでwidgetを配置できます。複数のwidgetをまとめて扱いたい場合はFrameを使います。

```app_tk.py
frame = tk.Frame(self, bg='#e5f8cf', padx=5)
start = tk.Button(frame, text='Start', command=self.start_new_game,
                  fg='#345834', font=('', 22, 'bold'))
start.pack(side=tk.LEFT, padx=5, pady=10)
self.counter_text = tk.StringVar()
self.counter_text.set('00:00')
self.counter_label = tk.Label(frame, textvariable=self.counter_text,
                              bg='#e5f8cf', fg='#345834',
                              font=('', 22, 'bold'))
self.counter_label.pack(side=tk.LEFT, padx=5, pady=10)
self.canvas.create_window(20, 20, window=frame, anchor=tk.NW)
```

## canvas上のクリックイベント

`bind()`でコールバックメソッドを割り当てます。コールバックメソッドには引数でイベントが渡され、.x, .yでクリックイベントが発生した座標を取得できます。

```app_tk.py
self.canvas.bind('<ButtonRelease-1>', self.on_click_canvas)

def on_click_canvas(self, event):
    # event.x, event.yで座標を取得
    x = (event.x - TREE_OFFSET_X) // CELL_LENGTH
    y = (event.y - TREE_OFFSET_Y) // CELL_LENGTH
    # 処理
```

# 迷路のアルゴリズム

電気の通り道は、木の根元をスタートとする迷路として表現することができます。
迷路作成のアルゴリズムは調べればいろいろあるのでしょうけど、我流で作りました。簡単に書くと以下のようなアルゴリズムです。

方眼紙のマス目をつなげて、迷路の経路を作ると考えます。

1. スタートのマスを使用済みとしてマークします。
2. 使用済みのマスと、隣接する未使用のマスの組み合わせをすべてリストアップします。
3. リストの中からランダムに一つ選択し、選んだマス同士を通路でつなげ、選んだマスを使用済みとしてマークします。。
4. すべてのマスが使用済みになるまで、2-3を繰り返します。

迷路が出来上がっていく様子を動画にするとこんな感じです。

![tree_build.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/470446/4cb43ca9-3820-d5d1-ee42-7513275cbe3a.gif)


# おまけ
クリスマスっぽいBGMを流したいと思って、Pythonでmp3ファイルを再生する方法を調べたら、Pygameというのがあることを知りました。画像の表示とかも当然できるようなので、初めからこれを使えばよかったかも・・・