Python でのテトリスの作り方(サンプルスクリプト付き)

Pythonでのテトリスの作り方の解説ページアイキャッチ

Python 勉強がてらに Tkinter を使ってテトリスを作成してみました!

今回はこのテトリスの Python スクリプトの紹介と、このスクリプトの解説をしていきたいと思います。

偉そうに解説してますが、実はまだ Python の書き方やクラス設計に関してはあまり自信がないです。このあたりで気になる点がありましたら、コメントや Twitter(@da1e2u3) でアドバイスしていただけると幸いです。

動作確認環境

このページで紹介するサンプルスクリプトは下記環境で動作確認を行っています

  • OS:macOS Catalina
  • Python:3.8
  • Tkinter:8.6

作成するテトリス

まず作成したテトリスの仕様の説明です。

基本的にテトリスのルールはご存知という前提で、ここでは簡単に説明させていただきます。

今回作成したテトリスは下の図のものになります。

テトリスの画面

右側にある「START」ボタンをクリックすることでテトリスゲームが開始され、ブロックの落下が始まります。

テトリスの開始

左側はテトリスのゲーム画面で、10 x 20のフィールドが存在し、落下中のブロックはこのフィールド内を下記のキーボード操作により移動させる事ができます。

  • 左キーが押された時:ブロックが左に移動する
  • 右キーが押された時:ブロックが右に移動する
  • 下キーが押された時:ブロックが下に移動する

また、時間経過とともにブロックが下記のように自動的に移動します。

  • 1秒経過:ブロックが下に移動する

ブロックが落下する様子

ただし、ブロックは下記のような場所には移動できないように制御しています。

  • フィールド外にはみ出す
  • 他のブロックが既に存在する

ブロックが移動可能なフィールドの中で一番下に到達した際には、ブロックの削除(削除できる場合のみ)と新しいブロックの落下の開始が行われます。

ブロックの削除では、ブロックが落下する事で1行全てにブロックが存在するようになった行のブロックを全て削除します。削除されたブロックの上に積まれているブロックは、削除された行分下方向に移動します。

ブロックが消える様子

テトリスのブロックが積まれていき、新しいブロックの落下ができなくなった際にはテトリスゲームが終了します。

こんな感じで大体テトリスと同様の動きはしてくれると思います。ですが、下記の制限がありますので注意してください。

  • ブロックは4種類のみ
  • ブロックの回転は不可

テトリスの Python スクリプト

では続いて今回作成したテトリスの Python スクリプトの紹介をしていきたいと思います。下記がそのスクリプトになります。

tetris.py
# -*- coding:utf-8 -*-
import tkinter as tk
import random

# 定数
BLOCK_SIZE = 25  # ブロックの縦横サイズpx
FIELD_WIDTH = 10  # フィールドの幅
FIELD_HEIGHT = 20  # フィールドの高さ

MOVE_LEFT = 0  # 左にブロックを移動することを示す定数
MOVE_RIGHT = 1  # 右にブロックを移動することを示す定数
MOVE_DOWN = 2  # 下にブロックを移動することを示す定数

# ブロックを構成する正方形のクラス
class TetrisSquare():
    def __init__(self, x=0, y=0, color="gray"):
        '1つの正方形を作成'
        self.x = x
        self.y = y
        self.color = color

    def set_cord(self, x, y):
        '正方形の座標を設定'
        self.x = x
        self.y = y

    def get_cord(self):
        '正方形の座標を取得'
        return int(self.x), int(self.y)

    def set_color(self, color):
        '正方形の色を設定'
        self.color = color

    def get_color(self):
        '正方形の色を取得'
        return self.color

    def get_moved_cord(self, direction):
        '移動後の正方形の座標を取得'

        # 移動前の正方形の座標を取得
        x, y = self.get_cord()

        # 移動方向を考慮して移動後の座標を計算
        if direction == MOVE_LEFT:
            return x - 1, y
        elif direction == MOVE_RIGHT:
            return x + 1, y
        elif direction == MOVE_DOWN:
            return x, y + 1
        else:
            return x, y

