欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

pyqt5实现俄罗斯方块游戏

程序员文章站 2023-11-18 08:09:28
本章我们要制作一个俄罗斯方块游戏。 tetris 译注:称呼:方块是由四个小方格组成的 俄罗斯方块游戏是世界上最流行的游戏之一。是由一名叫alexey pajitno...

本章我们要制作一个俄罗斯方块游戏。

tetris

译注:称呼:方块是由四个小方格组成的

俄罗斯方块游戏是世界上最流行的游戏之一。是由一名叫alexey pajitnov的俄罗斯程序员在1985年制作的,从那时起,这个游戏就风靡了各个游戏平台。

俄罗斯方块归类为下落块迷宫游戏。游戏有7个基本形状:s、z、t、l、反向l、直线、方块,每个形状都由4个方块组成,方块最终都会落到屏幕底部。所以玩家通过控制形状的左右位置和旋转,让每个形状都以合适的位置落下,如果有一行全部被方块填充,这行就会消失,并且得分。游戏结束的条件是有形状接触到了屏幕顶部。

方块展示:

pyqt5实现俄罗斯方块游戏

pyqt5是专门为创建图形界面产生的,里面一些专门为制作游戏而开发的组件,所以pyqt5是能制作小游戏的。

制作电脑游戏也是提高自己编程能力的一种很好的方式。

开发

没有图片,所以就自己用绘画画出来几个图形。每个游戏里都有数学模型的,这个也是。

开工之前:

  • 用qtcore.qbasictimer()创建一个游戏循环
  • 模型是一直下落的
  • 模型的运动是以小块为基础单位的,不是按像素
  • 从数学意义上来说,模型就是就是一串数字而已

代码由四个类组成:tetris, board, tetrominoe和shape。tetris类创建游戏,board是游戏主要逻辑。tetrominoe包含了所有的砖块,shape是所有砖块的代码。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

"""
zetcode pyqt5 tutorial 
this is a tetris game clone.

author: jan bodnar
website: zetcode.com 
last edited: august 2017
"""

from pyqt5.qtwidgets import qmainwindow, qframe, qdesktopwidget, qapplication
from pyqt5.qtcore import qt, qbasictimer, pyqtsignal
from pyqt5.qtgui import qpainter, qcolor 
import sys, random

class tetris(qmainwindow):
  
  def __init__(self):
    super().__init__()
    
    self.initui()
    
    
  def initui(self):  
    '''initiates application ui'''

    self.tboard = board(self)
    self.setcentralwidget(self.tboard)

    self.statusbar = self.statusbar()    
    self.tboard.msg2statusbar[str].connect(self.statusbar.showmessage)
    
    self.tboard.start()
    
    self.resize(180, 380)
    self.center()
    self.setwindowtitle('tetris')    
    self.show()
    

  def center(self):
    '''centers the window on the screen'''
    
    screen = qdesktopwidget().screengeometry()
    size = self.geometry()
    self.move((screen.width()-size.width())/2, 
      (screen.height()-size.height())/2)
    

