我们此次的任务就是在这条规则的基础上,利用 Python 和简单的 AI 算法,实现自动寻找最优位置和调整方块形态,达到快速高效得分的目的。(最终效果见文末视频)
代码分三部分:
utils.py:基础设置和界面设计
ai.py:AI算法和具体方式
AITetris.py:实现游戏主循环
一.基础设置和界面设计
方块共有7种形状,每块包含四个人小方块,分别写出小方块的相对坐标、定义获取小方块旋转角度、绝对坐标和相对坐标边界的函数。
1class tetrisShape():
2 def __init__(self, shape=0):
3 # 空块
4 self.shape_empty = 0
5 # 一字型块
6 self.shape_I = 1
7 # L型块
8 self.shape_L = 2
9 # 向左的L型块
10 self.shape_J = 3
11 # T型块
12 self.shape_T = 4
13 # 田字型块
14 self.shape_O = 5
15 # 反向Z型块
16 self.shape_S = 6
17 # Z型块
18 self.shape_Z = 7
19 # 每种块包含的四个小方块相对坐标分布
20 self.shapes_relative_coords = [
21 [[0, 0], [0, 0], [0, 0], [0, 0]],
22 [[0, -1], [0, 0], [0, 1], [0, 2]],
23 [[0, -1], [0, 0], [0, 1], [1, 1]],
24 [[0, -1], [0, 0], [0, 1], [-1, 1]],
25 [[0, -1], [0, 0], [0, 1], [1, 0]],
26 [[0, 0], [0, -1], [1, 0], [1, -1]],
27 [[0, 0], [0, -1], [-1, 0], [1, -1]],
28 [[0, 0], [0, -1], [1, 0], [-1, -1]]
29 ]
30 self.shape = shape
31 self.relative_coords = self.shapes_relative_coords[self.shape]
32 '''获得该形状当前旋转状态的四个小方块的相对坐标分布'''
33 def getRotatedRelativeCoords(self, direction):
34 # 初始分布
35 if direction == 0 or self.shape == self.shape_O:
36 return self.relative_coords
37 # 逆时针旋转90度
38 if direction == 1:
39 return [[-y, x] for x, y in self.relative_coords]
40 # 逆时针旋转180度
41 if direction == 2:
42 if self.shape in [self.shape_I, self.shape_Z, self.shape_S]:
43 return self.relative_coords
44 else:
45 return [[-x, -y] for x, y in self.relative_coords]
46 # 逆时针旋转270度
47 if direction == 3:
48 if self.shape in [self.shape_I, self.shape_Z, self.shape_S]:
49 return [[-y, x] for x, y in self.relative_coords]
50 else:
51 return [[y, -x] for x, y in self.relative_coords]
在方块移动时,也要实时判断方块是否越界或落地并及时处理,如果落地则将方块合并并整行消除,再创建新的方块,以此循环。
1class InnerBoard():
2 def __init__(self, width=10, height=22):
3 # 宽和长, 单位长度为小方块边长
4 self.width = width
5 self.height = height
6 self.reset()
7 '''判断当前俄罗斯方块是否可以移动到某位置'''
8 def ableMove(self, coord, direction=None):
9 assert len(coord) == 2
10 if direction is None:
11 direction = self.current_direction
12 for x, y in self.current_tetris.getAbsoluteCoords(direction, coord[0], coord[1]):
13 # 超出边界
14 if x >= self.width or x 0 or y >= self.height or y 0:
15 return False
16 # 该位置有俄罗斯方块了
17 if self.getCoordValue([x, y]) > 0:
18 return False
19 return True
20 '''向右移动'''
21 def moveRight(self):
22 if self.ableMove([self.current_coord[0]+1, self.current_coord[1]]):
23 self.current_coord[0] += 1
24 '''向左移动'''
25 def moveLeft(self):
26 if self.ableMove([self.current_coord[0]-1, self.current_coord[1]]):
27 self.current_coord[0] -= 1
28 '''顺时针转'''
29 def rotateClockwise(self):
30 if self.ableMove(self.current_coord, (self.current_direction-1) % 4):
31 self.current_direction = (self.current_direction-1) % 4
32 '''逆时针转'''
33 def rotateAnticlockwise(self):
34 if self.ableMove(self.current_coord, (self.current_direction+1) % 4):
35 self.current_direction = (self.current_direction+1) % 4
36 '''向下移动'''
37 def moveDown(self):
38 removed_lines = 0
39 if self.ableMove([self.current_coord[0], self.current_coord[1]+1]):
40 self.current_coord[1] += 1
41 else:
42 x_min, x_max, y_min, y_max = self.current_tetris.getRelativeBoundary(self.current_direction)
43 # 简单起见, 有超出屏幕就判定游戏结束
44 if self.current_coord[1] + y_min 0:
45 self.is_gameover = True
46 return removed_lines
47 self.mergeTetris()
48 removed_lines = self.removeFullLines()
49 self.createNewTetris()
50 return removed_lines
1class ExternalBoard(QFrame):
2 score_signal = pyqtSignal(str)
3 def __init__(self, parent, grid_size, inner_board):
4 super().__init__(parent)
5 self.grid_size = grid_size
6 self.inner_board = inner_board
7 self.setFixedSize(grid_size * inner_board.width, grid_size * inner_board.height)
8 self.initExternalBoard()
9 '''外部板块初始化'''
10 def initExternalBoard(self):
11 self.score = 0
12 '''把内部板块结构画出来'''
13 def paintEvent(self, event):
14 painter = QPainter(self)
15 for x in range(self.inner_board.width):
16 for y in range(self.inner_board.height):
17 shape = self.inner_board.getCoordValue([x, y])
18 drawCell(painter, x * self.grid_size, y * self.grid_size, shape, self.grid_size)
19 for x, y in self.inner_board.getCurrentTetrisCoords():
20 shape = self.inner_board.current_tetris.shape
21 drawCell(painter, x * self.grid_size, y * self.grid_size, shape, self.grid_size)
22 painter.setPen(QColor(0x777777))
23 painter.drawLine(0, self.height()-1, self.width(), self.height()-1)
24 painter.drawLine(self.width()-1, 0, self.width()-1, self.height())
25 painter.setPen(QColor(0xCCCCCC))
26 painter.drawLine(self.width(), 0, self.width(), self.height())
27 painter.drawLine(0, self.height(), self.width(), self.height())
28 '''数据更新'''
29 def updateData(self):
30 self.score_signal.emit(str(self.score))
31 self.update()
32
33
34'''
35侧面板, 右边显示下一个俄罗斯方块的形状
36'''
37class SidePanel(QFrame):
38 def __init__(self, parent, grid_size, inner_board):
39 super().__init__(parent)
40 self.grid_size = grid_size
41 self.inner_board = inner_board
42 self.setFixedSize(grid_size * 5, grid_size * inner_board.height)
43 self.move(grid_size * inner_board.width, 0)
44 '''画侧面板'''
45 def paintEvent(self, event):
46 painter = QPainter(self)
47 x_min, x_max, y_min, y_max = self.inner_board.next_tetris.getRelativeBoundary(0)
48 dy = 3 * self.grid_size
49 dx = (self.width() - (x_max - x_min) * self.grid_size) / 2
50 shape = self.inner_board.next_tetris.shape
51 for x, y in self.inner_board.next_tetris.getAbsoluteCoords(0, 0, -y_min):
52 drawCell(painter, x * self.grid_size + dx, y * self.grid_size + dy, shape, self.grid_size)
53 '''更新数据'''
54 def updateData(self):
55 self.update()
2.AI算法和具体方式
1# 简单的AI算法
2for d_now in current_direction_range:
3 x_now_min, x_now_max, y_now_min, y_now_max = self.inner_board.current_tetris.getRelativeBoundary(d_now)
4 for x_now in range(-x_now_min, self.inner_board.width - x_now_max):
5 board = self.getFinalBoardData(d_now, x_now)
6 for d_next in next_direction_range:
7 x_next_min, x_next_max, y_next_min, y_next_max = self.inner_board.next_tetris.getRelativeBoundary(d_next)
8 distances = self.getDropDistances(board, d_next, range(-x_next_min, self.inner_board.width-x_next_max))
9 for x_next in range(-x_next_min, self.inner_board.width-x_next_max):
10 score = self.calcScore(copy.deepcopy(board), d_next, x_next, distances)
11 if not action or action[2] 12 action = [d_now, x_now, score]
13return action
未来场景优劣评定考虑的因素有:
1)可消除的行数;
2)堆积后的俄罗斯方块内的虚洞数量;
3)堆积后的俄罗斯方块内的小方块数量;
4)堆积后的俄罗斯方块的最高点;
5)堆积后的俄罗斯方块的高度(每一列都有一个高度)标准差;
6)堆积后的俄罗斯方块的高度一阶前向差分;
7)堆积后的俄罗斯方块的高度一阶前向差分的标准差;
9)堆积后的俄罗斯方块的最高点和最低点之差。
1# 下个俄罗斯方块以某种方式模拟到达底部
2board = self.imitateDropDown(board, self.inner_board.next_tetris, d_next, x_next, distances[x_next])
3width, height = self.inner_board.width, self.inner_board.height
4# 下一个俄罗斯方块以某方案行动到达底部后的得分(可消除的行数)
5removed_lines = 0
6# 空位统计
7hole_statistic_0 = [0] * width
8hole_statistic_1 = [0] * width
9# 方块数量
10num_blocks = 0
11# 空位数量
12num_holes = 0
13# 每个x位置堆积俄罗斯方块的最高点
14roof_y = [0] * width
15for y in range(height-1, -1, -1):
16 # 是否有空位
17 has_hole = False
18 # 是否有方块
19 has_block = False
20 for x in range(width):
21 if board[x + y * width] == tetrisShape().shape_empty:
22 has_hole = True
23 hole_statistic_0[x] += 1
24 else:
25 has_block = True
26 roof_y[x] = height - y
27 if hole_statistic_0[x] > 0:
28 hole_statistic_1[x] += hole_statistic_0[x]
29 hole_statistic_0[x] = 0
30 if hole_statistic_1[x] > 0:
31 num_blocks += 1
32 if not has_block:
33 break
34 if not has_hole and has_block:
35 removed_lines += 1
36......
3.实现游戏主循环
定义俄罗斯方块游戏类初始化(包含块大小、下落速度、水平布局、AI控制等),并含有开始、暂停、界面更新等事件。
1class TetrisGame(QMainWindow):
2 def __init__(self):
3 super().__init__()
4 # 是否暂停ing
5 self.is_paused = False
6 # 是否开始ing
7 self.is_started = False
8 self.initUI()
9 '''界面初始化'''
10 def initUI(self):
11 # 块大小
12 self.grid_size = 22
13 # 游戏帧率
14 self.fps = 100
15 self.timer = QBasicTimer()
16 # 水平布局
17 layout_horizontal = QHBoxLayout()
18 self.inner_board = InnerBoard()
19 self.external_board = ExternalBoard(self, self.grid_size, self.inner_board)
20
21 .
22 .
23 .
24
25 '''按键事件'''
26 def keyPressEvent(self, event):
27 if not self.is_started or self.inner_board.current_tetris == tetrisShape().shape_empty:
28 super(TetrisGame, self).keyPressEvent(event)
29 return
30 key = event.key()
31 # P键暂停
32 if key == Qt.Key_P:
33 self.pause()
34 return
35 if self.is_paused:
36 return
37 else:
38 super(TetrisGame, self).keyPressEvent(event)
39 self.updateWindow()
40
41
42if __name__ == '__main__':
43 app = QApplication([])
最终效果视频展示
注:本次代码参考资料
https://github.com/LoveDaisy/tetris_game
以上就是本次Python自动耍俄罗斯方块的过程,后台回复“ 俄罗斯方块 ”即可获取源码。【完】
等等......今天公众号开通了流量主功能,大家随手点点底部广告呗,谢谢啦!
往期推荐
原创不易,如有帮忙,点个在看或赞赏呗