# テトリス画面を描画するキャンバスクラス
class TetrisCanvas(tk.Canvas):
    def __init__(self, master, field):
        'テトリスを描画するキャンバスを作成'

        canvas_width = field.get_width() * BLOCK_SIZE
        canvas_height = field.get_height() * BLOCK_SIZE

        # tk.Canvasクラスのinit
        super().__init__(master, width=canvas_width, height=canvas_height, bg="white")

        # キャンバスを画面上に設置
        self.place(x=25, y=25)

        # 10x20個の正方形を描画することでテトリス画面を作成
        for y in range(field.get_height()):
            for x in range(field.get_width()):
                square = field.get_square(x, y)
                x1 = x * BLOCK_SIZE
                x2 = (x + 1) * BLOCK_SIZE
                y1 = y * BLOCK_SIZE
                y2 = (y + 1) * BLOCK_SIZE
                self.create_rectangle(
                    x1, y1, x2, y2,
                    outline="white", width=1,
                    fill=square.get_color()
                )

        # 一つ前に描画したフィールドを設定
        self.before_field = field

    def update(self, field, block):
        'テトリス画面をアップデート'

        # 描画用のフィールド(フィールド+ブロック)を作成
        new_field = TetrisField()
        for y in range(field.get_height()):
            for x in range(field.get_width()):
                square = field.get_square(x, y)
                color = square.get_color()

                new_square = new_field.get_square(x, y)
                new_square.set_color(color)

        # フィールドにブロックの正方形情報を合成
        if block is not None:
            block_squares = block.get_squares()
            for block_square in block_squares:
                # ブロックの正方形の座標と色を取得
                x, y = block_square.get_cord()
                color = block_square.get_color()

                # 取得した座標のフィールド上の正方形の色を更新
                new_field_square = new_field.get_square(x, y)
                new_field_square.set_color(color)

        # 描画用のフィールドを用いてキャンバスに描画
        for y in range(field.get_height()):
            for x in range(field.get_width()):

                # (x,y)座標のフィールドの色を取得
                new_square = new_field.get_square(x, y)
                new_color = new_square.get_color()

                # (x,y)座標が前回描画時から変化ない場合は描画しない
                before_square = self.before_field.get_square(x, y)
                before_color = before_square.get_color()
                if(new_color == before_color):
                    continue

                x1 = x * BLOCK_SIZE
                x2 = (x + 1) * BLOCK_SIZE
                y1 = y * BLOCK_SIZE
                y2 = (y + 1) * BLOCK_SIZE
                # フィールドの各位置の色で長方形描画
                self.create_rectangle(
                    x1, y1, x2, y2,
                    outline="white", width=1, fill=new_color
                )

        # 前回描画したフィールドの情報を更新
        self.before_field = new_field

