脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Python - Python实现AI自动玩俄罗斯方块游戏

Python实现AI自动玩俄罗斯方块游戏

2022-11-09 11:02木木子学python Python

提到《俄罗斯方块》,那真是几乎无人不知无人不晓。其历史之悠久,可玩性之持久,能手轻轻一挥,吊打一大波游戏。本文将利用Python实现俄罗斯方块进阶版—AI自动玩俄罗斯方块,感兴趣的可以学习一下

导语

提到《俄罗斯方块》(Tetris),那真是几乎无人不知无人不晓。​

其历史之悠久,可玩性之持久,能手轻轻一挥,吊打一大波游戏。

对于绝大多数小友而言,《俄罗斯方块》的规则根本无需多言——将形状不一的方块填满一行消除即可。

这款火了30几年的《俄罗斯方块》游戏之前就已经写过的哈,往期的Pygame合集里面可以找找看!

但今天木木子介绍的是《俄罗斯方块》的新作——实现AI自动玩儿游戏。

估计会让你三观尽毁,下巴掉落,惊呼:我玩了假游戏吧!

 

正文

移动、掉落、填充、消除!

木木子你我的童年回忆《俄罗斯方块AI版本》已正式上线!

代码由三部分组成 Tetris.py、tetris_model.py 和 tetris_ai.py游戏的主要逻辑由 Tetis 控制,model 定义了方块的样式,AI 顾名思义实现了主要的 AI 算法。

1)Tetris.py

class Tetris(QMainWindow):
  def __init__(self):
      super().__init__()
      self.isStarted = False
      self.isPaused = False
      self.nextMove = None
      self.lastShape = Shape.shapeNone

      self.initUI()

  def initUI(self):
      self.gridSize = 22
      self.speed = 10

      self.timer = QBasicTimer()
      self.setFocusPolicy(Qt.StrongFocus)

      hLayout = QHBoxLayout()
      self.tboard = Board(self, self.gridSize)
      hLayout.addWidget(self.tboard)

      self.sidePanel = SidePanel(self, self.gridSize)
      hLayout.addWidget(self.sidePanel)

      self.statusbar = self.statusBar()
      self.tboard.msg2Statusbar[str].connect(self.statusbar.showMessage)

      self.start()

      self.center()
      self.setWindowTitle('AI俄罗斯方块儿')
      self.show()

      self.setFixedSize(self.tboard.width() + self.sidePanel.width(),
                        self.sidePanel.height() + self.statusbar.height())

  def center(self):
      screen = QDesktopWidget().screenGeometry()
      size = self.geometry()
      self.move((screen.width() - size.width()) // 2, (screen.height() - size.height()) // 2)

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

      self.isStarted = True
      self.tboard.score = 0
      BOARD_DATA.clear()

      self.tboard.msg2Statusbar.emit(str(self.tboard.score))

      BOARD_DATA.createNewPiece()
      self.timer.start(self.speed, self)

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

      self.isPaused = not self.isPaused

      if self.isPaused:
          self.timer.stop()
          self.tboard.msg2Statusbar.emit("paused")
      else:
          self.timer.start(self.speed, self)

      self.updateWindow()

  def updateWindow(self):
      self.tboard.updateData()
      self.sidePanel.updateData()
      self.update()

  def timerEvent(self, event):
      if event.timerId() == self.timer.timerId():
          if TETRIS_AI and not self.nextMove:
              self.nextMove = TETRIS_AI.nextMove()
          if self.nextMove:
              k = 0
              while BOARD_DATA.currentDirection != self.nextMove[0] and k < 4:
                  BOARD_DATA.rotateRight()
                  k += 1
              k = 0
              while BOARD_DATA.currentX != self.nextMove[1] and k < 5:
                  if BOARD_DATA.currentX > self.nextMove[1]:
                      BOARD_DATA.moveLeft()
                  elif BOARD_DATA.currentX < self.nextMove[1]:
                      BOARD_DATA.moveRight()
                  k += 1
          # lines = BOARD_DATA.dropDown()
          lines = BOARD_DATA.moveDown()
          self.tboard.score += lines
          if self.lastShape != BOARD_DATA.currentShape:
              self.nextMove = None
              self.lastShape = BOARD_DATA.currentShape
          self.updateWindow()
      else:
          super(Tetris, self).timerEvent(event)

  def keyPressEvent(self, event):
      if not self.isStarted or BOARD_DATA.currentShape == Shape.shapeNone:
          super(Tetris, self).keyPressEvent(event)
          return

      key = event.key()
      
      if key == Qt.Key_P:
          self.pause()
          return
          
      if self.isPaused:
          return
      elif key == Qt.Key_Left:
          BOARD_DATA.moveLeft()
      elif key == Qt.Key_Right:
          BOARD_DATA.moveRight()
      elif key == Qt.Key_Up:
          BOARD_DATA.rotateLeft()
      elif key == Qt.Key_Space:
          self.tboard.score += BOARD_DATA.dropDown()
      else:
          super(Tetris, self).keyPressEvent(event)

      self.updateWindow()


def drawSquare(painter, x, y, val, s):
  colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC,
                0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00]

  if val == 0:
      return

  color = QColor(colorTable[val])
  painter.fillRect(x + 1, y + 1, s - 2, s - 2, color)

  painter.setPen(color.lighter())
  painter.drawLine(x, y + s - 1, x, y)
  painter.drawLine(x, y, x + s - 1, y)

  painter.setPen(color.darker())
  painter.drawLine(x + 1, y + s - 1, x + s - 1, y + s - 1)
  painter.drawLine(x + s - 1, y + s - 1, x + s - 1, y + 1)


