EUPH のモノ置き小屋
Python 入門 (テトリスを作ってみる)

最近, Python をはじめてみました.
とりあえず練習のために, テトリスを作ってみました (図 1).
GUI の実装には, wxPython を使っています.
プログラムの行数は 270 行となりました. GUI アプリケーションとしては結構短く簡潔に書けますね.

遊び方は "スペース" でゲーム開始, "←", "→", "↓" で移動, "ENTER" で回転です.

python_teto_ss.png
図 1 スクリーンショット

1. 開発環境の準備と Python 入門

1.1 Python のインストール

Python と言っても色々実装[1]があるようですが, 今回は一番スタンダードな CPython を利用します.

ここから Python 2.7.2 をダウンロードします[2].

なお 32 ビット版と 64 ビット版がありますが, 私は 2 台のマシンにそれぞれインストールしました. 実行ファイル生成時は, 対応したバージョンの実行ファイルが生成されるようです. 32 ビット版と 64 ビット版を 1 台のマシンに混在できるかどうかは不明です.

1.2 wxPython のインストール

Python で GUI アプリケーションを開発するには, ライブラリの選択肢が色々ありますが, ここでは, C++ で実装されているライブラリ wxWidgets の Python ラッパーである wxPython を使用することにします.

ここから wxPython 2.8.12.1 をダウンロードします[3].

Python 2.7 に対応するバージョンをダウンロードします.

1.3 py2exe のインストール

Python はインタプリタですので, そのままでは実行ファイルを生成することができません. 実行ファイルを生成するためには, py2exe をインストールする必要があります.

ここから py2exe 0.6.9 をダウンロードします[4].

Python 2.7 に対応するバージョンをダウンロードします.

1.4 Python 入門

入門サイトが色々ありますが, こちらのサイトが非常に参考になりました[5]. wxPython についても書かれています.

2. テトリスのデータ構造

テトリスのアルゴリズムは[6]を参考にしました.

移動可能なテトリミノと固定されたテトリミノのそれぞれのデータがあり, 移動可能テトリミノはキーまたはタイマで移動されます. 移動可能テトリミノは落下して移動不能になれば, 固定テトリミノにコピーされる流れです.

固定テトリミノの座標系を図 2 のように設定し, 各マスに番号を割り当てます. なお, 左右端と下端は番兵として 1 マス分余分に割り当てます.

python_teto_blocks.png
図 2 テトリスの座標系

移動テトリミノは, 図 3 のようにテトリミノ中心を 0 番となるように相対的に番号を割り当てます.

python_teto_blocks.png
図 3 移動可能テトリミノのデータ構造

3. プログラム

リスト 1 にソースコードを示します. Game クラスがテトリミノの移動, 回転, 固定, 列削除などのアルゴリズムを実装しています. GamePanel クラスが wx.Panel を継承しており, テトリミノの表示や全体的なゲームの進行を制御しています.

