LoginSignup
8
10

More than 3 years have passed since last update.

Pythonでゲーム作り:numpy,OpenCVを用いてテトリス(Tetris)を実装

Last updated at Posted at 2019-03-13

テトリスの実装

オセロの次はテトリスの実装に挑戦しました.(過去記事:オセロの実装)

当たり判定が少し残念なことになっていますが,それなりに楽しく遊ぶことができます.(Wikipedia:テトリス)

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

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

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

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

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

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

ドット画像の作成にはInkscapeを使いました.(ダウンロードサイト:Inkscape)

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

無題.png

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

無題2.png

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

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

# -*- 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に行列データが収納されます.

# -*- 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

# -*- 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)

8
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
10