class SidePanel(QFrame):
  def __init__(self, parent, gridSize):
      super().__init__(parent)
      self.setFixedSize(gridSize * 5, gridSize * BOARD_DATA.height)
      self.move(gridSize * BOARD_DATA.width, 0)
      self.gridSize = gridSize

  def updateData(self):
      self.update()

  def paintEvent(self, event):
      painter = QPainter(self)
      minX, maxX, minY, maxY = BOARD_DATA.nextShape.getBoundingOffsets(0)

      dy = 3 * self.gridSize
      dx = (self.width() - (maxX - minX) * self.gridSize) / 2

      val = BOARD_DATA.nextShape.shape
      for x, y in BOARD_DATA.nextShape.getCoords(0, 0, -minY):
          drawSquare(painter, x * self.gridSize + dx, y * self.gridSize + dy, val, self.gridSize)


class Board(QFrame):
  msg2Statusbar = pyqtSignal(str)
  speed = 10

  def __init__(self, parent, gridSize):
      super().__init__(parent)
      self.setFixedSize(gridSize * BOARD_DATA.width, gridSize * BOARD_DATA.height)
      self.gridSize = gridSize
      self.initBoard()

  def initBoard(self):
      self.score = 0
      BOARD_DATA.clear()

  def paintEvent(self, event):
      painter = QPainter(self)

      # Draw backboard
      for x in range(BOARD_DATA.width):
          for y in range(BOARD_DATA.height):
              val = BOARD_DATA.getValue(x, y)
              drawSquare(painter, x * self.gridSize, y * self.gridSize, val, self.gridSize)

      # Draw current shape
      for x, y in BOARD_DATA.getCurrentShapeCoord():
          val = BOARD_DATA.currentShape.shape
          drawSquare(painter, x * self.gridSize, y * self.gridSize, val, self.gridSize)

      # Draw a border
      painter.setPen(QColor(0x777777))
      painter.drawLine(self.width()-1, 0, self.width()-1, self.height())
      painter.setPen(QColor(0xCCCCCC))
      painter.drawLine(self.width(), 0, self.width(), self.height())

  def updateData(self):
      self.msg2Statusbar.emit(str(self.score))
      self.update()


if __name__ == '__main__':
  # random.seed(32)
  app = QApplication([])
  tetris = Tetris()
  sys.exit(app.exec_())

2)Tetris_model.py​​

import random

