Progateの学習を終えたプログラム初心者が【テトリス】を作ってみた

Progateの学習を終えて、Pythonの理解を深めるためにテトリスをつくってみました。
アウトプットで書き留めようと思います。

プログラム初学者で初めて組んだプログラムです。
分からなかったところは参考サイトを参考に自分なりに書きました。
どうしても書けなかったところは、参考サイトを参考に自分なりに考えながら模写しています汗。

#参考にさせてもらったサイト一覧
[だえうホームページ](https://daeudaeu.com/)
[SAMURAIENGINEER](https://www.sejuku.net/blog/category/programming-language/python/python-study)
[TKinterによる簡単なGUI](https://oku.edu.mie-u.ac.jp/~okumura/python/tkinter.html)
[kusakarism](https://kusakarism.info/category/programming/python/)

#環境
・python: 3.10.1

#####使用したライブラリ
・TKinter
・random
・messagebox

#初めにどういう感じで作るか決める。
・ゲームのフィールドは二次元配列で作成
・当たり判定は、配列中の数字で判定させる
・テトリスブロックは、1つのブロックを4つ並べたもの
・キー操作をTKinterを用いて、イベントとして受け取る。
・ブロックの回転は座標でみた時に、90°回転させた時の値を変数の中に入れ直す。
・テトリスブロックの落下はTKinterのafterメソッドを用いる
・横に一列積まれたらその列を消して上に積んであるブロックを下に落とす
・最上段までテトリスブロックが積まれたら、ゲーム終了。

>after メソッドは、処理を遅らせて実行するメソッドです。
[Tkinterの使い方：after で処理を「遅らせて」or 処理を「定期的」に実行する] (https://daeudaeu.com/tkinter_after/)

#実際に作ってみた

```tetris.py

import tkinter 
import random
from tkinter import messagebox

size = 30

app = tkinter.Tk()
app.title =("タイトル")
can = tkinter.Canvas(app, width= size*12, height = size*21,)
can.pack()

app.mainloop()
```

初めにTKinterを使用するためにimportします。
そしてTKinterのTkクラスのインスタンスを生成 = appに格納
canvasの作成(テトリスは横10マス、縦20マスのゲーム。壁を配置したいため左右1マス、プラスしています)
can.pack()にてCanvasを生成

#次に再帰的処理を書く
>再帰的処理とは
>自分自身を呼び出す処理が書かれている関数を呼び出すこと

```tetris.py

def game_loop():
    can.delete("all")
    draw_tetris()
    can.after(50, game_loop)
```

```tetris.py
app = tkinter.Tk()
app.title =("タイトル")
can = tkinter.Canvas(app, width= size*12, height = size*21,)
can.pack()

game_loop()   #追加
app.mainloop()
```

#ブロックの作成

```tetris.py

tetroI = [0,0, 0,-1, 0,1, 0,2]
tetroJ = [-1,1, 0,-1, 0,0, 0,1]
tetroL = [0,0, 0,-1, 1,1, 0,1]
tetroO = [0,0, 1,0, 0,-1, 1,-1,]
tetroS = [-1,0, 0,-1, 1,-1, 0,0]
tetroT = [-1,0, 0,0, 1,0, 0,1]
tetroZ = [-1,-1, 0,-1, 1,0, 0,0]
tetro  = [tetroI, tetroJ, tetroL, tetroO, tetroS, tetroT, tetroZ]
color = ["red", "yellow", "lime", "green", "blue", "navy", "fuchsia", "white", "black"]
type = random.randint(0,6)
```

座標で入力してブロックを作っています。　(テトリスブロックは、1つのブロックを4つ並べたもの)
tetroZを参考にすると
元の配列: [-1,-1, 0,-1, 1,0, 0,0]
解説：　    [x-1,y-1][x0,y-1][x1,y0][x0,y0]　中心を[x0y0]とした時の座標に計4つのブロックを置いています

y軸(マイナス方向)[x0,y-1]
　　　|
　　　|
-----　　-----x軸(プラス方向)[x1,y0]
　　　|
　　　|
y軸(プラス方向)[x0,y1]

# テトリスブロックの描写

```tetris.py

moveX = 5   # 開始位置(X座標)
moveY = 1   # 開始位置(Y座標)

def draw_tetris():
    i = 0
    while i < 4:
        x = (tetro[type] [ i * 2 ] + moveX) * size
        y = (tetro[type] [ i * 2 + 1 ] + moveY) * size
        can.create_rectangle( x, y, x+size, y+size, fill=color[type])
        i += 1
```
先程作成したブロックの配列からxとy座標を抜き出し、4つのブロックを描写しています。

>四角形を書き表すときは、create_rectangle (始点x,始点y,終点x,終点y)　で書き表すことができる。

#実際にキー操作を入れてテトリスブロックを動かしてみる

```tetris.py

app = tk.Tk()
can = tk.Canvas(app, width= size*12, height = size*21, bg="#E5E5E5" )
can.pack()

app.bind("<KeyPress>", keypress)   #追加
game_loop()

app.mainloop()
```

```tetris.py

def keypress(event):
    global moveX, moveY
    if event.keysym == "Left":
        moveX -= 1
    elif event.keysym == "Right":
        moveX += 1
    elif event.keysym == "Up":
        moveY -= 1
    elif event.keysym == "Down":
        moveY += 1
```

キー操作でmoveに加減してあげることで操作ができるように

#キャンバスにマス目を表示させる

```tetris.py

def game_field():
    i = 0
    j = 0
    while i < 22:
        field_y = i * size
        while j < 12:
            field_x = j * size 
            can.create_rectangle( 
                        field_x, field_y,   #始点
                        field_x + size, field_y + size, #終点
                        width=1, fill="white",outline="#ACACAC")   #オプション
            j += 1
        i += 1
        j = 0
```
一個一個のブロックを上から順に並べて作成しました。
もっと良い書き方がありそう。。。

先程書いた再帰処理にも書いてあげる

```tetris.py
def game_loop():
    can.delete("all")
    game_field()   #追加
    draw_tetris()
    can.after(50, game_loop)
```

#フィールドの作成

```tetris.py

defence_field = [
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,7,7,7,7,7,7,7,7,7,7,8],
    [8,8,8,8,8,8,8,8,8,8,8,8]
]
```
二次元配列でテトリスのフィールドに見立てて作成しました。
ちなみに7がブロックの動ける範囲（後ほど処理を書きます）
（colorの要素を配列から取得させたいために7と8という数字を用いました）

ついでにgame_fieldも変更しておきます

```tetris.py
def game_field():
    i = 0
    j = 0
    while i < 22:
        field_y = i * size
        j = 0
        while j < 12:
            field_x = j * size 
            can.create_rectangle( 
                        field_x, field_y,   #始点
                        field_x + size, field_y + size, #終点
                        width=1, fill=color[defence_field[i][j]],outline="#ACACAC")
            if defence_field[i][j] == 8:
                can.create_rectangle( 
                        field_x, field_y,   #始点
                        field_x + size, field_y + size, #終点
                        fill=color[8])   #オプション
                    
            j += 1
        i += 1
```

#当たり判定の作成

```tetris.py

def judge(x, y):
    global moveX, moveY
    result = True
    i = 0
    while i < 4:
        j = tetro[type] [i * 2] + x
        k = tetro[type] [i * 2 + 1] + y
        if defence_field[ k ] [ j ] != 7:
            result = False
        i += 1
    if result == True:
            moveX = x
            moveY = y
    return result
```
defence_fieldの配列内の"7"という数字以外の時に"False"を与えます。そして、移動させない為に数字を与えません。

また、キー操作にも変更を加えておきます

```tetris.py
def keypress(event):
    global moveX, moveY
    judge_x = moveX
    judge_y = moveY
    if event.keysym == "Left":
        judge_x -= 1
    elif event.keysym == "Right":
        judge_x += 1
    elif event.keysym == "Up":
        judge_y -= 1
    elif event.keysym == "Down":
        judge_y += 1
    judge(judge_x, judge_y)
```

#回転処理

```tetris.py
def keypress(event):
    global moveX, moveY
    judge_x = moveX
    judge_y = moveY
    rotation = []
    rotation.extend(tetro[type])
    if event.keysym == "Left":
        judge_x -= 1
    elif event.keysym == "Right":
        judge_x += 1
    elif event.keysym == "Down":
        judge_y += 1
    elif event.keysym == "space":
        rotation.clear()
        i = 0
        while i < 4:
            rotation.append(tetro[type][i*2+1]*(-1))
            rotation.append(tetro[type][i*2])
            i += 1
    judge(judge_x, judge_y, rotation)
```

judge関数にも変更を加えました

```tetris.py
def judge(x, y, rotation):
    global moveX, moveY
    result = True
    i = 0
    while i < 4:
        j = rotation [i * 2] + x
        k = rotation [i * 2 + 1] + y
        if defence_field[ k ] [ j ] != 7:
            result = False
        i += 1
    if result == True:
            moveX = x
            moveY = y
            tetro[type].clear()
            tetro[type].extend(rotation)
    return result
```

新たに rotation関数を加え、回転する処理を施しています

#テトリスブロックが落ちる処理を作る

```tetris.py
def drop_tetris():
    global moveX, moveY, type
    rotation = []
    rotation.extend(tetro[type])
    result = judge(moveX, moveY +1, rotation)
    if result == False:
        i = 0
        while i < 4:
            x = tetro[type][i*2] + moveX
            y = tetro[type][i*2+1] + moveY
            defence_field[y][x] = type
            i += 1
        delete_line()　#次で使います
        type = random.randint(0,6)
        moveX = 4
        moveY = 1
    can.after(1000, drop_tetris)
```

```tetris.py

app = tkinter.Tk()
can = tkinter.Canvas(app, width= size*12, height = size*21, bg="#E5E5E5" )
can.pack()
#---処理---

app.bind("<KeyPress>", keypress)

game_loop()
drop_tetris() #追加

#----END---
app.mainloop()
```

一秒ごとに1マス下がるという処理をafterメゾットを用いて作っています。

#一列揃ったら消す処理とゲームオーバーの処理

```tetris.py
def delete_line():
    i = 1
    while i < 21:
        if 7 not in defence_field[i]:
            j = 0
            while j < i:
                k = 0
                while k < 12:
                    defence_field[i-j][k] = defence_field[i-j-1][k]
                    k += 1
                j += 1
        i += 1

    i = 1  #GAMEOVER
    while i < 11:
        if 7 != defence_field[1][i]:
            messagebox.showinfo("information", "GAMEOVER")
            exit()
        i += 1
```

defence_fieldには自由に動ける値として7を与えています。
その7という数字が列で見た時にひとつもない時、列を上の列から持ってくるという処理にしました。

#まとめ

知識を定着させようと「テトリス」を作ってみよう！と思ったのですが、
とんでもなく難しかったです。
ですが、プログラムのどうやったら実装できるかという考え方や調べる能力。
他の人のコードを見て、テスト環境を作ってプログラムの動き方の深堀り。
エラーに苦しめられ何度もトライする精神。

とても時間がかかりましたが、いい勉強になったと思います。

以上、クソ記事ですがこれからレベルアップしてもっと質の良い記事を書くことができるよう誠心して行こうと思います。