# 積まれたブロックの情報を管理するフィールドクラス
class TetrisField():
    def __init__(self):
        self.width = FIELD_WIDTH
        self.height = FIELD_HEIGHT

        # フィールドを初期化
        self.squares = []
        for y in range(self.height):
            for x in range(self.width):
                # フィールドを正方形インスタンスのリストとして管理
                self.squares.append(TetrisSquare(x, y, "gray"))

    def get_width(self):
        'フィールドの正方形の数(横方向)を取得'

        return self.width

    def get_height(self):
        'フィールドの正方形の数(縦方向)を取得'

        return self.height

    def get_squares(self):
        'フィールドを構成する正方形のリストを取得'

        return self.squares

    def get_square(self, x, y):
        '指定した座標の正方形を取得'

        return self.squares[y * self.width + x]

    def judge_game_over(self, block):
        'ゲームオーバーかどうかを判断'

        # フィールド上で既に埋まっている座標の集合作成
        no_empty_cord = set(square.get_cord() for square
                            in self.get_squares() if square.get_color() != "gray")

        # ブロックがある座標の集合作成
        block_cord = set(square.get_cord() for square
                         in block.get_squares())

        # ブロックの座標の集合と
        # フィールドの既に埋まっている座標の集合の積集合を作成
        collision_set = no_empty_cord & block_cord

        # 積集合が空であればゲームオーバーではない
        if len(collision_set) == 0:
            ret = False
        else:
            ret = True

        return ret

    def judge_can_move(self, block, direction):
        '指定した方向にブロックを移動できるかを判断'

        # フィールド上で既に埋まっている座標の集合作成
        no_empty_cord = set(square.get_cord() for square
                            in self.get_squares() if square.get_color() != "gray")

        # 移動後のブロックがある座標の集合作成
        move_block_cord = set(square.get_moved_cord(direction) for square
                              in block.get_squares())

        # フィールドからはみ出すかどうかを判断
        for x, y in move_block_cord:

            # はみ出す場合は移動できない
            if x < 0 or x >= self.width or \
                    y < 0 or y >= self.height:
                return False

        # 移動後のブロックの座標の集合と
        # フィールドの既に埋まっている座標の集合の積集合を作成
        collision_set = no_empty_cord & move_block_cord

        # 積集合が空なら移動可能
        if len(collision_set) == 0:
            ret = True
        else:
            ret = False

        return ret

    def fix_block(self, block):
        'ブロックを固定してフィールドに追加'

        for square in block.get_squares():
            # ブロックに含まれる正方形の座標と色を取得
            x, y = square.get_cord()
            color = square.get_color()

            # その座標と色をフィールドに反映
            field_square = self.get_square(x, y)
            field_square.set_color(color)

    def delete_line(self):
        '行の削除を行う'

        # 全行に対して削除可能かどうかを調べていく
        for y in range(self.height):
            for x in range(self.width):
                # 行内に1つでも空があると消せない
                square = self.get_square(x, y)
                if(square.get_color() == "gray"):
                    # 次の行へ
                    break
            else:
                # break されなかった場合はその行は空きがない
                # この行を削除し、この行の上側にある行を1行下に移動
                for down_y in range(y, 0, -1):
                    for x in range(self.width):
                        src_square = self.get_square(x, down_y - 1)
                        dst_square = self.get_square(x, down_y)
                        dst_square.set_color(src_square.get_color())
                # 一番上の行は必ず全て空きになる
                for x in range(self.width):
                    square = self.get_square(x, 0)
                    square.set_color("gray")

# テトリスのブロックのクラス
class TetrisBlock():
    def __init__(self):
        'テトリスのブロックを作成'

        # ブロックを構成する正方形のリスト
        self.squares = []

        # ブロックの形をランダムに決定
        block_type = random.randint(1, 4)

        # ブロックの形に応じて4つの正方形の座標と色を決定
        if block_type == 1:
            color = "red"
            cords = [
                [FIELD_WIDTH / 2, 0],
                [FIELD_WIDTH / 2, 1],
                [FIELD_WIDTH / 2, 2],
                [FIELD_WIDTH / 2, 3],
            ]
        elif block_type == 2:
            color = "blue"
            cords = [
                [FIELD_WIDTH / 2, 0],
                [FIELD_WIDTH / 2, 1],
                [FIELD_WIDTH / 2 - 1, 0],
                [FIELD_WIDTH / 2 - 1, 1],
            ]
        elif block_type == 3:
            color = "green"
            cords = [
                [FIELD_WIDTH / 2 - 1, 0],
                [FIELD_WIDTH / 2, 0],
                [FIELD_WIDTH / 2, 1],
                [FIELD_WIDTH / 2, 2],
            ]
        elif block_type == 4:
            color = "orange"
            cords = [
                [FIELD_WIDTH / 2, 0],
                [FIELD_WIDTH / 2 - 1, 0],
                [FIELD_WIDTH / 2 - 1, 1],
                [FIELD_WIDTH / 2 - 1, 2],
            ]

        # 決定した色と座標の正方形を作成してリストに追加
        for cord in cords:
            self.squares.append(TetrisSquare(cord[0], cord[1], color))

    def get_squares(self):
        'ブロックを構成する正方形を取得'

        # return [square for square in self.squares]
        return self.squares

    def move(self, direction):
        'ブロックを移動'

        # ブロックを構成する正方形を移動
        for square in self.squares:
            x, y = square.get_moved_cord(direction)
            square.set_cord(x, y)

