一、算法思想核心
这个俄罗斯方块AI的核心思想是通过穷举搜索当前形状和下一个形状的所有可能的放置组合,并为每个组合计算一个得分。得分是根据游戏板面的状态来评估每个组合的优劣,最终选择得分最高的放置策略。
具体的实现步骤和核心思想如下:
-
穷举搜索:通过嵌套的循环,穷举当前形状和下一个形状的所有可能的放置组合。首先确定当前形状的方向d0和在x轴上的起始位置x0,然后确定下一个形状的方向d1和在x轴上的起始位置x1。这样就得到一个可能的放置组合,然后再依次尝试所有的组合。
-
计算下落距离:对于每个放置组合,通过
calcNextDropDist()
方法计算下一个形状在当前位置下落的距离。这个距离将会在后续的计算中用到,因为在俄罗斯方块游戏中,形状在下落过程中会被堆积的方块所阻挡,不能无限下落。 -
计算得分:对于每个放置组合,通过
calculateScore()
方法计算其得分。在计算得分时,考虑了多个因素,包括消除的行数、洞的数量、方块的堆积高度等。具体得分计算方式在calculateScore()
方法中进行。 -
选择最优策略:在计算得分过程中,将每个放置组合的得分与当前最优得分进行比较,保留得分更高的组合作为当前的最优策略。最终,整个穷举搜索完成后,得到得分最高的放置策略。
这个算法的核心思想是通过搜索和评估不同的放置组合,寻找到最优的形状放置策略。
二、核心代码实现
class TetrisAI(object):
# 下一个形状的计算
def nextMove(self):
t1 = datetime.now() # 记录开始计算时间
if BOARD_DATA.currentShape == Shape.shapeNone: # 若当前没有正在下落的形状,则返回None
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) # 若当前形状为I、Z或S型,则有两个可能的方向
elif BOARD_DATA.currentShape.shape == Shape.shapeO:
d0Range = (0,) # 若当前形状为O型,则只有一个可能的方向
else:
d0Range = (0, 1, 2, 3) # 其他形状有四个可能的方向
if BOARD_DATA.nextShape.shape in (Shape.shapeI, Shape.shapeZ, Shape.shapeS):
d1Range = (0, 1) # 若下一个形状为I、Z或S型,则有两个可能的方向
elif BOARD_DATA.nextShape.shape == Shape.shapeO:
d1Range = (0,) # 若下一个形状为O型,则只有一个可能的方向
else:
d1Range = (0, 1, 2, 3) # 其他形状有四个可能的方向
# 遍历所有可能的放置组合
for d0 in d0Range:
minX, maxX, _, _ = BOARD_DATA.currentShape.getBoundingOffsets(d0) # 获取当前形状在x轴上的最小和最大边界
for x0 in range(-minX, BOARD_DATA.width - maxX): # 遍历所有可能的x轴位置
board = self.calcStep1Board(d0, x0) # 计算当前形状放置后的游戏板面状态
for d1 in d1Range:
minX, maxX, _, _ = BOARD_DATA.nextShape.getBoundingOffsets(d1) # 获取下一个形状在x轴上的最小和最大边界
dropDist = self.calcNextDropDist(board, d1, range(-minX, BOARD_DATA.width - maxX)) # 计算下一个形状的下落距离
for x1 in range(-minX, BOARD_DATA.width - maxX): # 遍历所有可能的x轴位置
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 = {} # 用于存储不同x轴位置对应的下落距离
for x0 in xRange:
if x0 not in res:
res[x0] = BOARD_DATA.height - 1 # 初始化下落距离为游戏板面的高度减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 # 记录每个x轴位置对应的最小下落距离
return res
# 计算当前形状在给定方向和x轴位置下,下落一步后的游戏板面状态
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 # 初始化下落距离为游戏板面的高度减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 # 记录形状在该位置的最小下落距离
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):
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 # 记录每个x轴位置可能的洞数
else:
hasBlock = True
roofY[x] = height - y # 记录每个x轴位置的方块堆叠高度
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 # 返回放置组合的得分
该代码用于计算俄罗斯方块游戏中当前形状和下一个形状的最优放置策略。
-
TetrisAI
类:定义了一个名为TetrisAI
的类,用于处理俄罗斯方块AI相关的逻辑和计算。 -
nextMove()
方法:该方法用于计算当前形状和下一个形状的最优放置策略。它首先检查当前是否有正在下落的形状,若没有则返回None,表示不需要进行任何操作。接下来,它依次尝试不同的放置组合,并计算每个组合的得分,最后返回得分最高的放置策略。 -
calcNextDropDist()
方法:该方法用于计算下一个形状在当前位置下落的距离。它将下落距离存储在一个字典中,字典的键为下落位置x0,值为对应的下落距离。 -
calcStep1Board()
方法:该方法用于计算在当前形状和给定方向、x0(x轴上的起始位置)下,下落一步后的游戏板面状态。 -
dropDown()
方法和dropDownByDist()
方法:这两个方法用于计算形状在给定位置下落的距离,并将形状的状态更新到游戏板面上。 -
calculateScore()
方法:该方法用于计算给定放置组合的得分,根据板面状态的不同特征来评估每个组合的优劣。得分是根据消除的行数、出现的洞(空白位置)、方块堆叠的高度等因素进行计算。
总体来说,这段代码实现了一个简单的俄罗斯方块AI,用于预测并选择最优的形状放置位置,以尽可能消除更多的方块行。
三、游戏实现与测试
四、完整代码功能解释及游戏玩法
完整的代码下:
tetris_game.py
是主函数,主要为应用界面的主函数。tetris_model.py
是游戏的数据模型。tetris_ai.py
是AI实现算法部分部分。
tetris_game.py代码中,可以通过修改如下:
speed的速度修改方块的下落的速度。
通过注释掉# TETRIS_AI = None可以实现认为控制玩俄罗斯方块。
自己玩的规则:游戏板面是一个矩形网格,您需要使用方向键和空格键来控制方块的移动和旋转。
- 使用向上键:旋转当前形状,使其适应不同的空间布局。
- 使用向左键:将当前形状向左移动一个格子,改变其在横向上的位置。
- 使用向右键:将当前形状向右移动一个格子,改变其在横向上的位置。
- 使用空格键:立即将当前形状下落到最底部,以便快速堆叠方块。
如果您想暂停游戏,只需按下键盘上的“P”键即可。游戏会暂停在当前状态,您可以随时继续游戏。右侧面板会显示下一个即将出现的形状,这样您可以提前做好相应的准备。
完整代码链接:
https://download.csdn.net/download/weixin_40651515/88111818
环境配置:
moviepy==1.0.3 numpy==1.19.5 PyQt5==5.15.4