返回介绍

The Tetris game in IronPython Mono Winforms

发布于 2025-02-22 22:19:49 字数 19325 浏览 0 评论 0 收藏 0

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.

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.

Tetrominoes
Figure: Tetrominoes

The development

We do not have images for our tetris game, we draw the tetrominoes using the drawing API available in the Winforms library. Behind every computer game, there is a mathematical model. So it is in tetris.

Some ideas behind the game.

  • We use 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

The following example is a modified version of the Tetris game, available with PyQt4 installation files.

tetris.py

#!/usr/bin/ipy

import clr

clr.AddReference("System.Windows.Forms")
clr.AddReference("System.Drawing")
clr.AddReference("System")

from System.Windows.Forms import Application, Form, FormBorderStyle
from System.Windows.Forms import UserControl, Keys, Timer, StatusBar
from System.Drawing import Size, Color, SolidBrush, Pen
from System.Drawing.Drawing2D import LineCap
from System.ComponentModel import Container
from System import Random
    
    
    
class Tetrominoes(object):
  NoShape = 0
  ZShape = 1
  SShape = 2
  LineShape = 3
  TShape = 4
  SquareShape = 5
  LShape = 6
  MirroredLShape = 7
    
    
class Board(UserControl):
  BoardWidth = 10
  BoardHeight = 22
  Speed = 200
  ID_TIMER = 1

  def __init__(self):
    self.Text = 'Snake'

    self.components = Container()
    self.isWaitingAfterLine = False
    self.curPiece = Shape()
    self.nextPiece = Shape()
    self.curX = 0
    self.curY = 0
    self.numLinesRemoved = 0
    self.board = []
    
    self.DoubleBuffered = True

    self.isStarted = False
    self.isPaused = False

    self.timer = Timer(self.components)
    self.timer.Enabled = True
    self.timer.Interval = Board.Speed
    self.timer.Tick += self.OnTick

    self.Paint += self.OnPaint
    self.KeyUp += self.OnKeyUp

    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.ClientSize.Width / Board.BoardWidth

  def SquareHeight(self):
    return self.ClientSize.Height / Board.BoardHeight

  def Start(self):
    if self.isPaused:
      return

    self.isStarted = True
    self.isWaitingAfterLine = False
    self.numLinesRemoved = 0
    self.ClearBoard()

    self.NewPiece()


  def Pause(self):
    if not self.isStarted:
      return

    self.isPaused = not self.isPaused    
    statusbar = self.Parent.statusbar

    if self.isPaused:
      self.timer.Stop()
      statusbar.Text = 'paused'
    else:
      self.timer.Start()
      statusbar.Text = 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):
   
    g = event.Graphics

    size = self.ClientSize
    boardTop = size.Height - 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(g,
            0 + j * self.SquareWidth(),
            boardTop + i * self.SquareHeight(), shape)

    if self.curPiece.GetShape() != Tetrominoes.NoShape:
      for i in range(4):
        x = self.curX + self.curPiece.x(i)
        y = self.curY - self.curPiece.y(i)
        self.DrawSquare(g, 0 + x * self.SquareWidth(),
          boardTop + (Board.BoardHeight - y - 1) * self.SquareHeight(),
          self.curPiece.GetShape())
          
    g.Dispose()

  def OnKeyUp(self, event): 
    
    if not self.isStarted or self.curPiece.GetShape() == Tetrominoes.NoShape:
      return
      
    key = event.KeyCode

    if key == Keys.P:
      self.Pause()
      return
    
    if self.isPaused:
      return  
    elif key == Keys.Left:
      self.TryMove(self.curPiece, self.curX - 1, self.curY)
    elif key == Keys.Right:
      self.TryMove(self.curPiece, self.curX + 1, self.curY)
    elif key == Keys.Down:
      self.TryMove(self.curPiece.RotatedRight(), self.curX, self.curY)
    elif key == Keys.Up:
      self.TryMove(self.curPiece.RotatedLeft(), self.curX, self.curY)
    elif key == Keys.Space:
      self.DropDown()
    elif key == Keys.D:
      self.OneLineDown()
    

  def OnTick(self, sender, event):

    if self.isWaitingAfterLine:
      self.isWaitingAfterLine = False
      self.NewPiece()
    else:
      self.OneLineDown()


  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.GetShape())

    self.RemoveFullLines()

    if not self.isWaitingAfterLine:
      self.NewPiece()


  def RemoveFullLines(self):
    numFullLines = 0

    statusbar = self.Parent.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.Text = str(self.numLinesRemoved)
      self.isWaitingAfterLine = True
      self.curPiece.SetShape(Tetrominoes.NoShape)
      self.Refresh()


  def NewPiece(self):
    self.curPiece = self.nextPiece
    statusbar = self.Parent.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.Text = '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, g, x, y, shape):
    colors = [ (0, 0, 0), (204, 102, 102), 
      (102, 204, 102), (102, 102, 204), 
      (204, 204, 102), (204, 102, 204), 
      (102, 204, 204), (218, 170, 0) ]
      
    light = [ (0, 0, 0), (248, 159, 171), 
      (121, 252, 121), (121, 121, 252), 
      (252, 252, 121), (252, 121, 252), 
      (121, 252, 252), (252, 198, 0) ]
    
    dark = [ (0, 0, 0), (128, 59, 59), 
      (59, 128, 59), (59, 59, 128), 
      (128, 128, 59), (128, 59, 128), 
      (59, 128, 128), (128, 98, 0) ]   
    
    
    pen = Pen(Color.FromArgb(light[shape][0], light[shape][1],
      light[shape][2]), 1)
    pen.StartCap = LineCap.Flat
    pen.EndCap = LineCap.Flat
    
    g.DrawLine(pen, x, y + self.SquareHeight() - 1, x, y)
    g.DrawLine(pen, x, y, x + self.SquareWidth() - 1, y)

    darkpen = Pen(Color.FromArgb(dark[shape][0], dark[shape][1],
      dark[shape][2]), 1)
    darkpen.StartCap = LineCap.Flat
    darkpen.EndCap = LineCap.Flat
    
    g.DrawLine(darkpen, x + 1, y + self.SquareHeight() - 1,
      x + self.SquareWidth() - 1, y + self.SquareHeight() - 1)
    g.DrawLine(darkpen, x + self.SquareWidth() - 1, 
      y + self.SquareHeight() - 1, x + self.SquareWidth() - 1, y + 1)
      
    g.FillRectangle(SolidBrush(Color.FromArgb(colors[shape][0], colors[shape][1], 
      colors[shape][2])), x + 1, y + 1, self.SquareWidth() - 1, 
      self.SquareHeight() - 2)
      
    pen.Dispose()
    darkpen.Dispose()



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 GetShape(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):
    rand = Random()
    self.SetShape(rand.Next(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 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 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   

    
class IForm(Form):

  def __init__(self):
    self.Text = 'Tetris'
    self.Width = 200
    self.Height = 430
    self.FormBorderStyle = FormBorderStyle.FixedSingle
    board = Board()
    board.Width = 195
    board.Height = 380
    self.Controls.Add(board)

    self.statusbar = StatusBar()
    self.statusbar.Parent = self
    self.statusbar.Text = 'Ready'
    board.Start()
    self.CenterToScreen()
    
    
Application.Run(IForm())

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.

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

There are seven different types of tetrominoes.

...
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 Tetrominoes . It represents the position of various shapes and remains of the shapes on the board.

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

The ClearBoard() method clears the board. It fills the self.board variable with Tetrominoes.NoShape values.

Painting in the tetris game is done in the OnPaint() method.

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(g,
        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. We access it using the ShapeAt() method.

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(g, 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.

In the OnKeyUp() method we check for pressed keys.

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

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.

In the TryMove() method we try to move our shapes. If we cannot move the piece, we return False.

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

If the shape is at the edge of the board or is adjacent to some other piece, we return False.

self.curPiece = newPiece
self.curX = newX
self.curY = newY
self.Refresh()
return True

Otherwise we place the current falling piece to a new position and return True.

def OnTick(self, sender, event):

   if self.isWaitingAfterLine:
     self.isWaitingAfterLine = False
     self.NewPiece()
   else:
     self.OneLineDown()

In the OnTick() method we either create a new piece, after the previous one hit the bottom, or we move a falling piece one line down.

If the piece hits the bottom, we call the RemoveFullLines() method. First we find out all full lines.

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)

We cycle throught the board. A row can have ten pieces of shapes. If the row is full, e.g. n is equal to 10, we store the line number for later removal.

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))

These code lines remove the full lines. We reverse the order of the rowsToRemove list so that we begin with the bottom most full line. What we do is remove a full line by placing all lines about it one line down. This happens for all full lines In our case we use a naive gravity. This means that the pieces may be lef floating above empty gaps.

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

The NewPiece() method creates randomly a new tetris piece. If the piece cannot go into its initial position, e.g. the TryMove() method returns False, the game is over.

colors = [ (0, 0, 0), (204, 102, 102), 
  ... ]

light = [ (0, 0, 0), (248, 159, 171), 
  ... ]

dark = [ (0, 0, 0), (128, 59, 59), 
  ... ]   

There are three lists of colours. The colours list stores color values for the fills of the squares. Each of seven pieces has its own color. The light and the dark store colours for lines that will make the square look 3D. These colours are the same, just are lighter and darker. We will draw two lines with light colour to the top and left sides of the squares and two lines with darker colour to the right and bottom sides.

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

These two lines draw the light lines for a square.

The Shape class saves information about the tetris piece.

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.

Coordinates
Figure: Coordinates

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.

The RotateLeft() method rotates a piece to the left.

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

If we have the Tetrominoes.SquareShape piece, we do nothing. This shape is always the same.

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

In other cases, we change coordinates of the piece. To understand this code, look at the above figure.

Tetris
Figure: Tetris

This was Tetris game in IronPython Winforms.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文