OpenCVとNumpyでテトリス

仕事でPythonを触ることになったので現在勉強してます

Numpyの独特な表記方法に慣れるために、OpenCVNumpyを使ったテトリスを作ってみることにしました。

新しい言語を覚えるときは取り敢えずテトリス作るやつ←

圧倒的に楽

作ってて感じたのは、Numpyの圧倒的な便利さでした。

テトリスとなると二次元配列か擬似的な1次元配列を用いてデータを管理するため、2重ループを多用して当たり判定やライン消しなどを行います。

Numpyには行列処理に特化したメソッドや表記法があり、多重ループをほとんど用いることなくソースを記述することが出来ました。

# 指定の行のブロックを消す処理
def EraseLine(row):
    vField = field[5:field_row + 5, 5:field_col + 5]    # 有効領域の取り出し
    vField[1:row+1, :] = vField[0:row,:]                # 指定行を削除し、それより上の行をずらす(上書き)
    vField[0, :] = 0                                    # 一番上に空行を追加  
    return 1

これはラインを消して、上の段を下げる処理ですが、たった5行で掛けてしまいます。

def ClearField():
    field[5:field_row + 5, 5:field_col + 5]=0
    return 1

フィールド上のブロックをまっさらにする処理。

ループすら使わず、行列の一部領域をくり抜いてまとめて0を代入できます。

cField = vField.copy()  # ライン判定用の配列をコピー作成
cField[vField>0] = True # 色番号をTrueに置き換える(1以上)

erase_cnt = 0           # 同時に消した行の数
row = cField.shape[0]   # フィールド行数を取得
for i in range(row):
    cLine = cField[i,:] # 1行ずつ取り出す
    if cLine.all():     # 行の値が全てTrueならば揃っている
        EraseLine(i)
        erase_cnt += 1

allメソッドは行列内の全ての要素がTrueか判定する関数

0以上の要素を全てTrueに置き換えて、1行ずつ取り出せば一発でラインが揃っているか判定できます。

全ソース

趣味プログラマなのでソースの汚さと英語力はお許しください(真っ先に言い訳)

import cv2
import numpy as np
import random

#グローバル変数の定義
game_title = r"Numpy Tetris"
wnd_width = 480      # ウィンドウ幅
wnd_height = 520     # ウィンドウの高さ

#global game_score
game_score = 0       # ゲームの得点
last_erase_cnt = 1   # 前回、揃えた段数(得点ボーナス用)
bomb_cnt_a = 3       # クリアボムの個数(Bキー)
bomb_cnt_b = 8       # チェンジボムの個数(Cキー)

block_size = 32      # ブロックの描画サイズ
block_x = 0          # 操作中ブロックのX座標
block_y = 0          # 操作中ブロックのY座標

field_col = 10       # フィールドの列数
field_row = 16       # フィールドの行数

color_ptn = [[255, 0, 0],
             [0, 255, 0],
             [0, 0, 255],
             [255, 255, 0],
             [0, 255, 255],
             [255, 0, 255],
             [128, 128, 0],
             [0, 128, 128],
             [128, 0, 128]] # ブロックのカラーパターン

global block_color
block_color = 0         # 現在のブロックの色、 color_ptnから番号指定

# フィールドブロックの初期化(左右下に5個の壁ブロックを置くことで場外判定を簡易化している)
field = np.zeros((field_row + 10, field_col + 10))
field[:, 0:5] = 1                               # 左の壁ブロック5列
field[:, field_col+5:field_col+10] = 1          # 右の壁ブロック5列
field[field_row+5:field_row+10,:] = 1           # 下の壁ブロック5列

# 描画用イメージの作成
def CreateBlankImage(width, height, color):
    img = np.zeros((height, width, 3), np.uint8)
    # ここに
    for h in range(0, height):
        for w in range(0, width):
            img[h, w] = color

    return img