# テトリスゲームを制御するクラス
class TetrisGame():

    def __init__(self, master):
        'テトリスのインスタンス作成'

        # ブロック管理リストを初期化
        self.field = TetrisField()

        # 落下ブロックをセット
        self.block = None

        # テトリス画面をセット
        self.canvas = TetrisCanvas(master, self.field)

        # テトリス画面アップデート
        self.canvas.update(self.field, self.block)

    def start(self, func):
        'テトリスを開始'

        # 終了時に呼び出す関数をセット
        self.end_func = func

        # ブロック管理リストを初期化
        self.field = TetrisField()

        # 落下ブロックを新規追加
        self.new_block()

    def new_block(self):
        'ブロックを新規追加'

        # 落下中のブロックインスタンスを作成
        self.block = TetrisBlock()

        if self.field.judge_game_over(self.block):
            self.end_func()
            print("GAMEOVER")

        # テトリス画面をアップデート
        self.canvas.update(self.field, self.block)

    def move_block(self, direction):
        'ブロックを移動'

        # 移動できる場合だけ移動する
        if self.field.judge_can_move(self.block, direction):

            # ブロックを移動
            self.block.move(direction)

            # 画面をアップデート
            self.canvas.update(self.field, self.block)

        else:
            # ブロックが下方向に移動できなかった場合
            if direction == MOVE_DOWN:
                # ブロックを固定する
                self.field.fix_block(self.block)
                self.field.delete_line()
                self.new_block()

# イベントを受け付けてそのイベントに応じてテトリスを制御するクラス
class EventHandller():
    def __init__(self, master, game):
        self.master = master

        # 制御するゲーム
        self.game = game

        # イベントを定期的に発行するタイマー
        self.timer = None

        # ゲームスタートボタンを設置
        button = tk.Button(master, text='START', command=self.start_event)
        button.place(x=25 + BLOCK_SIZE * FIELD_WIDTH + 25, y=30)

    def start_event(self):
        'ゲームスタートボタンを押された時の処理'

        # テトリス開始
        self.game.start(self.end_event)
        self.running = True

        # タイマーセット
        self.timer_start()

        # キー操作入力受付開始
        self.master.bind("<Left>", self.left_key_event)
        self.master.bind("<Right>", self.right_key_event)
        self.master.bind("<Down>", self.down_key_event)

    def end_event(self):
        'ゲーム終了時の処理'
        self.running = False

        # イベント受付を停止
        self.timer_end()
        self.master.unbind("<Left>")
        self.master.unbind("<Right>")
        self.master.unbind("<Down>")

    def timer_end(self):
        'タイマーを終了'

        if self.timer is not None:
            self.master.after_cancel(self.timer)
            self.timer = None

    def timer_start(self):
        'タイマーを開始'

        if self.timer is not None:
            # タイマーを一旦キャンセル
            self.master.after_cancel(self.timer)

        # テトリス実行中の場合のみタイマー開始
        if self.running:
            # タイマーを開始
            self.timer = self.master.after(1000, self.timer_event)

    def left_key_event(self, event):
        '左キー入力受付時の処理'

        # ブロックを左に動かす
        self.game.move_block(MOVE_LEFT)

    def right_key_event(self, event):
        '右キー入力受付時の処理'

        # ブロックを右に動かす
        self.game.move_block(MOVE_RIGHT)

    def down_key_event(self, event):
        '下キー入力受付時の処理'

        # ブロックを下に動かす
        self.game.move_block(MOVE_DOWN)

        # 落下タイマーを再スタート
        self.timer_start()

    def timer_event(self):
        'タイマー満期になった時の処理'

        # 下キー入力受付時と同じ処理を実行
        self.down_key_event(None)