class board(qframe):
  
  msg2statusbar = pyqtsignal(str)
  
  boardwidth = 10
  boardheight = 22
  speed = 300

  def __init__(self, parent):
    super().__init__(parent)
    
    self.initboard()
    
    
  def initboard(self):   
    '''initiates board'''

    self.timer = qbasictimer()
    self.iswaitingafterline = false
    
    self.curx = 0
    self.cury = 0
    self.numlinesremoved = 0
    self.board = []

    self.setfocuspolicy(qt.strongfocus)
    self.isstarted = false
    self.ispaused = false
    self.clearboard()
    
    
  def shapeat(self, x, y):
    '''determines shape at the board position'''
    
    return self.board[(y * board.boardwidth) + x]

    
  def setshapeat(self, x, y, shape):
    '''sets a shape at the board'''
    
    self.board[(y * board.boardwidth) + x] = shape
    

  def squarewidth(self):
    '''returns the width of one square'''
    
    return self.contentsrect().width() // board.boardwidth
    

  def squareheight(self):
    '''returns the height of one square'''
    
    return self.contentsrect().height() // board.boardheight
    

  def start(self):
    '''starts game'''
    
    if self.ispaused:
      return

    self.isstarted = true
    self.iswaitingafterline = false
    self.numlinesremoved = 0
    self.clearboard()

    self.msg2statusbar.emit(str(self.numlinesremoved))

    self.newpiece()
    self.timer.start(board.speed, self)

    
  def pause(self):
    '''pauses game'''
    
    if not self.isstarted:
      return

    self.ispaused = not self.ispaused
    
    if self.ispaused:
      self.timer.stop()
      self.msg2statusbar.emit("paused")
      
    else:
      self.timer.start(board.speed, self)
      self.msg2statusbar.emit(str(self.numlinesremoved))

    self.update()

    
  def paintevent(self, event):
    '''paints all shapes of the game'''
    
    painter = qpainter(self)
    rect = self.contentsrect()

    boardtop = rect.bottom() - 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 != tetrominoe.noshape:
          self.drawsquare(painter,
            rect.left() + j * self.squarewidth(),
            boardtop + i * self.squareheight(), shape)

    if self.curpiece.shape() != tetrominoe.noshape:
      
      for i in range(4):
        
        x = self.curx + self.curpiece.x(i)
        y = self.cury - self.curpiece.y(i)
        self.drawsquare(painter, rect.left() + x * self.squarewidth(),
          boardtop + (board.boardheight - y - 1) * self.squareheight(),
          self.curpiece.shape())

          
  def keypressevent(self, event):
    '''processes key press events'''
    
    if not self.isstarted or self.curpiece.shape() == tetrominoe.noshape:
      super(board, self).keypressevent(event)
      return

    key = event.key()
    
    if key == qt.key_p:
      self.pause()
      return
      
    if self.ispaused:
      return
        
    elif key == qt.key_left:
      self.trymove(self.curpiece, self.curx - 1, self.cury)
      
    elif key == qt.key_right:
      self.trymove(self.curpiece, self.curx + 1, self.cury)
      
    elif key == qt.key_down:
      self.trymove(self.curpiece.rotateright(), self.curx, self.cury)
      
    elif key == qt.key_up:
      self.trymove(self.curpiece.rotateleft(), self.curx, self.cury)
      
    elif key == qt.key_space:
      self.dropdown()
      
    elif key == qt.key_d:
      self.onelinedown()
      
    else:
      super(board, self).keypressevent(event)
        

  def timerevent(self, event):
    '''handles timer event'''
    
    if event.timerid() == self.timer.timerid():
      
      if self.iswaitingafterline:
        self.iswaitingafterline = false
        self.newpiece()
      else:
        self.onelinedown()
        
    else:
      super(board, self).timerevent(event)

      
  def clearboard(self):
    '''clears shapes from the board'''
    
    for i in range(board.boardheight * board.boardwidth):
      self.board.append(tetrominoe.noshape)

    
  def dropdown(self):
    '''drops down a shape'''
    
    newy = self.cury
    
    while newy > 0:
      
      if not self.trymove(self.curpiece, self.curx, newy - 1):
        break
        
      newy -= 1

    self.piecedropped()
    

  def onelinedown(self):
    '''goes one line down with a shape'''
    
    if not self.trymove(self.curpiece, self.curx, self.cury - 1):
      self.piecedropped()
      

  def piecedropped(self):
    '''after dropping shape, remove full lines and create new shape'''
    
    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):
    '''removes all full lines from the board'''
    
    numfulllines = 0
    rowstoremove = []

    for i in range(board.boardheight):
      
      n = 0
      for j in range(board.boardwidth):
        if not self.shapeat(j, i) == tetrominoe.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
      self.msg2statusbar.emit(str(self.numlinesremoved))
        
      self.iswaitingafterline = true
      self.curpiece.setshape(tetrominoe.noshape)
      self.update()
      

  def newpiece(self):
    '''creates a new shape'''
    
    self.curpiece = shape()
    self.curpiece.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(tetrominoe.noshape)
      self.timer.stop()
      self.isstarted = false
      self.msg2statusbar.emit("game over")



  def trymove(self, newpiece, newx, newy):
    '''tries to move a shape'''
    
    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) != tetrominoe.noshape:
        return false

    self.curpiece = newpiece
    self.curx = newx
    self.cury = newy
    self.update()
    
    return true
    

  def drawsquare(self, painter, x, y, shape):
    '''draws a square of a shape'''    
    
    colortable = [0x000000, 0xcc6666, 0x66cc66, 0x6666cc,
           0xcccc66, 0xcc66cc, 0x66cccc, 0xdaaa00]

    color = qcolor(colortable[shape])
    painter.fillrect(x + 1, y + 1, self.squarewidth() - 2, 
      self.squareheight() - 2, color)

    painter.setpen(color.lighter())
    painter.drawline(x, y + self.squareheight() - 1, x, y)
    painter.drawline(x, y, x + self.squarewidth() - 1, y)

    painter.setpen(color.darker())
    painter.drawline(x + 1, y + self.squareheight() - 1,
      x + self.squarewidth() - 1, y + self.squareheight() - 1)
    painter.drawline(x + self.squarewidth() - 1, 
      y + self.squareheight() - 1, x + self.squarewidth() - 1, y + 1)