# 操作用ブロックの作成(ptn: パターン番号1~7)
def MakeBlock(ptn):
    b = 255 # ブロックのID(足し算により当たり判定を行うために高い数値に設定する)
    if ptn == 0: # パターン0は使わない(0は空白を示すため)
        block = np.array([[0,0,0,0,0],
                          [0,0,0,0,0],
                          [0,0,0,0,0],
                          [0,0,0,0,0],
                          [0,0,0,0,0]])
    elif ptn == 1:
        block = np.array([[0,0,0,0,0],
                          [0,0,b,0,0],
                          [0,b,b,b,0],
                          [0,0,0,0,0],
                          [0,0,0,0,0]])
    elif ptn == 2:
        block = np.array([[0,0,0,0,0],
                          [0,b,0,0,0],
                          [0,b,b,b,0],
                          [0,0,0,0,0],
                          [0,0,0,0,0]])
    elif ptn == 3:
        block = np.array([[0,0,0,0,0],
                          [0,0,0,0,0],
                          [0,b,b,b,0],
                          [0,b,0,0,0],
                          [0,0,0,0,0]])
    elif ptn == 4:
        block = np.array([[0,0,0,0,0],
                          [0,0,b,b,0],
                          [0,0,b,b,0],
                          [0,0,0,0,0],
                          [0,0,0,0,0]])
    elif ptn == 5:
        block = np.array([[0,0,b,0,0],
                          [0,0,b,0,0],
                          [0,0,b,0,0],
                          [0,0,b,0,0],
                          [0,0,0,0,0]])
    elif ptn == 6:
        block = np.array([[0,0,0,0,0],
                          [0,b,b,0,0],
                          [0,0,b,b,0],
                          [0,0,0,0,0],
                          [0,0,0,0,0]])
    elif ptn == 7:
        block = np.array([[0,0,0,0,0],
                          [0,0,b,b,0],
                          [0,b,b,0,0],
                          [0,0,0,0,0],
                          [0,0,0,0,0]])

    global block_color
    block_color = ptn      # ブロックの形状と色パターンを同じIDにする
    return block


#フィールド上のブロックと操作ブロックの当たり判定
def IsHitField():
    # フィールドの配列をブロックサイズに切り取り、当たり判定を行う
    vField = field[block_y+5:block_y+10, block_x+5:block_x+10]

    # 操作ブロックとフィールドの加算を行い、255以上になれば当たっている(フィールドにはカラー番号が入っている)
    hit_block = vField + ope_block
    
    return np.any(hit_block > 255)

#操作中のブロックをフィールドに固定する
def PutBlock():
    vField = field[block_y+5:block_y+10, block_x+5:block_x+10] # フィールドの有効領域を切り取る
    block = vField + ope_block                                 # フィールドのブロックと操作中ブロックを合成する
    block[ope_block == 255] = block_color                      # 操作ブロックのIDは255なので、カラー番号に書き換える
    field[block_y+5:block_y+10, block_x+5:block_x+10] = block  # フィールドに書き込む
    return 1