class Application(tk.Tk):
    def __init__(self):
        super().__init__()

        # アプリウィンドウの設定
        self.geometry("400x600")
        self.title("テトリス")

        # テトリス生成
        game = TetrisGame(self)

        # イベントハンドラー生成
        EventHandller(self, game)


def main():
    'main関数'

    # GUIアプリ生成
    app = Application()
    app.mainloop()


if __name__ == "__main__":
    main()

スポンサーリンク

スクリプトの説明

最後に作成したスクリプトについて簡単に説明したいと思います。

スクリプトは複数のクラスから作成していますので、これらのクラスの関係を中心に解説していきます。

細かい処理の内容については、スクリプトにコメントを付けていますのでそちらを参照していただければと思います(不明点に関しては気軽に質問していただければと思います)。

テトリスを構成する EventHandller クラスと TetrisGame クラス

このテトリスを構成しているクラスは大きく EventHandller クラスと TetrisGame クラスに分けられます。

EventHandllerとTetrisGameクラスの関係図

EventHandller クラス

EventHandller クラスはキー入力・時間経過・ボタンクリックのイベントを受け付け、イベントに応じて TetrisGame クラスに指示を出します。

このイベントの受け付けを行うために Tkinter モジュールを利用しています。

Tkinter については下記ページで解説していますので、Tkinter や GUI アプリに興味のある方は是非読んで見てください。

tkinter解説ページのアイキャッtPython で Tkinter を使ってめちゃくちゃ簡単に GUI アプリを作る

TetrisGame クラス

