PythonでTETRISアルゴリズムを考える 1

PythonでTETRISアルゴリズムを考える 1

いろんなプログラミング言語で実現されているTETRISですが、Pythonで検索してもまともに動作するプログラムがなかったので、結局自分でほぼゼロから組み立てることになったので書いてみます。

このアルゴリズムで使うもの

  • numpy

ちなみに、作るのはアルゴリズムだけなので、表示形式とかがpygameとかpyqtとかの場合は、移植するという前提で考えてください。

手順

手順としては以下のように進めてゆきます。

  1. ボードの初期化
  2. テトリミノの初期化
  3. ボードを表示する
  4. テトリミノをランダムに生成する
  5. 左右・回転を追加する(この一部は次のページにあります
  6. テトリミノを保存する
  7. 終了条件を書く
  8. (ソースコードの全容)

1. ボードの初期化

ボードの初期化のために、ゼロ行列を作ります。

テトリスでは基本的に20行10列のマスを利用します。

なので、self.boardを20×10のゼロ行列にしましょう。

import numpy as np


class Tetris(object):

    def __init__(self):

        self.board_size = [20, 10]
        self.init_board()


    def init_board(self):

        self.board = np.zeros(self.board_size, dtype=np.int)

確認のためにこれを表示してみましょう。

if __name__ == '__main__':
    t = Tetris()
    print(t.board)
[[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 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]]

ちゃんとゼロ行列が表示されていますね。

2. テトリミノの初期化

テトリミノには上図のように7種類のパーツがあります。これらを最初に作っておきます。

ちなみにこの後にパーツを回転させる処理とかを楽にするために、回転動作と組み合わせて前段階で生成させておきます。

class Tetris(object):

    def __init__(self):

        self.board_size = [20, 10]
        self.init_t4mino()
        self.init_board()


    # ボードを初期化する
    def init_board(self):

        self.board = np.zeros(self.board_size, dtype=np.int)


    # テトリミノを初期化する
    def init_t4mino(self):
        
        # including rotation table
        self.t4mino_li = [
            # i
            [[[1, i] for i in range(4)],
             [[i, 1] for i in range(4)]] * 2,
            # o
            [[[0, 0], [0, 1], [1, 0], [1, 1]]] * 4,
            # t
            [[[0, 1]] + [[1, i] for i in range(3)],
             [[1, 2]] + [[i, 1] for i in range(3)],
             [[2, 1]] + [[1, i] for i in range(3)],
             [[1, 0]] + [[i, 1] for i in range(3)]],
            # j
            [[[0, 0]] + [[1, i] for i in range(3)],
             [[0, 2]] + [[i, 1] for i in range(3)],
             [[2, 2]] + [[1, i] for i in range(3)],
             [[2, 0]] + [[i, 1] for i in range(3)]],
            # l
            [[[0, 2]] + [[1, i] for i in range(3)],
             [[2, 2]] + [[i, 1] for i in range(3)],
             [[2, 0]] + [[1, i] for i in range(3)],
             [[0, 0]] + [[i, 1] for i in range(3)]],
            # s
            [[[0, 1], [0, 2], [1, 0], [1, 1]],
             [[0, 0], [1, 0], [1, 1], [2, 1]]] * 2,
            # t
            [[[0, 0], [0, 1], [1, 1], [1, 2]],
             [[0, 1], [1, 0], [1, 1], [2, 0]]] * 2
        ]

「インデックスで保存させておくのは見づらいからやめといて」と思うかもしれませんが、後々、回転させたり左右に移動させたりする際に必要となる「めり込み対策」を楽にするためには、この方法がベストなので、インデックスで保存させる方法を取りました。

3. ボードを表示する

ボードの表示をいちいちif __name__ == '__main__':で確認するのは面倒なので、ボードの表示に関して関数を組みます。

class Tetris(object):

    ...

    # ボードの表示
    def display(self):

        for i in range(self.board_size[0]):
            print('{:>2}|'.format(i), end='')
            for j in range(self.board_size[1]):
                tmp = self.board[i, j]
                print(' ' if tmp == 0 else tmp, end='')

            print('|')

        print('--+' + '-' * self.board_size[1] + '+')

self.boardの要素において、ゼロではない数字(これは後に積み上げたテトリミノになります)の場合はそのまま表示し、ゼロの場合は空白を表示することとします。

if __name__ == '__main__'をこのように書き換えます。

if __name__ == '__main__':
    
    t = Tetris()
    t.display()

結果はこのように出力されます。

 0|          |
 1|          |
 2|          |
 3|          |
 4|          |
 5|          |
 6|          |
 7|          |
 8|          |
 9|          |
10|          |
11|          |
12|          |
13|          |
14|          |
15|          |
16|          |
17|          |
18|          |
19|          |
--+----------+

ちゃんと表示されましたね。

4. テトリミノをランダムに生成する

次はテトリミノをランダムに生成してゆきます。

幸いなことに、numpyrandomモジュールではメルセンヌ・ツイスタ法という精度の良い乱数を生成することができるため、これを使います。

重要なのは、この後に必要になる現在のテトリミノがどこにあるのかという情報のためにself.ptがあったり、現在の回転情報がどうなっているのかというためにself.rotを宣言することです。

これがC++だったら、構造体とかを使ってもっとリーダブルになるのですが、それをPythonで実現しようとするとかえって複雑になるのでやめます。

class Tetris(object):...

    # テトリミノを生成する
    def gen_t4mino(self):

        self.cur = np.random.randint(7)
        # i, otjlst
        self.pt = [0, self.board_size[1] // 2 - (2 if self.cur == 0 else 1)]
        self.pt = [0, self.board_size[1] // 2 - 1]
        self.rot = 0

で、先ほどまであった__init__()display()を以下のように書き換えます。表示する要素を関数化させてdisplay()をすっきりさせるために、element(i, j)も新たに追加しました。

class Tetris(object):

    def __init__(self):

        self.board_size = [20, 10]
        self.init_t4mino()
        self.init_board()
        self.gen_t4mino()

    ...

    # ボードに表示する要素
    def element(self, i: int, j: int) -> str:

        ix_li = self.t4mino_li[self.cur][self.rot]
        pt = self.pt

        for ix in ix_li:
            if ix[0]+pt[0] == i and ix[1]+pt[1] == j:
                return self.cur

        tmp = self.board[i, j]

        return ' ' if tmp == 0 else tmp+1


    # ボードの表示
    def display(self):

        ix_li = self.t4mino_li[self.cur][self.rot]
        pt = self.pt
        for i in range(self.board_size[0]):
            print('{:>2}|'.format(i), end='')
            for j in range(self.board_size[1]):
                print(self.element(i, j), end='')

            print('|')

        print('--+' + '-' * self.board_size[1] + '+')

ここまで書けたら実行してみましょう!

 0|     66   |
 1|    66    |
 2|          |
 3|          |
 4|          |
 5|          |
 6|          |
 7|          |
 8|          |
 9|          |
10|          |
11|          |
12|          |
13|          |
14|          |
15|          |
16|          |
17|          |
18|          |
19|          |
--+----------+

(なお、ランダムにテトリミノを出現させているので、表示は多少異なります)

5. 左右・回転動作を追加する

ここまでできれば、あとは左右や回転の動作を追加するだけです!

まずはゲームのアルゴリズムとして、以下を踏まえる必要があります。

  1. hを入力したら左へ進み、lを入力したら右へ進み、aを入力したら左回転をし、fを入力したら右回転をする
  2. その他のキーが入力されたら、そのまま下へ進む
  3. 進んだ、もしくは回転した方向にブロックが壁がなければ、そのまま実行する
  4. 進んだ、もしくは回転した方向にブロックや壁があれば、実行しない
  5. これ以上落ちることができない場合はそこでブロックを止めて、新たなブロックを生成する
  6. 以上をひたすら繰り返す
  7. ゲームオーバーの条件を満たしたら終了

なお、入力するキーはvimやらnethackやらにしたがって、h: move left, l: move right, a: rotate left, f: rotate rightとしておきます。

まず、5. を実現するために、game()を宣言しましょう

class Tetris(object):...

    # ゲームのメイン部分
    def game(self):

        while True:

            self.display()
            key = input()
            self.move(key)

        print('END')

    ...

if __name__ == '__main__':
    
    t = Tetris()
    t.display()

次に、1. と2. を実現します。これは、先ほどself.move(key)とあったところから、move(key)で実装します。

ただし、pyqtやらを使っている人は、非同期でキーを取得できるため、ちょっと書き方は変わると思いますが、詳しくはググってください。

    # テトリミノの移動や回転をする
    def move(self, to: str=' ') -> bool:

        pt = self.pt
        # left
        if   to == 'h':
            q_li = [pt[0]  , pt[1]-1, self.rot]
        # right
        elif to == 'l':
            q_li = [pt[0]  , pt[1]+1, self.rot]
        # rotate right
        elif to == 'f':
            q_li = [pt[0]  , pt[1]  ,(self.rot+1)%4]
        # rotate left
        elif to == 'a':
            q_li = [pt[0]  , pt[1]  ,(self.rot-1)%4]
        # down
        else:
            q_li = [pt[0]+1, pt[1]  , self.rot]

        pt[0], pt[1], self.rot = q_li
        return True



ここまで書いたら実行してみましょう!

 0|    77    |
 1|     77   |
 2|          |
...
19|          |
--+----------+
h
 0|   77     |
 1|    77    |
 2|          |
...
19|          |
--+----------+
h
...
h
 0|77        |
 1| 77       |
 2|          |
...
19|          |
--+----------+
h
 0|7         |
 1|77        |
 2|          |
...
19|          |
--+----------+

これで実行してみるとわかると思いますが、hつまり左操作を5回(他のテトリミノでも近い回数)入力すると、テトリミノがのめり込んでしまうことがわかります。

これに対処するために、move()を以下のように書き換えます(次のページへ続く