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

作者:木木子学python 时间:2021-11-16 23:11:41 

导语

提到《俄罗斯方块》(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)视频展示&mdash;&mdash;

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

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

2)截图展示&mdash;&mdash;

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

来源:https://juejin.cn/post/7075247750740181006

标签:Python,AI,俄罗斯方块,游戏
0
投稿

猜你喜欢

  • python微信跳一跳游戏辅助代码解析

    2021-06-10 21:42:29
  • 成功解决ValueError: Supported target types are:('binary', 'multiclass'). Got 'continuous' instead.

    2023-01-24 03:59:00
  • python获取引用对象的个数方式

    2023-06-19 23:10:30
  • Python 操作mysql数据库查询之fetchone(), fetchmany(), fetchall()用法示例

    2023-07-09 00:11:24
  • Django框架模板语言实例小结【变量,标签,过滤器,继承,html转义】

    2023-11-11 06:41:39
  • 自然描述与自然任务

    2010-01-26 15:51:00
  • MySQLMerge存储引擎

    2024-01-14 07:39:25
  • Vue3-KeepAlive,多个页面使用keepalive方式

    2024-05-02 16:33:39
  • Python Web开发模板引擎优缺点总结

    2023-08-02 22:36:29
  • OpenAI的Whisper模型进行语音识别使用详解

    2022-02-24 23:50:51
  • MySQL建立唯一索引实现插入重复自动更新

    2024-01-12 13:46:36
  • 基于mysql replication的问题总结

    2024-01-29 12:50:52
  • Vue内部渲染视图的方法

    2024-04-28 09:19:57
  • 解决python写的windows服务不能启动的问题

    2023-01-21 04:10:38
  • python实现LBP方法提取图像纹理特征实现分类的步骤

    2023-05-24 02:12:27
  • Python入门之字典的使用教程

    2021-09-15 00:35:12
  • Python 实现将某一列设置为str类型

    2022-07-27 03:20:12
  • python数据分析之单因素分析线性拟合及地理编码

    2021-02-09 06:46:20
  • python基础教程之csv格式文件的写入与读取

    2021-05-24 09:20:12
  • vue3 中使用 jsx 开发的详细过程

    2024-06-07 16:02:31
  • asp之家 网络编程 m.aspxhome.com