# 描画処理を行う関数
def Draw(img, block : np.array):
    # 画面のクリア(黒で塗りつぶす)
    cv2.rectangle(img, (0, 0), (wnd_width, wnd_height), (0,0,0), thickness=-1)

    # フィールド枠の描画(枠線を描く)
    cv2.rectangle(img, (0, 0), (field_col * (block_size) + 2, field_row * (block_size) + 2), (255,255,255), thickness=2)

    # フィールドブロックの描画
    vField = field[5:field_row + 5, 5:field_col + 5]  # 壁を除いた有効領域を取り出す
    col = vField.shape[1]                             # 列数を取得
    row = vField.shape[0]                             # 行数を取得
    for r in range(row):                              # ブロックを1個ずつ描画 
        for c in range(col):
            ptn = int(vField[r, c])                   # カラー情報を取り出す
            if ptn != 0:
                b = block_size                        # 描画座標計算
                x = c * b
                y = r * b
                cv2.rectangle(img, (x + 2, y + 2), (x + b, y + b), color_ptn[ptn], thickness=-1)

    # 操作中ブロックの描画
    col = block.shape[1]                              # 列数を取得
    row = block.shape[0]                              # 行数を取得
    for r in range(row):
        for c in range(col):
            if block[r, c] != 0:
                b = block_size
                x = (c + block_x) * b
                y = (r + block_y) * b
                cv2.rectangle(img, (x + 2, y + 2), (x + b, y + b), color_ptn[block_color], thickness=-1)

    # 得点のテキスト描画
    cv2.putText(img, 'SCORE:' + str(game_score), (4, 20), cv2.FONT_HERSHEY_PLAIN, 1, (255, 0, 255))

    # ボム個数のテキスト描画
    cv2.putText(img, 'Change: ' + str(bomb_cnt_b), (4, 40), cv2.FONT_HERSHEY_PLAIN, 1, (255, 0, 255))
    cv2.putText(img, 'ClearBomb: ' + str(bomb_cnt_a), (4, 60), cv2.FONT_HERSHEY_PLAIN, 1, (255, 0, 255))

    # 操作説明欄のテキスト描画
    cv2.putText(img, 'W: Up ', (wnd_width -150, 40), cv2.FONT_HERSHEY_PLAIN, 1, (255, 0, 255))
    cv2.putText(img, 'A: Left', (wnd_width -150, 60), cv2.FONT_HERSHEY_PLAIN, 1, (255, 0, 255))
    cv2.putText(img, 'S: Down' , (wnd_width -150, 80), cv2.FONT_HERSHEY_PLAIN, 1, (255, 0, 255))
    cv2.putText(img, 'D: Right: ', (wnd_width -150, 100), cv2.FONT_HERSHEY_PLAIN, 1, (255, 0, 255))
    cv2.putText(img, 'R: Rotate', (wnd_width -150, 120), cv2.FONT_HERSHEY_PLAIN, 1, (255, 0, 255))
    cv2.putText(img, 'B: Bomb', (wnd_width -150, 140), cv2.FONT_HERSHEY_PLAIN, 1, (255, 0, 255))
    cv2.putText(img, 'C: Change', (wnd_width -150, 160), cv2.FONT_HERSHEY_PLAIN, 1, (255, 0, 255))

    return 0

# 指定の行のブロックを消す処理
def EraseLine(row):
    vField = field[5:field_row + 5, 5:field_col + 5]    # 有効領域の取り出し
    vField[1:row+1, :] = vField[0:row,:]                # 指定行を削除し、それより上の行をずらす(上書き)
    vField[0, :] = 0                                    # 一番上に空行を追加  
    return 1

# 列が揃っているかチェックし、消滅させる処理 + スコア計算
def CheckLine():
    global game_score, last_erase_cnt, bomb_cnt_a, bomb_cnt_b

    # 壁を除いたフィールド有効領域の取り出し
    vField = field[5:field_row + 5, 5:field_col + 5]

    # 判定簡易化のため0以外の値をTrueに置き換える
    cField = vField.copy()
    cField[vField>0] = True

    erase_cnt = 0           # 同時に消した行の数
    row = cField.shape[0]   # フィールド行数を取得
    for i in range(row):
        cLine = cField[i,:] # 1行ずつ取り出す
        if cLine.all():     # 行の値が全てTrueならば揃っている
            EraseLine(i)
            erase_cnt += 1

    # スコア加算
    if erase_cnt > 0:
        game_score += erase_cnt * erase_cnt * last_erase_cnt * 100   # 同時消し段数 × 二乗のスコア(前回の同時消し数により倍率UP)
        # 消した数に応じてボムボーナス
        bomb_cnt_b += erase_cnt         # ブロックチェンジボム を消した段数だけ追加
        #if erase_cnt == 4:
        #    bomb_cnt_a += 1            # 4段消しをした場合、全消しボムを1つ追加(イージーなのでやめる)
        last_erase_cnt = erase_cnt      # 前回の同時消し数として記録
    return 0

