Pythonでゲーム作り：numpy，OpenCVを用いてテトリス（Tetris）を実装

#テトリスの実装

オセロの次はテトリスの実装に挑戦しました．(過去記事：[オセロの実装](https://qiita.com/phd_fire/items/e72d60ff51d7bbe7005c))

当たり判定が少し残念なことになっていますが，それなりに楽しく遊ぶことができます．(Wikipedia：[テトリス](https://ja.wikipedia.org/wiki/%E3%83%86%E3%83%88%E3%83%AA%E3%82%B9))

ゲーム画面はOpenCVで表示しています．

#画像データの作成と読み込み

まずはゲームウィンドウとテトリスのブロック（テトリミノ）の作成です．

コード内で数字の配列を記述すれば，それぞれ定義することは可能ですが

今回は画像データを作成して，OpenCVのimreadによって読み込む作戦を取ります．

画像データを読み込んだ後，「数学的な処理に関しては二値化された配列」を「描画に関しては生データ」を用いるという作戦です．

ドット画像の作成にはInkscapeを使いました．(ダウンロードサイト：[Inkscape](https://inkscape.org/ja/))

以下は作成した画像データの様子です．

![無題.png](https://qiita-image-store.s3.amazonaws.com/0/374843/1168d362-e82e-8d78-d910-ab3c451e7242.png)

ボードとテトリミノ用にそれぞれ別々のフォルダを作成して，pngファイルとして収納しておきます．

![無題2.png](https://qiita-image-store.s3.amazonaws.com/0/374843/de1dfe90-91b3-f803-c7ee-0a5828b96ece.png)

次に画像データの読み込みです．

フォルダ内のファイルを一括して読み込む際にはosもしくはglobをimportして，以下のような記述をすればよいです．

```python
# -*- coding: utf-8 -*-
"""
@author: phd_mech
"""

import os
import glob

file_list = os.listdir(path) # file_listにはフォルダ内のファイル名のみが収納される
file_list = glob.glob(path + '*.*') # file_listにはPathを含めたファイル名が収納される

```

ファイル名までアクセスすることができたので，次に各ファイルを画像データとして読み込む必要があります．

画像を行列データとして読み込みます．以下のようなread_imgという関数を作りました．

パスを含めたファイル名のリスト(file_list)と拡張子(extension)を選択してやれば，cv2.imreadの指示が通ってimg_listに行列データが収納されます．

```python
# -*- coding: utf-8 -*-
"""
@author: phd_mech
"""

import glob
import cv2

def read_img(file_list, extension):
    img_list = []
    for file_id in file_list:
        if extension in file_id:
            img = cv2.imread(file_id)
            img_list.append(img)
    return img_list

PATH       = os.getcwd() # GET THE CURRENT WORKING DIRECTORY
FILE_BOARD = glob.glob(PATH + '\\board\\' + '*.*')
FILE_MINO  = glob.glob(PATH + '\\tetrimino\\' + '*.*')
BOARD      = read_img(FILE_BOARD, '.png')[0]
MINO_LIST  = read_img(FILE_MINO, '.png')

```

#コード全体

後はrandom関数，time関数，条件分岐をうまいこと使ってやればテトリスの完成です．

![マイ-ムービー.gif](https://qiita-image-store.s3.amazonaws.com/0/374843/d2a2ea8b-5322-ace9-c46b-3572d071d451.gif)


```python
# -*- coding: utf-8 -*-
"""
@author: phd_mech
"""

import os
import glob
import random
import time
import numpy as np
import cv2

def read_img(file_list, extension):
    """
    save image files as np.array in list
    """
    img_list = []
    for file_id in file_list:
        if extension in file_id:
            img = cv2.imread(file_id)
            img_list.append(img)
    return img_list

def binary(img):
    """
    BGR => BINARY
    """
    gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    binary_img = cv2.threshold(gray_img, 1, 1, cv2.THRESH_BINARY)[1]
    return binary_img

def board_manual(board):
    """
    draw manual
    """
    x_0, y_0 = 276, 327
    x_1, y_1 = 242, 357
    x_2, y_2 = 276, 357
    x_3, y_3 = 312, 357
    x_4, y_4 = 242, 395
    x_5, y_5 = 257, 427
    x_6, y_6 = 294, 427
    cv2.rectangle(board, (x_0, y_0), (x_0+25, y_0-25), (127, 127, 127), -1)
    cv2.rectangle(board, (x_1, y_1), (x_1+25, y_1-25), (127, 127, 127), -1)
    cv2.rectangle(board, (x_2, y_2), (x_2+25, y_2-25), (127, 127, 127), -1)
    cv2.rectangle(board, (x_3, y_3), (x_3+25, y_3-25), (127, 127, 127), -1)
    cv2.rectangle(board, (x_4, y_4), (x_4+95, y_4-33), (127, 127, 127), -1)
    cv2.rectangle(board, (x_5, y_5), (x_5+25, y_5-25), (127, 127, 127), -1)
    cv2.rectangle(board, (x_6, y_6), (x_6+25, y_6-25), (127, 127, 127), -1)
    cv2.putText(board, 'W', (276, 325),
                cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2)
    cv2.putText(board, 'A S D', (245, 355),
                cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2)
    cv2.putText(board, 'Space', (243, 386),
                cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
    cv2.putText(board, 'Q P', (259, 425),
                cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)

def board_mino_future():
    """
    draw future mino
    """
    minos_frame = np.ones([22, 7, 3], dtype=np.uint8)*225
    minos_frame[1:-1, 1:-1] = np.zeros([20, 5, 3], dtype=np.uint8)
    for i in range(4):
        d_h = i*5
        mino_height = MINO_FUTURE[i].shape[0]
        mino_width = MINO_FUTURE[i].shape[1]
        minos_frame[2+d_h:2+d_h+mino_height, 2:2+mino_width] += MINO_FUTURE[i]
    return minos_frame

def ghost_position(x_pos, y_pos, board, board_new, mino):
    """
    draw ghost mino position
    """
    board_h = board.shape[0]
    mino_h = mino.shape[0]
    mino_w = mino.shape[1]
    board_bi = binary(board)
    mino_bi = binary(mino)
    for i in range(board_h - mino_h):
        ghost_check = np.copy(board_bi)
        ghost_check[y_pos+i:y_pos+mino_h+i, x_pos:x_pos+mino_w] += mino_bi
        if np.any(ghost_check > 1):
            y_bottom = i-1
            break
    board_ghost = np.copy(board_new)
    board_ghost[y_pos+y_bottom:y_pos+mino_h+y_bottom, x_pos:x_pos+mino_w, :] += mino//2
    return board_ghost

def board_show(board, script):
    """
    draw main board
    """
    cv2.namedWindow(WIN_NAME, cv2.WINDOW_AUTOSIZE)
    board_height = board.shape[0]
    board_width = board.shape[1]
    board[0, 4:-4] = 0
    # MAIN BOARD
    magnification = 20
    board_entire = np.zeros([board_height, board_width+10, 3], dtype=np.uint8)
    board_entire[:, 0:board_width] = board
    board_entire = cv2.resize(board_entire,
                              dsize=None,
                              fx=magnification,
                              fy=magnification,
                              interpolation=cv2.INTER_AREA)
    # FUTURE MINO
    magnification = 12
    x_pos, y_pos = 240, 0
    mino_future_frame = board_mino_future()
    mino_future_frame = cv2.resize(mino_future_frame,
                                   dsize=None,
                                   fx=magnification,
                                   fy=magnification,
                                   interpolation=cv2.INTER_AREA)
    mino_future_height = mino_future_frame.shape[0]
    mino_future_width = mino_future_frame.shape[1]
    board_entire[y_pos:y_pos+mino_future_height,
                 x_pos:x_pos+mino_future_width] = mino_future_frame
    # MANUAL
    board_manual(board_entire)
    # SCRIPT
    if script is not None:
        cv2.rectangle(board_entire, (20, 104), (219, 75), (0, 255, 255), -1)
        cv2.putText(board_entire, script, (35, 100),
                    cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
    magnification = 2
    board_entire = cv2.resize(board_entire,
                              dsize=None,
                              fx=magnification,
                              fy=magnification,
                              interpolation=cv2.INTER_AREA)
    cv2.putText(board_entire, '{} MODE'.format(MODE),
                (650, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
    cv2.putText(board_entire, 'Turn = {}'.format(TURN),
                (650, 60), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
    cv2.putText(board_entire, 'Score = {}'.format(SCORE),
                (650, 90), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 2)
    cv2.imshow(WIN_NAME, board_entire)

def key_shift(key, x_pos, y_pos):
    """
    key control 1
    """
    if key == 97:
        x_pos += -1
    elif key == 115:
        y_pos += 1
    elif key == 100:
        x_pos += 1
    else:
        pass
    return x_pos, y_pos

def key_rotate(key, mino):
    """
    key control 2
    """
    if key == 32:
        mino = np.rot90(mino, k=-1)
    if key == 119:
        mino = np.rot90(mino, k=1)
    else:
        pass
    return mino

def mino_motion(board, mino):
    """
    move mino
    """
    cv2.namedWindow(WIN_NAME, cv2.WINDOW_AUTOSIZE)
    x_pos, y_pos = 5, 1 # INITIAL MINO POSITION
    t_save = round(time.time(), 1) # ORIGIN TIME
    while True:
        # mino info
        mino_h = mino.shape[0]
        mino_w = mino.shape[1]
        mino_binary = binary(mino)
        # current board info
        board_new = np.copy(board)
        board_new[y_pos:y_pos+mino_h, x_pos:x_pos+mino_w, :] += mino
        # board for check
        board_1_binary = binary(np.copy(board))  # check mino motion
        board_2_binary = binary(np.copy(board))  # check hit bottom
        board_2_binary[y_pos+1:y_pos+mino_h+1, x_pos:x_pos+mino_w] += mino_binary
        # board for show
        board_ghost = ghost_position(x_pos, y_pos, board, board_new, mino)
        board_show(board_ghost, None)
        key = cv2.waitKey(1)
        # check free fall
        if not np.any(board_2_binary > 1):
            # if not hit bottom
            if round(time.time(), 1) - t_save > FALL_SPEED:
                # timer for free fall
                y_pos += 1
                t_save = round(time.time(), 1) # reset time
            else:
                pass
        else:
            # hit bottom
            return None, board_new
        # KEY EVENT
        # SHIFT AND DOWN
        if key in (97, 100, 115): # A, S, D
            x_new, y_new = key_shift(key, x_pos, y_pos)
            board_1_binary[y_new:y_new+mino_h, x_new:x_new+mino_w] += mino_binary
            if not np.any(board_1_binary > 1):
                # if mino does not overlap
                x_pos, y_pos = x_new, y_new
        # ROTATE
        if key in (32, 119): # SPACE, W
            mino_binary_rot = key_rotate(key, mino_binary)
            mino_rot_h = mino_binary_rot.shape[0]
            mino_rot_w = mino_binary_rot.shape[1]
            if board_1_binary[y_pos:y_pos+mino_rot_h,
                              x_pos:x_pos+mino_rot_w].size == mino_binary_rot.size:
                # if mino does not overlap
                board_1_binary[y_pos:y_pos+mino_rot_h,
                               x_pos:x_pos+mino_rot_w] += mino_binary_rot
                if not np.any(board_1_binary > 1):
                    # if mino does not overlap
                    mino = key_rotate(key, mino)
        # EXIT
        if key == 27: # Esc
            return key, board_new
        # PAUSE
        if key == 112: # P
            board_show(board_ghost, 'PAUSE GAME')
            cv2.waitKey(500)
            while True:
                board_show(board_ghost, 'SPACE KEY')
                key = cv2.waitKey(500)
                # RESTART
                if key in (32, 112): # Space, P
                    t_save = round(time.time(), 1)
                    break
                # EXIT
                if key == 27: # Esc
                    return key, board_new
                board_show(board_ghost, 'TO RESTART')
                key = cv2.waitKey(500)

def delete_line(board, score):
    """
    delete bottom lines and count score
    """
    wo_frame = board[1:-1, 1:-1, :]
    wo_frame_bi = binary(wo_frame)
    wo_frame_h = wo_frame.shape[0]
    wo_frame_new = []
    for i in range(wo_frame_h):
        if not np.all(wo_frame_bi[i, :] == 1):
            wo_frame_new.append(wo_frame[i, :, :])
    wo_frame_new = np.array(wo_frame_new)
    board_h = board.shape[0]
    wo_frame_new_h = wo_frame_new.shape[0]
    board[board_h-wo_frame_new_h-1:-1, 1:-1, :] = wo_frame_new
    delete_num = wo_frame_h - wo_frame_new_h
    if delete_num == 1:
        score += 5
    if delete_num == 2:
        score += 10
    if delete_num == 3:
        score += 20
    if delete_num == 4:
        score += 100
    return board, score

def select_difficulty(mode):
    """
    return fall speed from the selected mode
    """
    if mode == "EASY":
        speed = .8
    elif mode == "NORMAL":
        speed = .5
    elif mode == "HARD":
        speed = .3
    elif mode == "DEATH":
        speed = .01
    return speed

if __name__ == '__main__':
    # IMPORT BOARD AND TETRIMINO AS A IMAGE FILE
    PATH = os.getcwd()
    FILE_BOARD = glob.glob(PATH + '\\01_board\\' + '*.*')
    FILE_MINO = glob.glob(PATH + '\\02_tetrimino\\' + '*.*')
    BOARD = read_img(FILE_BOARD, '.png')[0]
    MINO_LIST = read_img(FILE_MINO, '.png')
    NUM_MINO = len(MINO_LIST)

    # PREPARE PARAMETERS
    WIN_NAME = 'GAME WINDOW'
    MODE = "DEATH"
    TURN = 0
    SCORE = 0
    FALL_SPEED = select_difficulty(MODE)
    cv2.namedWindow(WIN_NAME, cv2.WINDOW_AUTOSIZE)

    # PREPARE INITIAL MINO LIST RANDOMLY
    MINOS = []
    for loop in range(5):
        MINOS.append(MINO_LIST[random.randint(0, NUM_MINO-1)])

    # OPEN WINDOW
    while True:
        MINO_FUTURE = MINOS[TURN:TURN+4]
        board_show(BOARD, 'SPACE KEY')
        KEY = cv2.waitKey(500)
        # START
        if KEY == 32:
            break
        board_show(BOARD, 'START GAME')
        KEY = cv2.waitKey(500)
        if KEY == 32:
            break

    # GAME START
    while True:
        MINO = MINOS[TURN] # CURRENT MINO
        MINO_FUTURE = MINOS[TURN+1:TURN+5] # FUTURE MINO
        KEY, BOARD = mino_motion(BOARD, MINO) # DROP MINO
        # GAME OVER
        if np.any(BOARD[1, 4:-4, :] != 0):
            while True:
                board_show(BOARD, 'GAME OVER')
                KEY = cv2.waitKey(500)
                if KEY == 27:
                    board_show(BOARD, 'BYE (^.^)b')
                    cv2.waitKey(1500)
                    cv2.destroyAllWindows()
                    break
                board_show(BOARD, '(*_*)/Esc')
                KEY = cv2.waitKey(500)
            break
        # EXIT
        if KEY == 27:
            board_show(BOARD, 'BYE (^.^)b')
            cv2.waitKey(1500)
            cv2.destroyAllWindows()
            break
        # UPDATA BOARD
        TURN += 1
        BOARD, SCORE = delete_line(BOARD, SCORE)
        MINOS.append(MINO_LIST[random.randint(0, len(MINO_LIST)-1)])
        board_show(BOARD, None)
        cv2.waitKey(1)

```