原文: http://zetcode.com/wxpython/thetetrisgame/

俄罗斯方块游戏是有史以来最受欢迎的计算机游戏之一。 原始游戏是由俄罗斯程序员 Alexey Pajitnov 于 1985 年设计和编程的。此后,几乎所有版本的几乎所有计算机平台上都可以使用俄罗斯方块。

俄罗斯方块被称为下降块益智游戏。 在这个游戏中,我们有七个不同的形状,称为 tetrominoes:S 形,Z 形,T 形,L 形,线形,MirroredL 形和方形。 这些形状中的每一个都形成有四个正方形。 形状从板上掉下来。 俄罗斯方块游戏的目的是移动和旋转形状,以使其尽可能地适合。 如果我们设法形成一行,则该行将被破坏并得分。 我们玩俄罗斯方块游戏,直到达到顶峰。

wxPython 中的俄罗斯方块游戏 - 图1

图:Tetrominoes

wxPython 是旨在创建应用的工具包。 还有其他一些旨在创建计算机游戏的库。 不过,可以使用 wxPython 和其他应用工具包来创建游戏。

开发

我们的俄罗斯方块游戏没有图像,我们使用 wxPython 中提供的绘图 API 绘制四方块。 每个计算机游戏的背后都有一个数学模型。 俄罗斯方块也是如此。

游戏背后的一些想法:

  • 我们使用wx.Timer创建游戏周期
  • 绘制四方块
  • 形状以正方形为单位移动(不是逐个像素移动)
  • 从数学上讲,棋盘是简单的数字列表

