https://zetcode.com/wxpython/thetetrisgame/
Tetris is one of the most popular computer games in history. The original game was designed and programmed by Russian programmer Alexey Pajitnov in 1985. Since then, Tetris can appear in many forms on almost every computer platform.
Tetris is called a puzzle game. In this game, we have seven different shapes, called quadruplets: S-shape, Z-shape, T-shape, L-shape, linear, mirror L-shape and square. Each of these shapes is formed by four squares. The shape is falling off the board. The goal of Tetris is to move and rotate shapes to make them fit as well as possible. If we try to form a line, the line will be destroyed and we will score. We played Tetris until we reached the top.
Figure: Tetromino
wxPython is a toolkit designed to create applications. There are other libraries designed to create computer games. Nevertheless, wxpthon and other application toolkits can be used to create games.
development history
We don't have an image of a Tetris game. We use the drawing API available in Wx Python to draw a tetrad. Behind every computer game, there is a mathematical model. So it's in Tetris.
Some ideas behind the game:
- We Wx Timer is used to create a game loop
- The tetrad is drawn
- The shape moves on the basis of a square (not a pixel by pixel)
- Mathematically, the chessboard is a simple list of numbers
#!/usr/bin/python """ ZetCode wxPython tutorial This is Tetris game clone in wxPython. author: Jan Bodnar website: www.zetcode.com last modified: April 2018 """ import wx import random class Tetris(wx.Frame): def __init__(self, parent): wx.Frame.__init__(self, parent, size=(320, 740), style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX) self.initFrame() def initFrame(self): self.statusbar = self.CreateStatusBar() self.statusbar.SetStatusText('0') self.board = Board(self) self.board.SetFocus() self.board.start() self.SetTitle("Tetris") self.Centre() class Board(wx.Panel): BoardWidth = 10 BoardHeight = 22 Speed = 300 ID_TIMER = 1 def __init__(self, parent): wx.Panel.__init__(self, parent, style=wx.WANTS_CHARS) self.initBoard() def initBoard(self): 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.Refresh() 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 = numFullLines + len(rowsToRemove) if numFullLines > 0: self.numLinesRemoved = 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): coordsTable = ( ((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.coordsTable[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.coords[0][0] for i in range(4): m = min(m, self.coords[i][0]) return m def maxX(self): m = self.coords[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 def main(): app = wx.App() ex = Tetris(None) ex.Show() app.MainLoop() if __name__ == '__main__': main()
The game has been simplified a little to make it easier to understand. It starts immediately after the application starts. We can pause the game by pressing the key p. The Space key quickly lowers the bottom of the falling Tetris. The d key drops one block a line. (it can be used to accelerate the falling.) The game runs at a constant speed without acceleration. The score is the number of rows we deleted.
def __init__(self, *args, **kw): super(Board, self).__init__(*args, **kw)
Precautions for Windows users. If you cannot use the arrow keys, add style = Wx WANTS_ Chars into the board constructor.
... self.curX = 0 self.curY = 0 self.numLinesRemoved = 0 self.board = [] ...
Before we start the game loop, we initialize some important variables. Self The board variable is a list of numbers from 0 7. It represents various shapes and the position of the remains in the shape of the plate.
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 the rest of the shapes that have fallen to the bottom of the board. All the squares are remembered in self Board list variable. We use the shapeAt() method to access it.
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 to draw the actual part that is falling.
elif keycode == wx.WXK_LEFT: self.tryMove(self.curPiece, self.curX - 1, self.curY)
In the OnKeyDown() method, we check the pressed key. If we press the left arrow key, we will try to move the piece to the left. We say we try because the work may not be able to move.
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 this tryMove() method, we try to move our shape. If the shape is on the edge of the chessboard or adjacent to other pieces, we return False; Otherwise, we put the currently falling pieces in a new position and return 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 this OnTimer() method, we either create a new piece, after the previous piece falls to the bottom, or we move the falling piece down one line.
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 fragment hits the bottom, we call the removeFullLines() method. First, we find all the complete rows and delete them. We do this by moving all lines above the current full line to the next line. Notice that we reversed the order of the rows to be deleted. Otherwise, it will not work properly. In our example, we use a simple gravity. This means that these fragments may float above the blank gap.
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 randomly creates a new Tetris. If the piece cannot enter its initial position, the game ends.
The Shape class saves the expansion film about Tetris.
self.coords = [[0,0] for i in range(4)]
When creating, we create an empty list of coordinates. This list will save the coordinates of Tetris. For example, tuples (0, - 1), (0, 0), (- 1, 0), (- 1, - 1) represent rotating S-shapes. The following illustration illustrates the shape.
Figure: coordinates
When we draw the currently falling chess pieces, we are in self Draw it at curx and {self curY position. Then we look at the coordinate table and draw all four squares.
Figure: Tetris
This is the Tetris game in wxpthon.