class Shape(object):
  shapeNone = 0
  shapeI = 1
  shapeL = 2
  shapeJ = 3
  shapeT = 4
  shapeO = 5
  shapeS = 6
  shapeZ = 7

  shapeCoord = (
      ((0, 0), (0, 0), (0, 0), (0, 0)),
      ((0, -1), (0, 0), (0, 1), (0, 2)),
      ((0, -1), (0, 0), (0, 1), (1, 1)),
      ((0, -1), (0, 0), (0, 1), (-1, 1)),
      ((0, -1), (0, 0), (0, 1), (1, 0)),
      ((0, 0), (0, -1), (1, 0), (1, -1)),
      ((0, 0), (0, -1), (-1, 0), (1, -1)),
      ((0, 0), (0, -1), (1, 0), (-1, -1))
  )

  def __init__(self, shape=0):
      self.shape = shape

  def getRotatedOffsets(self, direction):
      tmpCoords = Shape.shapeCoord[self.shape]
      if direction == 0 or self.shape == Shape.shapeO:
          return ((x, y) for x, y in tmpCoords)

      if direction == 1:
          return ((-y, x) for x, y in tmpCoords)

      if direction == 2:
          if self.shape in (Shape.shapeI, Shape.shapeZ, Shape.shapeS):
              return ((x, y) for x, y in tmpCoords)
          else:
              return ((-x, -y) for x, y in tmpCoords)

      if direction == 3:
          if self.shape in (Shape.shapeI, Shape.shapeZ, Shape.shapeS):
              return ((-y, x) for x, y in tmpCoords)
          else:
              return ((y, -x) for x, y in tmpCoords)

  def getCoords(self, direction, x, y):
      return ((x + xx, y + yy) for xx, yy in self.getRotatedOffsets(direction))

  def getBoundingOffsets(self, direction):
      tmpCoords = self.getRotatedOffsets(direction)
      minX, maxX, minY, maxY = 0, 0, 0, 0
      for x, y in tmpCoords:
          if minX > x:
              minX = x
          if maxX < x:
              maxX = x
          if minY > y:
              minY = y
          if maxY < y:
              maxY = y
      return (minX, maxX, minY, maxY)


class BoardData(object):
  width = 10
  height = 22

  def __init__(self):
      self.backBoard = [0] * BoardData.width * BoardData.height

      self.currentX = -1
      self.currentY = -1
      self.currentDirection = 0
      self.currentShape = Shape()
      self.nextShape = Shape(random.randint(1, 7))

      self.shapeStat = [0] * 8

  def getData(self):
      return self.backBoard[:]

  def getValue(self, x, y):
      return self.backBoard[x + y * BoardData.width]

  def getCurrentShapeCoord(self):
      return self.currentShape.getCoords(self.currentDirection, self.currentX, self.currentY)

  def createNewPiece(self):
      minX, maxX, minY, maxY = self.nextShape.getBoundingOffsets(0)
      result = False
      if self.tryMoveCurrent(0, 5, -minY):
          self.currentX = 5
          self.currentY = -minY
          self.currentDirection = 0
          self.currentShape = self.nextShape
          self.nextShape = Shape(random.randint(1, 7))
          result = True
      else:
          self.currentShape = Shape()
          self.currentX = -1
          self.currentY = -1
          self.currentDirection = 0
          result = False
      self.shapeStat[self.currentShape.shape] += 1
      return result

  def tryMoveCurrent(self, direction, x, y):
      return self.tryMove(self.currentShape, direction, x, y)

  def tryMove(self, shape, direction, x, y):
      for x, y in shape.getCoords(direction, x, y):
          if x >= BoardData.width or x < 0 or y >= BoardData.height or y < 0:
              return False
          if self.backBoard[x + y * BoardData.width] > 0:
              return False
      return True

  def moveDown(self):
      lines = 0
      if self.tryMoveCurrent(self.currentDirection, self.currentX, self.currentY + 1):
          self.currentY += 1
      else:
          self.mergePiece()
          lines = self.removeFullLines()
          self.createNewPiece()
      return lines

  def dropDown(self):
      while self.tryMoveCurrent(self.currentDirection, self.currentX, self.currentY + 1):
          self.currentY += 1
      self.mergePiece()
      lines = self.removeFullLines()
      self.createNewPiece()
      return lines

  def moveLeft(self):
      if self.tryMoveCurrent(self.currentDirection, self.currentX - 1, self.currentY):
          self.currentX -= 1

  def moveRight(self):
      if self.tryMoveCurrent(self.currentDirection, self.currentX + 1, self.currentY):
          self.currentX += 1

  def rotateRight(self):
      if self.tryMoveCurrent((self.currentDirection + 1) % 4, self.currentX, self.currentY):
          self.currentDirection += 1
          self.currentDirection %= 4

  def rotateLeft(self):
      if self.tryMoveCurrent((self.currentDirection - 1) % 4, self.currentX, self.currentY):
          self.currentDirection -= 1
          self.currentDirection %= 4

  def removeFullLines(self):
      newBackBoard = [0] * BoardData.width * BoardData.height
      newY = BoardData.height - 1
      lines = 0
      for y in range(BoardData.height - 1, -1, -1):
          blockCount = sum([1 if self.backBoard[x + y * BoardData.width] > 0 else 0 for x in range(BoardData.width)])
          if blockCount < BoardData.width:
              for x in range(BoardData.width):
                  newBackBoard[x + newY * BoardData.width] = self.backBoard[x + y * BoardData.width]
              newY -= 1
          else:
              lines += 1
      if lines > 0:
          self.backBoard = newBackBoard
      return lines

  def mergePiece(self):
      for x, y in self.currentShape.getCoords(self.currentDirection, self.currentX, self.currentY):
          self.backBoard[x + y * BoardData.width] = self.currentShape.shape

      self.currentX = -1
      self.currentY = -1
      self.currentDirection = 0
      self.currentShape = Shape()

  def clear(self):
      self.currentX = -1
      self.currentY = -1
      self.currentDirection = 0
      self.currentShape = Shape()
      self.backBoard = [0] * BoardData.width * BoardData.height


