提到《俄罗斯方块》 (Tetris),那真的是几乎所有人都知道的人。

​​

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

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

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

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

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

正文

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

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

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

1)Te

class Tetris(QMainWindow): def __init__(self): super().__init__() = False = False = None = S () def initUI(self): = 22 = 10 = QBasicTimer() ) hLayout = QHBoxLayout() = Board(self, ) () = SidePanel(self, ) () = () .msg2Statusbar[str].connect(.showMessage) () () ('AI俄罗斯方块儿') () (.width() + .width(), .height() + .height()) def center(self): screen = QDesktopWidget().screenGeometry() size = () () - ()) // 2, () - ()) // 2) def start(self): if : return = True .score = 0 BOARD_DATA.clear() .msg2Statusbar.emit(str(.score)) BOARD_DATA.createNewPiece() .start(, self) def pause(self): if not : return = not if : .stop() .msg2Statusbar.emit("paused") else: .start(, self) () def updateWindow(self): .updateData() .updateData() () def timerEvent(self, event): if event.timerId() == .timerId(): if TETRIS_AI and not : = TETRIS_AI.nextMove() if : k = 0 while BOARD_DATA.currentDirection != [0] and k < 4: BOARD_DATA.rotateRight() k += 1 k = 0 while BOARD_DATA.currentX != [1] and k < 5: if BOARD_DATA.currentX > [1]: BOARD_DATA.moveLeft() elif BOARD_DATA.currentX < [1]: BOARD_DATA.moveRight() k += 1 # lines = BOARD_DATA.dropDown() lines = BOARD_DATA.moveDown() .score += lines if != BOARD_DATA.currentShape: = None = BOARD_DATA.currentShape () else: super(Tetris, self).timerEvent(event) def keyPressEvent(self, event): if not or BOARD_DATA.currentShape == S: super(Tetris, self).keyPressEvent(event) return key = event.key() if key == Qt.Key_P: () return if : 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: .score += BOARD_DATA.dropDown() else: super(Tetris, self).keyPressEvent(event) () def drawSquare(painter, x, y, val, s): colorTable = [0x000000, 0xCC6666, 0x66CC66, 0x6666CC, 0xCCCC66, 0xCC66CC, 0x66CCCC, 0xDAAA00] if val == 0: return color = QColor(colorTable[val]) (x + 1, y + 1, s - 2, s - 2, color) ()) (x, y + s - 1, x, y) (x, y, x + s - 1, y) ()) (x + 1, y + s - 1, x + s - 1, y + s - 1) (x + s - 1, y + s - 1, x + s - 1, y + 1) class SidePanel(QFrame): def __init__(self, parent, gridSize): super().__init__(parent) (gridSize * 5, gridSize * BOARD_DATA.height) (gridSize * BOARD_DATA.width, 0) = gridSize def updateData(self): () def paintEvent(self, event): painter = QPainter(self) minX, maxX, minY, maxY = BOARD_DATA.nex(0) dy = 3 * dx = () - (maxX - minX) * ) / 2 val = BOARD_DATA.nex for x, y in BOARD_DATA.nex(0, 0, -minY): drawSquare(painter, x * + dx, y * + dy, val, ) class Board(QFrame): msg2Statusbar = pyqtSignal(str) speed = 10 def __init__(self, parent, gridSize): super().__init__(parent) (gridSize * BOARD_DATA.width, gridSize * BOARD_DATA.height) = gridSize () def initBoard(self): = 0 BOARD_DATA.clear() def paintEvent(self, event): painter = QPainter(self) # Draw backboard for x in range): for y in range): val = BOARD_DATA.getValue(x, y) drawSquare(painter, x * , y * , val, ) # Draw current shape for x, y in BOARD_DATA.getCurrentShapeCoord(): val = BOARD_DATA.curren drawSquare(painter, x * , y * , val, ) # Draw a border (QColor(0x777777)) ()-1, 0, ()-1, ()) (QColor(0xCCCCCC)) (), 0, (), ()) def updateData(self): (str()) () if __name__ == '__main__': # random.seed(32) app = QApplication([]) tetris = Tetris() ())

​2​

​​

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): = shape def getRotatedOffsets(self, direction): tmpCoords = S[] if direction == 0 or == S: return ((x, y) for x, y in tmpCoords) if direction == 1: return ((-y, x) for x, y in tmpCoords) if direction == 2: if in , S, S): return ((x, y) for x, y in tmpCoords) else: return ((-x, -y) for x, y in tmpCoords) if direction == 3: if in , S, S): 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 (direction)) def getBoundingOffsets(self, direction): tmpCoords = (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): = [0] * BoardDa * BoardDa = -1 = -1 = 0 = Shape() = Shape(1, 7)) Stat = [0] * 8 def getData(self): return [:] def getValue(self, x, y): return [x + y * BoardDa] def getCurrentShapeCoord(self): return self.curren(, , ) def createNewPiece(self): minX, maxX, minY, maxY = self.nex(0) result = False if (0, 5, -minY): = 5 = -minY = 0 = = Shape(1, 7)) result = True else: = Shape() = -1 = -1 = 0 result = False Stat[self.curren] += 1 return result def tryMoveCurrent(self, direction, x, y): return (, direction, x, y) def tryMove(self, shape, direction, x, y): for x, y in (direction, x, y): if x >= BoardDa or x < 0 or y >= BoardDa or y < 0: return False if [x + y * BoardDa] > 0: return False return True def moveDown(self): lines = 0 if (, , + 1): += 1 else: () lines = () () return lines def dropDown(self): while (, , + 1): += 1 () lines = () () return lines def moveLeft(self): if (, - 1, ): -= 1 def moveRight(self): if (, + 1, ): += 1 def rotateRight(self): if (( + 1) % 4, , ): += 1 %= 4 def rotateLeft(self): if (( - 1) % 4, , ): -= 1 %= 4 def removeFullLines(self): newBackBoard = [0] * BoardDa * BoardDa newY = BoardDa - 1 lines = 0 for y in range(BoardDa - 1, -1, -1): blockCount = sum([1 if [x + y * BoardDa] > 0 else 0 for x in range(BoardDa)]) if blockCount < BoardDa: for x in range(BoardDa): newBackBoard[x + newY * BoardDa] = [x + y * BoardDa] newY -= 1 else: lines += 1 if lines > 0: = newBackBoard return lines def mergePiece(self): for x, y in self.curren(, , ): [x + y * BoardDa] = self.curren = -1 = -1 = 0 = Shape() def clear(self): = -1 = -1 = 0 = Shape() = [0] * BoardDa * BoardDa BOARD_DATA = BoardData()

