テトリス
テトリスとは
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