tetris.py

  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. ZetCode wxPython tutorial
  5. This is Tetris game clone in wxPython.
  6. author: Jan Bodnar
  7. website: www.zetcode.com
  8. last modified: May 2018
  9. """
  10. import wx
  11. import random
  12. class Tetris(wx.Frame):
  13. def __init__(self, parent):
  14. wx.Frame.__init__(self, parent, size=(180, 380),
  15. style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX)
  16. self.initFrame()
  17. def initFrame(self):
  18. self.statusbar = self.CreateStatusBar()
  19. self.statusbar.SetStatusText('0')
  20. self.board = Board(self)
  21. self.board.SetFocus()
  22. self.board.start()
  23. self.SetTitle("Tetris")
  24. self.Centre()
  25. class Board(wx.Panel):
  26. BoardWidth = 10
  27. BoardHeight = 22
  28. Speed = 300
  29. ID_TIMER = 1
  30. def __init__(self, *args, **kw):
  31. super(Board, self).__init__(*args, **kw)
  32. self.initBoard()
  33. def initBoard(self):
  34. self.timer = wx.Timer(self, Board.ID_TIMER)
  35. self.isWaitingAfterLine = False
  36. self.curPiece = Shape()
  37. self.nextPiece = Shape()
  38. self.curX = 0
  39. self.curY = 0
  40. self.numLinesRemoved = 0
  41. self.board = []
  42. self.isStarted = False
  43. self.isPaused = False
  44. self.Bind(wx.EVT_PAINT, self.OnPaint)
  45. self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
  46. self.Bind(wx.EVT_TIMER, self.OnTimer, id=Board.ID_TIMER)
  47. self.clearBoard()
  48. def shapeAt(self, x, y):
  49. return self.board[(y * Board.BoardWidth) + x]
  50. def setShapeAt(self, x, y, shape):
  51. self.board[(y * Board.BoardWidth) + x] = shape
  52. def squareWidth(self):
  53. return self.GetClientSize().GetWidth() // Board.BoardWidth
  54. def squareHeight(self):
  55. return self.GetClientSize().GetHeight() // Board.BoardHeight
  56. def start(self):
  57. if self.isPaused:
  58. return
  59. self.isStarted = True
  60. self.isWaitingAfterLine = False
  61. self.numLinesRemoved = 0
  62. self.clearBoard()
  63. self.newPiece()
  64. self.timer.Start(Board.Speed)
  65. def pause(self):
  66. if not self.isStarted:
  67. return
  68. self.isPaused = not self.isPaused
  69. statusbar = self.GetParent().statusbar
  70. if self.isPaused:
  71. self.timer.Stop()
  72. statusbar.SetStatusText('paused')
  73. else:
  74. self.timer.Start(Board.Speed)
  75. statusbar.SetStatusText(str(self.numLinesRemoved))
  76. self.Refresh()
  77. def clearBoard(self):
  78. for i in range(Board.BoardHeight * Board.BoardWidth):
  79. self.board.append(Tetrominoes.NoShape)
  80. def OnPaint(self, event):
  81. dc = wx.PaintDC(self)
  82. size = self.GetClientSize()
  83. boardTop = size.GetHeight() - Board.BoardHeight * self.squareHeight()
  84. for i in range(Board.BoardHeight):
  85. for j in range(Board.BoardWidth):
  86. shape = self.shapeAt(j, Board.BoardHeight - i - 1)
  87. if shape != Tetrominoes.NoShape:
  88. self.drawSquare(dc,
  89. 0 + j * self.squareWidth(),
  90. boardTop + i * self.squareHeight(), shape)
  91. if self.curPiece.shape() != Tetrominoes.NoShape:
  92. for i in range(4):
  93. x = self.curX + self.curPiece.x(i)
  94. y = self.curY - self.curPiece.y(i)
  95. self.drawSquare(dc, 0 + x * self.squareWidth(),
  96. boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
  97. self.curPiece.shape())
  98. def OnKeyDown(self, event):
  99. if not self.isStarted or self.curPiece.shape() == Tetrominoes.NoShape:
  100. event.Skip()
  101. return
  102. keycode = event.GetKeyCode()
  103. if keycode == ord('P') or keycode == ord('p'):
  104. self.pause()
  105. return
  106. if self.isPaused:
  107. return
  108. elif keycode == wx.WXK_LEFT:
  109. self.tryMove(self.curPiece, self.curX - 1, self.curY)
  110. elif keycode == wx.WXK_RIGHT:
  111. self.tryMove(self.curPiece, self.curX + 1, self.curY)
  112. elif keycode == wx.WXK_DOWN:
  113. self.tryMove(self.curPiece.rotatedRight(), self.curX, self.curY)
  114. elif keycode == wx.WXK_UP:
  115. self.tryMove(self.curPiece.rotatedLeft(), self.curX, self.curY)
  116. elif keycode == wx.WXK_SPACE:
  117. self.dropDown()
  118. elif keycode == ord('D') or keycode == ord('d'):
  119. self.oneLineDown()
  120. else:
  121. event.Skip()
  122. def OnTimer(self, event):
  123. if event.GetId() == Board.ID_TIMER:
  124. if self.isWaitingAfterLine:
  125. self.isWaitingAfterLine = False
  126. self.newPiece()
  127. else:
  128. self.oneLineDown()
  129. else:
  130. event.Skip()
  131. def dropDown(self):
  132. newY = self.curY
  133. while newY > 0:
  134. if not self.tryMove(self.curPiece, self.curX, newY - 1):
  135. break
  136. newY -= 1
  137. self.pieceDropped()
  138. def oneLineDown(self):
  139. if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
  140. self.pieceDropped()
  141. def pieceDropped(self):
  142. for i in range(4):
  143. x = self.curX + self.curPiece.x(i)
  144. y = self.curY - self.curPiece.y(i)
  145. self.setShapeAt(x, y, self.curPiece.shape())
  146. self.removeFullLines()
  147. if not self.isWaitingAfterLine:
  148. self.newPiece()
  149. def removeFullLines(self):
  150. numFullLines = 0
  151. statusbar = self.GetParent().statusbar
  152. rowsToRemove = []
  153. for i in range(Board.BoardHeight):
  154. n = 0
  155. for j in range(Board.BoardWidth):
  156. if not self.shapeAt(j, i) == Tetrominoes.NoShape:
  157. n = n + 1
  158. if n == 10:
  159. rowsToRemove.append(i)
  160. rowsToRemove.reverse()
  161. for m in rowsToRemove:
  162. for k in range(m, Board.BoardHeight):
  163. for l in range(Board.BoardWidth):
  164. self.setShapeAt(l, k, self.shapeAt(l, k + 1))
  165. numFullLines = numFullLines + len(rowsToRemove)
  166. if numFullLines > 0:
  167. self.numLinesRemoved = self.numLinesRemoved + numFullLines
  168. statusbar.SetStatusText(str(self.numLinesRemoved))
  169. self.isWaitingAfterLine = True
  170. self.curPiece.setShape(Tetrominoes.NoShape)
  171. self.Refresh()
  172. def newPiece(self):
  173. self.curPiece = self.nextPiece
  174. statusbar = self.GetParent().statusbar
  175. self.nextPiece.setRandomShape()
  176. self.curX = Board.BoardWidth // 2 + 1
  177. self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
  178. if not self.tryMove(self.curPiece, self.curX, self.curY):
  179. self.curPiece.setShape(Tetrominoes.NoShape)
  180. self.timer.Stop()
  181. self.isStarted = False
  182. statusbar.SetStatusText('Game over')
  183. def tryMove(self, newPiece, newX, newY):
  184. for i in range(4):
  185. x = newX + newPiece.x(i)
  186. y = newY - newPiece.y(i)
  187. if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
  188. return False
  189. if self.shapeAt(x, y) != Tetrominoes.NoShape:
  190. return False
  191. self.curPiece = newPiece
  192. self.curX = newX
  193. self.curY = newY
  194. self.Refresh()
  195. return True
  196. def drawSquare(self, dc, x, y, shape):
  197. colors = ['#000000', '#CC6666', '#66CC66', '#6666CC',
  198. '#CCCC66', '#CC66CC', '#66CCCC', '#DAAA00']
  199. light = ['#000000', '#F89FAB', '#79FC79', '#7979FC',
  200. '#FCFC79', '#FC79FC', '#79FCFC', '#FCC600']
  201. dark = ['#000000', '#803C3B', '#3B803B', '#3B3B80',
  202. '#80803B', '#803B80', '#3B8080', '#806200']
  203. pen = wx.Pen(light[shape])
  204. pen.SetCap(wx.CAP_PROJECTING)
  205. dc.SetPen(pen)
  206. dc.DrawLine(x, y + self.squareHeight() - 1, x, y)
  207. dc.DrawLine(x, y, x + self.squareWidth() - 1, y)
  208. darkpen = wx.Pen(dark[shape])
  209. darkpen.SetCap(wx.CAP_PROJECTING)
  210. dc.SetPen(darkpen)
  211. dc.DrawLine(x + 1, y + self.squareHeight() - 1,
  212. x + self.squareWidth() - 1, y + self.squareHeight() - 1)
  213. dc.DrawLine(x + self.squareWidth() - 1,
  214. y + self.squareHeight() - 1, x + self.squareWidth() - 1, y + 1)
  215. dc.SetPen(wx.TRANSPARENT_PEN)
  216. dc.SetBrush(wx.Brush(colors[shape]))
  217. dc.DrawRectangle(x + 1, y + 1, self.squareWidth() - 2,
  218. self.squareHeight() - 2)
  219. class Tetrominoes(object):
  220. NoShape = 0
  221. ZShape = 1
  222. SShape = 2
  223. LineShape = 3
  224. TShape = 4
  225. SquareShape = 5
  226. LShape = 6
  227. MirroredLShape = 7
  228. class Shape(object):
  229. coordsTable = (
  230. ((0, 0), (0, 0), (0, 0), (0, 0)),
  231. ((0, -1), (0, 0), (-1, 0), (-1, 1)),
  232. ((0, -1), (0, 0), (1, 0), (1, 1)),
  233. ((0, -1), (0, 0), (0, 1), (0, 2)),
  234. ((-1, 0), (0, 0), (1, 0), (0, 1)),
  235. ((0, 0), (1, 0), (0, 1), (1, 1)),
  236. ((-1, -1), (0, -1), (0, 0), (0, 1)),
  237. ((1, -1), (0, -1), (0, 0), (0, 1))
  238. )
  239. def __init__(self):
  240. self.coords = [[0,0] for i in range(4)]
  241. self.pieceShape = Tetrominoes.NoShape
  242. self.setShape(Tetrominoes.NoShape)
  243. def shape(self):
  244. return self.pieceShape
  245. def setShape(self, shape):
  246. table = Shape.coordsTable[shape]
  247. for i in range(4):
  248. for j in range(2):
  249. self.coords[i][j] = table[i][j]
  250. self.pieceShape = shape
  251. def setRandomShape(self):
  252. self.setShape(random.randint(1, 7))
  253. def x(self, index):
  254. return self.coords[index][0]
  255. def y(self, index):
  256. return self.coords[index][1]
  257. def setX(self, index, x):
  258. self.coords[index][0] = x
  259. def setY(self, index, y):
  260. self.coords[index][1] = y
  261. def minX(self):
  262. m = self.coords[0][0]
  263. for i in range(4):
  264. m = min(m, self.coords[i][0])
  265. return m
  266. def maxX(self):
  267. m = self.coords[0][0]
  268. for i in range(4):
  269. m = max(m, self.coords[i][0])
  270. return m
  271. def minY(self):
  272. m = self.coords[0][1]
  273. for i in range(4):
  274. m = min(m, self.coords[i][1])
  275. return m
  276. def maxY(self):
  277. m = self.coords[0][1]
  278. for i in range(4):
  279. m = max(m, self.coords[i][1])
  280. return m
  281. def rotatedLeft(self):
  282. if self.pieceShape == Tetrominoes.SquareShape:
  283. return self
  284. result = Shape()
  285. result.pieceShape = self.pieceShape
  286. for i in range(4):
  287. result.setX(i, self.y(i))
  288. result.setY(i, -self.x(i))
  289. return result
  290. def rotatedRight(self):
  291. if self.pieceShape == Tetrominoes.SquareShape:
  292. return self
  293. result = Shape()
  294. result.pieceShape = self.pieceShape
  295. for i in range(4):
  296. result.setX(i, -self.y(i))
  297. result.setY(i, self.x(i))
  298. return result
  299. def main():
  300. app = wx.App()
  301. ex = Tetris(None)
  302. ex.Show()
  303. app.MainLoop()
  304. if __name__ == '__main__':
  305. main()

游戏进行了简化,以便于理解。 它在启动应用后立即启动。 我们可以通过按 p 键暂停游戏。 空格键将下降的俄罗斯方块片段立即放到底部。 d 键将棋子下降一行。 (可以用来加快跌落速度。)游戏以恒定速度进行,没有实现加速。 分数是我们已删除的行数。

  1. def __init__(self, *args, **kw):
  2. super(Board, self).__init__(*args, **kw)

Windows 用户注意事项。 如果无法使用箭头键,则将style=wx.WANTS_CHARS添加到板子构造器中。

  1. ...
  2. self.curX = 0
  3. self.curY = 0
  4. self.numLinesRemoved = 0
  5. self.board = []
  6. ...

在开始游戏周期之前,我们先初始化一些重要的变量。 self.board变量是一个从 0 到 7 的数字的列表。它表示各种形状的位置以及板上形状的其余部分。

  1. for i in range(Board.BoardHeight):
  2. for j in range(Board.BoardWidth):
  3. shape = self.shapeAt(j, Board.BoardHeight - i - 1)
  4. if shape != Tetrominoes.NoShape:
  5. self.drawSquare(dc,
  6. 0 + j * self.squareWidth(),
  7. boardTop + i * self.squareHeight(), shape)

游戏的绘图分为两个步骤。 在第一步中,我们绘制所有形状或已放置到板底部的形状的其余部分。 所有正方形都记在self.board列表变量中。 我们使用shapeAt()方法访问它。

  1. if self.curPiece.shape() != Tetrominoes.NoShape:
  2. for i in range(4):
  3. x = self.curX + self.curPiece.x(i)
  4. y = self.curY - self.curPiece.y(i)
  5. self.drawSquare(dc, 0 + x * self.squareWidth(),
  6. boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
  7. self.curPiece.shape())

下一步是绘制掉落的实际零件。

  1. elif keycode == wx.WXK_LEFT:
  2. self.tryMove(self.curPiece, self.curX - 1, self.curY)

OnKeyDown()方法中,我们检查按键是否按下。 如果按向左箭头键,我们将尝试将棋子向左移动。 我们说尝试,因为该部分可能无法移动。

  1. def tryMove(self, newPiece, newX, newY):
  2. for i in range(4):
  3. x = newX + newPiece.x(i)
  4. y = newY - newPiece.y(i)
  5. if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
  6. return False
  7. if self.shapeAt(x, y) != Tetrominoes.NoShape:
  8. return False
  9. self.curPiece = newPiece
  10. self.curX = newX
  11. self.curY = newY
  12. self.Refresh()
  13. return True

tryMove()方法中,我们尝试移动形状。 如果形状在板的边缘或与其他零件相邻,则返回False; 否则,我们将当前下降片放到新位置并返回True

  1. def OnTimer(self, event):
  2. if event.GetId() == Board.ID_TIMER:
  3. if self.isWaitingAfterLine:
  4. self.isWaitingAfterLine = False
  5. self.newPiece()
  6. else:
  7. self.oneLineDown()
  8. else:
  9. event.Skip()

OnTimer()方法中,我们可以创建一个新的片段,将前一个片段放到底部,或者将下降的片段向下移动一行。

  1. def removeFullLines(self):
  2. numFullLines = 0
  3. rowsToRemove = []
  4. for i in range(Board.BoardHeight):
  5. n = 0
  6. for j in range(Board.BoardWidth):
  7. if not self.shapeAt(j, i) == Tetrominoes.NoShape:
  8. n = n + 1
  9. if n == 10:
  10. rowsToRemove.append(i)
  11. rowsToRemove.reverse()
  12. for m in rowsToRemove:
  13. for k in range(m, Board.BoardHeight):
  14. for l in range(Board.BoardWidth):
  15. self.setShapeAt(l, k, self.shapeAt(l, k + 1))
  16. ...

如果片段触底,我们将调用removeFullLines()方法。 首先,我们找出所有实线并将其删除。 通过将所有行移动到当前全行上方来将其向下移动一行来实现。 请注意,我们颠倒了要删除的行的顺序。 否则,它将无法正常工作。 在我们的情况下,我们使用朴素重力。 这意味着碎片可能会漂浮在空的间隙上方。

  1. def newPiece(self):
  2. self.curPiece = self.nextPiece
  3. statusbar = self.GetParent().statusbar
  4. self.nextPiece.setRandomShape()
  5. self.curX = Board.BoardWidth / 2 + 1
  6. self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
  7. if not self.tryMove(self.curPiece, self.curX, self.curY):
  8. self.curPiece.setShape(Tetrominoes.NoShape)
  9. self.timer.Stop()
  10. self.isStarted = False
  11. statusbar.SetStatusText('Game over')

newPiece()方法随机创建一个新的俄罗斯方块。 如果棋子无法进入其初始位置,则游戏结束。

Shape类保存有关俄罗斯方块的信息。

  1. self.coords = [[0,0] for i in range(4)]

创建后,我们将创建一个空坐标列表。 该列表将保存俄罗斯方块的坐标。 例如,元组(0,-1),(0、0),(-1、0),(-1,-1)表示旋转的 S 形。 下图说明了形状。

wxPython 中的俄罗斯方块游戏 - 图2

图:坐标

当绘制当前下降片时,将其绘制在self.curXself.curY position处。 然后,我们查看坐标表并绘制所有四个正方形。

wxPython 中的俄罗斯方块游戏 - 图3

图:俄罗斯方块

这是 wxPython 中的俄罗斯方块游戏。