00001 #! c:/python27/python.exe
00002 # -*- coding: utf-8 -*-
00003 
00004 import wx
00005 import random
00006 
00007 BLOCK_BMP_SIZE = 20                 # 1 ブロックのビットマップ サイズ
00008 GAME_PANEL_W = BLOCK_BMP_SIZE * 10
00009 GAME_PANEL_H = BLOCK_BMP_SIZE * 20
00010 GAME_H = 21                         # 画面の高さ (番兵を含む)
00011 GAME_W = 12                         # 画面の幅 (番兵を含む)
00012 
00013 class Game:
00014     # コンストラクタ
00015     def __init__(self):
00016         # ブロック形状の定義
00017         self.blocks_ = [
00018             [0, 1, -GAME_W, -GAME_W - 1],   # Z, green
00019             [0, 1, -GAME_W, -GAME_W * 2],   # L, orange
00020             [0, 1, -GAME_W, GAME_W + 1],    # S, pink
00021             [0, 1, -GAME_W, 2],             # J, blue
00022             [0, 1, -GAME_W, -GAME_W + 1],   # O, yellow
00023             [-1, 0, 1, 2],                  # I, red
00024             [-1, 0, 1, -GAME_W]             # T, light blue
00025         ]
00026 
00027         # 固定ブロックの初期化
00028         self.fixed_ = []
00029         for j in range(GAME_H):
00030             for i in range(GAME_W):
00031                 if j == GAME_H - 1:
00032                     self.fixed_.append(-1)              # 下端
00033                 elif i == 0 or i == GAME_W - 1:
00034                     self.fixed_.append(-1)              # 左右端
00035                 else:
00036                     self.fixed_.append(0)               # それ以外
00037         
00038         # フローティング ブロックの初期化
00039         self.id_ = random.randint(1, len(self.blocks_))
00040         self.floating_ = self.blocks_[self.id_ - 1]
00041         self.pos_ = GAME_W + GAME_W / 2 - 1
00042 
00043     # クリアする
00044     def clear(self):
00045         for j in range(GAME_H - 1):
00046             for i in range(1, GAME_W - 1):
00047                 self.fixed_[j * GAME_W + i] = 0
00048 
00049     # 左右に移動する (引数 delta, -1: 左, 1: 右, 戻り値, True: 移動完了, False: 移動不可)
00050     def move(self, delta):
00051         # 移動可能か判定
00052         movable = True
00053         for b in self.floating_:
00054             pos = self.pos_ + b + delta
00055             if pos >= 0 and self.fixed_[pos] != 0:
00056                 movable = False
00057                 break
00058 
00059         # 移動可能であるときは移動
00060         if movable:
00061             self.pos_ = self.pos_ + delta
00062 
00063         return movable
00064     
00065     # 回転する (戻り値, True: 移動完了, False: 移動不可)
00066     def turn(self):
00067         movable = True
00068         rot = []
00069         for b in self.floating_:
00070             v = int(round(float(b) / float(GAME_W)))    # 回転先の x 座標
00071             w = b - v * GAME_W                          # 回転先の y 座標
00072             p = w * GAME_W - v                          # 回転先の位置
00073             rot.append(p)
00074             
00075             pos = self.pos_ + p
00076             if pos >= 0 and self.fixed_[pos] != 0:
00077                 movable = False
00078                 break
00079 
00080         if movable:
00081             self.floating_ = rot
00082         
00083         return movable
00084 
00085     # 下に移動する (戻り値, True: 移動完了, False: 移動不可)
00086     def fall(self):
00087         # 落下可能か判定
00088         movable = True
00089         for b in self.floating_:
00090             pos = self.pos_ + b + GAME_W
00091             if pos >= 0 and self.fixed_[pos] != 0:
00092                 movable = False
00093                 break
00094 
00095         # 落下可能である時は落下
00096         if movable:
00097             self.pos_ = self.pos_ + GAME_W
00098 
00099         return movable
00100 
00101     # 現在のフローティング ブロックを固定する
00102     def fix(self):
00103         # ブロック固定
00104         for b in self.floating_:
00105             self.fixed_[self.pos_ + b] = self.id_
00106 
00107     # 次のブロックを落とし始める (戻り値, True: 開始完了, False: 開始不能)
00108     def next(self):
00109         # 次のブロック決定
00110         nextId = random.randint(1, len(self.blocks_))
00111         nextFloating = self.blocks_[nextId - 1]
00112         nextPos = GAME_W + GAME_W / 2 - 1
00113 
00114         # 次のブロックを落とし始めることができるか判定
00115         starting = True
00116         for b in nextFloating:
00117             pos = nextPos + b
00118             if pos >= 0 and self.fixed_[pos] != 0:
00119                 starting = False
00120                 break
00121 
00122         # 落とし始めることができるときは開始
00123         if starting:
00124             self.id_ = nextId
00125             self.floating_ = nextFloating
00126             self.pos_ = nextPos
00127 
00128         return starting
00129 
00130     # 固定ブロックが 1 列がそろっていることの判定
00131     # (引数 line, 列番号, 戻り値: True: そろっている, False: そろっていない)
00132     def full(self, line):
00133         for b in self.fixed_[line * GAME_W: (line + 1) * GAME_W]:
00134             if b == 0:
00135                 return False
00136         return True
00137 
00138     # 固定ブロックの 1 列削除 (引数 line, 列番号)
00139     def remove(self, line):
00140         # 指定列を削除
00141         del self.fixed_[line * GAME_W: (line + 1) * GAME_W]
00142         
00143         # 1 列先頭について
00144         top = []
00145         for i in range(GAME_W):
00146             if i == 0 or i == GAME_W - 1:
00147                 top.append(-1)              # 左右端
00148             else:
00149                 top.append(0)               # それ以外
00150         self.fixed_[0: 0] = top
00151 
00152     # 指定した座標 (i, j) の値を返す
00153     def block(self, i, j):
00154         pos = j * GAME_W + i
00155 
00156         # フローティング ブロックであるか判定
00157         for block in self.floating_:
00158             if self.pos_ + block == pos:
00159                 return self.id_
00160         
00161         # フローティング ブロックでないときは固定ブロックの情報を返す
00162         return self.fixed_[pos]
00163     
00164 class GamePanel(wx.Panel):
00165     def __init__(self, parent, status):
00166         wx.Panel.__init__(self, parent, size=(GAME_PANEL_W, GAME_PANEL_H), style=wx.SUNKEN_BORDER)
00167         
00168         # ゲームを初期化
00169         self.game_ = Game()
00170         self.playing_ = False
00171         self.score_ = 0
00172 
00173         # ビットマップ データをロードする
00174         self.blockBitmaps_ = []
00175         blockBitmap = wx.Bitmap("blocks.png")
00176         for i in range(7):
00177             self.blockBitmaps_.append(blockBitmap.GetSubBitmap(
00178                 wx.Rect(BLOCK_BMP_SIZE * i, 0, BLOCK_BMP_SIZE, BLOCK_BMP_SIZE)))
00179         self.backgroundBitmap_ = wx.Bitmap("back.png")
00180 
00181         # タイマーの設定
00182         self.timer_ = wx.Timer(self, 1)
00183         
00184         # ステータス バーの設定
00185         self.status_ = status
00186         status.SetStatusText("SPACE key to start the game!")
00187 
00188         # イベントハンドラの設定
00189         self.Bind(wx.EVT_PAINT, self.OnPaint)
00190         self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
00191         self.Bind(wx.EVT_TIMER, self.OnTimer)
00192     
00193     # ゲーム開始
00194     def Start(self):
00195         self.game_.clear()
00196         self.timer_.Start(250)
00197         self.playing_ = True
00198         self.score_ = 0
00199         status.SetStatusText("Score: " + str(self.score_))
00200 
00201     # ゲームオーバー
00202     def Over(self):
00203         self.timer_.Stop()
00204         self.playing_ = False
00205         status.SetStatusText("Score: " + str(self.score_) + ". SPACE key to restart!")
00206 
00207     def FallAndTest(self):
00208         if not self.game_.fall():
00209             # 落下できないとき, 移動ブロックを固定
00210             self.game_.fix()
00211 
00212             # そろった列を削除
00213             lines = 0
00214             for line in range(GAME_H - 1):
00215                 if self.game_.full(line):
00216                     self.game_.remove(line)
00217                     lines = lines + 1
00218             
00219             # スコア加算
00220             if lines != 0:
00221                 self.score_ = self.score_ + 100 * pow(2, lines - 1)
00222                 status.SetStatusText("Score: " + str(self.score_))
00223 
00224             # 次のブロックを落とし始める
00225             if not self.game_.next():
00226                 # 落とし始めることができないときは, ゲームオーバー
00227                 self.Over()
00228 
00229     def OnPaint(self, evt):
00230         dc = wx.PaintDC(self)
00231         dc = wx.BufferedDC(dc)  # ダブルバッファ
00232         dc.DrawBitmap(self.backgroundBitmap_, 0, 0)
00233         for j in range(GAME_H - 1):
00234             for i in range(GAME_W - 2):
00235                 b = self.game_.block(i + 1, j)
00236                 if b >= 1 and b <= len(self.blockBitmaps_):
00237                     dc.DrawBitmap(self.blockBitmaps_[b - 1], BLOCK_BMP_SIZE * i, BLOCK_BMP_SIZE * j)
00238 
00239     def OnKeyDown(self, evt):
00240         if self.playing_:
00241             if evt.KeyCode == wx.WXK_LEFT:
00242                 self.game_.move(-1)
00243                 self.Refresh(False)
00244             elif evt.KeyCode == wx.WXK_RIGHT:
00245                 self.game_.move(1)
00246                 self.Refresh(False)
00247             elif evt.KeyCode == wx.WXK_DOWN:
00248                 self.FallAndTest()
00249                 self.Refresh(False)
00250             elif evt.KeyCode == wx.WXK_RETURN:
00251                 self.game_.turn()
00252                 self.Refresh(False)
00253         else:
00254             if evt.KeyCode == wx.WXK_SPACE:
00255                 self.Start()
00256     
00257     def OnTimer(self, evt):
00258         if self.playing_:
00259             self.FallAndTest()
00260             self.Refresh(False)
00261 
00262 # メイン処理
00263 if __name__ == "__main__":
00264     app = wx.App()
00265     frame = wx.Frame(None, wx.ID_ANY, "Tetris",
00266             style=wx.MINIMIZE_BOX | wx.CLOSE_BOX | wx.SYSTEM_MENU | wx.CAPTION)
00267     status = frame.CreateStatusBar()
00268     gamePanel = GamePanel(frame, status)
00269     frame.SetClientSizeWH(gamePanel.Size.width + 4, gamePanel.Size.height + 4)  # SUNKEN_BORDER の幅は 2 px らしい
00270     frame.Show()
00271     app.MainLoop()
00272 
リスト 1 テトリスのプログラム

参考資料

[1] Python - Wikipedia
[2] Download Python (http://python.org/)
[3] Download wxPython (http://www.wxpython.org/)
[4] py2exe (http://www.py2exe.org/)
[5] Python-izm
[6] わずか565バイトテトリスのプログラミング解説


このページの最終更新は 2013/09/16 00:56:02 (rev. 13 ).