TetrisGame クラスはテトリスのゲームそのものを実現するクラスで、主に EventHandller クラスからの指示に応じて下記のようなテトリスゲームに必要な処理を行います。

  • テトリスゲームの開始(start
  • 落下中ブロックの移動(move_block

また、これらの処理の中でさらに下記のような処理も行います(実際の処理は後述する他のクラスで行うものもあります)。

  • 新しいブロックの追加
  • ブロックの削除
  • ゲームオーバー処理
  • テトリス画面の描画

TetrisGame クラスを構成する TetrisBlockTetrisFieldTetrisCanvas クラス

さらにその TetrisGame クラスは下記の3つのクラスから構成されています。

  • TetrisBlock クラス
  • TetrisField クラス
  • TetrisCanvas クラス

TetrisFieldとTetrisBlockとTetrisCanvsの関係図

TetrisBlock クラス

TetrisBlock クラスは「落下ブロック」を管理・制御するクラスです。

TetrisBlockクラス

この TetrisBlock クラスは4つの正方形(後に説明する TetrisSquare クラス)から構成されます。

落下中ブロックが正方形から構成される様子

TetrisGame クラスからの指示に応じて TetrisBlock クラスは、例えば下記のような処理を行います。

  • 落下ブロックの移動(move
  • 落下ブロックの作成(__init__

特に「落下中ブロックの作成」においては、ランダムにブロックの形を決定し、その形に応じた落下ブロックの作成を行います。

TetrisField クラス

TetrisBlock クラスが「落下ブロック」の管理・制御を行うのに対し、TetrisField クラスは「フィールド」全体の管理・制御を行います。

TetrisFieldクラス

この TetrisField クラスは10 x 20個の正方形(後に説明する TetrisSquare クラス)から構成されます。

フィールドが正方形から構成される様子

「フィールド全体」の管理・制御というと曖昧なので、もう少し具体的に説明します。

フィールド全体の10 x 20個の各正方形(マス目)下記の2種類に分けられます。

  • 空いていない(落下済みのブロックが残っている)
  • 空いている

例えば、ブロックを移動する際に、ブロックが移動できるかどうかは、移動後のブロックの位置のフィールドの正方形が「空いている」かどうかで判断できます。

またブロックが落下しきった際に、ブロックが消えるかどうかは1行全ての正方形が「空いていない」かどうかで判断できます。

こんな感じで、フィールド内の情報(+落下中のブロックの情報)に基づいて、様々な判断や処理を行うのがこの TetrisField クラスです。

TetrisGame クラスからの指示に応じ、 TetrisField クラスはフィールドの各正方形や落下ブロックの各正方形を考慮して、例えば下記のような処理を行います。

  • ブロックが移動できるかどうかの判断(judge_can_move
  • ゲームオーバーかどうかの判断(judge_game_over
  • 落下しきったブロックの固定(fix_block
  • ブロックの削除(delete_line

TetrisCanvas クラス

TetrisCanvas クラスは、TetrisGame クラスからの指示に応じて下記の処理を行うクラスです。

  • テトリス画面の描画(update

TetrisCanvasクラス

具体的には TetrisBlock クラスの「落下ブロック」と TetrisField クラスの「フィールド全体」の各正方形の色に応じて画面に色付きの正方形を描画します。

フィールドとブロックを描画する様子

この正方形の描画には EventHandller クラスでも紹介した Tkinter モジュール(Canvas)を利用しています。

Tkinter を利用する事で簡単に図形の描画ができます。こういったゲームや GUI アプリに興味のある方は是非下記ページを参考にして利用してみてください。

tkinter解説ページのアイキャッtPython で Tkinter を使ってめちゃくちゃ簡単に GUI アプリを作る

TetrisBlock クラスとTetrisField クラスを構成する TetrisSquare クラス

前述の TetrisBlock クラスとTetrisField クラスは TetrisSquare から構成されます。

落下中ブロックに関しては4つの正方形から、フィールドは10 x 20個の正方形から構成されていますよね?

この正方形を表すクラスが TetrisSquare です。

フィールドと落下中ブロックが正方形から構成される様子

TetrisSquare クラスは座標(x, y)と色を持っており、TetrisSquare クラスの各インスタンスで表される正方形の座標と色が分かるようにしています。また色が "gray"(灰色)の正方形は空きであることを示すようにしています。

これにより落下中ブロックがどの位置にあるのか、フィールドのどの部分に空きがあるのかを判断する事ができるようになっています。

TetrisSquareとブロック・フィールドの関係

TetrisSquare クラスは TetrisBlock クラスや TetrisField クラスからの指示によって下記のような処理を行います。

  • 正方形の座標を取得(get_cord
  • 移動後の正方形の座標を取得(get_moved_cord
  • 正方形の色を取得(get_color
  • 正方形の色を設定(set_color

以上が特にクラスに焦点を当てたスクリプトの解説になります。

このあたりを頭に入れておくとスクリプトが読みやすくなると思います。

まとめ

このページではテトリスゲームの Python プログラムの紹介およびその解説を行いました。

ブロックの回転が出来ない・ブロックの数が少ないので、かなりのクソゲー感はあるのですが、テトリスっぽく動作している画面を見ると嬉しくなりました。個人的に満足です。

今回紹介したテトリスのように、ゲームのプログラミングは楽しく学べるので、プログラミング初心者の方やプログラミング学習のモチベーションが落ちてきた方にオススメです。

特に Python の場合は Tkinter を使うことで簡単にキー入力などできるようになりますので、ゲーム作るのも簡単です。是非試してみてください!

tkinter解説ページのアイキャッtPython で Tkinter を使ってめちゃくちゃ簡単に GUI アプリを作る

オススメ参考書

このページで紹介した「ゲーム開発に興味のある方」「ゲームを作りながら楽しく Python プログラミングを学びたい方」にはPythonでつくる ゲーム開発 入門講座がオススメです。

この本は、このページで紹介した「落ちものゲーム」だけでなく、「すごろく」などの簡単なゲームから本格的な「RPG」を開発しながら Python プログラミングを学べる参考書になります。 初心者向け解説も多く、 Python の基本からゲーム開発について学ぶ事ができます。

また初心者の方にとっては、楽しく Python を学べる点がオススメの理由です。ゲーム開発を通して Python プログラミングを学ぶ事ができ、自分の作成したプログラムの動作を実際に目で確認できるので楽しく勉強できま。

楽しいのでプログラミング学習のモチベーション低下も防ぐ事ができます。

とっつきやすさという点で、Python 入門者向け参考書としてはナンバーワンだと思います。

コメントを残す

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