Python実践入門
はじめに
本記事ではプログラミングの基礎的な考え方を習得を目指して、Python実践の入門としてテトリスを実装する。テトリスは、プログラミングに入門する際の最初の題材として難易度が適切で、テトリスの実装を通して以下の4つの基礎が習得できる。
最終的なコード
最終的なコード(課題の解答を除く)はテストコードも付けてGitHubにあげてあるので合わせて参照してほしい(わずかだがテストしやすいようにコードを変更した部分もあるので注意されたい)。
前提
前提として、読者はPythonの環境構築は実施済みであり、Pythonを実行できる環境が整っているものとする。
さらに、今回利用するnumpyをインストールし、最終的なファイル構成を確認しておく。
$ pip install numpy
my_tetris
├ application.py
├ game.py
└ atelier.py
以降、これらのファイル内にコードを書き込んでいく(どのファイルに書き込むかはコードの最初の行にコメントで明記する)。
テトリスの実装
# application.py
import tkinter as tk
root = tk.Tk()
root.mainloop()
Output
※ tkinterについて tkinterはGUIを実装するためのPython標準ライブラリ。tkinterの代表的な機能を以下に示す。
名称 | 一般的な名称 | 説明 / 備考 |
Label | ラベル | ウィンドウ上に文字を表示する |
Entry | テキストボックス | 文字列の入力できるテキストボックスを表示する |
Button | ボタン | ボタンを表示する |
CheckButton | チェックボックス | チェックボックスを表示する |
RadioButton | ラジオボタン | ラジオボタンを表示する |
Frame | フレーム | LabelやButtonなどの複数の部品をまとめて配置できるコンテナ |
Canvas | キャンバス | 図形・画像を描画できる |
# 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
オブジェクト指向プログラミングでは、このようにクラスを継承して、各メソッドで定義されている動作を上書き(オーバーライド)することで、必要な機能を実装していくことがよくある。
# 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
# 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
# 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
# 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
# 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
# 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)
# 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
# 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
※ テトロミノが描画されていることを確認する。
# 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
※ テトロミノが落ちていっている様を確認する。
# 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
※ 描画が上書きされなくなったことを確認する。
# 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
※ テトロミノが壁をすり抜けなくなったことを確認する。
# 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"
)
# 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
※ テトロミノが積み上がっていくことを確認する。
# 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
※ テトロミノを移動できることを確認する。
※ 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 | キーに対応する名前 |
# 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
※ テトロミノを回転できることを確認する。
# 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
※ 落下速度がゆっくりになっていることを確認する。
# 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
※ ブロックが揃ったら消滅することを確認する。
# 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
※ テトロミノの色が青くなったことを確認する。
# 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
※ 様々な種類のテトロミノが落ちてくることを確認する。
# 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
※ GAME OVERと表示されることを確認する。
課題
以下の課題に対して、コードに変更を加えてみよう。