BOARD_DATA = BoardData()

3)Tetris_ai.py​​

from tetris_model import BOARD_DATA, Shape
import math
from datetime import datetime
import numpy as np


class TetrisAI(object):

  def nextMove(self):
      t1 = datetime.now()
      if BOARD_DATA.currentShape == Shape.shapeNone:
          return None

      currentDirection = BOARD_DATA.currentDirection
      currentY = BOARD_DATA.currentY
      _, _, minY, _ = BOARD_DATA.nextShape.getBoundingOffsets(0)
      nextY = -minY

      # print("=======")
      strategy = None
      if BOARD_DATA.currentShape.shape in (Shape.shapeI, Shape.shapeZ, Shape.shapeS):
          d0Range = (0, 1)
      elif BOARD_DATA.currentShape.shape == Shape.shapeO:
          d0Range = (0,)
      else:
          d0Range = (0, 1, 2, 3)

      if BOARD_DATA.nextShape.shape in (Shape.shapeI, Shape.shapeZ, Shape.shapeS):
          d1Range = (0, 1)
      elif BOARD_DATA.nextShape.shape == Shape.shapeO:
          d1Range = (0,)
      else:
          d1Range = (0, 1, 2, 3)

      for d0 in d0Range:
          minX, maxX, _, _ = BOARD_DATA.currentShape.getBoundingOffsets(d0)
          for x0 in range(-minX, BOARD_DATA.width - maxX):
              board = self.calcStep1Board(d0, x0)
              for d1 in d1Range:
                  minX, maxX, _, _ = BOARD_DATA.nextShape.getBoundingOffsets(d1)
                  dropDist = self.calcNextDropDist(board, d1, range(-minX, BOARD_DATA.width - maxX))
                  for x1 in range(-minX, BOARD_DATA.width - maxX):
                      score = self.calculateScore(np.copy(board), d1, x1, dropDist)
                      if not strategy or strategy[2] < score:
                          strategy = (d0, x0, score)
      print("===", datetime.now() - t1)
      return strategy

  def calcNextDropDist(self, data, d0, xRange):
      res = {}
      for x0 in xRange:
          if x0 not in res:
              res[x0] = BOARD_DATA.height - 1
          for x, y in BOARD_DATA.nextShape.getCoords(d0, x0, 0):
              yy = 0
              while yy + y < BOARD_DATA.height and (yy + y < 0 or data[(y + yy), x] == Shape.shapeNone):
                  yy += 1
              yy -= 1
              if yy < res[x0]:
                  res[x0] = yy
      return res

  def calcStep1Board(self, d0, x0):
      board = np.array(BOARD_DATA.getData()).reshape((BOARD_DATA.height, BOARD_DATA.width))
      self.dropDown(board, BOARD_DATA.currentShape, d0, x0)
      return board

  def dropDown(self, data, shape, direction, x0):
      dy = BOARD_DATA.height - 1
      for x, y in shape.getCoords(direction, x0, 0):
          yy = 0
          while yy + y < BOARD_DATA.height and (yy + y < 0 or data[(y + yy), x] == Shape.shapeNone):
              yy += 1
          yy -= 1
          if yy < dy:
              dy = yy
      # print("dropDown: shape {0}, direction {1}, x0 {2}, dy {3}".format(shape.shape, direction, x0, dy))
      self.dropDownByDist(data, shape, direction, x0, dy)

  def dropDownByDist(self, data, shape, direction, x0, dist):
      for x, y in shape.getCoords(direction, x0, 0):
          data[y + dist, x] = shape.shape

  def calculateScore(self, step1Board, d1, x1, dropDist):
      # print("calculateScore")
      t1 = datetime.now()
      width = BOARD_DATA.width
      height = BOARD_DATA.height

      self.dropDownByDist(step1Board, BOARD_DATA.nextShape, d1, x1, dropDist[x1])
      # print(datetime.now() - t1)

      # Term 1: lines to be removed
      fullLines, nearFullLines = 0, 0
      roofY = [0] * width
      holeCandidates = [0] * width
      holeConfirm = [0] * width
      vHoles, vBlocks = 0, 0
      for y in range(height - 1, -1, -1):
          hasHole = False
          hasBlock = False
          for x in range(width):
              if step1Board[y, x] == Shape.shapeNone:
                  hasHole = True
                  holeCandidates[x] += 1
              else:
                  hasBlock = True
                  roofY[x] = height - y
                  if holeCandidates[x] > 0:
                      holeConfirm[x] += holeCandidates[x]
                      holeCandidates[x] = 0
                  if holeConfirm[x] > 0:
                      vBlocks += 1
          if not hasBlock:
              break
          if not hasHole and hasBlock:
              fullLines += 1
      vHoles = sum([x ** .7 for x in holeConfirm])
      maxHeight = max(roofY) - fullLines
      # print(datetime.now() - t1)

      roofDy = [roofY[i] - roofY[i+1] for i in range(len(roofY) - 1)]

      if len(roofY) <= 0:
          stdY = 0
      else:
          stdY = math.sqrt(sum([y ** 2 for y in roofY]) / len(roofY) - (sum(roofY) / len(roofY)) ** 2)
      if len(roofDy) <= 0:
          stdDY = 0
      else:
          stdDY = math.sqrt(sum([y ** 2 for y in roofDy]) / len(roofDy) - (sum(roofDy) / len(roofDy)) ** 2)

      absDy = sum([abs(x) for x in roofDy])
      maxDy = max(roofY) - min(roofY)
      # print(datetime.now() - t1)

      score = fullLines * 1.8 - vHoles * 1.0 - vBlocks * 0.5 - maxHeight ** 1.5 * 0.02 \
          - stdY * 0.0 - stdDY * 0.01 - absDy * 0.2 - maxDy * 0.3
      # print(score, fullLines, vHoles, vBlocks, maxHeight, stdY, stdDY, absDy, roofY, d0, x0, d1, x1)
      return score


TETRIS_AI = TetrisAI()

 

效果展示

1)视频展示——

Python实现AI自动玩俄罗斯方块游戏

【普通玩家VS高手玩家】一带传奇游戏《俄罗斯方块儿》AI版!

2)截图展示——

Python实现AI自动玩俄罗斯方块游戏

以上就是Python实现AI自动玩俄罗斯方块游戏的详细内容,更多关于Python俄罗斯方块的资料请关注服务器之家其它相关文章!

原文链接:https://juejin.cn/post/7075247750740181006

延伸 · 阅读

精彩推荐