http://zetcode.com/tutorials/ironpythontutorial/tetris/

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

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

IronPython Mono Winforms 中的俄罗斯方块游戏 - 图1

图:Tetrominoes

开发

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

游戏背后的一些想法。

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

以下示例是俄罗斯方块游戏的修改版,可用于 PyQt4 安装文件。

tetris.py

  1. #!/usr/bin/ipy
  2. import clr
  3. clr.AddReference("System.Windows.Forms")
  4. clr.AddReference("System.Drawing")
  5. clr.AddReference("System")
  6. from System.Windows.Forms import Application, Form, FormBorderStyle
  7. from System.Windows.Forms import UserControl, Keys, Timer, StatusBar
  8. from System.Drawing import Size, Color, SolidBrush, Pen
  9. from System.Drawing.Drawing2D import LineCap
  10. from System.ComponentModel import Container
  11. from System import Random
  12. class Tetrominoes(object):
  13. NoShape = 0
  14. ZShape = 1
  15. SShape = 2
  16. LineShape = 3
  17. TShape = 4
  18. SquareShape = 5
  19. LShape = 6
  20. MirroredLShape = 7
  21. class Board(UserControl):
  22. BoardWidth = 10
  23. BoardHeight = 22
  24. Speed = 200
  25. ID_TIMER = 1
  26. def __init__(self):
  27. self.Text = 'Snake'
  28. self.components = Container()
  29. self.isWaitingAfterLine = False
  30. self.curPiece = Shape()
  31. self.nextPiece = Shape()
  32. self.curX = 0
  33. self.curY = 0
  34. self.numLinesRemoved = 0
  35. self.board = []
  36. self.DoubleBuffered = True
  37. self.isStarted = False
  38. self.isPaused = False
  39. self.timer = Timer(self.components)
  40. self.timer.Enabled = True
  41. self.timer.Interval = Board.Speed
  42. self.timer.Tick += self.OnTick
  43. self.Paint += self.OnPaint
  44. self.KeyUp += self.OnKeyUp
  45. self.ClearBoard()
  46. def ShapeAt(self, x, y):
  47. return self.board[(y * Board.BoardWidth) + x]
  48. def SetShapeAt(self, x, y, shape):
  49. self.board[(y * Board.BoardWidth) + x] = shape
  50. def SquareWidth(self):
  51. return self.ClientSize.Width / Board.BoardWidth
  52. def SquareHeight(self):
  53. return self.ClientSize.Height / Board.BoardHeight
  54. def Start(self):
  55. if self.isPaused:
  56. return
  57. self.isStarted = True
  58. self.isWaitingAfterLine = False
  59. self.numLinesRemoved = 0
  60. self.ClearBoard()
  61. self.NewPiece()
  62. def Pause(self):
  63. if not self.isStarted:
  64. return
  65. self.isPaused = not self.isPaused
  66. statusbar = self.Parent.statusbar
  67. if self.isPaused:
  68. self.timer.Stop()
  69. statusbar.Text = 'paused'
  70. else:
  71. self.timer.Start()
  72. statusbar.Text = str(self.numLinesRemoved)
  73. self.Refresh()
  74. def ClearBoard(self):
  75. for i in range(Board.BoardHeight * Board.BoardWidth):
  76. self.board.append(Tetrominoes.NoShape)
  77. def OnPaint(self, event):
  78. g = event.Graphics
  79. size = self.ClientSize
  80. boardTop = size.Height - Board.BoardHeight * self.SquareHeight()
  81. for i in range(Board.BoardHeight):
  82. for j in range(Board.BoardWidth):
  83. shape = self.ShapeAt(j, Board.BoardHeight - i - 1)
  84. if shape != Tetrominoes.NoShape:
  85. self.DrawSquare(g,
  86. 0 + j * self.SquareWidth(),
  87. boardTop + i * self.SquareHeight(), shape)
  88. if self.curPiece.GetShape() != Tetrominoes.NoShape:
  89. for i in range(4):
  90. x = self.curX + self.curPiece.x(i)
  91. y = self.curY - self.curPiece.y(i)
  92. self.DrawSquare(g, 0 + x * self.SquareWidth(),
  93. boardTop + (Board.BoardHeight - y - 1) * self.SquareHeight(),
  94. self.curPiece.GetShape())
  95. g.Dispose()
  96. def OnKeyUp(self, event):
  97. if not self.isStarted or self.curPiece.GetShape() == Tetrominoes.NoShape:
  98. return
  99. key = event.KeyCode
  100. if key == Keys.P:
  101. self.Pause()
  102. return
  103. if self.isPaused:
  104. return
  105. elif key == Keys.Left:
  106. self.TryMove(self.curPiece, self.curX - 1, self.curY)
  107. elif key == Keys.Right:
  108. self.TryMove(self.curPiece, self.curX + 1, self.curY)
  109. elif key == Keys.Down:
  110. self.TryMove(self.curPiece.RotatedRight(), self.curX, self.curY)
  111. elif key == Keys.Up:
  112. self.TryMove(self.curPiece.RotatedLeft(), self.curX, self.curY)
  113. elif key == Keys.Space:
  114. self.DropDown()
  115. elif key == Keys.D:
  116. self.OneLineDown()
  117. def OnTick(self, sender, event):
  118. if self.isWaitingAfterLine:
  119. self.isWaitingAfterLine = False
  120. self.NewPiece()
  121. else:
  122. self.OneLineDown()
  123. def DropDown(self):
  124. newY = self.curY
  125. while newY > 0:
  126. if not self.TryMove(self.curPiece, self.curX, newY - 1):
  127. break
  128. newY -= 1
  129. self.PieceDropped()
  130. def OneLineDown(self):
  131. if not self.TryMove(self.curPiece, self.curX, self.curY - 1):
  132. self.PieceDropped()
  133. def PieceDropped(self):
  134. for i in range(4):
  135. x = self.curX + self.curPiece.x(i)
  136. y = self.curY - self.curPiece.y(i)
  137. self.SetShapeAt(x, y, self.curPiece.GetShape())
  138. self.RemoveFullLines()
  139. if not self.isWaitingAfterLine:
  140. self.NewPiece()
  141. def RemoveFullLines(self):
  142. numFullLines = 0
  143. statusbar = self.Parent.statusbar
  144. rowsToRemove = []
  145. for i in range(Board.BoardHeight):
  146. n = 0
  147. for j in range(Board.BoardWidth):
  148. if not self.ShapeAt(j, i) == Tetrominoes.NoShape:
  149. n = n + 1
  150. if n == 10:
  151. rowsToRemove.append(i)
  152. rowsToRemove.reverse()
  153. for m in rowsToRemove:
  154. for k in range(m, Board.BoardHeight):
  155. for l in range(Board.BoardWidth):
  156. self.SetShapeAt(l, k, self.ShapeAt(l, k + 1))
  157. numFullLines = numFullLines + len(rowsToRemove)
  158. if numFullLines > 0:
  159. self.numLinesRemoved = self.numLinesRemoved + numFullLines
  160. statusbar.Text = str(self.numLinesRemoved)
  161. self.isWaitingAfterLine = True
  162. self.curPiece.SetShape(Tetrominoes.NoShape)
  163. self.Refresh()
  164. def NewPiece(self):
  165. self.curPiece = self.nextPiece
  166. statusbar = self.Parent.statusbar
  167. self.nextPiece.SetRandomShape()
  168. self.curX = Board.BoardWidth / 2 + 1
  169. self.curY = Board.BoardHeight - 1 + self.curPiece.MinY()
  170. if not self.TryMove(self.curPiece, self.curX, self.curY):
  171. self.curPiece.SetShape(Tetrominoes.NoShape)
  172. self.timer.Stop()
  173. self.isStarted = False
  174. statusbar.Text = 'Game over'
  175. def TryMove(self, newPiece, newX, newY):
  176. for i in range(4):
  177. x = newX + newPiece.x(i)
  178. y = newY - newPiece.y(i)
  179. if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
  180. return False
  181. if self.ShapeAt(x, y) != Tetrominoes.NoShape:
  182. return False
  183. self.curPiece = newPiece
  184. self.curX = newX
  185. self.curY = newY
  186. self.Refresh()
  187. return True
  188. def DrawSquare(self, g, x, y, shape):
  189. colors = [ (0, 0, 0), (204, 102, 102),
  190. (102, 204, 102), (102, 102, 204),
  191. (204, 204, 102), (204, 102, 204),
  192. (102, 204, 204), (218, 170, 0) ]
  193. light = [ (0, 0, 0), (248, 159, 171),
  194. (121, 252, 121), (121, 121, 252),
  195. (252, 252, 121), (252, 121, 252),
  196. (121, 252, 252), (252, 198, 0) ]
  197. dark = [ (0, 0, 0), (128, 59, 59),
  198. (59, 128, 59), (59, 59, 128),
  199. (128, 128, 59), (128, 59, 128),
  200. (59, 128, 128), (128, 98, 0) ]
  201. pen = Pen(Color.FromArgb(light[shape][0], light[shape][1],
  202. light[shape][2]), 1)
  203. pen.StartCap = LineCap.Flat
  204. pen.EndCap = LineCap.Flat
  205. g.DrawLine(pen, x, y + self.SquareHeight() - 1, x, y)
  206. g.DrawLine(pen, x, y, x + self.SquareWidth() - 1, y)
  207. darkpen = Pen(Color.FromArgb(dark[shape][0], dark[shape][1],
  208. dark[shape][2]), 1)
  209. darkpen.StartCap = LineCap.Flat
  210. darkpen.EndCap = LineCap.Flat
  211. g.DrawLine(darkpen, x + 1, y + self.SquareHeight() - 1,
  212. x + self.SquareWidth() - 1, y + self.SquareHeight() - 1)
  213. g.DrawLine(darkpen, x + self.SquareWidth() - 1,
  214. y + self.SquareHeight() - 1, x + self.SquareWidth() - 1, y + 1)
  215. g.FillRectangle(SolidBrush(Color.FromArgb(colors[shape][0], colors[shape][1],
  216. colors[shape][2])), x + 1, y + 1, self.SquareWidth() - 1,
  217. self.SquareHeight() - 2)
  218. pen.Dispose()
  219. darkpen.Dispose()
  220. class Shape(object):
  221. coordsTable = (
  222. ((0, 0), (0, 0), (0, 0), (0, 0)),
  223. ((0, -1), (0, 0), (-1, 0), (-1, 1)),
  224. ((0, -1), (0, 0), (1, 0), (1, 1)),
  225. ((0, -1), (0, 0), (0, 1), (0, 2)),
  226. ((-1, 0), (0, 0), (1, 0), (0, 1)),
  227. ((0, 0), (1, 0), (0, 1), (1, 1)),
  228. ((-1, -1), (0, -1), (0, 0), (0, 1)),
  229. ((1, -1), (0, -1), (0, 0), (0, 1))
  230. )
  231. def __init__(self):
  232. self.coords = [[0,0] for i in range(4)]
  233. self.pieceShape = Tetrominoes.NoShape
  234. self.SetShape(Tetrominoes.NoShape)
  235. def GetShape(self):
  236. return self.pieceShape
  237. def SetShape(self, shape):
  238. table = Shape.coordsTable[shape]
  239. for i in range(4):
  240. for j in range(2):
  241. self.coords[i][j] = table[i][j]
  242. self.pieceShape = shape
  243. def SetRandomShape(self):
  244. rand = Random()
  245. self.SetShape(rand.Next(1, 7))
  246. def x(self, index):
  247. return self.coords[index][0]
  248. def y(self, index):
  249. return self.coords[index][1]
  250. def SetX(self, index, x):
  251. self.coords[index][0] = x
  252. def SetY(self, index, y):
  253. self.coords[index][1] = y
  254. def MaxX(self):
  255. m = self.coords[0][0]
  256. for i in range(4):
  257. m = max(m, self.coords[i][0])
  258. return m
  259. def MinY(self):
  260. m = self.coords[0][1]
  261. for i in range(4):
  262. m = min(m, self.coords[i][1])
  263. return m
  264. def RotatedLeft(self):
  265. if self.pieceShape == Tetrominoes.SquareShape:
  266. return self
  267. result = Shape()
  268. result.pieceShape = self.pieceShape
  269. for i in range(4):
  270. result.SetX(i, self.y(i))
  271. result.SetY(i, -self.x(i))
  272. return result
  273. def RotatedRight(self):
  274. if self.pieceShape == Tetrominoes.SquareShape:
  275. return self
  276. result = Shape()
  277. result.pieceShape = self.pieceShape
  278. for i in range(4):
  279. result.SetX(i, -self.y(i))
  280. result.SetY(i, self.x(i))
  281. return result
  282. class IForm(Form):
  283. def __init__(self):
  284. self.Text = 'Tetris'
  285. self.Width = 200
  286. self.Height = 430
  287. self.FormBorderStyle = FormBorderStyle.FixedSingle
  288. board = Board()
  289. board.Width = 195
  290. board.Height = 380
  291. self.Controls.Add(board)
  292. self.statusbar = StatusBar()
  293. self.statusbar.Parent = self
  294. self.statusbar.Text = 'Ready'
  295. board.Start()
  296. self.CenterToScreen()
  297. Application.Run(IForm())

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

  1. class Tetrominoes(object):
  2. NoShape = 0
  3. ZShape = 1
  4. SShape = 2
  5. LineShape = 3
  6. TShape = 4
  7. SquareShape = 5
  8. LShape = 6
  9. MirroredLShape = 7

