当前位置: 移动技术网 > IT编程>脚本编程>Python > pyqt5实现俄罗斯方块游戏

pyqt5实现俄罗斯方块游戏

2019年06月01日  | 移动技术网IT编程  | 我要评论

又见白娘子全集下载,网游小说完本排行榜,神秘首席的契约

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

tetris

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

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

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

方块展示:

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

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

坐标系示意图:

上面的图片可以帮助我们更好的理解坐标值的意义。比如元组(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()方法向右旋转一个方块。正方形的方块就没必要旋转,就直接返回了。其他的是返回一个新的,能表示这个形状旋转了的坐标。

程序展示:

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

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网