テトリス
テトリスとは
The tetris game is one of the most popular computer games ever created. The original game was designed and programmed by a russian programmer Alexey Pajitnov in 1985. Since then, tetris is available on almost every computer platform in lots of variations. Even my mobile phone has a modified version of the tetris game.
テトリスはこの世で最も有名なコンピュータゲームのひとつです。 オリジナルは、ロシア人プログラマのアレクセイ・パジトノフによって1985年に開発されました。 それ以来、テトリスはありとあらゆるコンピュータ上で、多くの種類を遊ぶことができます。 私の携帯電話にもテトリスの改良版が入っています。
Tetris is called a falling block puzzle game. In this game, we have seven different shapes called tetrominoes. S-shape, Z-shape, T-shape, L-shape, Line-shape, MirroredL-shape and a Square-shape. Each of these shapes is formed with four squares. The shapes are falling down the board. The object of the tetris game is to move and rotate the shapes, so that they fit as much as possible.
If we manage to form a row, the row is destroyed and we score. We play the tetris game until we top out.
テトリスは落ち物パズルゲームと呼ばれています。 テトリスには、「テトリミノ」と呼ばれる7つの異なる図形が登場します。 S-テトリミノ、Z-テトリミノ、T-テトリミノ、L-テトリミノ、I-テトリミノ、J-テトリミノ、O-テトリミノの7種です。 それぞれのミノは、4つの正方形で構成されます。 ミノはゲーム板の上から下へ落ちていきます。 テトリスの目的は、これらのミノができるだけぴったり合わさるように、 ミノを動かしたり回したりすることです。
行をきっちり埋めると、その行は消滅し得点になります。 一番上の行が詰まるとゲーム終了です。
wxPython is a toolkit designed to create applications. There are other libraries which are targeted at creating computer games. Nevertheless, wxPython and other application toolkits can be used to create games.
wxPythonはアプリケーションを作るために設計されたツールキットです。 コンピュータゲームを作るためのライブラリは他にもあります。 とはいえ、wxPythonやその他のアプリケーション向けツールキットでもゲームを作ることはできます。
開発
We do not have images for our tetris game, we draw the tetrominoes using the drawing API available in the wxPython programming toolkit. Behind every computer game, there is a mathematical model. So it is in tetris.
これから作るテトリスは画像を使いません。 テトリミノはwxPythonの描画用APIを使って表示することにします。
コンピュータゲームの背後には、数学的なモデルがあります。 もちろんテトリスにもです。
Some ideas behind the game.
- We use wx.Timer to create a game cycle
- The tetrominoes are drawn
- The shapes move on a square by square basis (not pixel by pixel)
- Mathematically a board is a simple list of numbers
テトリスの背後にある考え方。
- ゲームサイクルを作るために wx.Timer を使う。
- テトリミノは、画像ではなく、APIを使って描画される。
- ミノは、ピクセル単位でなく、正方形のブロック単位で移動する。
- 数学的に、ゲーム板はシンプルな数字のリストとする。
The following example is a modified version of the tetris game, available with PyQt4 installation files.
以下のサンプルは、PyQt4版のテトリスを改造したものです。
#! /usr/bin/env python
# tetris.py
import wx
import random
class Tetris(wx.Frame):
def __init__(self, parent, id, title):
wx.Frame.__init__(self, parent, id, title, size=(180, 380))
self.statusbar = self.CreateStatusBar()
self.statusbar.SetStatusText('0')
self.board = Board(self)
self.board.SetFocus()
self.board.start()
self.Centre()
self.Show(True)
class Board(wx.Panel):
BoardWidth = 10
BoardHeight = 22
Speed = 300
ID_TIMER = 1
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.timer = wx.Timer(self, Board.ID_TIMER)
self.isWaitingAfterLine = False
self.curPiece = Shape()
self.nextPiece = Shape()
self.curX = 0
self.curY = 0
self.numLinesRemoved = 0
self.board = []
self.isStarted = False
self.isPaused = False
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
self.Bind(wx.EVT_TIMER, self.OnTimer, id=Board.ID_TIMER)
self.clearBoard()
def shapeAt(self, x, y):
return self.board[(y * Board.BoardWidth) + x]
def setShapeAt(self, x, y, shape):
self.board[(y * Board.BoardWidth) + x] = shape
def squareWidth(self):
return self.GetClientSize().GetWidth() / Board.BoardWidth
def squareHeight(self):
return self.GetClientSize().GetHeight() / Board.BoardHeight
def start(self):
if self.isPaused:
return
self.isStarted = True
self.isWaitingAfterLine = False
self.numLinesRemoved = 0
self.clearBoard()
self.newPiece()
self.timer.Start(Board.Speed)
def pause(self):
if not self.isStarted:
return
self.isPaused = not self.isPaused
statusbar = self.GetParent().statusbar
if self.isPaused:
self.timer.Stop()
statusbar.SetStatusText('paused')
else:
self.timer.Start(Board.Speed)
statusbar.SetStatusText(str(self.numLinesRemoved))
self.Reflesh()
def clearBoard(self):
for i in range(Board.BoardHeight * Board.BoardWidth):
self.board.append(Tetrominoes.NoShape)
def OnPaint(self, event):
dc = wx.PaintDC(self)
size = self.GetClientSize()
boardTop = size.GetHeight() - Board.BoardHeight * self.squareHeight()
for i in range(Board.BoardHeight):
for j in range(Board.BoardWidth):
shape = self.shapeAt(j, Board.BoardHeight - i - 1)
if shape != Tetrominoes.NoShape:
self.drawSquare(dc,
0 + j * self.squareWidth(),
boardTop + i * self.squareHeight(), shape)
if self.curPiece.shape() != Tetrominoes.NoShape:
for i in range(4):
x = self.curX + self.curPiece.x(i)
y = self.curY - self.curPiece.y(i)
self.drawSquare(dc, 0 + x * self.squareWidth(),
boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
self.curPiece.shape())
def OnKeyDown(self, event):
if not self.isStarted or self.curPiece.shape() == Tetrominoes.NoShape:
event.Skip()
return
keycode = event.GetKeyCode()
if keycode == ord('P') or keycode == ord('p'):
self.pause()
return
if self.isPaused:
return
elif keycode == wx.WXK_LEFT:
self.tryMove(self.curPiece, self.curX - 1, self.curY)
elif keycode == wx.WXK_RIGHT:
self.tryMove(self.curPiece, self.curX + 1, self.curY)
elif keycode == wx.WXK_DOWN:
self.tryMove(self.curPiece.rotatedRight(), self.curX, self.curY)
elif keycode == wx.WXK_UP:
self.tryMove(self.curPiece.rotatedLeft(), self.curX, self.curY)
elif keycode == wx.WXK_SPACE:
self.dropDown()
elif keycode == ord('D') or keycode == ord('d'):
self.oneLineDown()
else:
event.Skip()
def OnTimer(self, event):
if event.GetId() == Board.ID_TIMER:
if self.isWaitingAfterLine:
self.isWaitingAfterLine = False
self.newPiece()
else:
self.oneLineDown()
else:
event.Skip()
def dropDown(self):
newY = self.curY
while newY > 0:
if not self.tryMove(self.curPiece, self.curX, newY - 1):
break
newY -= 1
self.pieceDropped()
def oneLineDown(self):
if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
self.pieceDropped()
def pieceDropped(self):
for i in range(4):
x = self.curX + self.curPiece.x(i)
y = self.curY - self.curPiece.y(i)
self.setShapeAt(x, y, self.curPiece.shape())
self.removeFullLines()
if not self.isWaitingAfterLine:
self.newPiece()
def removeFullLines(self):
numFullLines = 0
statusbar = self.GetParent().statusbar
rowsToRemove = []
for i in range(Board.BoardHeight):
n = 0
for j in range(Board.BoardWidth):
if not self.shapeAt(j, i) == Tetrominoes.NoShape:
n = n + 1
if n == 10:
rowsToRemove.append(i)
rowsToRemove.reverse()
for m in rowsToRemove:
for k in range(m, Board.BoardHeight):
for l in range(Board.BoardWidth):
self.setShapeAt(l, k, self.shapeAt(l, k+1))
numFullLines += len(rowsToRemove)
if numFullLines > 0:
self.numLinesRemoved += numFullLines
statusbar.SetStatusText(str(self.numLinesRemoved))
self.isWaitingAfterLine = True
self.curPiece.setShape(Tetrominoes.NoShape)
self.Refresh()
def newPiece(self):
self.curPiece = self.nextPiece
statusbar = self.GetParent().statusbar
self.nextPiece.setRandomShape()
self.curX = Board.BoardWidth / 2 + 1
self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
if not self.tryMove(self.curPiece, self.curX, self.curY):
self.curPiece.setShape(Tetrominoes.NoShape)
self.timer.Stop()
self.isStarted = False
statusbar.SetStatusText('Game over')
def tryMove(self, newPiece, newX, newY):
for i in range(4):
x = newX + newPiece.x(i)
y = newY - newPiece.y(i)
if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
return False
if self.shapeAt(x, y) != Tetrominoes.NoShape:
return False
self.curPiece = newPiece
self.curX = newX
self.curY = newY
self.Refresh()
return True
def drawSquare(self, dc, x, y, shape):
colors = ['#000000', '#cc6666', '#66cc66', '#6666cc',
'#cccc66', '#cc66cc', '#66cccc', '#daaa00']
light = ['#000000', '#f89fab', '#79fc79', '#7979fc',
'#fcfc79', '#fc79fc', '#79fcfc', '#fcc600']
dark = ['#000000', '#803c3b', '#3b803b', '#3b3b80',
'#80803b', '#803b80', '#3b8080', '#806200']
pen = wx.Pen(light[shape])
pen.SetCap(wx.CAP_PROJECTING)
dc.SetPen(pen)
dc.DrawLine(x, y + self.squareHeight() - 1, x, y)
dc.DrawLine(x, y, x + self.squareWidth() - 1, y)
darkPen = wx.Pen(dark[shape])
darkPen.SetCap(wx.CAP_PROJECTING)
dc.SetPen(darkPen)
dc.DrawLine(x + 1, y + self.squareHeight() - 1,
x + self.squareWidth() - 1, y + self.squareHeight() - 1)
dc.DrawLine(x + self.squareWidth() - 1, y + self.squareHeight() -1,
x + self.squareWidth() - 1, y + 1)
dc.SetPen(wx.TRANSPARENT_PEN)
dc.SetBrush(wx.Brush(colors[shape]))
dc.DrawRectangle(x + 1, y + 1, self.squareWidth() - 2,
self.squareHeight() - 2)
class Tetrominoes(object):
NoShape = 0
ZShape = 1
SShape = 2
LineShape = 3
TShape = 4
SquareShape = 5
LShape = 6
MirroredLShape = 7
class Shape(object):
coordTable = (
((0, 0), (0, 0), (0, 0), (0, 0)),
((0, -1), (0, 0), (-1, 0), (-1, 1)),
((0, -1), (0, 0), (1, 0), (1, 1)),
((0, -1), (0, 0), (0, 1), (0, 2)),
((-1, 0), (0, 0), (1, 0), (0, 1)),
((0, 0), (1, 0), (0, 1), (1, 1)),
((-1, -1), (0, -1), (0, 0), (0, 1)),
((1, -1), (0, -1), (0, 0), (0, 1)))
def __init__(self):
self.coords = [[0, 0] for i in range(4)]
self.pieceShape = Tetrominoes.NoShape
self.setShape(Tetrominoes.NoShape)
def shape(self):
return self.pieceShape
def setShape(self, shape):
table = Shape.coordTable[shape]
for i in range(4):
for j in range(2):
self.coords[i][j] = table[i][j]
self.pieceShape = shape
def setRandomShape(self):
self.setShape(random.randint(1, 7))
def x(self, index):
return self.coords[index][0]
def y(self, index):
return self.coords[index][1]
def setX(self, index, x):
self.coords[index][0] = x
def setY(self, index, y):
self.coords[index][1] = y
def minX(self):
m = self.coord[0][0]
for i in range(4):
m = min(m, self.coords[i][0])
return m
def maxX(self):
m = self.coord[0][0]
for i in range(4):
m = max(m, self.coords[i][0])
return m
def minY(self):
m = self.coords[0][1]
for i in range(4):
m = min(m, self.coords[i][1])
return m
def maxY(self):
m = self.coords[0][1]
for i in range(4):
m = max(m, self.coords[i][1])
return m
def rotatedLeft(self):
if self.pieceShape == Tetrominoes.SquareShape:
return self
result = Shape()
result.pieceShape = self.pieceShape
for i in range(4):
result.setX(i, self.y(i))
result.setY(i, -self.x(i))
return result
def rotatedRight(self):
if self.pieceShape == Tetrominoes.SquareShape:
return self
result = Shape()
result.pieceShape = self.pieceShape
for i in range(4):
result.setX(i, -self.y(i))
result.setY(i, self.x(i))
return result
app = wx.App()
Tetris(None, -1, 'tetris.py')
app.MainLoop()
I have simplified the game a bit, so that it is easier to understand. The game starts immediately, after it is launched. We can pause the game by pressing the p key. The space key will drop the tetris piece immediately to the bottom. The d key will drop the piece one line down. (It can be used to speed up the falling a bit.) The game goes at constant speed, no acceleration is implemented. The score is the number of lines, that we have removed.
ゲームは少し簡略化してあるので理解しやすいと思います。 アプリケーションを立ち上げるとすぐにゲームが開始されます。 pキーを押すと、ゲームを中断することができます。 スペースキーでテトリミノを即座に落下させることができます。 dキーはミノを1行ずつ落下させます。(落下速度を少し速くしたい場合に使いましょう) ゲーム速度は一定で、速くなることはありません。 スコアは消去した行数です。
... self.curX = 0 self.curY = 0 self.numLinesRemoved = 0 self.board = [] ...
Before we start the game cycle, we initialize some important variables. The self.board variable is a list of numbers from 0 … 7. It represents the position of various shapes and remains of the shapes on the board.
ゲームループを開始する前に、重要な変数をいくつか初期化します。 self.board 変数は0〜7の数のリストです。 これは、ゲーム盤上のミノとその残骸の位置を示します。
for i in range(Board.BoardHeight):
for j in range(Board.BoardWidth):
shape = self.shapeAt(j, Board.BoardHeight - i - 1)
if shape != Tetrominoes.NoShape:
self.drawSquare(dc,
0 + j * self.squareWidth(),
boardTop + i * self.squareHeight(), shape)
The painting of the game is divided into two steps. In the first step, we draw all the shapes, or remains of the shapes, that have been dropped to the bottom of the board. All the squares are rememberd in the self.board list variable. We access it using the shapeAt() method.
ゲームの描画は2段階に分けられます。 第1段階では、ゲーム盤の底に落下した全てのミノとその残骸を描画します。 全ての正方形が self.board リスト変数に記録されます。 shapeAt() メソッドを使うことで、この変数にアクセスできます。
if self.curPiece.shape() != Tetrominoes.NoShape:
for i in range(4):
x = self.curX + self.curPiece.x(i)
y = self.curY - self.curPiece.y(i)
self.drawSquare(dc, 0 + x * self.squareWidth(),
boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
self.curPiece.shape())
The next step is drawing of the actual piece, that is falling down.
次の段階では現在落下中のミノを描画します。
elif keycode == wx.WXK_LEFT:
self.tryMove(self.curPiece, self.curX - 1, self.curY)
In the OnKeyDown() method we check for pressed keys. If we press the left arrow key, we try to move the piece to the left. We say try, because the piece might not be able to move.
OnKeyDown() メソッドで、押下されたキーを調べます。 左矢印キーを押すと、ミノを左に 動かそうと します。 なぜ「動かそうとする」なのかというと、ミノは移動できないかもしれないからです。
def tryMove(self, newPiece, newX, newY):
for i in range(4):
x = newX + newPiece.x(i)
y = newY - newPiece.y(i)
if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
return False
if self.shapeAt(x, y) != Tetrominoes.NoShape:
return False
self.curPiece = newPiece
self.curX = newX
self.curY = newY
self.Refresh()
return True
In the tryMove() method we try to move our shapes. If the shape is at the edge of the board or is adjacent to some other piece, we return false. Otherwise we place the current falling piece to a new position and return true.
tryMove() メソッドで、ミノを動かそうとします。 もしミノがゲーム盤の端だったり、他のミノに隣接していたりすると False を返します。 そうでなければ、現在落下中のミノを新しい位置に置き、 True を返します。
def OnTimer(self, event):
if event.GetId() == Board.ID_TIMER:
if self.isWaitingAfterLine:
self.isWaitingAfterLine = False
self.newPiece()
else:
self.oneLineDown()
else:
event.Skip()
In the OnTimer() method we either create a new piece, after the previous one was dropped to the bottom, or we move a falling piece one line down.
OnTimer() メソッドでは、以前のミノが底に落下しきった後に新しいミノを生成したり、落下中のミノを1行落としたりします。
def removeFullLines(self):
numFullLines = 0
rowsToRemove = []
for i in range(Board.BoardHeight):
n = 0
for j in range(Board.BoardWidth):
if not self.shapeAt(j, i) == Tetrominoes.NoShape:
n = n + 1
if n == 10:
rowsToRemove.append(i)
rowsToRemove.reverse()
for m in rowsToRemove:
for k in range(m, Board.BoardHeight):
for l in range(Board.BoardWidth):
self.setShapeAt(l, k, self.shapeAt(l, k + 1))
...
If the piece hits the bottom, we call the removeFullLines() method. First we find out all full lines. And we remove them. We do it by moving all lines above the current full line to be removed one line down. Notice, that we reverse the order of the lines to be removed. Otherwise, it would not work correctly. In our case we use a naive gravity. This means, that the pieces may be floating above empty gaps.
ミノが底に当たると、 removeFullLines() メソッドを呼びます。 揃った行を見つけて、その行を削除します。 現在揃っている行を取り除き、その上の行全てを1行下げることでこれを実現します。 削除される行の順番を逆にしていることに注意してください。 そうでもしなければ、正しく動かないでしょうからね。 今回は単純な重力を使用しています。 つまり、ミノは空の隙間の上に浮かぶこともありうるということです。
def newPiece(self):
self.curPiece = self.nextPiece
statusbar = self.GetParent().statusbar
self.nextPiece.setRandomShape()
self.curX = Board.BoardWidth / 2 + 1
self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
if not self.tryMove(self.curPiece, self.curX, self.curY):
self.curPiece.setShape(Tetrominoes.NoShape)
self.timer.Stop()
self.isStarted = False
statusbar.SetStatusText('Game over')
The newPiece() method creates randomly a new tetris piece. If the piece cannot go into it's initial position, the game is over.
newPiece() メソッドはランダムに新しいミノを生成します。 ミノを初期位置に配置できなければ、ゲームオーバーです。
The Shape class saves information about the tetris piece.
Shape クラスはテトリミノの情報を保持しています。
self.coords = [[0,0] for i in range(4)]
Upon creation we create an empty coordinates list. The list will save the coordinates of the tetris piece. For example, these tuples (0, -1), (0, 0), (1, 0), (1, 1) represent a rotated S-shape. The following diagram illustrates the shape.
ミノの作成中に、 空の座標リストを作成します。 このリストは、テトリミノの座標を保持します。 例えば、(0, -1), (0, 0), (1, 0), (1, 1) のタプルは回転したS-テトリミノを表します。 以下の図表がミノを図示しています。
When we draw the current falling piece, we draw it at self.curX, self.curY position. Then we look at the coordinates table and draw all the four squares.
現在落下中のミノを描画するときは、ミノを self.curX と self.curY の位置に描画します。 そして、座標テーブルを参照し、4つの正方形全てを描画します。
The original page is here.
Date: 2010-11-17 16:56:35 UTC
HTML generated by org-mode 7.3 in emacs 23