3​

​​

from tetris_model import BOARD_DATA, Shape import math from datetime import datetime import numpy as np class TetrisAI(object): def nextMove(self): t1 = da() if BOARD_DATA.currentShape == S: return None currentDirection = BOARD_DATA.currentDirection currentY = BOARD_DATA.currentY _, _, minY, _ = BOARD_DATA.nex(0) nextY = -minY # print("=======") strategy = None if BOARD_DATA.curren in , S, S): d0Range = (0, 1) elif BOARD_DATA.curren == S: d0Range = (0,) else: d0Range = (0, 1, 2, 3) if BOARD_DATA.nex in , S, S): d1Range = (0, 1) elif BOARD_DATA.nex == S: d1Range = (0,) else: d1Range = (0, 1, 2, 3) for d0 in d0Range: minX, maxX, _, _ = BOARD_DATA.curren(d0) for x0 in range(-minX, BOARD_DATA.width - maxX): board = (d0, x0) for d1 in d1Range: minX, maxX, _, _ = BOARD_DATA.nex(d1) dropDist = (board, d1, range(-minX, BOARD_DATA.width - maxX)) for x1 in range(-minX, BOARD_DATA.width - maxX): score = (board), d1, x1, dropDist) if not strategy or strategy[2] < score: strategy = (d0, x0, score) print("===", da() - 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.nex(d0, x0, 0): yy = 0 while yy + y < BOARD_DATA.height and (yy + y < 0 or data[(y + yy), x] == S): yy += 1 yy -= 1 if yy < res[x0]: res[x0] = yy return res def calcStep1Board(self, d0, x0): board = np.array()).reshape(, BOARD_DATA.width)) (board, BOARD_DATA.currentShape, d0, x0) return board def dropDown(self, data, shape, direction, x0): dy = BOARD_DATA.height - 1 for x, y in (direction, x0, 0): yy = 0 while yy + y < BOARD_DATA.height and (yy + y < 0 or data[(y + yy), x] == S): yy += 1 yy -= 1 if yy < dy: dy = yy # print("dropDown: shape {0}, direction {1}, x0 {2}, dy {3}".forma, direction, x0, dy)) ByDist(data, shape, direction, x0, dy) def dropDownByDist(self, data, shape, direction, x0, dist): for x, y in (direction, x0, 0): data[y + dist, x] = def calculateScore(self, step1Board, d1, x1, dropDist): # print("calculateScore") t1 = da() width = BOARD_DATA.width height = BOARD_DATA.height ByDist(step1Board, BOARD_DATA.nextShape, d1, x1, dropDist[x1]) # print(da() - 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] == S: 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(da() - t1) roofDy = [roofY[i] - roofY[i+1] for i in range(len(roofY) - 1)] if len(roofY) <= 0: stdY = 0 else: stdY = ma(sum([y ** 2 for y in roofY]) / len(roofY) - (sum(roofY) / len(roofY)) ** 2) if len(roofDy) <= 0: stdDY = 0 else: stdDY = ma(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(da() - 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)视频展示——

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

2)截图展示——

总结

于茫茫人海相遇——感谢你的阅读!相遇即是缘分,如有帮助到你,记得三连哦~

我是木木子,一个不止能编程的女码农,还能教你玩游戏、制作节日惊喜、甚至撩小姐姐、小哥哥的表白小程序哦......

写在最后——往期也有很多精彩内容,欢迎阅读!关注我,每日更新

完整的免费源码领取处:找我吖!私信小编06即可啦

往期推荐阅读——

项目0.1 免费测试—姓名猜性别小程序

【免费测试】姓名测算性别—仅需这几步即可:来看程序员是如何解决的?爱了爱了~

项目1.0 花样表白神器

【程序员】多久没有真诚表白了?新晋表白神器了解一下(让你感动到哭出声来~)

项目7.0 赛车游戏

【Pygame实战】如果你是赛车爱好者:这款新赛车游戏分分钟让你上瘾(超跑又是谁的梦想?)

项目7.1 虐单身狗游戏

Pygame实战:慎点|虐单身狗的最高境界是…【附源码】

文章汇总——

项目1.0 Python—2021 |已有文章汇总 | 持续更新,直接看这篇就够了

(更多内容+源码都在文章汇总哦!!欢迎阅读~)