tetrominoes 有七种不同类型。

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

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

  1. def ClearBoard(self):
  2. for i in range(Board.BoardHeight * Board.BoardWidth):
  3. self.board.append(Tetrominoes.NoShape)

ClearBoard()方法清除电路板。 它用Tetrominoes.NoShape值填充self.board变量。

俄罗斯方块游戏中的绘图是通过OnPaint()方法完成的。

  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(g,
  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(g, 0 + x * self.squareWidth(),
  6. boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
  7. self.curPiece.shape())

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

OnKeyUp()方法中,我们检查按键是否按下。

  1. elif key == Keys.Left:
  2. self.tryMove(self.curPiece, self.curX - 1, self.curY)

如果按向左箭头键,我们将尝试将棋子向左移动。 我们说尝试,因为这片可能无法移动。

TryMove()方法中,我们尝试移动形状。 如果无法移动该片段,则返回False

  1. for i in range(4):
  2. x = newX + newPiece.x(i)
  3. y = newY - newPiece.y(i)
  4. if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
  5. return False
  6. if self.ShapeAt(x, y) != Tetrominoes.NoShape:
  7. return False

如果形状在板的边缘或与其他零件相邻,则返回False

  1. self.curPiece = newPiece
  2. self.curX = newX
  3. self.curY = newY
  4. self.Refresh()
  5. return True

否则,我们将当前的下降片放到新位置并返回True

  1. def OnTick(self, sender, event):
  2. if self.isWaitingAfterLine:
  3. self.isWaitingAfterLine = False
  4. self.NewPiece()
  5. else:
  6. self.OneLineDown()

OnTick()方法中,我们要么在前一个击中底部之后创建一个新片段,要么将下降的片段向下移动一行。

如果片段触底,我们将调用RemoveFullLines()方法。 首先,我们找出所有实线。

  1. rowsToRemove = []
  2. for i in range(Board.BoardHeight):
  3. n = 0
  4. for j in range(Board.BoardWidth):
  5. if not self.ShapeAt(j, i) == Tetrominoes.NoShape:
  6. n = n + 1
  7. if n == 10:
  8. rowsToRemove.append(i)

我们在董事会中循环。 一排可以有十个形状。 如果该行已满,例如 n 等于 10,我们存储行号以供以后删除。

  1. rowsToRemove.reverse()
  2. for m in rowsToRemove:
  3. for k in range(m, Board.BoardHeight):
  4. for l in range(Board.BoardWidth):
  5. self.SetShapeAt(l, k, self.ShapeAt(l, k + 1))

这些代码行将删除所有行。 我们颠倒了rowsToRemove列表的顺序,因此我们从最底部的全行开始。 我们要做的是通过将一行中的所有行向下放置一行来删除整行。 对于所有实线都会发生这种情况。在我们的情况下,我们使用天真重力。 这意味着碎片可能漂浮在空的间隙上方。

  1. def NewPiece(self):
  2. self.curPiece = self.nextPiece
  3. statusbar = self.Parent.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.Text = 'Game over'

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

  1. colors = [ (0, 0, 0), (204, 102, 102),
  2. ... ]
  3. light = [ (0, 0, 0), (248, 159, 171),
  4. ... ]
  5. dark = [ (0, 0, 0), (128, 59, 59),
  6. ... ]

一共有三种颜色。 colours列表存储正方形填充的颜色值。 七块每个都有其自己的颜色。 lightdark存储线条的颜色,使正方形看起来像 3D。 这些颜色是相同的,只是越来越浅。 我们将在正方形的顶部和左侧绘制两条浅色的线条,并在右侧和底部绘制两条深色的线条。

  1. g.DrawLine(pen, x, y + self.SquareHeight() - 1, x, y)
  2. g.DrawLine(pen, x, y, x + self.SquareWidth() - 1, y)

这两条线绘制一个正方形的亮线。

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

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

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

IronPython Mono Winforms 中的俄罗斯方块游戏 - 图2

图:坐标

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

RotateLeft()方法将一块向左旋转。

  1. if self.pieceShape == Tetrominoes.SquareShape:
  2. return self

如果我们有Tetrominoes.SquareShape个,我们什么也不做。 此形状始终相同。

  1. result = Shape()
  2. result.pieceShape = self.pieceShape
  3. for i in range(4):
  4. result.SetX(i, self.y(i))
  5. result.SetY(i, -self.x(i))
  6. return result

在其他情况下,我们更改作品的坐标。 要了解此代码,请查看上图。

IronPython Mono Winforms 中的俄罗斯方块游戏 - 图3

图:俄罗斯方块

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