Python Tkinterでテトリスを作ってみる

#0. はじめに#
　progateを終えたら何をしようか。「実際に何か作ってみたいが何を作ればいいかわからない」、「一人で0から作ることができるか不安」などの意見があるだろう。そこで、私がPythonの基礎を詰め込んだ後に実際に作成したテトリスについてまとめてみようと思う。作成当時に[YouTube](https://www.youtube.com/watch?v=PUmvTpNG8Tw)にソースコードを開示した。こちらも参照してほしいが、今回は作成当初より短くコードをまとめている。
　Pythonの基本文法が既に理解していることを前提にする。また、Tikinter文法については一緒にまとめる。

#1. 使用モジュール
`Tkinter`と`randm`ライブラリをインポートする。messageboxは`msgbox`、randintは`rnd`として呼び出せるようにする。

```python
from tkinter import *
from tkinter import messagebox as msgbox
from random import randint as rnd
```

#2. グローバル変数の定義
グローバル変数を定義する。関数を作成中に追加することもあるが、あらかじめ利用するとわかっている変数は定義しておく。なお色は16進数のカラーコードで定義する。

```python
vertical = 20     #基盤の縦のマス数
side = 10     #基盤の横のマス数
size = 30     #1マスの大きさ
mino_size = 4    #ミノの(縦横最大の)ブロック数
form = 0    #ミノの種類
mode = 0    #ミノの向き
y = -1     #ミノのy座標
x = 4     #ミノのx座標
speed = 500     #落下速度
#色の定義
colors = ["#00ffff", #I:0
          "#0000ff", #J:1
          "#ffa500", #L:2
          "#ffff00", #O:3
          "#008000", #S:4
          "#800080", #T:5
          "#ff0000", #Z:6
          "#404040"] #foundation:7
```

#3. 基盤の作成
基盤を作成する。何もないマスを7、壁を8として2次元のリストで基盤を表す。[YouTube](https://www.youtube.com/watch?v=PUmvTpNG8Tw)で図解しているから参照してほしい。

```python
foundation = [[7 for i in range(side + 2)] for j in range(vertical + 2)]
for i in range(vertical + 2):
    foundation[i][0], foundation[i][side + 1] = 8, 8
foundation[vertical + 1] = [8 for i in range(side + 2)]
```

次に、`draw_foundation()`関数を定義し基盤を描画する。`create_rectangle(x1, y1, x2, y2, fill="色")`で(x1, y1), (x2, y2)からなる四角形を描画することができる。

```python:draw_foundation()

def draw_foundation():
    for v in range(vertical):
        v1 = v * size
        v2 = v1 + size
        for s in range(side):
            s1 = s * size
            s2 = s1 + size
            for c in range(len(colors)):
                if foundation[v + 1][s + 1] == c:
                    color = colors[c]
                    cv.create_rectangle(s1, v1, s2, v2, fill=color)     #四角形を描画
```

#4. ミノの作成
ミノを作成する。4×4のマス内にミノのブロックがあるところのみ描画する。`mino_data`にミノの1マス1マスの座標を入力。`mino`には4×4のマスを用意し、mino_dataの座標のみ数値を変える。こちらも[YouTube](https://www.youtube.com/watch?v=VXox8RUnS-Y)で図解している。

```python

mino_data = [[[[2, 0], [2, 1], [2, 2], [2, 3]], #I:0
              [[0, 1], [1, 1], [2, 1], [3, 1]],
              [[1, 0], [1, 1], [1, 2], [1, 3]],
              [[0, 2], [1, 2], [2, 2], [3, 2]]],
             [[[1, 0], [2, 0], [2, 1], [2, 2]], #J:1
              [[1, 1], [1, 2], [2, 1], [3, 1]],
              [[2, 0], [2, 1], [2, 2], [3, 2]],
              [[1, 1], [2, 1], [3, 0], [3, 1]]],
             [[[1, 2], [2, 0], [2, 1], [2, 2]], #L:2
              [[1, 1], [2, 1], [3, 1], [3, 2]],
              [[2, 0], [2, 1], [2, 2], [3, 0]],
              [[1, 0], [1, 1], [2, 1], [3, 1]]],
             [[[1, 1], [1, 2], [2, 1], [2, 2]], #O:3
              [[1, 1], [1, 2], [2, 1], [2, 2]],
              [[1, 1], [1, 2], [2, 1], [2, 2]],
              [[1, 1], [1, 2], [2, 1], [2, 2]]],
             [[[1, 1], [1, 2], [2, 0], [2, 1]], #S:4
              [[1, 1], [2, 1], [2, 2], [3, 2]],
              [[2, 1], [2, 2], [3, 0], [3, 1]],
              [[1, 0], [2, 0], [2, 1], [3, 1]]],
             [[[1, 1], [2, 0], [2, 1], [2, 2]], #T:5
              [[1, 1], [2, 1], [2, 2], [3, 1]],
              [[2, 0], [2, 1], [2, 2], [3, 1]],
              [[1, 1], [2, 0], [2, 1], [3, 1]]],
             [[[1, 0], [1, 1], [2, 1], [2, 2]], #Z:6
              [[1, 2], [2, 1], [2, 2], [3, 1]],
              [[2, 0], [2, 1], [3, 1], [3, 2]],
              [[1, 1], [2, 0], [2, 1], [3, 0]]]]
mino = [[7 for i in range(mino_size)] for j in range(mino_size)]
```

次に`create_mino()`関数を定義しランダムにミノを生成する。`randint`で0~6までランダムに数字を選び、ミノの種類を指定、mino_dataから座標を取り出す。

```python:create_mino()
def create_mino():
    global form, mino
    form = rnd(0, 6)
    for i in range(len(mino_data[form][mode % 4])):
        y = mino_data[form][mode % 4][i][0]
        x = mino_data[form][mode % 4][i][1]
        mino[y][x] = form
```
ミノを描画する。`mino`のマスが`form`なら描画する。それ以外は描画しない。これによってミノのブロック部分のみを描画することができる。

```python:draw_mino()
def draw_mino():
    for v in range(mino_size):
        v1 = (v + y - 1) * size
        v2 = v1 + size
        for s in range(mino_size):
            s1 = (s + x - 1) * size
            s2 = s1 + size
            if mino[v][s] == form:
                cv.create_rectangle(s1, v1, s2, v2, fill = colors[form])
```

#5. メイン関数を定義
メイン関数を定義して実際にデバッグしてみる。`Tk()`でウィンドウを作成、`Cancas("ウィンドウ", width="幅", height="高さ")`でキャンバスを作成することができる。また、`pack()`でオブジェクトを配置するオプションを指定する。`mainloop()`はプログラムが終了するまでループさせる。

```python:main()
def main():
    global win, cv
    win = Tk()     #ウィンドウの作成
    cv = Canvas(win, width=side*size, height=vertical * size)     #キャンバスの作成
    cv.pack()    #オブジェクト配置のオプション
    create_mino()
    draw_foundation()
    draw_mino()
    win.mainloop()
```

定義した`main()`関数を呼び出す。

```python

if __name__ == "__main__":
    main()
````

デバッグしてみよう。基盤が描画され、その上部中央にミノが描画されていたらここまでは完璧だ。ミノは上部が画面外にはみ出すように描画している。

#6. ミノの移動と回転
ミノの移動と回転はクラスで定義する。ミノを1マス移動後、あるいは回転後の基盤の状態を調べ、何もなければ実行、壁や他のミノなどがあれば実行しないようにする。
まずはミノの座標と基盤の状態を保存しておくglobal変数を追加定義する。

```python
y_data = [0, 0, 0, 0]
x_data = [0, 0, 0, 0]
foundation_data = [7, 7, 7, 7]
```

次にクラスを作成する。インスタンス変数にはx移動、y移動、方向を引数として受け取り定義する。

```python:Move_mino
class Move_mino:
    def __init__(self, next_y, next_x, next_mode):
        self.next_y = next_y
        self.next_x = next_x
        self.next_mode = next_mode
```

次に動作後の基盤状況を調べるインスタンスメソッドを作成する。現在のミノの座標から実行後にミノの位置する基盤の状態を抽出する。

```python:Move_mino.reference
def reference(self):
    global x_data, y_data, foundation_data
    for i in range(len(mino_data[form][mode % 4])):
        y_data[i] = mino_data[form][(mode + self.next_mode) % 4][i][0] + y
        x_data[i] = mino_data[form][(mode + self.next_mode) % 4][i][1] + x
        foundation_data[i] = foundation[y_data[i] + self.next_y][x_data[i] + self.next_x]
```

抽出した基盤データを元に次のマスにブロックがなければ移動する関数`move_mino()`を定義する。キーボード処理を行う関数には引数として`e(Event)`を渡す必要がある。また、`delete("all")`でキャンバスの描画をすべて消すことができる。これを行わないと上書きされ続け処理が重くなってしまう。

```python:Move_mino.move_mino()
def move_mino(self, e):
    global x, y
    self.reference()
    if foundation_data == [7, 7, 7, 7]:
        y += 1 * self.next_y
        x += 1 * self.next_x
    cv.delete("all")     #キャンバス描画の消去
    draw_foundation()
    draw_mino()
```

上記の関数を用いてミノが落下する関数`drop_ mino()`を定義する。落下スピードが200より大きい場合は落下速度を早くする。`after("ミリ秒", "関数")`は指定ミリ秒後に関数を呼び出すことができる。また、キーボードで操作する関数を呼び出しているので、引数に`Event`を渡す必要がある。

```python:Move_mino.drop_mino()
def drop_mino(self):
    global speed
    self.move_mino(Event)
    if speed > 200:
        speed -= 1
    win.after(speed, self.drop_mino)
```

ミノがy軸方向に進行不可能な場合の処理を追加する。ミノを基盤情報に追加し、新しいミノを作成する。

```python:Move_mino.move_mino()
global foundation, mino, mode     #グローバル変数を追加

        x += 1 * self.next_x     #記載済み
    if self.next_y == 1 and foundation_data != [7, 7, 7, 7]:
        for i in range(len(y_data)):
            foundation[y_data[i]][x_data[i]] = form
        mode = 0
        y = -1
        x = 4
        mino = [[7 for i in range(mino_size)] for j in range(mino_size)]
        create_mino()
    draw_foundation()     #記載済み
```

回転後の基盤に何もなければ回転する関数`spin_mino()`を定義する。

```python:Move_mino.spin_mino()
def spin_mino(self, e):
    self.reference()
    global mode, mino
    if foundation_data == [7, 7, 7, 7]:
        mode += 1 * self.next_mode
        mino = [[7 for i in range(mino_size)] for j in range(mino_size)]
        for i in range(len(mino_data[form][mode % 4])):
            y = mino_data[form][mode % 4][i][0]
            x = mino_data[form][mode % 4][i][1]
            mino[y][x] = form
        cv.delete("all")
        draw_foundation()
        draw_mino()
```

ここまで出来たら一度動くか確かめてみよう。`main()`関数に以下を追加しデバッグしてみる。なお`main()`関数内に前記した`draw_foundation()`と`draw_mino()`は消しておく。

```python:main()
draw_foundation()     #削除
draw_mino()     #削除

left = Move_mino(0, -1, 0)
right = Move_mino(0, 1, 0)
under = Move_mino(1, 0, 0)
left_spin = Move_mino(0, 0, -1)
right_spin = Move_mino(0, 0, 1)

win.bind("<Left>", left.move_mino)     #左キーが押されたら左に移動
win.bind("<Right>", right.move_mino)     #右気ーが押されたら右に移動
win.bind("<Return>", under.move_mino)     #エンターキーが押されたら下に移動
win.bind("<Up>", right_spin.spin_mino)     #上キーが押されたら右回転
win.bind("<Down>", left_spin.spin_mino)     #下キーが押されたら左回転
under.drop_mino()
win.mainloop()     #記載済み
```

動いていたら成功だ。上記のbind`(""<Key>"", "関数")`を利用することにより指定した`Key`が押された際に関数を呼び出すことができる。

#7. 列消去とゲームオーバー判定
minoが停止した際に列がそろったら消す`delete()`関数を定義する。

```python:delete()
def delete():
    for v in range(len(foundation)):
        if (7 in foundation[v]) == False and foundation[v] != [8 for i in range(side + 2)]:
            del foundation[v]
            add_foundation = [7 for i in range(side + 2)]
            add_foundation[0], add_foundation[side + 1] = 8, 8
            foundation.insert(0, add_foundation)
```

次にミノが最上列を超えたらゲームを終了する`game_over()`関数を定義する。`msgboc.showinfo(message="メッセージ")`とすることでメッセージボックスを用いてメッセージを表示できる。

```python:game_over()
def game_over():
    top_foundation = [7 for i in range(side + 2)]
    top_foundation[0], top_foundation[side + 1] = 8, 8
    if foundation[1] != top_foundation:
        msgbox.showinfo(message = "Game Over")     #メッセージの表示
        quit()     #プログラムの終了
```

作成した`delete()`と`game_over()`をミノが停止した際の処理に追加する。

```python:Move_mino.move_mino()
    foundation[y_data[i]][x_data[i]] = form     #記載済み
delete()
game_over()
mode = 0     #記載済み
```

以上で完成。

#8. まとめ
　コーディングお疲れさまでした。これでテトリスの作成は終了です。長い記事になってしまいましたが最後まで読んでいただきありがとうございました。
　今後も作成したものを記事にまとめていきますので[フォロー]("https://qiita.com/1219mai0410")よろしくお願いします♪また、[YouTube]("https://www.youtube.com/channel/UChZ2R4JtzVeHMfG1OmRgZjw")と[Twitter]("https://twitter.com/1219mai0410")も更新してますのでご覧ください。
　それではまた、次の記事でお会いしましょう。