Python実践入門

はじめに

本記事ではプログラミングの基礎的な考え方を習得を目指して、Python実践の入門としてテトリスを実装する。テトリスは、プログラミングに入門する際の最初の題材として難易度が適切で、テトリスの実装を通して以下の4つの基礎が習得できる。

  • プログラミングの基礎的な考え方
  • Pythonの基礎
  • ゲーム作りの基礎
  • オブジェクト指向プログラミングの基礎

  • Notion Image

    最終的なコード

    最終的なコード(課題の解答を除く)はテストコードも付けてGitHubにあげてあるので合わせて参照してほしい(わずかだがテストしやすいようにコードを変更した部分もあるので注意されたい)。

    前提

    前提として、読者はPythonの環境構築は実施済みであり、Pythonを実行できる環境が整っているものとする。

    さらに、今回利用するnumpyをインストールし、最終的なファイル構成を確認しておく。

  • ターミナルを開いて以下のコマンドを実行し、numpyをインストールしておく。
  • $ pip install numpy

  • 最終的なファイル構成は以下のようになる。実装しながらファイルが必要になった時点で新規作成してもよいが、今回は全体を見通しやすくするため、これらのファイル群(空ファイル)を作成しておく。(my_tetrisディレクトリは任意の場所に作成すればOK)
  • my_tetris
        ├ application.py
        ├ game.py
        └ atelier.py

    以降、これらのファイル内にコードを書き込んでいく(どのファイルに書き込むかはコードの最初の行にコメントで明記する)。

    テトリスの実装

  • まずは以下のコードを実行し、ゲームの土台となるウィンドウを表示させる。
  • # application.py
    
    import tkinter as tk
    root = tk.Tk()
    root.mainloop()

    Output

    Notion Image

    ※ tkinterについて tkinterはGUIを実装するためのPython標準ライブラリ。tkinterの代表的な機能を以下に示す。

    名称一般的な名称説明 / 備考
    Labelラベルウィンドウ上に文字を表示する
    Entryテキストボックス文字列の入力できるテキストボックスを表示する
    Buttonボタンボタンを表示する
    CheckButtonチェックボックスチェックボックスを表示する
    RadioButtonラジオボタンラジオボタンを表示する
    FrameフレームLabelやButtonなどの複数の部品をまとめて配置できるコンテナ
    Canvasキャンバス図形・画像を描画できる

  • 次に、tk.Frameクラスを継承する方法に変更して、前のコードと同じ挙動を実装する。このコードを実行すると、前のコードの時と同じウィンドウが表示される。
  • # application.py
    
    import tkinter as tk
    
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
    
    
    if __name__ == "__main__":
        root = tk.Tk()
        app = Application(master=root)
        app.mainloop()

    Output

    Notion Image

    オブジェクト指向プログラミングでは、このようにクラスを継承して、各メソッドで定義されている動作を上書き(オーバーライド)することで、必要な機能を実装していくことがよくある。


  • ここからゲーム画面を描画していくために必要なCanvas領域を作成する。わかりやすいようにCanvas領域はスカイブルーで表示する。
  • # application.py
    
    import tkinter as tk
    
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            master.title("マイテトリス")
            self.canvas = tk.Canvas(master, width=400, height=400, bg="skyblue")
            self.canvas.pack()
    
    
    if __name__ == "__main__":
        root = tk.Tk()
        app = Application(master=root)
        app.mainloop()

    Output

    Notion Image

  • Canvas領域上にブロックを一つ描画するように変更する。わかりやすいようにブロックはグレイで表示する。
  • # application.py
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            master.title("マイテトリス")
            self.canvas = tk.Canvas(master, width=400, height=400, bg="skyblue")
            self.canvas.pack()
            self.draw_block()
    
        def draw_block(self):
            self.canvas.create_rectangle(0, 0, 20, 20, fill="grey")

    Output

    Notion Image

  • 前のコードでは、create_rectangleの引数の座標を直書きしてしまっていて、draw_block関数の使い回しができない。そこで、draw_block関数に引数としてxとyを渡すように変更する。
  • # application.py
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            master.title("マイテトリス")
            self.canvas = tk.Canvas(master, width=400, height=400, bg="skyblue")
            self.canvas.pack()
            self.block_size = 20
            self.draw_block(0, 0)
    
        def draw_block(self, x, y):
            self.canvas.create_rectangle(
                x, y, x + self.block_size, y + self.block_size, fill="grey"
            )

  • 先ほど描画したブロックの隣にもう一つブロックを描画する。
  • # application.py
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            master.title("マイテトリス")
            self.canvas = tk.Canvas(master, width=400, height=400, bg="skyblue")
            self.canvas.pack()
            self.block_size = 20
            self.draw_block(0, 0)
            self.draw_block(20, 0)

    Output

    Notion Image

  • この調子でブロックを並べてマス目を作っていくのだが、一つ一つ書いてはいられないので、ループ処理を利用して10ブロックを横一列に並べる。
  • # application.py
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            master.title("マイテトリス")
            self.canvas = tk.Canvas(master, width=400, height=400, bg="skyblue")
            self.canvas.pack()
            self.block_size = 20
            for x in range(0, 200, 20):
                self.draw_block(x, 0)

    Output

    Notion Image

  • 同様に、ループ処理を利用して縦方向にも10ブロック並べて、計100ブロックを描画する。
  • # application.py
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            master.title("マイテトリス")
            self.canvas = tk.Canvas(master, width=400, height=400, bg="skyblue")
            self.canvas.pack()
            self.block_size = 20
            for y in range(0, 200, 20):
                for x in range(0, 200, 20):
                    self.draw_block(x, y)

    Output

    Notion Image

  • xとyの単位がピクセル数だと扱いづらいので、マス目の位置を表すように変更する。
  • # application.py
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            master.title("マイテトリス")
            self.canvas = tk.Canvas(master, width=400, height=400, bg="skyblue")
            self.canvas.pack()
            self.block_size = 20
            for y in range(0, 10):
                for x in range(0, 10):
                    self.draw_block(x, y)
    
        def draw_block(self, x, y):
            self.canvas.create_rectangle(
                self.block_size * x,
                self.block_size * y,
                self.block_size * (x + 1),
                self.block_size * (y + 1),
                fill="grey"
            )

  • ここまででゲームに必要なマス目が自由に描画できるようになった。変更しやすいように、マス目を描画する処理は関数として切り出しておく。
  • # application.py
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            master.title("マイテトリス")
            self.canvas = tk.Canvas(master, width=400, height=400, bg="skyblue")
            self.canvas.pack()
            self.block_size = 20
            self.draw_field()
    
        def draw_field(self):
            for y in range(0, 10):
                for x in range(0, 10):
                    self.draw_block(x, y)

  • ここで、画面の更新を簡単にできるように二次元配列マップを導入する。Field.tilesの1のところのみにブロックが描画されるようにして、テトリスの壁を作成する。
  • # application.py
    
    from game import Field
    
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            master.title("マイテトリス")
            self.canvas = tk.Canvas(master, width=400, height=400, bg="skyblue")
            self.canvas.pack()
            self.block_size = 20
            self.field = Field()
            self.draw_field(self.field)
    
        def draw_field(self, field):
            for y in range(field.tiles_size["y"]):
                for x in range(field.tiles_size["x"]):
                    block_type = field.tiles[y][x]
                    if block_type != 0:
                        self.draw_block(x, y)
    # game.py
    
    import numpy as np
    
    
    class Field():
        def __init__(self):
            self.tiles = np.array([
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
                [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
            ])
            self.tiles_size = {
                "x": self.tiles.shape[1],
                "y": self.tiles.shape[0],
            }

    Output

    Notion Image

  • ブロックとテトラミノのクラスを追加して、テトラミノ(ブロックの塊)を描画する。
  • # application.py
    
    from game import Field, Tetromino
    
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            master.title("マイテトリス")
            self.canvas = tk.Canvas(master, width=400, height=400, bg="skyblue")
            self.canvas.pack()
            self.block_size = 20
            self.field = Field()
            self.tetromino = Tetromino(7, 1)
            self.draw_field(self.field)
            self.draw_tetromino(self.tetromino)
    
        def draw_tetromino(self, tetromino):
            blocks = tetromino.calc_blocks()
            for block in blocks:
                self.draw_block(block.x, block.y)
    # game.py
    
    class Block():
        def __init__(self, x, y):
            self.x = x
            self.y = y
    
    
    class Tetromino():
        def __init__(self, x, y, rot=0, shape=2):
            self.x = x
            self.y = y
            self.rot = rot
            self.shape = shape
    
        def calc_blocks(self):
            blocks = [Block(-1, 0), Block(0, 0), Block(0, -1), Block(1, 0)]
            return [Block(self.x + block.x, self.y + block.y) for block in blocks]

    Output

    Notion Image

    ※ テトロミノが描画されていることを確認する。


  • このままではどれだけ待ってもブロックが落ちてこないので、updateメソッドを追加して落下するように変更する。画面は都度更新するため、draw_fieldメソッドとdraw_tetrominoメソッドの実行は今後はupdateメソッドで行うことにする。
  • # application.py
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            master.title("マイテトリス")
            self.canvas = tk.Canvas(master, width=400, height=400, bg="skyblue")
            self.canvas.pack()
            self.block_size = 20
            self.field = Field()
            self.tetromino = Tetromino(7, 1)
            # self.draw_field(self.field)
            # self.draw_tetromino(self.tetromino)
    
        def update(self):
            self.tetromino.y += 1
            self.draw_field(self.field)
            self.draw_tetromino(self.tetromino)
            self.after(50, self.update)
    
    
    if __name__ == "__main__":
        root = tk.Tk()
        app = Application(master=root)
        app.update()
        app.mainloop()

    Output

    Notion Image

    ※ テトロミノが落ちていっている様を確認する。


  • Canvas領域にテトラミノの描画がどんどん上書きされていっているようなので、tk.Canvas.deleteメソッドにより更新のたびにCanvas領域を白紙に戻すようにする。
  • # application.py
    
    class Application(tk.Frame):
        ...
    
        def update(self):
            self.tetromino.y += 1
            self.delete_all()
            self.draw_field(self.field)
            self.draw_tetromino(self.tetromino)
            self.after(50, self.up)
            
        def delete_all(self):
            self.canvas.delete("all")

    Output

    Notion Image

    ※ 描画が上書きされなくなったことを確認する。


  • テトラミノが壁をすり抜けて落ちていってしまっているので、衝突判定を追加して、衝突したら新たなテトロミノを作成するように変更する。
  • # application.py
    
    class Application(tk.Frame):
        ...
    
        def update(self):
            # self.tetromino.y += 1
            self.drop_proc()
            self.delete_all()
            self.draw_field(self.field)
            self.draw_tetromino(self.tetromino)
            self.after(50, self.update)
    
        def drop_proc(self):
            future_tetromino = self.tetromino.copy()
            future_tetromino.y += 1
            if self.field.is_allowed(future_tetromino):
                self.tetromino.y += 1
            else:
                self.tetromino = Tetromino(7, 1)
    # game.py
    
    class Field():
        ...
    
        def is_allowed(self, tetromino):
            blocks = tetromino.calc_blocks()
            return all(
                self.tiles[block.y][block.x] == 0
                for block in blocks
                if 0 <= block.x and block.x < self.tiles_size["x"]
                and 0 <= block.y and block.y < self.tiles_size["y"]
            )
    
    
    class Tetromino():
        ...
    
        def copy(self):
            return Tetromino(self.x, self.y, self.rot, self.shape)

    Output

    Notion Image

    ※ テトロミノが壁をすり抜けなくなったことを確認する。


  • Applicationのメソッドがごちゃごちゃしてきたので、新たにAtelierクラスを作って描画系の処理をそちらに委譲する。
  • # application.py
    
    from atelier import Atelier
    
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            # master.title("マイテトリス")
            # self.canvas = tk.Canvas(master, width=400, height=400, bg="skyblue")
            # self.canvas.pack()
            # self.block_size = 20
            self.atelier = Atelier(master)
            self.field = Field()
            self.tetromino = Tetromino(7, 1)
    
        # def delete_all(self):
        #     self.canvas.delete("all")
    
        # def draw_tetromino(self, tetromino):
        #     blocks = tetromino.calc_blocks()
        #     for block in blocks:
        #         self.draw_block(block.x, block.y)
    
        # def draw_field(self, field):
        #     for y in range(field.tiles_size["y"]):
        #         for x in range(field.tiles_size["x"]):
        #             block_type = field.tiles[y][x]
        #             if block_type != 0:
        #                 self.draw_block(x, y)
    
        # def draw_block(self, x, y):
        #     self.canvas.create_rectangle(
        #         self.block_size * x,
        #         self.block_size * y,
        #         self.block_size * (x + 1),
        #         self.block_size * (y + 1),
        #         fill="grey"
        #     )
    
        def update(self):
            self.drop_proc()
            self.atelier.delete_all()
            self.atelier.draw_field(self.field)
            self.atelier.draw_tetromino(self.tetromino)
            self.after(50, self.update)
    
        def drop_proc(self):
            future_tetromino = self.tetromino.copy()
            future_tetromino.y += 1
            if self.field.is_allowed(future_tetromino):
                self.tetromino.y += 1
            else:
                self.tetromino = Tetromino(7, 1)
    # atelier.py
    
    import tkinter as tk
    
    
    class Atelier():
        def __init__(self, master):
            master.title("マイテトリス")
            self.canvas = tk.Canvas(master, width=400, height=400, bg="skyblue")
            self.canvas.pack()
            self.block_size = 20
    
        def delete_all(self):
            self.canvas.delete("all")
    
        def draw_tetromino(self, tetromino):
            blocks = tetromino.calc_blocks()
            for block in blocks:
                self.draw_block(block.x, block.y)
    
        def draw_field(self, field):
            for y in range(field.tiles_size["y"]):
                for x in range(field.tiles_size["x"]):
                    block_type = field.tiles[y][x]
                    if block_type != 0:
                        self.draw_block(x, y)
    
        def draw_block(self, x, y):
            self.canvas.create_rectangle(
                self.block_size * x,
                self.block_size * y,
                self.block_size * (x + 1),
                self.block_size * (y + 1),
                fill="grey"
            )

  • テトラミノが壁をすり抜けなくなったが、ブロックが着地の瞬間に消えてなくなってしまっているので、着地したらfieldを更新してブロックが積み上がるようにする。
  • # application.py
    
    class Application(tk.Frame):
        ...
    
        def drop_proc(self):
            future_tetromino = self.tetromino.copy()
            future_tetromino.y += 1
            if self.field.is_allowed(future_tetromino):
                self.tetromino.y += 1
            else:
                for block in self.tetromino.calc_blocks():
                    self.field.put_block(block.x, block.y, self.tetromino.shape)
                self.tetromino = Tetromino(7, 1)
    # game.py
    
    class Field():
        ...
       
        def put_block(self, x, y, shape):
            self.tiles[y][x] = shape

    Output

    Notion Image

    ※ テトロミノが積み上がっていくことを確認する。


  • キーボード操作を追加して、”1”を押すと左に、”2”を押すと下に、”3”を押すと右にブロックを移動できるようにする。
  • # application.py
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            self.atelier = Atelier(master)
            self.field = Field()
            self.tetromino = Tetromino(7, 1)
            self.controller = {"x": 0, "y": 0}
            master.bind("<KeyPress>", self.key_event)
    
        def update(self):
            self.control_proc()
            self.drop_proc()
            self.atelier.delete_all()
            self.atelier.draw_field(self.field)
            self.atelier.draw_tetromino(self.tetromino)
            self.after(50, self.update)
    
        def control_proc(self):
            future_tetromino = self.tetromino.copy()
            future_tetromino.x += self.controller["x"]
            future_tetromino.y += self.controller["y"]
            if self.field.is_allowed(future_tetromino):
                self.tetromino.x += self.controller["x"]
                self.tetromino.y += self.controller["y"]
            self.controller = {"x": 0, "y": 0}
    
        def key_event(self, event):
            key = event.keysym
            if key == "1":
                self.controller = {"x": -1, "y": 0}
            if key == "2":
                self.controller = {"x": 0, "y": 1}
            if key == "3":
                self.controller = {"x": 1, "y": 0}

    Output

    Notion Image

    ※ テトロミノを移動できることを確認する。


    ※ tkinterのイベントについて tkinterではbindメソッドを用いて、イベントと処理を紐づけることができる。<KeyPress>の他にも以下のようなイベントがある。

    イベントの種類説明
    <Key>または<KeyPress>キーが押された
    <KeyRelease>キーが離された
    <KeyPress-a>aキーが押された
    <Control-a>Ctrl + aキーが押された
    <Shift-a>Shift + aキーが押された
    <Alt-a>Alt + aキーが押された
    <Button>または<ButtonPress>マウスのボタンが押された
    <ButtonRelease>マウスのボタンが離された
    <Motion>マウスの移動
    <Enter>マウスカーソルがウィンドウの中に入った
    <Leave>マウスカーソルがウィンドウの外に出た
    <Button-1>マウスの左クリック
    <Button-2>マウスホイールクリック
    <Button-3>マウスの右クリック
    <Double-1>マウスの左ダブルクリック

    イベントオブジェクトにはkeysym以外にも以下のような情報が格納されている。

    イベントオブジェクト説明
    numマウスボタンの番号
    x, yマウスカーソルの座標
    timeイベントの発生時刻
    charキー対応する文字
    keysymキーに対応する名前

  • さらにキーボード操作を追加して、”4”を押すとブロックを回転できるようにする。
  • # application.py
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            self.atelier = Atelier(master)
            self.field = Field()
            self.tetromino = Tetromino(7, 1)
            self.controller = {"x": 0, "y": 0, "rot": 0}
            master.bind("<KeyPress>", self.key_event)
    
        def control_proc(self):
            future_tetromino = self.tetromino.copy()
            future_tetromino.x += self.controller["x"]
            future_tetromino.y += self.controller["y"]
            future_tetromino.rot += self.controller["rot"]
            if self.field.is_allowed(future_tetromino):
                self.tetromino.x += self.controller["x"]
                self.tetromino.y += self.controller["y"]
                self.tetromino.rot += self.controller["rot"]
            self.controller = {"x": 0, "y": 0, "rot": 0}
    
        def key_event(self, event):
            key = event.keysym
            if key == "1":
                self.controller = {"x": -1, "y": 0, "rot": 0}
            if key == "2":
                self.controller = {"x": 0, "y": 1, "rot": 0}
            if key == "3":
                self.controller = {"x": 1, "y": 0, "rot": 0}
            if key == "4":
                self.controller = {"x": 0, "y": 0, "rot": 1}
    
    # game.py
    
    class Tetromino():
        ...
        
        def calc_blocks(self):
            blocks = [Block(-1, 0), Block(0, 0), Block(0, -1), Block(1, 0)]
            blocks = self.rotate(blocks, self.rot)
            return [Block(self.x + block.x, self.y + block.y) for block in blocks]
    
        @staticmethod
        def rotate(blocks, rot):
            rot = rot % 4
            for _ in range(rot):
                blocks = [Block(block.y, -block.x) for block in blocks]
            return blocks

    Output

    Notion Image

    ※ テトロミノを回転できることを確認する。


  • ここで、Applicationクラスのdrop_procメソッドやcontrol_procメソッドでは、Tetrominoインスタンスのxやyといったの属性を直接書き換えてしまっていることに注目する。このままでは、Tetrominoクラスへの依存度を高めてしまっていて良くないので、Tetrominoクラスにcontrollerに渡して、Tetrominoクラス自身に状態を更新させるように変更する。
  • # application.py
    
    class Application(tk.Frame):
        ...
        
        def drop_proc(self):
            future_tetromino = self.tetromino.next({"x": 0, "y": 1, "rot": 0})
            if self.field.is_allowed(future_tetromino):
                self.tetromino = future_tetromino
            else:
                for block in self.tetromino.calc_blocks():
                    self.field.put_block(block.x, block.y, self.tetromino.shape)
                self.tetromino = Tetromino(7, 1)
                
        def control_proc(self):
            future_tetromino = self.tetromino.next(self.controller)
            if self.field.is_allowed(future_tetromino):
                self.tetromino = future_tetromino
            self.controller = {"x": 0, "y": 0, "rot": 0}
    # game.py
    
    class Tetromino():
        ...
        
        # def copy(self):
        #     return Tetromino(self.x, self.y, self.rot, self.shape)
        
        def next(self, controller):
            copy_tetromino = Tetromino(self.x, self.y, self.rot, self.shape)
            copy_tetromino.x += controller["x"]
            copy_tetromino.y += controller["y"]
            copy_tetromino.rot += controller["rot"]
            return copy_tetromino

  • ブロックの落下速度を変更して、難易度を調整する。
  • # application.py
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            self.atelier = Atelier(master)
            self.field = Field()
            self.tetromino = Tetromino(7, 1)
            self.controller = {"x": 0, "y": 0, "rot": 0}
            master.bind("<KeyPress>", self.key_event)
            self.count = 0
    
        def update(self):
            self.count += 1
            self.control_proc()
            if self.count % 10 == 0:
                self.drop_proc()
            self.atelier.delete_all()
            self.atelier.draw_field(self.field)
            self.atelier.draw_tetromino(self.tetromino)
            self.after(50, self.update)

    Output

    Notion Image

    ※ 落下速度がゆっくりになっていることを確認する。


  • まだブロックが消える機能を実装していなかったので、横方向にブロックが揃ったら消滅するように修正する。
  • # application.py
    
    class Application(tk.Frame):
        ...
    
        def update(self):
            self.count += 1
            self.control_proc()
            self.fill_proc()
            if self.count % 10 == 0:
                self.drop_proc()
            self.atelier.delete_all()
            self.atelier.draw_field(self.field)
            self.atelier.draw_tetromino(self.tetromino)
            self.after(50, self.update)
    
        def fill_proc(self):
            self.field.check()
    # game.py
    
    class Field():
        ...
    
        def check(self):
            for y in range(self.tiles_size["y"]-1):
                if all(tile != 0 for tile in self.tiles[y]):
                    new_line = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
                    new_tiles = np.delete(self.tiles, y, 0)
                    new_tiles = np.vstack((new_line, new_tiles))
                    self.tiles = new_tiles

    Output

    Notion Image

    ※ ブロックが揃ったら消滅することを確認する。


  • 壁とブロックが同じ色で見づらいので、ブロックの色を青に変更する。
  • # atelier.py
    
    class Atelier():
        ...
    
        def draw_tetromino(self, tetromino):
            blocks = tetromino.calc_blocks()
            for block in blocks:
                self.draw_block(block.x, block.y, block_type=tetromino.shape)
    
        def draw_field(self, field):
            for y in range(field.tiles_size["y"]):
                for x in range(field.tiles_size["x"]):
                    block_type = field.tiles[y][x]
                    if block_type != 0:
                        self.draw_block(x, y, block_type)
    
        def draw_block(self, x, y, block_type):
            if block_type == 1:
                color = "grey"
            else:
                color = "blue"
    
            self.canvas.create_rectangle(
                self.block_size * x,
                self.block_size * y,
                self.block_size * (x + 1),
                self.block_size * (y + 1),
                fill=color
            )

    Output

    Notion Image

    ※ テトロミノの色が青くなったことを確認する。


  • ブロックの種類を増やして、ランダムで落ちてくるように変更する。
  • # application.py
    
    import random
    
    
    class Application(tk.Frame):
        def __init__(self, master):
            super().__init__(master)
            self.atelier = Atelier(master)
            self.field = Field()
            self.tetromino = self.make_tetromino()
            self.controller = {"x": 0, "y": 0, "rot": 0}
            master.bind("<KeyPress>", self.key_event)
            self.count = 0
    
        def make_tetromino(self):
            return Tetromino(7, 1, 0, random.choice([2, 3, 4, 5, 6, 7, 8]))
    
        def drop_proc(self):
            future_tetromino = self.tetromino.next({"x": 0, "y": 1, "rot": 0})
            if self.field.is_allowed(future_tetromino):
                self.tetromino = future_tetromino
            else:
                for block in self.tetromino.calc_blocks():
                    self.field.put_block(block.x, block.y, self.tetromino.shape)
                self.tetromino = self.make_tetromino()
    # game.py
    
    class Tetromino():
        ...
    
        def calc_blocks(self):
            blocks = self.get_blocks(self.shape)
            blocks = self.rotate(blocks, self.rot)
            return [Block(self.x + block.x, self.y + block.y) for block in blocks]
            
        @staticmethod
        def get_blocks(shape):
            # T型
            if shape == 2:
                return [Block(-1, 0), Block(0, 0), Block(0, -1), Block(1, 0)]
            # Z型
            elif shape == 3:
                return [Block(-1, -1), Block(0, -1), Block(0, 0), Block(1, 0)]
            # S型
            elif shape == 4:
                return [Block(-1, 0), Block(0, 0), Block(0, -1), Block(1, -1)]
            # L型
            elif shape == 5:
                return [Block(0, -1), Block(0, 0), Block(0, 1), Block(1, 1)]
            # J型
            elif shape == 6:
                return [Block(0, -1), Block(0, 0), Block(0, 1), Block(-1, 1)]
            # O型
            elif shape == 7:
                return [Block(-1, -1), Block(-1, 0), Block(0, 0), Block(0, -1)]
            # I型
            elif shape == 8:
                return [Block(-2, 0), Block(-1, 0), Block(0, 0), Block(1, 0)]

    Output

    Notion Image

    ※ 様々な種類のテトロミノが落ちてくることを確認する。


  • 最後に、ゲーム終了判定を追加し、ゲーム終了時点でGAME OVERと画面に表示されるようにする。
  • # application.py
    
    class Application(tk.Frame):
        ...
        
        def update(self):
            if max(self.field.tiles.flatten()) == 9:
                self.atelier.draw_game_over()
            else:
                self.count += 1
                self.control_proc()
                self.fill_proc()
                if self.count % 10 == 0:
                    self.drop_proc()
                self.atelier.delete_all()
                self.atelier.draw_field(self.field)
                self.atelier.draw_tetromino(self.tetromino)
                self.after(50, self.update)
    
    # game.py
    
    class Field():
        ...
    
        def put_block(self, x, y, shape):
            if self.tiles[y][x] == 0:
                self.tiles[y][x] = shape
            else:
                self.tiles[y][x] = 9
    # atelier.py
    
    class Atelier():
        ...
    
        def draw_game_over(self):
            self.canvas.create_text(
                50, 50,
                text="GAME OVER",
                font=("Times", 12),
                fill="red"
            )

    Output

    Notion Image

    ※ GAME OVERと表示されることを確認する。

    課題

    以下の課題に対して、コードに変更を加えてみよう。

  • GAME OVERの文字を中央に大きく表示されるようにする
  • 壁の位置を縦横1マスずつ大きくしてフィールドを広げる
  • ウィンドウの大きさを壁の位置にぴったりに合わせる(余白をなくす)
  • テトラミノの形によってブロックの色を変える
  • キー"5"でテトラミノを逆回転できるようにする
  • Enterキーでゲームがリセットされるようにする
  • スコアを表示し、ブロックが揃って消えたらスコアを加算するようにする
  • その他、ゲームの改善(あるいは改造)をする
  • 参考資料


    著者画像

    ゆうき

    2018/04からITエンジニアとして活動、2021/11から独立。主な使用言語はPython, TypeScript, SAS, etc.