导语
提到《俄罗斯方块》(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()
4)效果展示
1)视频展示——
2)截图展示——
总结
于茫茫人海相遇——感谢你的阅读!相遇即是缘分,如有帮助到你,记得三连哦~
我是木木子,一个不止能编程的女码农,还能教你玩游戏、制作节日惊喜、甚至撩小姐姐、小哥哥的表白小程序哦......
写在最后——往期也有很多精彩内容,欢迎阅读!关注我,每日更新