# フィールドのブロックを全て消去する
def ClearField():
    field[5:field_row + 5, 5:field_col + 5]=0
    return 1

# ゲームオーバー処理
def GameOver():
    global game_score, bomb_cnt_a, bomb_cnt_b
    game_score = 0      # スコアを0にする
    bomb_cnt_a = 3
    bomb_cnt_b = 8
    ClearField()        # フィールドをクリアする
    return 1

# --- メイン処理 ---

# 描画用のイメージを作成
img = CreateBlankImage(wnd_width, wnd_height, (0, 0, 0))

# 操作ブロックをランダムに作成
ptn = random.randint(1, 7)      # 新しいブロックのパターンをランダムに選ぶ
ope_block = MakeBlock(ptn)      # 新しいブロックを作成

down_frame = 30 # 落下までのフレームカウント数
cnt_frame = 0   # 現在のフレームカウント数

while(1):
    Draw(img, ope_block)            # 画面の描画
    cv2.imshow(game_title , img)    # 表示

    # キー判定による操作処理(自動落下はSキーと一緒に判定して処理を共有)
    key = cv2.waitKey(33)
    if key == ord("q"):
        break
    elif (key == ord("a")) and last_key != key:
        # Aキーを押したときの処理(左移動)
        block_x -=1
        if IsHitField():
            block_x += 1
    elif (key == ord("s") and last_key != key) or cnt_frame > down_frame:
        # Sキーを押したときの処理(下降) or 落下時間になったとき
        block_y += 1
        if IsHitField():
            block_y -=1                     # これ以上下がらないので戻す
            PutBlock()                      # ブロック固定処理
            ptn = random.randint(1, 7)      # 新しいブロックのパターンをランダムに選ぶ
            ope_block = MakeBlock(ptn)      # 新しいブロックを作成
            block_x = 4                     # ブロックの座標をリセット
            block_y = 0
            CheckLine()
            if IsHitField():                # ブロック生成時に当たっていたらゲームオーバー
                GameOver()
        cnt_frame = 0
    elif key == ord("d") and last_key != key:
        # Dキーを押したときの処理(右移動)
        block_x += 1
        if IsHitField():
            block_x -= 1
    elif key == ord("r") and last_key != key:
        # Rキーを押したときの処理(回転)
        ope_block = np.rot90(ope_block)
        if IsHitField():
            ope_block = np.rot90(ope_block,3)
    elif key == ord("b") and last_key != key:
        # Bキーを押したときの処理(フィールドクリア)
        if bomb_cnt_a >= 1:
            ClearField()
            bomb_cnt_a -= 1
    elif key == ord("c") and last_key != key:
        # Cキーを押したときの処理(ブロックチェンジ)
        if bomb_cnt_b >= 1:
            ptn = random.randint(1, 7)      # 新しいブロックのパターンをランダムに選ぶ
            ope_block = MakeBlock(ptn)      # 新しいブロックを作成
            block_x = 4                     # ブロックの座標をリセット
            block_y = 0
            bomb_cnt_b -= 1

  
    # 連射防止に最後に押したキーを記録する
    last_key = key
    cnt_frame += 1

特徴としては、テトリスにあるまじきボム機能搭載

連続で同時消しをすると高得点になるようなスコア計算をしています。

場外判定を当たり判定で済ませるために、左右下に5個の壁を配置したりと工夫をしています。

おわりに

朝8時から夕方6時まで黙々とソースを書いてましたが

1時間くらいは黙々とプレイしてました(自分の作ったゲームで中毒になるやつ)

Numpyの表記方法を初めて見たときはギョッとしましたが、この便利さを知ると手放せなくなりそうですね

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です