class tetrominoe(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 = tetrominoe.noshape

    self.setshape(tetrominoe.noshape)
    

  def shape(self):
    '''returns shape'''
    
    return self.pieceshape
    

  def setshape(self, shape):
    '''sets a 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):
    '''chooses a random shape'''
    
    self.setshape(random.randint(1, 7))

    
  def x(self, index):
    '''returns x coordinate'''
    
    return self.coords[index][0]

    
  def y(self, index):
    '''returns y coordinate'''
    
    return self.coords[index][1]

    
  def setx(self, index, x):
    '''sets x coordinate'''
    
    self.coords[index][0] = x

    
  def sety(self, index, y):
    '''sets y coordinate'''
    
    self.coords[index][1] = y

    
  def minx(self):
    '''returns min x value'''
    
    m = self.coords[0][0]
    for i in range(4):
      m = min(m, self.coords[i][0])

    return m

    
  def maxx(self):
    '''returns max x value'''
    
    m = self.coords[0][0]
    for i in range(4):
      m = max(m, self.coords[i][0])

    return m

    
  def miny(self):
    '''returns min y value'''
    
    m = self.coords[0][1]
    for i in range(4):
      m = min(m, self.coords[i][1])

    return m

    
  def maxy(self):
    '''returns max y value'''
    
    m = self.coords[0][1]
    for i in range(4):
      m = max(m, self.coords[i][1])

    return m

    
  def rotateleft(self):
    '''rotates shape to the left'''
    
    if self.pieceshape == tetrominoe.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 rotateright(self):
    '''rotates shape to the right'''
    
    if self.pieceshape == tetrominoe.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


if __name__ == '__main__':
  
  app = qapplication([])
  tetris = tetris()  
  sys.exit(app.exec_())

游戏很简单,所以也就很好理解。程序加载之后游戏也就直接开始了,可以用p键暂停游戏,空格键让方块直接落到最下面。游戏的速度是固定的,并没有实现加速的功能。分数就是游戏中消除的行数。

self.tboard = board(self)
self.setcentralwidget(self.tboard)

创建了一个board类的实例,并设置为应用的中心组件。

self.statusbar = self.statusbar()    
self.tboard.msg2statusbar[str].connect(self.statusbar.showmessage)

创建一个statusbar来显示三种信息:消除的行数,游戏暂停状态或者游戏结束状态。msg2statusbar是一个自定义的信号,用在(和)board类(交互),showmessage()方法是一个内建的,用来在statusbar上显示信息的方法。

self.tboard.start()

初始化游戏:

class board(qframe):
  
  msg2statusbar = pyqtsignal(str)
...  

创建了一个自定义信号msg2statusbar,当我们想往statusbar里显示信息的时候,发出这个信号就行了。

boardwidth = 10
boardheight = 22
speed = 300

这些是board类的变量。boardwidthboardheight分别是board的宽度和高度。speed是游戏的速度,每300ms出现一个新的方块。

...
self.curx = 0
self.cury = 0
self.numlinesremoved = 0
self.board = []
...

initboard()里初始化了一些重要的变量。self.board定义了方块的形状和位置,取值范围是0-7。

def shapeat(self, x, y):
  return self.board[(y * board.boardwidth) + x]

shapeat()决定了board里方块的的种类。

def squarewidth(self):
  return self.contentsrect().width() // board.boardwidth

board的大小可以动态的改变。所以方格的大小也应该随之变化。squarewidth()计算并返回每个块应该占用多少像素--也即board.boardwidth

def pause(self):
  '''pauses game'''

  if not self.isstarted:
    return

  self.ispaused = not self.ispaused

  if self.ispaused:
    self.timer.stop()
    self.msg2statusbar.emit("paused")

  else:
    self.timer.start(board.speed, self)
    self.msg2statusbar.emit(str(self.numlinesremoved))

  self.update()

pause()方法用来暂停游戏,停止计时并在statusbar上显示一条信息。

def paintevent(self, event):
  '''paints all shapes of the game'''

  painter = qpainter(self)
  rect = self.contentsrect()
...

渲染是在paintevent()方法里发生的qpainter负责pyqt5里所有低级绘画操作。

for i in range(board.boardheight):
  for j in range(board.boardwidth):
    shape = self.shapeat(j, board.boardheight - i - 1)
    
    if shape != tetrominoe.noshape:
      self.drawsquare(painter,
        rect.left() + j * self.squarewidth(),
        boardtop + i * self.squareheight(), shape)

渲染游戏分为两步。第一步是先画出所有已经落在最下面的的图,这些保存在self.board里。可以使用shapeat()查看这个这个变量。

if self.curpiece.shape() != tetrominoe.noshape:
  
  for i in range(4):
    
    x = self.curx + self.curpiece.x(i)
    y = self.cury - self.curpiece.y(i)
    self.drawsquare(painter, rect.left() + x * self.squarewidth(),
      boardtop + (board.boardheight - y - 1) * self.squareheight(),
      self.curpiece.shape())

第二步是画出更在下落的方块。

elif key == qt.key_right:
  self.trymove(self.curpiece, self.curx + 1, self.cury)

keypressevent()方法获得用户按下的按键。如果按下的是右方向键,就尝试把方块向右移动,说尝试是因为有可能到边界不能移动了。

elif key == qt.key_up:
  self.trymove(self.curpiece.rotateleft(), self.curx, self.cury)

上方向键是把方块向左旋转一下

elif key == qt.key_space:
  self.dropdown()

空格键会直接把方块放到底部

elif key == qt.key_d:
  self.onelinedown()

d键是加速一次下落速度。

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) != tetrominoe.noshape:
      return false

  self.curpiece = newpiece
  self.curx = newx
  self.cury = newy
  self.update()
  return true

trymove()是尝试移动方块的方法。如果方块已经到达board的边缘或者遇到了其他方块,就返回false。否则就把方块下落到想要

def timerevent(self, event):
  
  if event.timerid() == self.timer.timerid():
    
    if self.iswaitingafterline:
      self.iswaitingafterline = false
      self.newpiece()
    else:
      self.onelinedown()
      
  else:
    super(board, self).timerevent(event)

在计时器事件里,要么是等一个方块下落完之后创建一个新的方块,要么是让一个方块直接落到底(move a falling piece one line down)。

def clearboard(self):
  
  for i in range(board.boardheight * board.boardwidth):
    self.board.append(tetrominoe.noshape)

clearboard()方法通过tetrominoe.noshape清空broad

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) == tetrominoe.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)
 ...

如果方块碰到了底部,就调用removefulllines()方法,找到所有能消除的行消除它们。消除的具体动作就是把符合条件的行消除掉之后,再把它上面的行下降一行。注意移除满行的动作是倒着来的,因为我们是按照重力来表现游戏的,如果不这样就有可能出现有些方块浮在空中的现象。

def newpiece(self):
  
  self.curpiece = shape()
  self.curpiece.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(tetrominoe.noshape)
    self.timer.stop()
    self.isstarted = false
    self.msg2statusbar.emit("game over")

newpiece()方法是用来创建形状随机的方块。如果随机的方块不能正确的出现在预设的位置,游戏结束。

class tetrominoe(object):
  
  noshape = 0
  zshape = 1
  sshape = 2
  lineshape = 3
  tshape = 4
  squareshape = 5
  lshape = 6
  mirroredlshape = 7

tetrominoe类保存了所有方块的形状。我们还定义了一个noshape的空形状。

shape类保存类方块内部的信息。

class shape(object):
  
  coordstable = (
    ((0, 0),   (0, 0),   (0, 0),   (0, 0)),
    ((0, -1),  (0, 0),   (-1, 0),  (-1, 1)),
    ...
  )
...  

coordstable元组保存了所有的方块形状的组成。是一个构成方块的坐标模版。

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

上面创建了一个新的空坐标数组,这个数组将用来保存方块的坐标。

坐标系示意图:

pyqt5实现俄罗斯方块游戏

上面的图片可以帮助我们更好的理解坐标值的意义。比如元组(0, -1), (0, 0), (-1, 0), (-1, -1)代表了一个z形状的方块。这个图表就描绘了这个形状。

def rotateleft(self):
  
  if self.pieceshape == tetrominoe.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

rotateleft()方法向右旋转一个方块。正方形的方块就没必要旋转,就直接返回了。其他的是返回一个新的,能表示这个形状旋转了的坐标。

程序展示:

pyqt5实现俄罗斯方块游戏

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。